feat: HA light output targets — cast LED colors to Home Assistant lights
Some checks failed
Lint & Test / test (push) Has been cancelled
Some checks failed
Lint & Test / test (push) Has been cancelled
New output target type `ha_light` that sends averaged LED colors to HA light entities via WebSocket service calls (light.turn_on/turn_off): Backend: - HARuntime.call_service(): fire-and-forget WS service calls - HALightOutputTarget: data model with light mappings, update rate, transition - HALightTargetProcessor: processing loop with delta detection, rate limiting - ProcessorManager.add_ha_light_target(): registration - API schemas/routes updated for ha_light target type Frontend: - HA Light Targets section in Targets tab tree nav - Modal editor: HA source picker, CSS source picker, light entity mappings - Target cards with start/stop/clone/edit actions - i18n keys for all new UI strings
This commit is contained in:
@@ -25,6 +25,11 @@ from wled_controller.storage.key_colors_output_target import (
|
||||
KeyColorsSettings,
|
||||
KeyColorsOutputTarget,
|
||||
)
|
||||
from wled_controller.storage.ha_light_output_target import (
|
||||
HALightMapping,
|
||||
HALightOutputTarget,
|
||||
)
|
||||
from wled_controller.api.schemas.output_targets import HALightMappingSchema
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
@@ -76,7 +81,6 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
protocol=target.protocol,
|
||||
description=target.description,
|
||||
tags=target.tags,
|
||||
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
@@ -89,7 +93,31 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
key_colors_settings=_kc_settings_to_schema(target.settings),
|
||||
description=target.description,
|
||||
tags=target.tags,
|
||||
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
elif isinstance(target, HALightOutputTarget):
|
||||
return OutputTargetResponse(
|
||||
id=target.id,
|
||||
name=target.name,
|
||||
target_type=target.target_type,
|
||||
ha_source_id=target.ha_source_id,
|
||||
color_strip_source_id=target.color_strip_source_id,
|
||||
ha_light_mappings=[
|
||||
HALightMappingSchema(
|
||||
entity_id=m.entity_id,
|
||||
led_start=m.led_start,
|
||||
led_end=m.led_end,
|
||||
brightness_scale=m.brightness_scale,
|
||||
)
|
||||
for m in target.light_mappings
|
||||
],
|
||||
update_rate=target.update_rate,
|
||||
ha_transition=target.transition,
|
||||
color_tolerance=target.color_tolerance,
|
||||
min_brightness_threshold=target.min_brightness_threshold,
|
||||
description=target.description,
|
||||
tags=target.tags,
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
@@ -100,7 +128,6 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
target_type=target.target_type,
|
||||
description=target.description,
|
||||
tags=target.tags,
|
||||
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
@@ -108,7 +135,10 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
|
||||
# ===== CRUD ENDPOINTS =====
|
||||
|
||||
@router.post("/api/v1/output-targets", response_model=OutputTargetResponse, tags=["Targets"], status_code=201)
|
||||
|
||||
@router.post(
|
||||
"/api/v1/output-targets", response_model=OutputTargetResponse, tags=["Targets"], status_code=201
|
||||
)
|
||||
async def create_target(
|
||||
data: OutputTargetCreate,
|
||||
_auth: AuthRequired,
|
||||
@@ -125,7 +155,22 @@ async def create_target(
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=422, detail=f"Device {data.device_id} not found")
|
||||
|
||||
kc_settings = _kc_schema_to_settings(data.key_colors_settings) if data.key_colors_settings else None
|
||||
kc_settings = (
|
||||
_kc_schema_to_settings(data.key_colors_settings) if data.key_colors_settings else None
|
||||
)
|
||||
ha_mappings = (
|
||||
[
|
||||
HALightMapping(
|
||||
entity_id=m.entity_id,
|
||||
led_start=m.led_start,
|
||||
led_end=m.led_end,
|
||||
brightness_scale=m.brightness_scale,
|
||||
)
|
||||
for m in data.ha_light_mappings
|
||||
]
|
||||
if data.ha_light_mappings
|
||||
else None
|
||||
)
|
||||
|
||||
# Create in store
|
||||
target = target_store.create_target(
|
||||
@@ -144,6 +189,11 @@ async def create_target(
|
||||
key_colors_settings=kc_settings,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
ha_source_id=data.ha_source_id,
|
||||
ha_light_mappings=ha_mappings,
|
||||
update_rate=data.update_rate,
|
||||
transition=data.transition,
|
||||
color_tolerance=data.color_tolerance,
|
||||
)
|
||||
|
||||
# Register in processor manager
|
||||
@@ -196,7 +246,9 @@ async def batch_target_metrics(
|
||||
return {"metrics": manager.get_all_target_metrics()}
|
||||
|
||||
|
||||
@router.get("/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"])
|
||||
@router.get(
|
||||
"/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"]
|
||||
)
|
||||
async def get_target(
|
||||
target_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -210,7 +262,9 @@ async def get_target(
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"])
|
||||
@router.put(
|
||||
"/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"]
|
||||
)
|
||||
async def update_target(
|
||||
target_id: str,
|
||||
data: OutputTargetUpdate,
|
||||
@@ -246,7 +300,9 @@ async def update_target(
|
||||
smoothing=incoming.get("smoothing", ex.smoothing),
|
||||
pattern_template_id=incoming.get("pattern_template_id", ex.pattern_template_id),
|
||||
brightness=incoming.get("brightness", ex.brightness),
|
||||
brightness_value_source_id=incoming.get("brightness_value_source_id", ex.brightness_value_source_id),
|
||||
brightness_value_source_id=incoming.get(
|
||||
"brightness_value_source_id", ex.brightness_value_source_id
|
||||
),
|
||||
)
|
||||
kc_settings = _kc_schema_to_settings(merged)
|
||||
else:
|
||||
@@ -282,14 +338,18 @@ async def update_target(
|
||||
await asyncio.to_thread(
|
||||
target.sync_with_manager,
|
||||
manager,
|
||||
settings_changed=(data.fps is not None or
|
||||
data.keepalive_interval is not None or
|
||||
data.state_check_interval is not None or
|
||||
data.min_brightness_threshold is not None or
|
||||
data.adaptive_fps is not None or
|
||||
data.key_colors_settings is not None),
|
||||
settings_changed=(
|
||||
data.fps is not None
|
||||
or data.keepalive_interval is not None
|
||||
or data.state_check_interval is not None
|
||||
or data.min_brightness_threshold is not None
|
||||
or data.adaptive_fps is not None
|
||||
or data.key_colors_settings is not None
|
||||
),
|
||||
css_changed=data.color_strip_source_id is not None,
|
||||
brightness_vs_changed=(data.brightness_value_source_id is not None or kc_brightness_vs_changed),
|
||||
brightness_vs_changed=(
|
||||
data.brightness_value_source_id is not None or kc_brightness_vs_changed
|
||||
),
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.debug("Processor config update skipped for target %s: %s", target_id, e)
|
||||
|
||||
Reference in New Issue
Block a user