Add entity CRUD events over WebSocket with auto-refresh

Broadcast entity_changed and device_health_changed events via the event
bus so the frontend can auto-refresh cards without polling. Adds
exponential backoff on WS reconnect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 11:09:09 +03:00
parent 1ce25caa35
commit 73562cd525
18 changed files with 169 additions and 10 deletions

View File

@@ -393,7 +393,7 @@ class AutomationEngine:
def _fire_event(self, automation_id: str, action: str) -> None:
try:
self._manager._fire_event({
self._manager.fire_event({
"type": "automation_state_changed",
"automation_id": automation_id,
"action": action,

View File

@@ -158,7 +158,7 @@ class ProcessorManager:
device_store=self._device_store,
color_strip_stream_manager=self._color_strip_stream_manager,
value_stream_manager=self._value_stream_manager,
fire_event=self._fire_event,
fire_event=self.fire_event,
get_device_info=self._get_device_info,
)
@@ -203,8 +203,8 @@ class ProcessorManager:
if queue in self._event_queues:
self._event_queues.remove(queue)
def _fire_event(self, event: dict) -> None:
"""Push event to all subscribers (non-blocking)."""
def fire_event(self, event: dict) -> None:
"""Push event to all subscribers (non-blocking). Public API for route handlers."""
for q in self._event_queues:
try:
q.put_nowait(event)
@@ -854,7 +854,7 @@ class ProcessorManager:
f"[AUTO-RESTART] Target {target_id} crashed {rs.attempts} times "
f"in {now - rs.first_crash_time:.0f}s — giving up"
)
self._fire_event({
self.fire_event({
"type": "state_change",
"target_id": target_id,
"processing": False,
@@ -872,7 +872,7 @@ class ProcessorManager:
f"{_RESTART_MAX_ATTEMPTS}), restarting in {backoff:.1f}s"
)
self._fire_event({
self.fire_event({
"type": "state_change",
"target_id": target_id,
"processing": False,
@@ -916,7 +916,7 @@ class ProcessorManager:
await self.start_processing(target_id)
except Exception as e:
logger.error(f"[AUTO-RESTART] Failed to restart {target_id}: {e}")
self._fire_event({
self.fire_event({
"type": "state_change",
"target_id": target_id,
"processing": False,
@@ -1050,11 +1050,21 @@ class ProcessorManager:
state = self._devices.get(device_id)
if not state:
return
prev_online = state.health.online
client = await self._get_http_client()
state.health = await check_device_health(
state.device_type, state.device_url, client, state.health,
)
# Fire event when online status changes
if state.health.online != prev_online:
self.fire_event({
"type": "device_health_changed",
"device_id": device_id,
"online": state.health.online,
"latency_ms": state.health.latency_ms,
})
# Auto-sync LED count
reported = state.health.device_led_count
if reported and reported != state.led_count and self._device_store: