feat: Home Assistant provider — WebSocket subscription + bot commands
Adds Home Assistant as a service provider with two coordinated surfaces: Notifications (subscription): - Long-lived WebSocket client (aiohttp ws_connect) with auth handshake, exponential-backoff reconnect, bounded event queue, and area-registry enrichment cached per (re)connect - ServiceProvider ABC gains an optional `subscribe()` method for push-style providers; HomeAssistantServiceProvider uses it via a per-provider supervisor task started in the FastAPI lifespan - 4 event types (state_changed, automation_triggered, call_service, event_fired), 4 default Jinja templates (en + ru), HA-specific tracker filters (entity_glob, domain_allowlist, exact entity ids) - Extracted shared dispatch pipeline (api/webhooks.py → services/ event_dispatch.py) so subscription and webhook ingest share the same event_log + deferred-dispatch + quiet-hours code path Bot commands: - /status, /entities [glob], /state <entity_id>, /areas - Multi-command WS session so /status and /areas cost one handshake - Sensitive-attribute blocklist (camera access_token, entity_picture, etc.) and 30-attribute cap to keep /state output safe and within Telegram's message size - Error-message redaction strips URL userinfo before surfacing to chat Frontend: - HA descriptor with toggle ConfigField type (new) and tag-input filter mode for free-text glob/domain lists (new TagInput component) - 15 command slots + 4 notification slots wired into the existing template-config UI
This commit is contained in:
@@ -139,12 +139,20 @@ async def lifespan(app: FastAPI):
|
||||
set_webhook_secret(_secret or None)
|
||||
from .services.scheduler import start_scheduler, get_scheduler
|
||||
await start_scheduler()
|
||||
# Phase 1 of the Home Assistant provider: subscription-based ingest runs
|
||||
# outside the polling scheduler. ``start_all`` spawns one supervisor task
|
||||
# per enabled HA provider row. No-op when no HA providers are configured.
|
||||
from .services.ha_subscription import start_all as start_ha_subscriptions
|
||||
await start_ha_subscriptions()
|
||||
_READY = True
|
||||
yield
|
||||
# Graceful shutdown — stop the scheduler FIRST so in-flight jobs finish
|
||||
# before we close their HTTP session. Then close the shared session and
|
||||
# dispose the DB engine.
|
||||
# Graceful shutdown — cancel HA supervisors FIRST so they release their
|
||||
# WS connections before the shared HTTP session is closed. Then stop the
|
||||
# polling scheduler. Order matters: scheduler.shutdown(wait=True) drains
|
||||
# in-flight jobs that may also use the shared session.
|
||||
_READY = False
|
||||
from .services.ha_subscription import stop_all as stop_ha_subscriptions
|
||||
await stop_ha_subscriptions()
|
||||
scheduler = get_scheduler()
|
||||
if scheduler.running:
|
||||
scheduler.shutdown(wait=True)
|
||||
|
||||
Reference in New Issue
Block a user