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:
@@ -157,6 +157,23 @@ def get_sync_clock_manager() -> SyncClockManager:
|
||||
return _sync_clock_manager
|
||||
|
||||
|
||||
def fire_entity_event(entity_type: str, action: str, entity_id: str) -> None:
|
||||
"""Fire an entity_changed event via the ProcessorManager event bus.
|
||||
|
||||
Args:
|
||||
entity_type: e.g. "device", "output_target", "color_strip_source"
|
||||
action: "created", "updated", or "deleted"
|
||||
entity_id: The entity's unique ID
|
||||
"""
|
||||
if _processor_manager is not None:
|
||||
_processor_manager.fire_event({
|
||||
"type": "entity_changed",
|
||||
"entity_type": entity_type,
|
||||
"action": action,
|
||||
"id": entity_id,
|
||||
})
|
||||
|
||||
|
||||
def init_dependencies(
|
||||
device_store: DeviceStore,
|
||||
template_store: TemplateStore,
|
||||
|
||||
@@ -9,6 +9,7 @@ from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_audio_source_store,
|
||||
get_audio_template_store,
|
||||
get_color_strip_store,
|
||||
@@ -84,6 +85,7 @@ async def create_audio_source(
|
||||
audio_template_id=data.audio_template_id,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("audio_source", "created", source.id)
|
||||
return _to_response(source)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -123,6 +125,7 @@ async def update_audio_source(
|
||||
audio_template_id=data.audio_template_id,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("audio_source", "updated", source_id)
|
||||
return _to_response(source)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -146,6 +149,7 @@ async def delete_audio_source(
|
||||
)
|
||||
|
||||
store.delete_source(source_id)
|
||||
fire_entity_event("audio_source", "deleted", source_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import get_audio_template_store, get_audio_source_store, get_processor_manager
|
||||
from wled_controller.api.dependencies import fire_entity_event, get_audio_template_store, get_audio_source_store, get_processor_manager
|
||||
from wled_controller.api.schemas.audio_templates import (
|
||||
AudioEngineInfo,
|
||||
AudioEngineListResponse,
|
||||
@@ -66,6 +66,7 @@ async def create_audio_template(
|
||||
engine_config=data.engine_config, description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("audio_template", "created", template.id)
|
||||
return AudioTemplateResponse(
|
||||
id=template.id, name=template.name, engine_type=template.engine_type,
|
||||
engine_config=template.engine_config, tags=getattr(template, 'tags', []),
|
||||
@@ -112,6 +113,7 @@ async def update_audio_template(
|
||||
engine_type=data.engine_type, engine_config=data.engine_config,
|
||||
description=data.description, tags=data.tags,
|
||||
)
|
||||
fire_entity_event("audio_template", "updated", template_id)
|
||||
return AudioTemplateResponse(
|
||||
id=t.id, name=t.name, engine_type=t.engine_type,
|
||||
engine_config=t.engine_config, tags=getattr(t, 'tags', []),
|
||||
@@ -135,6 +137,7 @@ async def delete_audio_template(
|
||||
"""Delete an audio template."""
|
||||
try:
|
||||
store.delete_template(template_id, audio_source_store=audio_source_store)
|
||||
fire_entity_event("audio_template", "deleted", template_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
|
||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_automation_engine,
|
||||
get_automation_store,
|
||||
get_scene_preset_store,
|
||||
@@ -174,6 +175,7 @@ async def create_automation(
|
||||
if automation.enabled:
|
||||
await engine.trigger_evaluate()
|
||||
|
||||
fire_entity_event("automation", "created", automation.id)
|
||||
return _automation_to_response(automation, engine, request)
|
||||
|
||||
|
||||
@@ -273,6 +275,7 @@ async def update_automation(
|
||||
if automation.enabled:
|
||||
await engine.trigger_evaluate()
|
||||
|
||||
fire_entity_event("automation", "updated", automation_id)
|
||||
return _automation_to_response(automation, engine, request)
|
||||
|
||||
|
||||
@@ -296,6 +299,8 @@ async def delete_automation(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
fire_entity_event("automation", "deleted", automation_id)
|
||||
|
||||
|
||||
# ===== Enable/Disable =====
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from wled_controller.core.devices.led_client import (
|
||||
get_provider,
|
||||
)
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_device_store,
|
||||
get_output_target_store,
|
||||
get_processor_manager,
|
||||
@@ -146,6 +147,7 @@ async def create_device(
|
||||
zone_mode=device.zone_mode,
|
||||
)
|
||||
|
||||
fire_entity_event("device", "created", device.id)
|
||||
return _device_to_response(device)
|
||||
|
||||
except HTTPException:
|
||||
@@ -332,6 +334,7 @@ async def update_device(
|
||||
if update_data.zone_mode is not None:
|
||||
ds.zone_mode = update_data.zone_mode
|
||||
|
||||
fire_entity_event("device", "updated", device_id)
|
||||
return _device_to_response(device)
|
||||
|
||||
except ValueError as e:
|
||||
@@ -369,6 +372,7 @@ async def delete_device(
|
||||
# Delete from storage
|
||||
store.delete_device(device_id)
|
||||
|
||||
fire_entity_event("device", "deleted", device_id)
|
||||
logger.info(f"Deleted device {device_id}")
|
||||
|
||||
except HTTPException:
|
||||
|
||||
@@ -12,6 +12,7 @@ from PIL import Image
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_color_strip_store,
|
||||
get_device_store,
|
||||
get_pattern_template_store,
|
||||
@@ -181,6 +182,7 @@ async def create_target(
|
||||
except ValueError as e:
|
||||
logger.warning(f"Could not register target {target.id} in processor manager: {e}")
|
||||
|
||||
fire_entity_event("output_target", "created", target.id)
|
||||
return _target_to_response(target)
|
||||
|
||||
except HTTPException:
|
||||
@@ -319,6 +321,7 @@ async def update_target(
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
fire_entity_event("output_target", "updated", target_id)
|
||||
return _target_to_response(target)
|
||||
|
||||
except HTTPException:
|
||||
@@ -354,6 +357,7 @@ async def delete_target(
|
||||
# Delete from store
|
||||
target_store.delete_target(target_id)
|
||||
|
||||
fire_entity_event("output_target", "deleted", target_id)
|
||||
logger.info(f"Deleted target {target_id}")
|
||||
|
||||
except ValueError as e:
|
||||
|
||||
@@ -4,6 +4,7 @@ from fastapi import APIRouter, HTTPException, Depends
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_pattern_template_store,
|
||||
get_output_target_store,
|
||||
)
|
||||
@@ -73,6 +74,7 @@ async def create_pattern_template(
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("pattern_template", "created", template.id)
|
||||
return _pat_template_to_response(template)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -117,6 +119,7 @@ async def update_pattern_template(
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("pattern_template", "updated", template_id)
|
||||
return _pat_template_to_response(template)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -143,6 +146,7 @@ async def delete_pattern_template(
|
||||
"Please reassign those targets before deleting.",
|
||||
)
|
||||
store.delete_template(template_id)
|
||||
fire_entity_event("pattern_template", "deleted", template_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
|
||||
@@ -12,6 +12,7 @@ from fastapi.responses import Response
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_picture_source_store,
|
||||
get_output_target_store,
|
||||
get_pp_template_store,
|
||||
@@ -199,6 +200,7 @@ async def create_picture_source(
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("picture_source", "created", stream.id)
|
||||
return _stream_to_response(stream)
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -244,6 +246,7 @@ async def update_picture_source(
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("picture_source", "updated", stream_id)
|
||||
return _stream_to_response(stream)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -271,6 +274,7 @@ async def delete_picture_source(
|
||||
"Please reassign those targets before deleting.",
|
||||
)
|
||||
store.delete_stream(stream_id)
|
||||
fire_entity_event("picture_source", "deleted", stream_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
|
||||
@@ -11,6 +11,7 @@ from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSock
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_picture_source_store,
|
||||
get_pp_template_store,
|
||||
get_template_store,
|
||||
@@ -84,6 +85,7 @@ async def create_pp_template(
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("pp_template", "created", template.id)
|
||||
return _pp_template_to_response(template)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -123,6 +125,7 @@ async def update_pp_template(
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("pp_template", "updated", template_id)
|
||||
return _pp_template_to_response(template)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -150,6 +153,7 @@ async def delete_pp_template(
|
||||
"Please reassign those streams before deleting.",
|
||||
)
|
||||
store.delete_template(template_id)
|
||||
fire_entity_event("pp_template", "deleted", template_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
|
||||
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_output_target_store,
|
||||
get_processor_manager,
|
||||
get_scene_preset_store,
|
||||
@@ -87,6 +88,7 @@ async def create_scene_preset(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
fire_entity_event("scene_preset", "created", preset.id)
|
||||
return _preset_to_response(preset)
|
||||
|
||||
|
||||
@@ -175,6 +177,7 @@ async def update_scene_preset(
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404 if "not found" in str(e).lower() else 400, detail=str(e))
|
||||
fire_entity_event("scene_preset", "updated", preset_id)
|
||||
return _preset_to_response(preset)
|
||||
|
||||
|
||||
@@ -194,6 +197,7 @@ async def delete_scene_preset(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
fire_entity_event("scene_preset", "deleted", preset_id)
|
||||
|
||||
|
||||
# ===== Recapture =====
|
||||
@@ -259,4 +263,5 @@ async def activate_scene_preset(
|
||||
if not errors:
|
||||
logger.info(f"Scene preset '{preset.name}' activated successfully")
|
||||
|
||||
fire_entity_event("scene_preset", "updated", preset_id)
|
||||
return ActivateResponse(status=status, errors=errors)
|
||||
|
||||
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_color_strip_store,
|
||||
get_sync_clock_manager,
|
||||
get_sync_clock_store,
|
||||
@@ -70,6 +71,7 @@ async def create_sync_clock(
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("sync_clock", "created", clock.id)
|
||||
return _to_response(clock, manager)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -110,6 +112,7 @@ async def update_sync_clock(
|
||||
# Hot-update runtime speed
|
||||
if data.speed is not None:
|
||||
manager.update_speed(clock_id, clock.speed)
|
||||
fire_entity_event("sync_clock", "updated", clock_id)
|
||||
return _to_response(clock, manager)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -133,6 +136,7 @@ async def delete_sync_clock(
|
||||
)
|
||||
manager.release_all_for(clock_id)
|
||||
store.delete_clock(clock_id)
|
||||
fire_entity_event("sync_clock", "deleted", clock_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@@ -152,6 +156,7 @@ async def pause_sync_clock(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
manager.pause(clock_id)
|
||||
fire_entity_event("sync_clock", "updated", clock_id)
|
||||
return _to_response(clock, manager)
|
||||
|
||||
|
||||
@@ -168,6 +173,7 @@ async def resume_sync_clock(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
manager.resume(clock_id)
|
||||
fire_entity_event("sync_clock", "updated", clock_id)
|
||||
return _to_response(clock, manager)
|
||||
|
||||
|
||||
@@ -184,4 +190,5 @@ async def reset_sync_clock(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
manager.reset(clock_id)
|
||||
fire_entity_event("sync_clock", "updated", clock_id)
|
||||
return _to_response(clock, manager)
|
||||
|
||||
@@ -10,6 +10,7 @@ from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSock
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_picture_source_store,
|
||||
get_pp_template_store,
|
||||
get_template_store,
|
||||
@@ -96,6 +97,7 @@ async def create_template(
|
||||
tags=template_data.tags,
|
||||
)
|
||||
|
||||
fire_entity_event("capture_template", "created", template.id)
|
||||
return TemplateResponse(
|
||||
id=template.id,
|
||||
name=template.name,
|
||||
@@ -156,6 +158,7 @@ async def update_template(
|
||||
tags=update_data.tags,
|
||||
)
|
||||
|
||||
fire_entity_event("capture_template", "updated", template_id)
|
||||
return TemplateResponse(
|
||||
id=template.id,
|
||||
name=template.name,
|
||||
@@ -202,6 +205,7 @@ async def delete_template(
|
||||
|
||||
# Proceed with deletion
|
||||
template_store.delete_template(template_id)
|
||||
fire_entity_event("capture_template", "deleted", template_id)
|
||||
|
||||
except HTTPException:
|
||||
raise # Re-raise HTTP exceptions as-is
|
||||
|
||||
@@ -8,6 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSock
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_output_target_store,
|
||||
get_processor_manager,
|
||||
get_value_source_store,
|
||||
@@ -100,6 +101,7 @@ async def create_value_source(
|
||||
auto_gain=data.auto_gain,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("value_source", "created", source.id)
|
||||
return _to_response(source)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -150,6 +152,7 @@ async def update_value_source(
|
||||
)
|
||||
# Hot-reload running value streams
|
||||
pm.update_value_source(source_id)
|
||||
fire_entity_event("value_source", "updated", source_id)
|
||||
return _to_response(source)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -174,6 +177,7 @@ async def delete_value_source(
|
||||
)
|
||||
|
||||
store.delete_source(source_id)
|
||||
fire_entity_event("value_source", "deleted", source_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user