Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d
This commit is contained in:
commit
1dfbca08a1
528 files changed
+131054
No files matched your search
@@ -0,0 +1,197 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from sqlmodel import Session
|
||||
|
||||
from ..core.config import settings
|
||||
from ..core.time import utc_now
|
||||
from .secret_store import get_secret
|
||||
from .voice_catalog import list_commands
|
||||
|
||||
|
||||
def _timeout_seconds() -> float:
|
||||
try:
|
||||
return max(2.0, min(20.0, float(settings.apis.get('HOME_ASSISTANT_TIMEOUT_SECONDS') or 8)))
|
||||
except (TypeError, ValueError):
|
||||
return 8.0
|
||||
|
||||
|
||||
def _get_json(url: str, token: str, path: str, verify_tls: bool) -> tuple[Any, str, int | None]:
|
||||
try:
|
||||
with httpx.Client(timeout=_timeout_seconds(), verify=verify_tls) as client:
|
||||
response = client.get(
|
||||
f'{url.rstrip("/")}{path}',
|
||||
headers={
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
)
|
||||
except httpx.RequestError:
|
||||
return None, 'unreachable', None
|
||||
|
||||
if response.status_code in {401, 403}:
|
||||
return None, 'auth_failed', response.status_code
|
||||
if response.status_code >= 400:
|
||||
return None, 'error', response.status_code
|
||||
try:
|
||||
return response.json(), 'ok', response.status_code
|
||||
except ValueError:
|
||||
return None, 'invalid_response', response.status_code
|
||||
|
||||
|
||||
def _step(step_id: str, label: str, status: str, detail: str) -> dict[str, str]:
|
||||
return {'id': step_id, 'label': label, 'status': status, 'detail': detail}
|
||||
|
||||
|
||||
def build_readiness(session: Session) -> dict[str, Any]:
|
||||
url = str(settings.apis.get('HOME_ASSISTANT_URL') or '').strip().rstrip('/')
|
||||
token = get_secret(session, 'HOME_ASSISTANT_TOKEN', touch=False) or str(settings.apis.get('HOME_ASSISTANT_TOKEN') or '').strip()
|
||||
verify_tls = bool(settings.apis.get('HOME_ASSISTANT_VERIFY_TLS', False))
|
||||
commands = list_commands(session)
|
||||
expected_scripts = sorted({
|
||||
f'script.{command["homeAssistantScriptName"]}'
|
||||
for command in commands
|
||||
if command.get('homeAssistantScriptName')
|
||||
})
|
||||
core_script = 'script.appops_check_setup'
|
||||
|
||||
if not url or not token:
|
||||
missing = [
|
||||
name for name, value in (
|
||||
('HOME_ASSISTANT_URL', url),
|
||||
('HOME_ASSISTANT_TOKEN', token),
|
||||
)
|
||||
if not value
|
||||
]
|
||||
return {
|
||||
'status': 'missing_config',
|
||||
'operational': False,
|
||||
'apiReachable': False,
|
||||
'homeAssistantUrl': url,
|
||||
'missing': missing,
|
||||
'expectedScriptCount': len(expected_scripts),
|
||||
'detectedScriptCount': 0,
|
||||
'detectedScripts': [],
|
||||
'missingScripts': expected_scripts,
|
||||
'conversationAvailable': False,
|
||||
'geminiConversationAvailable': False,
|
||||
'llmExposureStatus': 'manual_verification_required',
|
||||
'steps': [
|
||||
_step('connector', 'Home Assistant connector', 'warning', f'Missing: {", ".join(missing)}'),
|
||||
],
|
||||
'nextActions': [
|
||||
{
|
||||
'id': 'configure_connector',
|
||||
'label': 'Configure Home Assistant connector',
|
||||
'href': '/setup?step=connectors&connector=home_assistant',
|
||||
},
|
||||
],
|
||||
'lastCheckedAt': utc_now().isoformat(),
|
||||
}
|
||||
|
||||
config_payload, config_status, config_code = _get_json(url, token, '/api/config', verify_tls)
|
||||
states_payload, states_status, states_code = _get_json(url, token, '/api/states', verify_tls)
|
||||
api_status = config_status if config_status != 'ok' else states_status
|
||||
if api_status != 'ok':
|
||||
return {
|
||||
'status': api_status,
|
||||
'operational': False,
|
||||
'apiReachable': False,
|
||||
'homeAssistantUrl': url,
|
||||
'missing': [],
|
||||
'statusCode': config_code or states_code,
|
||||
'expectedScriptCount': len(expected_scripts),
|
||||
'detectedScriptCount': 0,
|
||||
'detectedScripts': [],
|
||||
'missingScripts': expected_scripts,
|
||||
'conversationAvailable': False,
|
||||
'geminiConversationAvailable': False,
|
||||
'llmExposureStatus': 'manual_verification_required',
|
||||
'steps': [
|
||||
_step('connector', 'Home Assistant connector', 'complete', 'URL and token are stored.'),
|
||||
_step('api', 'Home Assistant API', 'warning', f'Live API check returned {api_status}.'),
|
||||
],
|
||||
'nextActions': [
|
||||
{
|
||||
'id': 'test_connector',
|
||||
'label': 'Review Home Assistant connector',
|
||||
'href': '/setup?step=connectors&connector=home_assistant',
|
||||
},
|
||||
],
|
||||
'lastCheckedAt': utc_now().isoformat(),
|
||||
}
|
||||
|
||||
components = set()
|
||||
if isinstance(config_payload, dict) and isinstance(config_payload.get('components'), list):
|
||||
components = {str(item) for item in config_payload['components']}
|
||||
entity_ids = {
|
||||
str(item.get('entity_id'))
|
||||
for item in states_payload
|
||||
if isinstance(states_payload, list) and isinstance(item, dict) and item.get('entity_id')
|
||||
}
|
||||
detected_scripts = sorted(entity_id for entity_id in expected_scripts if entity_id in entity_ids)
|
||||
missing_scripts = sorted(set(expected_scripts) - set(detected_scripts))
|
||||
conversation_available = 'conversation' in components
|
||||
gemini_available = 'google_generative_ai_conversation' in components
|
||||
core_script_available = core_script in entity_ids
|
||||
operational = core_script_available and conversation_available and gemini_available
|
||||
coverage = int((len(detected_scripts) / len(expected_scripts)) * 100) if expected_scripts else 100
|
||||
|
||||
steps = [
|
||||
_step('connector', 'Home Assistant connector', 'complete', 'Stored configuration is available.'),
|
||||
_step('api', 'Home Assistant API', 'complete', 'Authenticated API requests succeeded.'),
|
||||
_step(
|
||||
'scripts',
|
||||
'AppOps scripts',
|
||||
'complete' if not missing_scripts else ('partial' if detected_scripts else 'warning'),
|
||||
f'{len(detected_scripts)} of {len(expected_scripts)} allowlisted scripts detected.',
|
||||
),
|
||||
_step(
|
||||
'conversation',
|
||||
'Home Assistant Conversation',
|
||||
'complete' if conversation_available else 'warning',
|
||||
'Conversation component is loaded.' if conversation_available else 'Conversation component was not reported by Home Assistant.',
|
||||
),
|
||||
_step(
|
||||
'gemini',
|
||||
'Google Gemini Conversation',
|
||||
'complete' if gemini_available else 'warning',
|
||||
'Google Gemini Conversation integration is loaded.' if gemini_available else 'Google Gemini Conversation integration was not reported by Home Assistant.',
|
||||
),
|
||||
_step(
|
||||
'exposure',
|
||||
'Scripts exposed to Assist',
|
||||
'manual',
|
||||
'Home Assistant does not expose reliable LLM script-exposure state through this API check; verify this in Assist settings.',
|
||||
),
|
||||
]
|
||||
next_actions = []
|
||||
if missing_scripts:
|
||||
next_actions.append({'id': 'install_scripts', 'label': 'Install or reload AppOps scripts', 'href': '/voice?tab=ha'})
|
||||
if not gemini_available:
|
||||
next_actions.append({'id': 'configure_gemini', 'label': 'Configure Gemini Conversation', 'href': f'{url}/config/integrations'})
|
||||
if not conversation_available:
|
||||
next_actions.append({'id': 'configure_assist', 'label': 'Configure an Assist pipeline', 'href': f'{url}/config/voice-assistants/assistants'})
|
||||
next_actions.append({'id': 'verify_exposure', 'label': 'Verify script exposure to Assist', 'href': f'{url}/config/voice-assistants/expose'})
|
||||
|
||||
return {
|
||||
'status': 'ready' if operational else 'partial',
|
||||
'operational': operational,
|
||||
'apiReachable': True,
|
||||
'homeAssistantUrl': url,
|
||||
'missing': [],
|
||||
'expectedScriptCount': len(expected_scripts),
|
||||
'detectedScriptCount': len(detected_scripts),
|
||||
'scriptCoveragePercent': coverage,
|
||||
'detectedScripts': detected_scripts,
|
||||
'missingScripts': missing_scripts,
|
||||
'coreScriptAvailable': core_script_available,
|
||||
'conversationAvailable': conversation_available,
|
||||
'geminiConversationAvailable': gemini_available,
|
||||
'llmExposureStatus': 'manual_verification_required',
|
||||
'steps': steps,
|
||||
'nextActions': next_actions,
|
||||
'lastCheckedAt': utc_now().isoformat(),
|
||||
}
|
||||
Reference in new issue
Block a user