583 lines
26 KiB
Python
583 lines
26 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from sqlmodel import Session
|
|
|
|
from ..core.config import settings
|
|
from .policy_engine import evaluate_policy
|
|
|
|
|
|
VOICE_COMMANDS: list[dict[str, Any]] = [
|
|
{
|
|
"id": "unraid.health",
|
|
"label": "Check Unraid",
|
|
"spokenPhraseNl": "controleer unraid",
|
|
"spokenPhraseEn": "check unraid",
|
|
"category": "Infrastructure",
|
|
"endpoint": "/api/voice/actions/unraid/health",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "unraid.health_summary",
|
|
"target": "unraid",
|
|
"risk": "low",
|
|
"enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_unraid",
|
|
"homeAssistantScriptName": "appops_check_unraid",
|
|
"description": "Read-only Unraid health summary through the voice policy path.",
|
|
"safetyNotes": ["Read-only", "No mover, disk or array mutation"],
|
|
},
|
|
{
|
|
"id": "appops.setup.check",
|
|
"label": "Check AppOps setup",
|
|
"spokenPhraseNl": "controleer appops setup",
|
|
"spokenPhraseEn": "check appops setup",
|
|
"category": "Gateway",
|
|
"endpoint": "/api/voice/actions/setup/status",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "setup.status",
|
|
"target": "appops",
|
|
"risk": "low",
|
|
"enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_setup",
|
|
"homeAssistantScriptName": "appops_check_setup",
|
|
"description": "Read-only setup readiness check for the AppOps Gateway.",
|
|
"safetyNotes": ["Read-only", "Reports setup state only"],
|
|
},
|
|
{
|
|
"id": "safety.summary",
|
|
"label": "Check Safety",
|
|
"spokenPhraseNl": "controleer veiligheid",
|
|
"spokenPhraseEn": "check safety",
|
|
"category": "Safety",
|
|
"endpoint": "/api/voice/actions/safety/summary",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "safety.summary",
|
|
"target": "safety",
|
|
"risk": "low",
|
|
"enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_safety",
|
|
"homeAssistantScriptName": "appops_check_safety",
|
|
"description": "Read-only Safety Center summary.",
|
|
"safetyNotes": ["Read-only", "No remediation execution"],
|
|
},
|
|
{
|
|
"id": "media.pipeline.check",
|
|
"label": "Check Media Pipeline",
|
|
"spokenPhraseNl": "controleer media pipeline",
|
|
"spokenPhraseEn": "check media pipeline",
|
|
"category": "Media",
|
|
"endpoint": "/api/voice/actions/media-pipeline/check",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "media_pipeline.check",
|
|
"target": "media-pipeline",
|
|
"risk": "low",
|
|
"enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_media_pipeline",
|
|
"homeAssistantScriptName": "appops_check_media_pipeline",
|
|
"description": "Runs the read-only media pipeline diagnostic check path.",
|
|
"safetyNotes": ["Read-only connector summaries", "No media add/delete actions"],
|
|
},
|
|
{
|
|
"id": "apps.widefrog.health",
|
|
"label": "Check Widefrog",
|
|
"spokenPhraseNl": "controleer widefrog",
|
|
"spokenPhraseEn": "check widefrog",
|
|
"category": "Apps",
|
|
"endpoint": "/api/voice/actions/apps/widefrog/health",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "widefrog.health",
|
|
"target": "widefrog",
|
|
"risk": "low",
|
|
"enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_widefrog",
|
|
"homeAssistantScriptName": "appops_check_widefrog",
|
|
"description": "Read-only Widefrog connector health.",
|
|
"safetyNotes": ["Read-only", "No retry or scan"],
|
|
},
|
|
{
|
|
"id": "apps.deluge.status",
|
|
"label": "Check Downloads",
|
|
"spokenPhraseNl": "controleer downloads",
|
|
"spokenPhraseEn": "check downloads",
|
|
"category": "Downloads",
|
|
"endpoint": "/api/voice/actions/apps/deluge/status",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "deluge.session_status",
|
|
"target": "deluge",
|
|
"risk": "low",
|
|
"enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_downloads",
|
|
"homeAssistantScriptName": "appops_check_downloads",
|
|
"description": "Read-only Deluge/download status summary.",
|
|
"safetyNotes": ["Read-only", "No pause, resume or delete-with-data"],
|
|
},
|
|
{
|
|
"id": "apps.plex.activity",
|
|
"label": "Check Plex Activity",
|
|
"spokenPhraseNl": "controleer plex activiteit",
|
|
"spokenPhraseEn": "check plex activity",
|
|
"category": "Media",
|
|
"endpoint": "/api/voice/actions/apps/plex/activity",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "plex.active_sessions",
|
|
"target": "plex",
|
|
"risk": "low",
|
|
"enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_plex_activity",
|
|
"homeAssistantScriptName": "appops_check_plex_activity",
|
|
"description": "Read-only Plex active stream summary.",
|
|
"safetyNotes": ["Read-only", "No library scan or session kill"],
|
|
},
|
|
{
|
|
"id": "notifications.summary",
|
|
"label": "Check Notifications",
|
|
"spokenPhraseNl": "controleer notificaties",
|
|
"spokenPhraseEn": "check notifications",
|
|
"category": "Notifications",
|
|
"endpoint": "/api/voice/actions/notifications/summary",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "notifications.summary",
|
|
"target": "notifications",
|
|
"risk": "low",
|
|
"enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_notifications",
|
|
"homeAssistantScriptName": "appops_check_notifications",
|
|
"description": "Read-only notification rule/channel summary.",
|
|
"safetyNotes": ["Read-only", "Does not send Home Assistant notifications"],
|
|
},
|
|
{
|
|
"id": "unraid.storage",
|
|
"label": "Check Storage",
|
|
"spokenPhraseNl": "controleer opslag",
|
|
"spokenPhraseEn": "check storage",
|
|
"category": "Infrastructure",
|
|
"endpoint": "/api/voice/actions/unraid/storage",
|
|
"method": "POST", "payload": {}, "action": "unraid.health_summary", "target": "unraid",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_storage", "homeAssistantScriptName": "appops_check_storage",
|
|
"description": "Returns live disk, cache and parity status without changing storage.",
|
|
"safetyNotes": ["Read-only", "No mover, format or array action"],
|
|
},
|
|
{
|
|
"id": "unraid.vms",
|
|
"label": "Check Virtual Machines",
|
|
"spokenPhraseNl": "controleer virtuele machines",
|
|
"spokenPhraseEn": "check virtual machines",
|
|
"category": "Infrastructure",
|
|
"endpoint": "/api/voice/actions/unraid/vms",
|
|
"method": "POST", "payload": {}, "action": "unraid.health_summary", "target": "unraid",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_vms", "homeAssistantScriptName": "appops_check_vms",
|
|
"description": "Returns the VM inventory and power states visible to the Unraid API.",
|
|
"safetyNotes": ["Read-only", "Does not start or stop a VM"],
|
|
},
|
|
{
|
|
"id": "apps.sonarr.health",
|
|
"label": "Check Sonarr",
|
|
"spokenPhraseNl": "controleer sonarr",
|
|
"spokenPhraseEn": "check sonarr",
|
|
"category": "Media",
|
|
"endpoint": "/api/voice/actions/apps/sonarr/health",
|
|
"method": "POST", "payload": {}, "action": "sonarr.health", "target": "sonarr",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_sonarr", "homeAssistantScriptName": "appops_check_sonarr",
|
|
"description": "Returns Sonarr health without modifying series.", "safetyNotes": ["Read-only"],
|
|
},
|
|
{
|
|
"id": "apps.sonarr.queue",
|
|
"label": "Check Sonarr Queue",
|
|
"spokenPhraseNl": "controleer sonarr wachtrij",
|
|
"spokenPhraseEn": "check sonarr queue",
|
|
"category": "Media",
|
|
"endpoint": "/api/voice/actions/apps/sonarr/queue",
|
|
"method": "POST", "payload": {}, "action": "sonarr.queue_status", "target": "sonarr",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_sonarr_queue", "homeAssistantScriptName": "appops_check_sonarr_queue",
|
|
"description": "Returns the current Sonarr queue summary.", "safetyNotes": ["Read-only"],
|
|
},
|
|
{
|
|
"id": "apps.radarr.health",
|
|
"label": "Check Radarr",
|
|
"spokenPhraseNl": "controleer radarr",
|
|
"spokenPhraseEn": "check radarr",
|
|
"category": "Media",
|
|
"endpoint": "/api/voice/actions/apps/radarr/health",
|
|
"method": "POST", "payload": {}, "action": "radarr.health", "target": "radarr",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_radarr", "homeAssistantScriptName": "appops_check_radarr",
|
|
"description": "Returns Radarr health without modifying movies.", "safetyNotes": ["Read-only"],
|
|
},
|
|
{
|
|
"id": "apps.radarr.queue",
|
|
"label": "Check Radarr Queue",
|
|
"spokenPhraseNl": "controleer radarr wachtrij",
|
|
"spokenPhraseEn": "check radarr queue",
|
|
"category": "Media",
|
|
"endpoint": "/api/voice/actions/apps/radarr/queue",
|
|
"method": "POST", "payload": {}, "action": "radarr.queue_status", "target": "radarr",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_radarr_queue", "homeAssistantScriptName": "appops_check_radarr_queue",
|
|
"description": "Returns the current Radarr queue summary.", "safetyNotes": ["Read-only"],
|
|
},
|
|
{
|
|
"id": "apps.prowlarr.indexers",
|
|
"label": "Check Prowlarr Indexers",
|
|
"spokenPhraseNl": "controleer prowlarr indexers",
|
|
"spokenPhraseEn": "check prowlarr indexers",
|
|
"category": "Media",
|
|
"endpoint": "/api/voice/actions/apps/prowlarr/indexers",
|
|
"method": "POST", "payload": {}, "action": "prowlarr.indexer_health", "target": "prowlarr",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_prowlarr", "homeAssistantScriptName": "appops_check_prowlarr",
|
|
"description": "Returns Prowlarr indexer health and failures.", "safetyNotes": ["Read-only"],
|
|
},
|
|
{
|
|
"id": "apps.deluge.stalled",
|
|
"label": "Check Stalled Downloads",
|
|
"spokenPhraseNl": "controleer vastgelopen downloads",
|
|
"spokenPhraseEn": "check stalled downloads",
|
|
"category": "Downloads",
|
|
"endpoint": "/api/voice/actions/apps/deluge/stalled",
|
|
"method": "POST", "payload": {}, "action": "deluge.stalled_downloads", "target": "deluge",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_stalled_downloads", "homeAssistantScriptName": "appops_check_stalled_downloads",
|
|
"description": "Returns stalled Deluge downloads without changing them.", "safetyNotes": ["Read-only", "No delete-with-data"],
|
|
},
|
|
{
|
|
"id": "apps.plex.recent",
|
|
"label": "Check Recent Plex Media",
|
|
"spokenPhraseNl": "wat is recent toegevoegd aan plex",
|
|
"spokenPhraseEn": "check recent plex media",
|
|
"category": "Media",
|
|
"endpoint": "/api/voice/actions/apps/plex/recent",
|
|
"method": "POST", "payload": {}, "action": "plex.recent_media", "target": "plex",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_plex_recent", "homeAssistantScriptName": "appops_check_plex_recent",
|
|
"description": "Returns recently added Plex media.", "safetyNotes": ["Read-only"],
|
|
},
|
|
{
|
|
"id": "apps.tautulli.activity",
|
|
"label": "Check Tautulli Activity",
|
|
"spokenPhraseNl": "controleer tautulli activiteit",
|
|
"spokenPhraseEn": "check tautulli activity",
|
|
"category": "Media",
|
|
"endpoint": "/api/voice/actions/apps/tautulli/activity",
|
|
"method": "POST", "payload": {}, "action": "tautulli.activity", "target": "tautulli",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_tautulli", "homeAssistantScriptName": "appops_check_tautulli",
|
|
"description": "Returns current Tautulli stream and user activity.", "safetyNotes": ["Read-only"],
|
|
},
|
|
{
|
|
"id": "security.exposure",
|
|
"label": "Check Public Exposure",
|
|
"spokenPhraseNl": "controleer publieke blootstelling",
|
|
"spokenPhraseEn": "check public exposure",
|
|
"category": "Safety",
|
|
"endpoint": "/api/voice/actions/security/exposure",
|
|
"method": "POST", "payload": {}, "action": "npm.exposure_summary", "target": "nginx_proxy_manager",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_exposure", "homeAssistantScriptName": "appops_check_exposure",
|
|
"description": "Returns Nginx Proxy Manager exposure and certificate posture.", "safetyNotes": ["Read-only", "No proxy mutation"],
|
|
},
|
|
{
|
|
"id": "security.authentik",
|
|
"label": "Check Authentik",
|
|
"spokenPhraseNl": "controleer authentik",
|
|
"spokenPhraseEn": "check authentik",
|
|
"category": "Safety",
|
|
"endpoint": "/api/voice/actions/security/authentik",
|
|
"method": "POST", "payload": {}, "action": "authentik.health", "target": "authentik",
|
|
"risk": "low", "enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_check_authentik", "homeAssistantScriptName": "appops_check_authentik",
|
|
"description": "Returns Authentik health without changing identity configuration.", "safetyNotes": ["Read-only", "No identity mutation"],
|
|
},
|
|
{
|
|
"id": "container.widefrog.restart",
|
|
"label": "Restart Widefrog",
|
|
"spokenPhraseNl": "herstart widefrog",
|
|
"spokenPhraseEn": "restart widefrog",
|
|
"category": "Apps",
|
|
"endpoint": "/api/voice/actions/container/widefrog/restart",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "container.restart",
|
|
"target": "widefrog",
|
|
"risk": "medium",
|
|
"enabledByDefault": True,
|
|
"homeAssistantRestCommandName": "appops_restart_widefrog",
|
|
"homeAssistantScriptName": "appops_restart_widefrog",
|
|
"description": "Policy-aware Widefrog container restart.",
|
|
"safetyNotes": ["Only exported while policy allows voice restart", "Audited", "No stack redeploy"],
|
|
},
|
|
{
|
|
"id": "playbooks.fix-widefrog.run",
|
|
"label": "Run Fix Widefrog Playbook",
|
|
"spokenPhraseNl": "voer widefrog herstel uit",
|
|
"spokenPhraseEn": "run fix widefrog playbook",
|
|
"category": "Playbooks",
|
|
"endpoint": "/api/voice/actions/playbooks/fix-widefrog/run",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "playbook.run",
|
|
"target": "fix-widefrog",
|
|
"risk": "medium",
|
|
"enabledByDefault": False,
|
|
"homeAssistantRestCommandName": "appops_run_fix_widefrog_playbook",
|
|
"homeAssistantScriptName": "appops_run_fix_widefrog_playbook",
|
|
"description": "Requests policy evaluation for the Widefrog recovery playbook.",
|
|
"safetyNotes": ["Approval may be required", "No direct connector call before policy"],
|
|
},
|
|
{
|
|
"id": "notifications.evaluate.safety",
|
|
"label": "Evaluate Safety Notifications",
|
|
"spokenPhraseNl": "evalueer veiligheidsnotificaties",
|
|
"spokenPhraseEn": "evaluate safety notifications",
|
|
"category": "Notifications",
|
|
"endpoint": "/api/voice/actions/notifications/evaluate/safety",
|
|
"method": "POST",
|
|
"payload": {},
|
|
"action": "notifications.evaluate_safety",
|
|
"target": "notifications",
|
|
"risk": "medium",
|
|
"enabledByDefault": False,
|
|
"homeAssistantRestCommandName": "appops_evaluate_safety_notifications",
|
|
"homeAssistantScriptName": "appops_evaluate_safety_notifications",
|
|
"description": "Policy-aware safety notification evaluation.",
|
|
"safetyNotes": ["May create notification events", "Does not expose HOME_ASSISTANT_TOKEN"],
|
|
},
|
|
]
|
|
|
|
BLOCKED_VOICE_EXAMPLES = [
|
|
{
|
|
"id": "container.authentik.restart",
|
|
"label": "Restart Authentik direct command",
|
|
"action": "container.restart",
|
|
"target": "authentik",
|
|
"reason": "Critical identity service restart is not exported as direct voice command.",
|
|
},
|
|
{
|
|
"id": "bulk-download-pause",
|
|
"label": "Pause all downloads",
|
|
"target": "deluge",
|
|
"reason": "Bulk download actions require explicit operator control.",
|
|
},
|
|
{
|
|
"id": "stack-redeploy",
|
|
"label": "Stack redeploy",
|
|
"target": "stack",
|
|
"reason": "Redeploy changes runtime topology and is not exported to voice.",
|
|
},
|
|
{
|
|
"id": "unraid-mover-start",
|
|
"label": "Unraid mover",
|
|
"target": "unraid",
|
|
"reason": "High-impact storage maintenance is blocked for voice.",
|
|
},
|
|
{
|
|
"id": "volume-delete",
|
|
"label": "Delete/prune/volume",
|
|
"target": "volume",
|
|
"reason": "Data destructive operations are blocked.",
|
|
},
|
|
]
|
|
|
|
|
|
def list_commands(session: Session) -> list[dict[str, Any]]:
|
|
exported_at = datetime.now(UTC).isoformat()
|
|
commands = []
|
|
for command in VOICE_COMMANDS:
|
|
policy = evaluate_policy(session, action=command["action"], target=command["target"], source="voice")
|
|
decision = str(policy["decision"])
|
|
if decision == "blocked":
|
|
continue
|
|
commands.append(
|
|
{
|
|
**_public_command(command),
|
|
"policyDecision": decision,
|
|
"requiresApproval": decision == "approval_required",
|
|
"lastExportedAt": exported_at,
|
|
}
|
|
)
|
|
return commands
|
|
|
|
|
|
def command_by_id(session: Session, command_id: str) -> dict[str, Any] | None:
|
|
return next((command for command in list_commands(session) if command["id"] == command_id), None)
|
|
|
|
|
|
def test_intent(session: Session, text: str) -> dict[str, Any]:
|
|
normalized = " ".join(text.lower().strip().split())
|
|
if not normalized:
|
|
return {"status": "unknown", "executed": False, "message": "No spoken phrase supplied."}
|
|
|
|
for command in list_commands(session):
|
|
phrases = [
|
|
str(command.get("spokenPhraseNl", "")).lower(),
|
|
str(command.get("spokenPhraseEn", "")).lower(),
|
|
str(command.get("label", "")).lower(),
|
|
]
|
|
if normalized in phrases or any(phrase and phrase in normalized for phrase in phrases):
|
|
return dry_run(session, command["id"])
|
|
|
|
return {"status": "unknown", "executed": False, "message": "No allowlisted voice command matched."}
|
|
|
|
|
|
def dry_run(session: Session, command_id: str) -> dict[str, Any]:
|
|
command = command_by_id(session, command_id)
|
|
if not command:
|
|
return {"status": "unknown", "executed": False, "message": "Voice command is not allowlisted or is blocked."}
|
|
|
|
policy = evaluate_policy(session, action=command["action"], target=command["target"], source="voice")
|
|
return {
|
|
"status": "matched",
|
|
"executed": False,
|
|
"command": command,
|
|
"policyPreview": {
|
|
"decision": policy["decision"],
|
|
"reason": policy["reason"],
|
|
"risk": policy.get("risk", command["risk"]),
|
|
},
|
|
"request": {
|
|
"method": command["method"],
|
|
"endpoint": command["endpoint"],
|
|
"payload": command["payload"],
|
|
},
|
|
}
|
|
|
|
|
|
def request_approval(session: Session, command_id: str) -> dict[str, Any]:
|
|
command = command_by_id(session, command_id)
|
|
if not command:
|
|
return {"success": False, "status": "unknown", "message": "Voice command is not allowlisted or is blocked."}
|
|
policy = evaluate_policy(session, action=command["action"], target=command["target"], source="voice")
|
|
if policy["decision"] == "allowed":
|
|
return {"success": False, "status": "not_required", "message": "Command is allowed and does not need approval.", "command": command}
|
|
if policy["decision"] == "blocked":
|
|
return {"success": False, "status": "blocked", "message": policy["reason"], "command": command}
|
|
|
|
from .approval_service import create_approval
|
|
|
|
approval = create_approval(
|
|
session,
|
|
action=command["action"],
|
|
target=command["target"],
|
|
source="voice",
|
|
risk=str(policy.get("risk", command["risk"])),
|
|
reason=policy["reason"],
|
|
requester="voice",
|
|
)
|
|
return {"success": True, "status": "approval_requested", "approvalId": approval.id, "command": command}
|
|
|
|
|
|
def home_assistant_export(session: Session, base_url: str = 'http://appops:1216') -> dict[str, Any]:
|
|
commands = list_commands(session)
|
|
normalized_base_url = base_url.rstrip('/')
|
|
return {
|
|
"yaml": "\n\n".join([rest_commands_yaml(commands, normalized_base_url), scripts_yaml(commands), dashboard_yaml(commands), secrets_yaml()]),
|
|
"restCommandsYaml": rest_commands_yaml(commands, normalized_base_url),
|
|
"scriptsYaml": scripts_yaml(commands),
|
|
"dashboardYaml": dashboard_yaml(commands),
|
|
"secretsYaml": secrets_yaml(),
|
|
"commands": commands,
|
|
"blocked": BLOCKED_VOICE_EXAMPLES,
|
|
"endpoints": [
|
|
{
|
|
"name": command["label"],
|
|
"path": command["endpoint"],
|
|
"method": command["method"],
|
|
"policyDecision": command["policyDecision"],
|
|
"requiresApproval": command["requiresApproval"],
|
|
}
|
|
for command in commands
|
|
],
|
|
"token": {
|
|
"name": "APP_OPS_VOICE_TOKEN",
|
|
"status": "required",
|
|
"homeAssistantSecret": "appops_voice_authorization",
|
|
"note": "HOME_ASSISTANT_TOKEN is only for AppOps-to-Home-Assistant notification delivery.",
|
|
},
|
|
"publicBaseUrl": normalized_base_url,
|
|
"homeAssistantUrl": str(settings.apis.get('HOME_ASSISTANT_URL') or '').rstrip('/'),
|
|
"setupGuide": [
|
|
{"id": "token", "title": "Bewaar de AppOps bearer header", "detail": "Plaats 'Bearer ' plus APP_OPS_VOICE_TOKEN als appops_voice_authorization in Home Assistant secrets.yaml."},
|
|
{"id": "rest", "title": "Installeer rest_command", "detail": "Voeg het rest_command-blok toe aan configuration.yaml of een Home Assistant package."},
|
|
{"id": "scripts", "title": "Installeer de scripts", "detail": "Voeg het script-blok toe en herlaad scripts of herstart Home Assistant."},
|
|
{"id": "test", "title": "Test eerst read-only", "detail": "Voer Check AppOps setup handmatig uit en controleer AppOps Audit."},
|
|
{"id": "gemini", "title": "Koppel Gemini aan Assist", "detail": "Configureer Google Gemini Conversation met Control Home Assistant ingeschakeld en exposeer alleen de gewenste AppOps-scripts aan Assist."},
|
|
{"id": "phone", "title": "Gebruik de Home Assistant Assist-pipeline", "detail": "Selecteer de Gemini conversation agent in je Assist-pipeline en start die pipeline vanuit de Home Assistant Companion-app op je telefoon."},
|
|
],
|
|
}
|
|
|
|
|
|
def rest_commands_yaml(commands: list[dict[str, Any]], base_url: str = 'http://appops:1216') -> str:
|
|
lines = ["rest_command:"]
|
|
for command in commands:
|
|
lines.extend(
|
|
[
|
|
f" {command['homeAssistantRestCommandName']}:",
|
|
f" url: \"{base_url.rstrip('/')}{command['endpoint']}\"",
|
|
f" method: {command['method'].lower()}",
|
|
" headers:",
|
|
" Authorization: !secret appops_voice_authorization",
|
|
" Content-Type: \"application/json\"",
|
|
" payload: '{}'",
|
|
]
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def scripts_yaml(commands: list[dict[str, Any]]) -> str:
|
|
lines = ["script:"]
|
|
for command in commands:
|
|
lines.extend(
|
|
[
|
|
f" {command['homeAssistantScriptName']}:",
|
|
f" alias: \"{command['label']}\"",
|
|
f" description: \"{command['description']} Tell the user the returned AppOps status and never claim execution beyond the response.\"",
|
|
" mode: single",
|
|
" sequence:",
|
|
" - action: rest_command." + command["homeAssistantRestCommandName"],
|
|
" response_variable: appops_result",
|
|
" - variables:",
|
|
" appops_response:",
|
|
" status: \"{{ appops_result.status | default(0) }}\"",
|
|
" content: \"{{ appops_result.content | default('{}') }}\"",
|
|
" - stop: \"AppOps request completed\"",
|
|
" response_variable: appops_response",
|
|
]
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def dashboard_yaml(commands: list[dict[str, Any]]) -> str:
|
|
lines = [
|
|
"# Lovelace dashboard snippet",
|
|
"type: entities",
|
|
"title: AppOps Voice Commands",
|
|
"entities:",
|
|
]
|
|
for command in commands:
|
|
lines.append(f" - entity: script.{command['homeAssistantScriptName']}")
|
|
lines.append(f" name: \"{command['label']}\"")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def secrets_yaml() -> str:
|
|
return 'secrets.yaml:\n appops_voice_authorization: "Bearer PASTE_APP_OPS_VOICE_TOKEN_HERE"'
|
|
|
|
|
|
def _public_command(command: dict[str, Any]) -> dict[str, Any]:
|
|
public = {key: value for key, value in command.items() if key not in {"action", "target"}}
|
|
public["action"] = command["action"]
|
|
public["target"] = command["target"]
|
|
return public
|