feat(ui): customisable card icon for all entity types
Extends the icon-plate work from devices and output targets to every
remaining card type — 18 new entities, 20 in total. Users can now pick
a curated icon (with optional colour override) for any card on any tab,
and the picker reuses the same modal, recent-strip, search, and
category tabs introduced for the device picker.
Foundation:
- icon-picker.ts — replace the hardcoded 2-entry adapter record with a
Map<EntityType, EntityTypeAdapter> and expose
registerIconEntityType() + makeSimpleIconAdapter() so each feature
module owns its own adapter (~6 lines per type).
- bodyExtras hook on adapters, keyed off id, lets discriminated routes
(output-targets target_type, picture-sources stream_type, audio /
value / color-strip-sources source_type) accept icon-only PUTs.
- core/card-icon.ts — new makeCardIconFields(type, id, entity) helper
spreads iconHtml / iconColor / iconAttrs into a mod-card head in one
line.
- _onDocumentClick now accepts any registered type instead of a
hardcoded device/target check.
Backend (purely additive — no migrations needed thanks to JSON-blob
storage):
- 18 dataclasses gained icon: str = "" + icon_color: str = "" with
emit-when-truthy serialisation and "" defaults on load.
- All matching Create / Update / Response Pydantic schemas gained the
fields with the standard Optional[str] + max_length=64/32 +
description set.
- All routes' response builders use
getattr(entity, "icon", "") or "" so existing rows render unchanged.
- ValueSource and CSS handle icon/icon_color on the base class so all
source-type subclasses inherit them automatically.
Frontend wiring (12 modules):
- streams.ts — picture sources, capture templates, PP templates,
CSPT, audio sources, audio templates, gradients (built-in
gradients keep no plate).
- automations, scene-presets, sync-clocks, weather-sources,
value-sources, mqtt-sources, home-assistant-sources,
game-integration, audio-processing-templates, assets,
color-strips/cards.
- pattern-templates skipped — uses the legacy wrapCard({content,
actions}) string API, separate migration.
Dashboard cards now also display the chosen icon:
- Targets already had it (with device inheritance for LED targets).
- Sync clocks, automations, and scene presets gained the same plate
via a shared _dashboardIconPlate helper that mirrors the mod-card
layout (mod-head--with-icon class flips on when present).
i18n: 20 new device.icon.entity.<type> labels in en/ru/zh.
Verification:
- ruff check src/ tests/ — clean.
- npx tsc --noEmit — clean.
- npm run build — 2.6 MB bundle.
- pytest tests/ --no-cov — 949 passed (no regressions).
Pending: manual smoke test on each card type — open picker, save, and
confirm the channel-color preview matches the live card.
This commit is contained in:
@@ -1,5 +1,95 @@
|
||||
# LedGrab TODO
|
||||
|
||||
## Custom card icons — extend to all card types
|
||||
|
||||
Migrate the existing icon-plate work (devices, LED targets, HA-light targets)
|
||||
to all remaining card types. ~17 entity types. Branch: `feat/icons-everywhere`.
|
||||
|
||||
### Foundation
|
||||
|
||||
- [x] Refactor `icon-picker.ts` — replace hardcoded 2-entry `_adapters`
|
||||
record with a `Map<EntityType, EntityTypeAdapter>` and expose
|
||||
`registerIconEntityType()` for feature modules to register their
|
||||
own. Added `makeSimpleIconAdapter()` helper that reduces a
|
||||
registration to ~6 lines.
|
||||
- [x] Generalised `bodyExtras` for discriminated routes (output-targets
|
||||
`target_type` etc.) — now keyed off id, adapter does its own
|
||||
lookup.
|
||||
- [x] `_onDocumentClick` accepts any registered type instead of
|
||||
hardcoded device/target check.
|
||||
- [x] Locale entity-type labels added to en/ru/zh for 18 new types
|
||||
(picture_source, audio_source, weather_source, value_source,
|
||||
mqtt_source, ha_source, automation, scene_preset, sync_clock,
|
||||
game_integration, audio_processing_template, pattern_template,
|
||||
capture_template, pp_template, cspt, audio_template, gradient,
|
||||
color_strip_source, asset).
|
||||
|
||||
### Backend (storage + schemas + routes per entity)
|
||||
|
||||
Recipe: add `icon: str = ""` + `icon_color: str = ""` to dataclass,
|
||||
emit-when-truthy in `to_dict`, default `""` in `from_dict`; add 3
|
||||
`Optional[str]` Field defs to Create/Response/Update schemas; thread
|
||||
`getattr(entity, "icon", "") or ""` into the response builder.
|
||||
SQLite JSON-blob storage means **no migration required**.
|
||||
|
||||
- [x] Integrations (6): weather_sources, value_sources, mqtt_source,
|
||||
home_assistant_source, sync_clocks, game_integration
|
||||
- [x] Streams (10): picture_source, audio_source, audio_template,
|
||||
audio_processing_template, pattern_template, postprocessing_template,
|
||||
color_strip_processing_template, color_strip_source, gradient,
|
||||
capture_template (`storage/template.py` — was missed by initial pass)
|
||||
- [x] Other (3): automation, scene_preset, asset
|
||||
|
||||
### Frontend (per feature module)
|
||||
|
||||
For each card render call:
|
||||
|
||||
- Use the new `core/card-icon.ts` helper:
|
||||
`...makeCardIconFields('<type>', entity.id, entity)` spread into the
|
||||
mod-card head — computes `iconHtml`/`iconColor`/`iconAttrs` in one go.
|
||||
- Register the entity type in the feature module via
|
||||
`registerIconEntityType('<type>', makeSimpleIconAdapter({ … }))`.
|
||||
|
||||
Modules wired:
|
||||
|
||||
- [x] streams.ts (7 cards: picture, capture, pp, cspt, audio source,
|
||||
audio template, gradient — built-in gradients skip the plate)
|
||||
- [x] automations.ts
|
||||
- [x] scene-presets.ts
|
||||
- [x] sync-clocks.ts
|
||||
- [x] weather-sources.ts
|
||||
- [x] value-sources.ts (bodyExtras propagates `source_type`)
|
||||
- [x] mqtt-sources.ts
|
||||
- [x] home-assistant-sources.ts
|
||||
- [x] game-integration.ts
|
||||
- [x] audio-processing-templates.ts
|
||||
- [x] assets.ts
|
||||
- [x] color-strips/cards.ts (bodyExtras propagates `source_type`)
|
||||
- [WONTDO] pattern-templates.ts — uses legacy `wrapCard({content, actions})`
|
||||
string API, not the mod-card system. Migration would be a separate
|
||||
effort and the cards are tiny (name + rect count) so the value is low.
|
||||
|
||||
### Discriminated routes
|
||||
|
||||
Adapters provide `bodyExtras` to inject the discriminator field on PUT
|
||||
so the Pydantic discriminated-union route validators don't reject the
|
||||
icon-only update:
|
||||
|
||||
- output-targets → `target_type` (already wired before)
|
||||
- color-strip-sources → `source_type`
|
||||
- audio-sources → `source_type`
|
||||
- value-sources → `source_type`
|
||||
- picture-sources → `stream_type`
|
||||
|
||||
### Verification
|
||||
|
||||
- [x] `cd server && ruff check src/ tests/` clean
|
||||
- [x] `cd server && npx tsc --noEmit` clean
|
||||
- [x] `cd server && npm run build` produces 2.6 MB bundle
|
||||
- [x] `cd server && py -3.13 -m pytest tests/ --no-cov -q` — 949 passed
|
||||
- [ ] Manual: open picker on each card type, confirm save persists,
|
||||
confirm channel-color preview matches the live card
|
||||
|
||||
## Device Event Notifications
|
||||
|
||||
Notify the user when LED devices come online/go offline (configured targets), and when new
|
||||
|
||||
@@ -142,6 +142,8 @@ async def update_asset(
|
||||
name=body.name,
|
||||
description=body.description,
|
||||
tags=body.tags,
|
||||
icon=body.icon,
|
||||
icon_color=body.icon_color,
|
||||
)
|
||||
except EntityNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Asset not found: {asset_id}")
|
||||
|
||||
@@ -36,6 +36,8 @@ def _apt_to_response(t) -> AudioProcessingTemplateResponse:
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
tags=t.tags,
|
||||
icon=getattr(t, "icon", "") or "",
|
||||
icon_color=getattr(t, "icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -73,6 +75,8 @@ async def create_audio_processing_template(
|
||||
filters=filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("audio_processing_template", "created", template.id)
|
||||
return _apt_to_response(template)
|
||||
@@ -129,6 +133,8 @@ async def update_audio_processing_template(
|
||||
filters=filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("audio_processing_template", "updated", template_id)
|
||||
# Hot-update: rebuild filter pipelines for running streams using this template
|
||||
|
||||
@@ -46,6 +46,8 @@ _RESPONSE_MAP = {
|
||||
tags=s.tags,
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
device_index=s.device_index,
|
||||
is_loopback=s.is_loopback,
|
||||
audio_template_id=s.audio_template_id,
|
||||
@@ -57,6 +59,8 @@ _RESPONSE_MAP = {
|
||||
tags=s.tags,
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
audio_source_id=s.audio_source_id,
|
||||
audio_processing_template_id=s.audio_processing_template_id,
|
||||
),
|
||||
@@ -75,6 +79,8 @@ def _to_response(source: AudioSource) -> AudioSourceResponse:
|
||||
tags=source.tags,
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
icon=getattr(source, "icon", "") or "",
|
||||
icon_color=getattr(source, "icon_color", "") or "",
|
||||
device_index=getattr(source, "device_index", -1),
|
||||
is_loopback=getattr(source, "is_loopback", True),
|
||||
audio_template_id=getattr(source, "audio_template_id", None),
|
||||
|
||||
@@ -53,6 +53,8 @@ async def list_audio_templates(
|
||||
created_at=t.created_at,
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
icon=getattr(t, "icon", "") or "",
|
||||
icon_color=getattr(t, "icon_color", "") or "",
|
||||
)
|
||||
for t in templates
|
||||
]
|
||||
@@ -81,6 +83,8 @@ async def create_audio_template(
|
||||
engine_config=data.engine_config,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("audio_template", "created", template.id)
|
||||
return AudioTemplateResponse(
|
||||
@@ -92,6 +96,8 @@ async def create_audio_template(
|
||||
created_at=template.created_at,
|
||||
updated_at=template.updated_at,
|
||||
description=template.description,
|
||||
icon=getattr(template, "icon", "") or "",
|
||||
icon_color=getattr(template, "icon_color", "") or "",
|
||||
)
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
@@ -127,6 +133,8 @@ async def get_audio_template(
|
||||
created_at=t.created_at,
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
icon=getattr(t, "icon", "") or "",
|
||||
icon_color=getattr(t, "icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -150,6 +158,8 @@ async def update_audio_template(
|
||||
engine_config=data.engine_config,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("audio_template", "updated", template_id)
|
||||
return AudioTemplateResponse(
|
||||
@@ -161,6 +171,8 @@ async def update_audio_template(
|
||||
created_at=t.created_at,
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
icon=getattr(t, "icon", "") or "",
|
||||
icon_color=getattr(t, "icon_color", "") or "",
|
||||
)
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
@@ -122,6 +122,8 @@ def _automation_to_response(
|
||||
last_activated_at=state.get("last_activated_at"),
|
||||
last_deactivated_at=state.get("last_deactivated_at"),
|
||||
tags=automation.tags,
|
||||
icon=getattr(automation, "icon", "") or "",
|
||||
icon_color=getattr(automation, "icon_color", "") or "",
|
||||
created_at=automation.created_at,
|
||||
updated_at=automation.updated_at,
|
||||
)
|
||||
@@ -191,6 +193,8 @@ async def create_automation(
|
||||
deactivation_mode=data.deactivation_mode,
|
||||
deactivation_scene_preset_id=data.deactivation_scene_preset_id,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
|
||||
if automation.enabled:
|
||||
@@ -285,6 +289,8 @@ async def update_automation(
|
||||
rules=rules,
|
||||
deactivation_mode=data.deactivation_mode,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
if data.scene_preset_id is not None:
|
||||
update_kwargs["scene_preset_id"] = data.scene_preset_id
|
||||
|
||||
@@ -43,6 +43,8 @@ def _cspt_to_response(t) -> ColorStripProcessingTemplateResponse:
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
tags=t.tags,
|
||||
icon=getattr(t, "icon", "") or "",
|
||||
icon_color=getattr(t, "icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -84,6 +86,8 @@ async def create_cspt(
|
||||
filters=filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("cspt", "created", template.id)
|
||||
return _cspt_to_response(template)
|
||||
@@ -141,6 +145,8 @@ async def update_cspt(
|
||||
filters=filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("cspt", "updated", template_id)
|
||||
return _cspt_to_response(template)
|
||||
|
||||
@@ -65,6 +65,8 @@ def _common_response_kwargs(source, overlay_active: bool = False) -> dict:
|
||||
tags=source.tags,
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
icon=getattr(source, "icon", "") or "",
|
||||
icon_color=getattr(source, "icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -158,6 +158,8 @@ def _config_to_response(config: Any) -> GameIntegrationResponse:
|
||||
updated_at=config.updated_at,
|
||||
description=config.description,
|
||||
tags=config.tags,
|
||||
icon=getattr(config, "icon", "") or "",
|
||||
icon_color=getattr(config, "icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -255,6 +257,8 @@ async def create_integration(
|
||||
event_mappings=mappings,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
|
||||
fire_entity_event("game_integration", "created", config.id)
|
||||
@@ -323,6 +327,8 @@ async def update_integration(
|
||||
event_mappings=mappings,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
|
||||
fire_entity_event("game_integration", "updated", integration_id)
|
||||
|
||||
@@ -35,6 +35,8 @@ def _to_response(gradient: Gradient) -> GradientResponse:
|
||||
tags=gradient.tags,
|
||||
created_at=gradient.created_at,
|
||||
updated_at=gradient.updated_at,
|
||||
icon=getattr(gradient, "icon", "") or "",
|
||||
icon_color=getattr(gradient, "icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -66,6 +68,8 @@ async def create_gradient(
|
||||
stops=[s.model_dump() for s in data.stops],
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("gradient", "created", gradient.id)
|
||||
return _to_response(gradient)
|
||||
@@ -103,6 +107,8 @@ async def update_gradient(
|
||||
stops=stops,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("gradient", "updated", gradient_id)
|
||||
return _to_response(gradient)
|
||||
|
||||
@@ -55,6 +55,8 @@ def _to_response(
|
||||
entity_count=len(runtime.get_all_states()) if runtime else 0,
|
||||
description=source.description,
|
||||
tags=source.tags,
|
||||
icon=getattr(source, "icon", "") or "",
|
||||
icon_color=getattr(source, "icon_color", "") or "",
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
token=token_field,
|
||||
@@ -105,6 +107,8 @@ async def create_ha_source(
|
||||
entity_filters=data.entity_filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -158,6 +162,8 @@ async def update_ha_source(
|
||||
entity_filters=data.entity_filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
except EntityNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Home Assistant source {source_id} not found")
|
||||
|
||||
@@ -45,6 +45,8 @@ def _to_response(source: MQTTSource, manager: MQTTManager) -> MQTTSourceResponse
|
||||
connected=runtime.is_connected if runtime else False,
|
||||
description=source.description,
|
||||
tags=source.tags,
|
||||
icon=getattr(source, "icon", "") or "",
|
||||
icon_color=getattr(source, "icon_color", "") or "",
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
)
|
||||
@@ -90,6 +92,8 @@ async def create_mqtt_source(
|
||||
base_topic=data.base_topic,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -139,6 +143,8 @@ async def update_mqtt_source(
|
||||
base_topic=data.base_topic,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
except EntityNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"MQTT source {source_id} not found")
|
||||
|
||||
@@ -39,6 +39,8 @@ def _pat_template_to_response(t) -> PatternTemplateResponse:
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
tags=t.tags,
|
||||
icon=getattr(t, "icon", "") or "",
|
||||
icon_color=getattr(t, "icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -83,6 +85,8 @@ async def create_pattern_template(
|
||||
rectangles=rectangles,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("pattern_template", "created", template.id)
|
||||
return _pat_template_to_response(template)
|
||||
@@ -139,6 +143,8 @@ async def update_pattern_template(
|
||||
rectangles=rectangles,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("pattern_template", "updated", template_id)
|
||||
return _pat_template_to_response(template)
|
||||
|
||||
@@ -65,6 +65,8 @@ _RESPONSE_MAP = {
|
||||
tags=s.tags,
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
display_index=s.display_index,
|
||||
capture_template_id=s.capture_template_id,
|
||||
target_fps=s.target_fps,
|
||||
@@ -76,6 +78,8 @@ _RESPONSE_MAP = {
|
||||
tags=s.tags,
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
source_stream_id=s.source_stream_id,
|
||||
postprocessing_template_id=s.postprocessing_template_id,
|
||||
),
|
||||
@@ -86,6 +90,8 @@ _RESPONSE_MAP = {
|
||||
tags=s.tags,
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
image_asset_id=s.image_asset_id,
|
||||
),
|
||||
VideoCaptureSource: lambda s: VideoPictureSourceResponse(
|
||||
@@ -95,6 +101,8 @@ _RESPONSE_MAP = {
|
||||
tags=s.tags,
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
video_asset_id=s.video_asset_id,
|
||||
loop=s.loop,
|
||||
playback_speed=s.playback_speed,
|
||||
|
||||
@@ -49,6 +49,8 @@ def _pp_template_to_response(t) -> PostprocessingTemplateResponse:
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
tags=t.tags,
|
||||
icon=getattr(t, "icon", "") or "",
|
||||
icon_color=getattr(t, "icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
@@ -86,6 +88,8 @@ async def create_pp_template(
|
||||
filters=filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("pp_template", "created", template.id)
|
||||
return _pp_template_to_response(template)
|
||||
@@ -143,6 +147,8 @@ async def update_pp_template(
|
||||
filters=filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("pp_template", "updated", template_id)
|
||||
return _pp_template_to_response(template)
|
||||
|
||||
@@ -51,6 +51,8 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
|
||||
],
|
||||
order=preset.order,
|
||||
tags=preset.tags,
|
||||
icon=getattr(preset, "icon", "") or "",
|
||||
icon_color=getattr(preset, "icon_color", "") or "",
|
||||
created_at=preset.created_at,
|
||||
updated_at=preset.updated_at,
|
||||
)
|
||||
@@ -84,6 +86,8 @@ async def create_scene_preset(
|
||||
targets=targets,
|
||||
order=store.count(),
|
||||
tags=data.tags if data.tags is not None else [],
|
||||
icon=data.icon or "",
|
||||
icon_color=data.icon_color or "",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
@@ -182,6 +186,8 @@ async def update_scene_preset(
|
||||
order=data.order,
|
||||
targets=new_targets,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -38,6 +38,8 @@ def _to_response(clock: SyncClock, manager: SyncClockManager) -> SyncClockRespon
|
||||
speed=rt.speed if rt else clock.speed,
|
||||
description=clock.description,
|
||||
tags=clock.tags,
|
||||
icon=getattr(clock, "icon", "") or "",
|
||||
icon_color=getattr(clock, "icon_color", "") or "",
|
||||
is_running=rt.is_running if rt else True,
|
||||
elapsed_time=rt.get_time() if rt else 0.0,
|
||||
created_at=clock.created_at,
|
||||
@@ -75,6 +77,8 @@ async def create_sync_clock(
|
||||
speed=data.speed,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
fire_entity_event("sync_clock", "created", clock.id)
|
||||
return _to_response(clock, manager)
|
||||
@@ -120,6 +124,8 @@ async def update_sync_clock(
|
||||
speed=data.speed,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
# Hot-update runtime speed
|
||||
if data.speed is not None:
|
||||
|
||||
@@ -45,6 +45,21 @@ logger = get_logger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _template_to_response(t) -> TemplateResponse:
|
||||
return TemplateResponse(
|
||||
id=t.id,
|
||||
name=t.name,
|
||||
engine_type=t.engine_type,
|
||||
engine_config=t.engine_config,
|
||||
tags=t.tags,
|
||||
created_at=t.created_at,
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
icon=getattr(t, "icon", "") or "",
|
||||
icon_color=getattr(t, "icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
# ===== CAPTURE TEMPLATE ENDPOINTS =====
|
||||
|
||||
|
||||
@@ -57,19 +72,7 @@ async def list_templates(
|
||||
try:
|
||||
templates = template_store.get_all_templates()
|
||||
|
||||
template_responses = [
|
||||
TemplateResponse(
|
||||
id=t.id,
|
||||
name=t.name,
|
||||
engine_type=t.engine_type,
|
||||
engine_config=t.engine_config,
|
||||
tags=t.tags,
|
||||
created_at=t.created_at,
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
)
|
||||
for t in templates
|
||||
]
|
||||
template_responses = [_template_to_response(t) for t in templates]
|
||||
|
||||
return TemplateListResponse(
|
||||
templates=template_responses,
|
||||
@@ -100,19 +103,12 @@ async def create_template(
|
||||
engine_config=template_data.engine_config,
|
||||
description=template_data.description,
|
||||
tags=template_data.tags,
|
||||
icon=template_data.icon,
|
||||
icon_color=template_data.icon_color,
|
||||
)
|
||||
|
||||
fire_entity_event("capture_template", "created", template.id)
|
||||
return TemplateResponse(
|
||||
id=template.id,
|
||||
name=template.name,
|
||||
engine_type=template.engine_type,
|
||||
engine_config=template.engine_config,
|
||||
tags=template.tags,
|
||||
created_at=template.created_at,
|
||||
updated_at=template.updated_at,
|
||||
description=template.description,
|
||||
)
|
||||
return _template_to_response(template)
|
||||
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
@@ -138,16 +134,7 @@ async def get_template(
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail=f"Template {template_id} not found")
|
||||
|
||||
return TemplateResponse(
|
||||
id=template.id,
|
||||
name=template.name,
|
||||
engine_type=template.engine_type,
|
||||
engine_config=template.engine_config,
|
||||
tags=template.tags,
|
||||
created_at=template.created_at,
|
||||
updated_at=template.updated_at,
|
||||
description=template.description,
|
||||
)
|
||||
return _template_to_response(template)
|
||||
|
||||
|
||||
@router.put(
|
||||
@@ -168,19 +155,12 @@ async def update_template(
|
||||
engine_config=update_data.engine_config,
|
||||
description=update_data.description,
|
||||
tags=update_data.tags,
|
||||
icon=update_data.icon,
|
||||
icon_color=update_data.icon_color,
|
||||
)
|
||||
|
||||
fire_entity_event("capture_template", "updated", template_id)
|
||||
return TemplateResponse(
|
||||
id=template.id,
|
||||
name=template.name,
|
||||
engine_type=template.engine_type,
|
||||
engine_config=template.engine_config,
|
||||
tags=template.tags,
|
||||
created_at=template.created_at,
|
||||
updated_at=template.updated_at,
|
||||
description=template.description,
|
||||
)
|
||||
return _template_to_response(template)
|
||||
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
@@ -64,6 +64,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
value=s.value,
|
||||
@@ -73,6 +75,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
waveform=s.waveform,
|
||||
@@ -85,6 +89,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
audio_source_id=s.audio_source_id,
|
||||
@@ -100,6 +106,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
speed=s.speed,
|
||||
@@ -114,6 +122,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
color=list(s.color),
|
||||
@@ -123,6 +133,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
colors=[list(c) for c in s.colors],
|
||||
@@ -135,6 +147,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
schedule=s.schedule,
|
||||
@@ -144,6 +158,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
ha_source_id=s.ha_source_id,
|
||||
@@ -158,6 +174,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
value_source_id=s.value_source_id,
|
||||
@@ -169,6 +187,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
color_strip_source_id=s.color_strip_source_id,
|
||||
@@ -180,6 +200,8 @@ _RESPONSE_MAP = {
|
||||
name=s.name,
|
||||
description=s.description,
|
||||
tags=s.tags,
|
||||
icon=getattr(s, "icon", "") or "",
|
||||
icon_color=getattr(s, "icon_color", "") or "",
|
||||
created_at=s.created_at,
|
||||
updated_at=s.updated_at,
|
||||
metric=s.metric,
|
||||
@@ -204,6 +226,8 @@ def _to_response(source: ValueSource) -> ValueSourceResponse:
|
||||
name=source.name,
|
||||
description=source.description,
|
||||
tags=source.tags,
|
||||
icon=getattr(source, "icon", "") or "",
|
||||
icon_color=getattr(source, "icon_color", "") or "",
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
picture_source_id=source.picture_source_id,
|
||||
@@ -218,6 +242,8 @@ def _to_response(source: ValueSource) -> ValueSourceResponse:
|
||||
name=source.name,
|
||||
description=source.description,
|
||||
tags=source.tags,
|
||||
icon=getattr(source, "icon", "") or "",
|
||||
icon_color=getattr(source, "icon_color", "") or "",
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
schedule=source.schedule,
|
||||
@@ -233,6 +259,8 @@ def _to_response(source: ValueSource) -> ValueSourceResponse:
|
||||
name=source.name,
|
||||
description=source.description,
|
||||
tags=source.tags,
|
||||
icon=getattr(source, "icon", "") or "",
|
||||
icon_color=getattr(source, "icon_color", "") or "",
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
value=getattr(source, "value", 1.0),
|
||||
|
||||
@@ -39,6 +39,8 @@ def _to_response(source: WeatherSource) -> WeatherSourceResponse:
|
||||
update_interval=d["update_interval"],
|
||||
description=d.get("description"),
|
||||
tags=d.get("tags", []),
|
||||
icon=getattr(source, "icon", "") or "",
|
||||
icon_color=getattr(source, "icon_color", "") or "",
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
)
|
||||
@@ -79,6 +81,8 @@ async def create_weather_source(
|
||||
update_interval=data.update_interval,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -125,6 +129,8 @@ async def update_weather_source(
|
||||
update_interval=data.update_interval,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
icon=data.icon,
|
||||
icon_color=data.icon_color,
|
||||
)
|
||||
except EntityNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Weather source {source_id} not found")
|
||||
|
||||
@@ -12,6 +12,16 @@ class AssetUpdate(BaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100, description="Display name")
|
||||
description: Optional[str] = Field(None, max_length=500, description="Optional description")
|
||||
tags: Optional[List[str]] = Field(None, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class AssetResponse(BaseModel):
|
||||
@@ -26,6 +36,16 @@ class AssetResponse(BaseModel):
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
prebuilt: bool = Field(False, description="Whether this is a shipped prebuilt asset")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
|
||||
@@ -17,6 +17,16 @@ class AudioProcessingTemplateCreate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class AudioProcessingTemplateUpdate(BaseModel):
|
||||
@@ -28,6 +38,16 @@ class AudioProcessingTemplateUpdate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class AudioProcessingTemplateResponse(BaseModel):
|
||||
@@ -42,6 +62,16 @@ class AudioProcessingTemplateResponse(BaseModel):
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class AudioProcessingTemplateListResponse(BaseModel):
|
||||
|
||||
@@ -19,6 +19,16 @@ class _AudioSourceResponseBase(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class CaptureAudioSourceResponse(_AudioSourceResponseBase):
|
||||
@@ -53,6 +63,16 @@ class _AudioSourceCreateBase(BaseModel):
|
||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class CaptureAudioSourceCreate(_AudioSourceCreateBase):
|
||||
@@ -87,6 +107,16 @@ class _AudioSourceUpdateBase(BaseModel):
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class CaptureAudioSourceUpdate(_AudioSourceUpdateBase):
|
||||
|
||||
@@ -16,6 +16,16 @@ class AudioTemplateCreate(BaseModel):
|
||||
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class AudioTemplateUpdate(BaseModel):
|
||||
@@ -26,6 +36,16 @@ class AudioTemplateUpdate(BaseModel):
|
||||
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class AudioTemplateResponse(BaseModel):
|
||||
@@ -39,6 +59,16 @@ class AudioTemplateResponse(BaseModel):
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class AudioTemplateListResponse(BaseModel):
|
||||
|
||||
@@ -67,6 +67,16 @@ class AutomationCreate(BaseModel):
|
||||
None, description="Scene preset for fallback deactivation"
|
||||
)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class AutomationUpdate(BaseModel):
|
||||
@@ -84,6 +94,16 @@ class AutomationUpdate(BaseModel):
|
||||
None, description="Scene preset for fallback deactivation"
|
||||
)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class AutomationResponse(BaseModel):
|
||||
@@ -108,6 +128,16 @@ class AutomationResponse(BaseModel):
|
||||
last_deactivated_at: Optional[datetime] = Field(
|
||||
None, description="Last time this automation was deactivated"
|
||||
)
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
|
||||
@@ -17,6 +17,16 @@ class ColorStripProcessingTemplateCreate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class ColorStripProcessingTemplateUpdate(BaseModel):
|
||||
@@ -28,6 +38,16 @@ class ColorStripProcessingTemplateUpdate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class ColorStripProcessingTemplateResponse(BaseModel):
|
||||
@@ -40,6 +60,16 @@ class ColorStripProcessingTemplateResponse(BaseModel):
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class ColorStripProcessingTemplateListResponse(BaseModel):
|
||||
|
||||
@@ -95,6 +95,16 @@ class _CSSResponseBase(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class PictureCSSResponse(_CSSResponseBase):
|
||||
@@ -266,6 +276,16 @@ class _CSSCreateBase(BaseModel):
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class PictureCSSCreate(_CSSCreateBase):
|
||||
@@ -450,6 +470,16 @@ class _CSSUpdateBase(BaseModel):
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class PictureCSSUpdate(_CSSUpdateBase):
|
||||
|
||||
@@ -42,6 +42,16 @@ class GameIntegrationCreate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Integration description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class GameIntegrationUpdate(BaseModel):
|
||||
@@ -56,6 +66,16 @@ class GameIntegrationUpdate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Integration description", max_length=500)
|
||||
tags: Optional[List[str]] = Field(None, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class GameIntegrationResponse(BaseModel):
|
||||
@@ -71,6 +91,16 @@ class GameIntegrationResponse(BaseModel):
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Integration description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
)
|
||||
|
||||
|
||||
class GameIntegrationListResponse(BaseModel):
|
||||
|
||||
@@ -20,6 +20,16 @@ class GradientCreate(BaseModel):
|
||||
stops: List[GradientStopSchema] = Field(description="Color stops", min_length=2)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class GradientUpdate(BaseModel):
|
||||
@@ -29,6 +39,16 @@ class GradientUpdate(BaseModel):
|
||||
stops: Optional[List[GradientStopSchema]] = Field(None, description="Color stops", min_length=2)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class GradientResponse(BaseModel):
|
||||
@@ -42,6 +62,16 @@ class GradientResponse(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class GradientListResponse(BaseModel):
|
||||
|
||||
@@ -18,6 +18,16 @@ class HomeAssistantSourceCreate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class HomeAssistantSourceUpdate(BaseModel):
|
||||
@@ -30,6 +40,16 @@ class HomeAssistantSourceUpdate(BaseModel):
|
||||
entity_filters: Optional[List[str]] = Field(None, description="Entity ID filter patterns")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class HomeAssistantSourceResponse(BaseModel):
|
||||
@@ -44,6 +64,16 @@ class HomeAssistantSourceResponse(BaseModel):
|
||||
entity_count: int = Field(default=0, description="Number of cached entities")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
)
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
token: Optional[str] = Field(
|
||||
|
||||
@@ -18,6 +18,16 @@ class MQTTSourceCreate(BaseModel):
|
||||
base_topic: str = Field(default="ledgrab", description="Base topic prefix")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class MQTTSourceUpdate(BaseModel):
|
||||
@@ -32,6 +42,16 @@ class MQTTSourceUpdate(BaseModel):
|
||||
base_topic: Optional[str] = Field(None, description="Base topic prefix")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class MQTTSourceResponse(BaseModel):
|
||||
@@ -48,6 +68,16 @@ class MQTTSourceResponse(BaseModel):
|
||||
connected: bool = Field(default=False, description="Whether the broker connection is active")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
)
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
|
||||
@@ -17,6 +17,16 @@ class PatternTemplateCreate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class PatternTemplateUpdate(BaseModel):
|
||||
@@ -28,6 +38,16 @@ class PatternTemplateUpdate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class PatternTemplateResponse(BaseModel):
|
||||
@@ -40,6 +60,16 @@ class PatternTemplateResponse(BaseModel):
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class PatternTemplateListResponse(BaseModel):
|
||||
|
||||
@@ -19,6 +19,16 @@ class _PictureSourceResponseBase(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class RawPictureSourceResponse(_PictureSourceResponseBase):
|
||||
@@ -72,6 +82,16 @@ class _PictureSourceCreateBase(BaseModel):
|
||||
name: str = Field(description="Stream name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Stream description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class RawPictureSourceCreate(_PictureSourceCreateBase):
|
||||
@@ -127,6 +147,16 @@ class _PictureSourceUpdateBase(BaseModel):
|
||||
name: Optional[str] = Field(None, description="Stream name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Stream description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class RawPictureSourceUpdate(_PictureSourceUpdateBase):
|
||||
|
||||
@@ -17,6 +17,16 @@ class PostprocessingTemplateCreate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class PostprocessingTemplateUpdate(BaseModel):
|
||||
@@ -28,6 +38,16 @@ class PostprocessingTemplateUpdate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class PostprocessingTemplateResponse(BaseModel):
|
||||
@@ -40,6 +60,16 @@ class PostprocessingTemplateResponse(BaseModel):
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class PostprocessingTemplateListResponse(BaseModel):
|
||||
|
||||
@@ -23,6 +23,16 @@ class ScenePresetCreate(BaseModel):
|
||||
None, description="Target IDs to capture (all if omitted)"
|
||||
)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class ScenePresetUpdate(BaseModel):
|
||||
@@ -36,6 +46,16 @@ class ScenePresetUpdate(BaseModel):
|
||||
description="Update target list: keep state for existing, capture fresh for new, drop removed",
|
||||
)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class ScenePresetResponse(BaseModel):
|
||||
@@ -47,6 +67,16 @@ class ScenePresetResponse(BaseModel):
|
||||
targets: List[TargetSnapshotSchema]
|
||||
order: int
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@@ -13,6 +13,16 @@ class SyncClockCreate(BaseModel):
|
||||
speed: float = Field(default=1.0, description="Speed multiplier (0.1–10.0)", ge=0.1, le=10.0)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class SyncClockUpdate(BaseModel):
|
||||
@@ -22,6 +32,16 @@ class SyncClockUpdate(BaseModel):
|
||||
speed: Optional[float] = Field(None, description="Speed multiplier (0.1–10.0)", ge=0.1, le=10.0)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class SyncClockResponse(BaseModel):
|
||||
@@ -32,6 +52,16 @@ class SyncClockResponse(BaseModel):
|
||||
speed: float = Field(description="Speed multiplier")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
)
|
||||
is_running: bool = Field(True, description="Whether clock is currently running")
|
||||
elapsed_time: float = Field(0.0, description="Current elapsed time in seconds")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
|
||||
@@ -14,6 +14,16 @@ class TemplateCreate(BaseModel):
|
||||
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class TemplateUpdate(BaseModel):
|
||||
@@ -24,6 +34,16 @@ class TemplateUpdate(BaseModel):
|
||||
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class TemplateResponse(BaseModel):
|
||||
@@ -37,6 +57,12 @@ class TemplateResponse(BaseModel):
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
None, max_length=64, description="Icon id from the curated icon library."
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None, max_length=32, description="Optional CSS color override for the icon."
|
||||
)
|
||||
|
||||
|
||||
class TemplateListResponse(BaseModel):
|
||||
|
||||
@@ -17,6 +17,16 @@ class _ValueSourceResponseBase(BaseModel):
|
||||
name: str = Field(description="Source name")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
)
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
@@ -171,6 +181,16 @@ class _ValueSourceCreateBase(BaseModel):
|
||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class StaticValueSourceCreate(_ValueSourceCreateBase):
|
||||
@@ -320,6 +340,16 @@ class _ValueSourceUpdateBase(BaseModel):
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class StaticValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
|
||||
@@ -25,6 +25,16 @@ class WeatherSourceCreate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class WeatherSourceUpdate(BaseModel):
|
||||
@@ -44,6 +54,16 @@ class WeatherSourceUpdate(BaseModel):
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
)
|
||||
|
||||
|
||||
class WeatherSourceResponse(BaseModel):
|
||||
@@ -60,6 +80,16 @@ class WeatherSourceResponse(BaseModel):
|
||||
update_interval: int = Field(description="API poll interval in seconds")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
)
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Card icon plate helper — builds the ``iconHtml`` / ``iconColor`` /
|
||||
* ``iconAttrs`` slots for a mod-card head from any entity that has
|
||||
* optional ``icon`` and ``icon_color`` string fields.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
*
|
||||
* const mod: ModCardOpts = {
|
||||
* head: {
|
||||
* badge: { text: 'WEATHER · IN' },
|
||||
* name: source.name,
|
||||
* ...makeCardIconFields('weather_source', source.id, source),
|
||||
* // ...
|
||||
* },
|
||||
* };
|
||||
*
|
||||
* The icon picker is opened by document-level click delegation on
|
||||
* ``[data-icon-picker-trigger]``; each feature module registers its
|
||||
* adapter via ``registerIconEntityType()`` from ``icon-picker.ts``.
|
||||
*/
|
||||
|
||||
import { renderDeviceIconSvg } from './device-icons.ts';
|
||||
import type { ModHeadOpts } from './mod-card.ts';
|
||||
|
||||
export interface IconableEntity {
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
}
|
||||
|
||||
type IconHeadFields = Pick<ModHeadOpts, 'iconHtml' | 'iconColor' | 'iconAttrs'>;
|
||||
|
||||
export function makeCardIconFields(
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
entity: IconableEntity,
|
||||
opts: { size?: number } = {},
|
||||
): IconHeadFields {
|
||||
const iconId = entity.icon ?? '';
|
||||
const iconColor = entity.icon_color || undefined;
|
||||
return {
|
||||
iconHtml: iconId ? renderDeviceIconSvg(iconId, { size: opts.size ?? 24 }) : '',
|
||||
iconColor,
|
||||
iconAttrs: { 'data-icon-picker-trigger': `${entityType}:${entityId}` },
|
||||
};
|
||||
}
|
||||
@@ -404,6 +404,8 @@ export interface GradientEntity {
|
||||
is_builtin: boolean;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
}
|
||||
|
||||
export const gradientsCache = new DataCache<GradientEntity[]>({
|
||||
|
||||
@@ -12,9 +12,23 @@ import * as P from '../core/icon-paths.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import { loadPictureSources } from './streams.ts';
|
||||
import type { Asset } from '../types.ts';
|
||||
|
||||
registerIconEntityType('asset', makeSimpleIconAdapter<Asset>({
|
||||
cache: assetsCache,
|
||||
endpointPrefix: '/assets',
|
||||
reload: async () => {
|
||||
assetsCache.invalidate();
|
||||
await loadPictureSources();
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.asset',
|
||||
typeLabelFallback: 'Asset',
|
||||
cardSelectors: (id) => [`[data-card-section="assets"] [data-id="${CSS.escape(id)}"]`],
|
||||
}));
|
||||
|
||||
const _icon = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||
const ICON_PLAY_SOUND = _icon(P.play);
|
||||
const ICON_UPLOAD = _icon(P.fileUp);
|
||||
@@ -162,6 +176,7 @@ export function createAssetCard(asset: Asset): string {
|
||||
name: asset.name,
|
||||
metaHtml: escapeHtml(`${typeLabel} · ${sizeStr}`),
|
||||
leds: ['on'],
|
||||
...makeCardIconFields('asset', asset.id, asset),
|
||||
menu: {
|
||||
hideOnclick: `toggleCardHidden('assets','${asset.id}')`,
|
||||
deleteOnclick: `deleteAsset('${asset.id}')`,
|
||||
|
||||
@@ -24,8 +24,22 @@ import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { FilterListManager } from '../core/filter-list.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ModCardOpts } from '../core/mod-card.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import { loadPictureSources } from './streams.ts';
|
||||
|
||||
registerIconEntityType('audio_processing_template', makeSimpleIconAdapter<any>({
|
||||
cache: audioProcessingTemplatesCache,
|
||||
endpointPrefix: '/audio-processing-templates',
|
||||
reload: async () => {
|
||||
audioProcessingTemplatesCache.invalidate();
|
||||
await loadPictureSources();
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.audio_processing_template',
|
||||
typeLabelFallback: 'Audio processing template',
|
||||
cardSelectors: (id) => [`[data-apt-id="${CSS.escape(id)}"]`],
|
||||
}));
|
||||
|
||||
// ── Module state ─────────────────────────────────────────────
|
||||
|
||||
let _aptTagsInput: TagInput | null = null;
|
||||
@@ -286,6 +300,7 @@ export function createAudioProcessingTemplateCard(tmpl: any): string {
|
||||
name: tmpl.name,
|
||||
metaHtml: escapeHtml(`${filters.length} ${t('audio_processing.title') || 'filters'}`),
|
||||
leds: ['off'],
|
||||
...makeCardIconFields('audio_processing_template', tmpl.id, tmpl),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneAudioProcessingTemplate('${tmpl.id}')`,
|
||||
hideOnclick: `toggleCardHidden('audio-processing-templates','${tmpl.id}')`,
|
||||
|
||||
@@ -16,6 +16,8 @@ import * as P from '../core/icon-paths.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import { getBaseOrigin } from './settings.ts';
|
||||
import { IconSelect } from '../core/icon-select.ts';
|
||||
import { EntitySelect } from '../core/entity-palette.ts';
|
||||
@@ -24,6 +26,20 @@ import { TreeNav } from '../core/tree-nav.ts';
|
||||
import { csScenes, createSceneCard, initScenePresetDelegation } from './scene-presets.ts';
|
||||
import type { Automation } from '../types.ts';
|
||||
|
||||
registerIconEntityType('automation', makeSimpleIconAdapter<Automation>({
|
||||
cache: automationsCacheObj,
|
||||
endpointPrefix: '/automations',
|
||||
reload: async () => {
|
||||
automationsCacheObj.invalidate();
|
||||
if (typeof (window as any).loadAutomations === 'function') {
|
||||
await (window as any).loadAutomations();
|
||||
}
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.automation',
|
||||
typeLabelFallback: 'Automation',
|
||||
cardSelectors: (id) => [`[data-automation-id="${CSS.escape(id)}"]`],
|
||||
}));
|
||||
|
||||
// ── HA rule entity cache ──
|
||||
let _haRuleEntities: any[] = [];
|
||||
|
||||
@@ -401,6 +417,7 @@ function createAutomationCard(automation: Automation, sceneMap = new Map()) {
|
||||
name: automation.name,
|
||||
metaHtml,
|
||||
leds: [ledState],
|
||||
...makeCardIconFields('automation', automation.id, automation),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneAutomation('${automation.id}')`,
|
||||
hideOnclick: `toggleCardHidden('automations','${automation.id}')`,
|
||||
|
||||
@@ -23,6 +23,23 @@ import type { ColorStripSource } from '../../types.ts';
|
||||
import { bindableValue, bindableColor } from '../../types.ts';
|
||||
import { renderTagChips } from '../../core/tag-input.ts';
|
||||
import { rgbArrayToHex } from '../css-gradient-editor.ts';
|
||||
import { makeCardIconFields } from '../../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from '../icon-picker.ts';
|
||||
|
||||
registerIconEntityType('color_strip_source', makeSimpleIconAdapter<ColorStripSource>({
|
||||
cache: colorStripSourcesCache,
|
||||
endpointPrefix: '/color-strip-sources',
|
||||
reload: async () => {
|
||||
colorStripSourcesCache.invalidate();
|
||||
if (typeof (window as any).loadPictureSources === 'function') {
|
||||
await (window as any).loadPictureSources();
|
||||
}
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.color_strip_source',
|
||||
typeLabelFallback: 'Color strip',
|
||||
cardSelectors: (id) => [`[data-css-id="${CSS.escape(id)}"]`],
|
||||
bodyExtras: (rec) => ({ source_type: (rec as any)?.source_type ?? 'static' }),
|
||||
}));
|
||||
|
||||
/* ── Types ────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -332,6 +349,7 @@ export function createColorStripCard(source: ColorStripSource, pictureSourceMap:
|
||||
name: source.name,
|
||||
metaHtml: escapeHtml(metaText),
|
||||
leds: ['off'],
|
||||
...makeCardIconFields('color_strip_source', source.id, source),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneColorStrip('${source.id}')`,
|
||||
hideOnclick: `toggleCardHidden('color-strips','${source.id}')`,
|
||||
|
||||
@@ -457,8 +457,11 @@ function renderDashboardSyncClock(clock: SyncClock): string {
|
||||
const btnLabel = clock.is_running ? (t('sync_clock.action.pause') || 'Pause') : (t('sync_clock.action.resume') || 'Resume');
|
||||
|
||||
const scStyle = cardColorStyle(clock.id);
|
||||
const iconPlate = _dashboardIconPlate(clock as any);
|
||||
const headCls = iconPlate ? 'mod-head mod-head--with-icon' : 'mod-head';
|
||||
return `<div class="dashboard-target dashboard-autostart dashboard-card-link ${clock.is_running ? 'is-running' : ''}" data-sync-clock-id="${clock.id}" onclick="if(!event.target.closest('button')){navigateToCard('streams','sync','sync-clocks','data-id','${clock.id}')}"${scStyle ? ` style="${scStyle}"` : ''}>
|
||||
<div class="mod-head">
|
||||
<div class="${headCls}">
|
||||
${iconPlate}
|
||||
<div class="mod-id">
|
||||
<span class="mod-badge">CLK · ${escapeHtml(short)}</span>
|
||||
<div class="mod-name"><span>${escapeHtml(clock.name)}</span></div>
|
||||
@@ -958,6 +961,20 @@ export async function loadDashboard(forceFullRender: boolean = false): Promise<v
|
||||
}
|
||||
}
|
||||
|
||||
/** Render an icon plate (`.mod-icon`) for a dashboard card from the
|
||||
* entity's own ``icon`` / ``icon_color`` fields. Returns empty string
|
||||
* when no icon is set, so the head reverts to its legacy badge-only
|
||||
* layout. */
|
||||
function _dashboardIconPlate(entity: { icon?: string; icon_color?: string }): string {
|
||||
const iconId = entity.icon || '';
|
||||
if (!iconId) return '';
|
||||
const iconColor = entity.icon_color || '';
|
||||
const styleAttr = iconColor
|
||||
? ` style="--ch:${escapeHtml(iconColor)};color:${escapeHtml(iconColor)}"`
|
||||
: '';
|
||||
return `<div class="mod-icon"${styleAttr}>${renderDeviceIconSvg(iconId, { size: 24 })}</div>`;
|
||||
}
|
||||
|
||||
/** Resolve the effective custom icon for a dashboard target card.
|
||||
* LED targets inherit from their referenced device when no own icon is
|
||||
* set; HA-light targets have no inheritance source. Returns the HTML
|
||||
@@ -1153,8 +1170,11 @@ function renderDashboardAutomation(automation: Automation, sceneMap: Map<string,
|
||||
metaLines.push(`${ICON_SCENE} ${sceneName}`);
|
||||
|
||||
const aStyle = cardColorStyle(automation.id);
|
||||
const iconPlate = _dashboardIconPlate(automation as any);
|
||||
const headCls = iconPlate ? 'mod-head mod-head--with-icon' : 'mod-head';
|
||||
return `<div class="dashboard-target dashboard-automation dashboard-card-link ${isActive ? 'is-running' : ''}" data-automation-id="${automation.id}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'automations','data-automation-id','${automation.id}')}"${aStyle ? ` style="${aStyle}"` : ''}>
|
||||
<div class="mod-head">
|
||||
<div class="${headCls}">
|
||||
${iconPlate}
|
||||
<div class="mod-id">
|
||||
<span class="mod-badge">AUTO · ${escapeHtml(short)}</span>
|
||||
<div class="mod-name"><span>${escapeHtml(automation.name)}</span></div>
|
||||
|
||||
@@ -14,6 +14,8 @@ import { CardSection } from '../core/card-sections.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import { IconSelect, type IconSelectItem } from '../core/icon-select.ts';
|
||||
import {
|
||||
ICON_GAMEPAD, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_TRASH,
|
||||
@@ -25,6 +27,22 @@ import type {
|
||||
EffectPreset,
|
||||
} from '../types.ts';
|
||||
|
||||
registerIconEntityType('game_integration', makeSimpleIconAdapter<GameIntegration>({
|
||||
cache: gameIntegrationsCache,
|
||||
endpointPrefix: '/game-integrations',
|
||||
reload: async () => {
|
||||
gameIntegrationsCache.invalidate();
|
||||
if (typeof (window as any).loadIntegrations === 'function') {
|
||||
await (window as any).loadIntegrations();
|
||||
}
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.game_integration',
|
||||
typeLabelFallback: 'Game integration',
|
||||
cardSelectors: (id) => [
|
||||
`[data-card-section="game-integrations"] [data-gi-id="${CSS.escape(id)}"]`,
|
||||
],
|
||||
}));
|
||||
|
||||
const _icon = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||
|
||||
// ── Bulk actions ──
|
||||
@@ -566,6 +584,7 @@ export function createGameIntegrationCard(gi: GameIntegration): string {
|
||||
name: gi.name,
|
||||
metaHtml: escapeHtml(`${adapterName} · ${mappingCount} ${t('game_integration.mappings') || 'events'}`),
|
||||
leds,
|
||||
...makeCardIconFields('game_integration', gi.id, gi),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneGameIntegration('${gi.id}')`,
|
||||
hideOnclick: `toggleCardHidden('game-integrations','${gi.id}')`,
|
||||
|
||||
@@ -12,8 +12,25 @@ import * as P from '../core/icon-paths.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import type { HomeAssistantSource } from '../types.ts';
|
||||
|
||||
registerIconEntityType('ha_source', makeSimpleIconAdapter<HomeAssistantSource>({
|
||||
cache: haSourcesCache,
|
||||
endpointPrefix: '/home-assistant/sources',
|
||||
reload: async () => {
|
||||
if (typeof (window as any).loadIntegrations === 'function') {
|
||||
await (window as any).loadIntegrations();
|
||||
}
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.ha_source',
|
||||
typeLabelFallback: 'Home Assistant source',
|
||||
cardSelectors: (id) => [
|
||||
`[data-card-section="ha-sources"] [data-id="${CSS.escape(id)}"]`,
|
||||
],
|
||||
}));
|
||||
|
||||
const ICON_HA = `<svg class="icon" viewBox="0 0 24 24">${P.home}</svg>`;
|
||||
|
||||
// ── Modal ──
|
||||
@@ -240,6 +257,7 @@ export function createHASourceCard(source: HomeAssistantSource) {
|
||||
name: source.name,
|
||||
metaHtml: escapeHtml(`${source.host}${isConnected ? ` · ${source.entity_count} entities` : ''}`),
|
||||
leds,
|
||||
...makeCardIconFields('ha_source', source.id, source),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneHASource('${source.id}')`,
|
||||
hideOnclick: `toggleCardHidden('ha-sources','${source.id}')`,
|
||||
|
||||
@@ -34,17 +34,22 @@ const RECENT_MAX = 10;
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Entity-type registry
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Built-in types (device, target) are registered below. Other feature
|
||||
// modules call ``registerIconEntityType()`` at import time to add their
|
||||
// own — keeps icon-picker.ts decoupled from each feature's cache and
|
||||
// reload pathway.
|
||||
|
||||
type EntityType = 'device' | 'target';
|
||||
export type EntityType = string;
|
||||
|
||||
interface EntityRecord {
|
||||
export interface EntityRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
icon_color: string;
|
||||
}
|
||||
|
||||
interface InheritedIcon {
|
||||
export interface InheritedIcon {
|
||||
/** The icon id we'd render if this entity has no own icon. */
|
||||
iconId: string;
|
||||
/** Effective color for the inherited icon. */
|
||||
@@ -53,83 +58,149 @@ interface InheritedIcon {
|
||||
fromName: string;
|
||||
}
|
||||
|
||||
interface EntityTypeAdapter {
|
||||
export interface EntityTypeAdapter {
|
||||
/** Look up the entity by id. Returns null when missing from the cache. */
|
||||
lookup(id: string): EntityRecord | null;
|
||||
/** Build the PUT endpoint URL for icon updates. */
|
||||
/** Build the PUT endpoint URL (path only — fetchWithAuth prepends ``/api/v1``). */
|
||||
endpoint(id: string): string;
|
||||
/** Invalidate the cache and reload the relevant view. */
|
||||
reload(): Promise<void>;
|
||||
/** Optional fallback icon (e.g. LED target → parent device). */
|
||||
inheritedFrom(id: string): InheritedIcon | null;
|
||||
/** Display label like "Device" / "LED target" / "HA light target". */
|
||||
/** Display label like "Device" / "LED target" / "Picture source". */
|
||||
typeLabel(id: string): string;
|
||||
/** Optional CSS selector(s) to find the live card element so the
|
||||
* picker preview can read its channel accent. Tried in order. */
|
||||
cardSelectors?: (id: string) => string[];
|
||||
/** Optional extra fields merged into the PUT body — used for
|
||||
* discriminator-keyed routes (e.g. output-targets needs
|
||||
* ``target_type`` for the ``OutputTargetUpdate`` discriminated
|
||||
* union). Receives the entity id; adapter does its own lookup. */
|
||||
bodyExtras?: (id: string) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
function _readDevice(id: string): EntityRecord | null {
|
||||
const dev = (devicesCache.data ?? []).find((d: any) => d.id === id);
|
||||
if (!dev) return null;
|
||||
const _adapters: Map<EntityType, EntityTypeAdapter> = new Map();
|
||||
|
||||
/** Public registration entry used by feature modules. Registering an
|
||||
* unknown type is idempotent — re-registering replaces the adapter. */
|
||||
export function registerIconEntityType(type: EntityType, adapter: EntityTypeAdapter): void {
|
||||
_adapters.set(type, adapter);
|
||||
}
|
||||
|
||||
/** Helper for the common case: a single cache + a single PUT endpoint
|
||||
* that accepts ``{icon, icon_color}`` directly. Reduces per-feature
|
||||
* registration to ~6 lines. */
|
||||
export function makeSimpleIconAdapter<T extends { id: string; name?: string; icon?: string; icon_color?: string }>(opts: {
|
||||
cache: { data: T[] | null; invalidate: () => void };
|
||||
/** PUT path prefix (no ``/api/v1``). Final URL = ``${endpointPrefix}/${id}``. */
|
||||
endpointPrefix: string;
|
||||
/** Async refresh hook. Cache is invalidated automatically. */
|
||||
reload: () => Promise<void> | void;
|
||||
typeLabelKey: string;
|
||||
typeLabelFallback: string;
|
||||
cardSelectors?: (id: string) => string[];
|
||||
bodyExtras?: (rec: T) => Record<string, unknown>;
|
||||
}): EntityTypeAdapter {
|
||||
const _find = (id: string): T | undefined =>
|
||||
(opts.cache.data ?? []).find((x) => x.id === id);
|
||||
return {
|
||||
id: dev.id,
|
||||
name: dev.name ?? dev.id,
|
||||
icon: (dev.icon as string | undefined) ?? '',
|
||||
icon_color: (dev.icon_color as string | undefined) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function _readTarget(id: string): EntityRecord | null {
|
||||
const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
|
||||
if (!tgt) return null;
|
||||
return {
|
||||
id: tgt.id,
|
||||
name: tgt.name ?? tgt.id,
|
||||
icon: (tgt.icon as string | undefined) ?? '',
|
||||
icon_color: (tgt.icon_color as string | undefined) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const _adapters: Record<EntityType, EntityTypeAdapter> = {
|
||||
device: {
|
||||
lookup: _readDevice,
|
||||
endpoint: (id) => `/devices/${id}`,
|
||||
reload: async () => {
|
||||
devicesCache.invalidate();
|
||||
await window.loadDevices?.();
|
||||
},
|
||||
inheritedFrom: () => null,
|
||||
typeLabel: () => t('device.icon.entity.device') || 'Device',
|
||||
},
|
||||
target: {
|
||||
lookup: _readTarget,
|
||||
endpoint: (id) => `/output-targets/${id}`,
|
||||
reload: async () => {
|
||||
outputTargetsCache.invalidate();
|
||||
await window.loadTargetsTab?.();
|
||||
},
|
||||
inheritedFrom: (id) => {
|
||||
const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
|
||||
if (!tgt) return null;
|
||||
// Only LED targets inherit from a device — HA-light targets don't.
|
||||
const deviceId = (tgt as any).device_id;
|
||||
if (!deviceId) return null;
|
||||
const dev = (devicesCache.data ?? []).find((d: any) => d.id === deviceId);
|
||||
const iconId = (dev?.icon as string | undefined) ?? '';
|
||||
if (!iconId) return null;
|
||||
lookup: (id: string) => {
|
||||
const rec = _find(id);
|
||||
if (!rec) return null;
|
||||
return {
|
||||
iconId,
|
||||
color: (dev?.icon_color as string | undefined) || '',
|
||||
fromName: dev?.name ?? deviceId,
|
||||
id: rec.id,
|
||||
name: (rec.name as string | undefined) ?? rec.id,
|
||||
icon: (rec.icon as string | undefined) ?? '',
|
||||
icon_color: (rec.icon_color as string | undefined) ?? '',
|
||||
};
|
||||
},
|
||||
typeLabel: (id: string) => {
|
||||
const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
|
||||
if ((tgt as any)?.target_type === 'ha_light') {
|
||||
return t('device.icon.entity.ha_light_target') || 'HA light target';
|
||||
}
|
||||
return t('device.icon.entity.target') || 'LED target';
|
||||
endpoint: (id: string) => `${opts.endpointPrefix}/${id}`,
|
||||
reload: async () => {
|
||||
opts.cache.invalidate();
|
||||
await opts.reload();
|
||||
},
|
||||
inheritedFrom: () => null,
|
||||
typeLabel: () => t(opts.typeLabelKey) || opts.typeLabelFallback,
|
||||
cardSelectors: opts.cardSelectors,
|
||||
bodyExtras: opts.bodyExtras
|
||||
? (id: string) => {
|
||||
const rec = _find(id);
|
||||
return rec ? opts.bodyExtras!(rec) : {};
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Built-in adapters: device + target ──────────────────────────
|
||||
|
||||
registerIconEntityType('device', {
|
||||
lookup: (id: string) => {
|
||||
const dev = (devicesCache.data ?? []).find((d: any) => d.id === id);
|
||||
if (!dev) return null;
|
||||
return {
|
||||
id: dev.id,
|
||||
name: dev.name ?? dev.id,
|
||||
icon: (dev.icon as string | undefined) ?? '',
|
||||
icon_color: (dev.icon_color as string | undefined) ?? '',
|
||||
};
|
||||
},
|
||||
};
|
||||
endpoint: (id) => `/devices/${id}`,
|
||||
reload: async () => {
|
||||
devicesCache.invalidate();
|
||||
await window.loadDevices?.();
|
||||
},
|
||||
inheritedFrom: () => null,
|
||||
typeLabel: () => t('device.icon.entity.device') || 'Device',
|
||||
cardSelectors: (id) => [`[data-device-id="${CSS.escape(id)}"]`],
|
||||
});
|
||||
|
||||
registerIconEntityType('target', {
|
||||
lookup: (id: string) => {
|
||||
const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
|
||||
if (!tgt) return null;
|
||||
return {
|
||||
id: tgt.id,
|
||||
name: tgt.name ?? tgt.id,
|
||||
icon: (tgt.icon as string | undefined) ?? '',
|
||||
icon_color: (tgt.icon_color as string | undefined) ?? '',
|
||||
};
|
||||
},
|
||||
endpoint: (id) => `/output-targets/${id}`,
|
||||
reload: async () => {
|
||||
outputTargetsCache.invalidate();
|
||||
await window.loadTargetsTab?.();
|
||||
},
|
||||
inheritedFrom: (id) => {
|
||||
const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
|
||||
if (!tgt) return null;
|
||||
// Only LED targets inherit from a device — HA-light targets don't.
|
||||
const deviceId = (tgt as any).device_id;
|
||||
if (!deviceId) return null;
|
||||
const dev = (devicesCache.data ?? []).find((d: any) => d.id === deviceId);
|
||||
const iconId = (dev?.icon as string | undefined) ?? '';
|
||||
if (!iconId) return null;
|
||||
return {
|
||||
iconId,
|
||||
color: (dev?.icon_color as string | undefined) || '',
|
||||
fromName: dev?.name ?? deviceId,
|
||||
};
|
||||
},
|
||||
typeLabel: (id: string) => {
|
||||
const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
|
||||
if ((tgt as any)?.target_type === 'ha_light') {
|
||||
return t('device.icon.entity.ha_light_target') || 'HA light target';
|
||||
}
|
||||
return t('device.icon.entity.target') || 'LED target';
|
||||
},
|
||||
cardSelectors: (id) => [
|
||||
`[data-target-id="${CSS.escape(id)}"]`,
|
||||
`[data-ha-target-id="${CSS.escape(id)}"]`,
|
||||
],
|
||||
bodyExtras: (id: string) => {
|
||||
const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
|
||||
return { target_type: ((tgt as any)?.target_type as string | undefined) ?? 'led' };
|
||||
},
|
||||
});
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Picker state
|
||||
@@ -188,7 +259,7 @@ function _pushRecent(iconId: string): void {
|
||||
/** Open the picker for the given entity. Reads current icon from cache. */
|
||||
export function openIconPicker(entityType: EntityType, entityId: string): void {
|
||||
if (!entityId) return;
|
||||
const adapter = _adapters[entityType];
|
||||
const adapter = _adapters.get(entityType);
|
||||
if (!adapter) return;
|
||||
|
||||
const rec = adapter.lookup(entityId);
|
||||
@@ -197,13 +268,14 @@ export function openIconPicker(entityType: EntityType, entityId: string): void {
|
||||
const inherited = adapter.inheritedFrom(entityId);
|
||||
|
||||
// Resolve channel color from the live card so the preview matches.
|
||||
// LED-target cards use ``data-target-id``; HA-light-target cards use
|
||||
// ``data-ha-target-id``. Try the LED selector first and fall back to
|
||||
// the HA-light one when the entity is a target.
|
||||
const card = entityType === 'device'
|
||||
? document.querySelector(`[data-device-id="${CSS.escape(entityId)}"]`) as HTMLElement | null
|
||||
: (document.querySelector(`[data-target-id="${CSS.escape(entityId)}"]`)
|
||||
?? document.querySelector(`[data-ha-target-id="${CSS.escape(entityId)}"]`)) as HTMLElement | null;
|
||||
// Adapters provide the candidate selectors; we try them in order
|
||||
// and fall back to the global accent when no card is found.
|
||||
const selectors = adapter.cardSelectors?.(entityId) ?? [];
|
||||
let card: HTMLElement | null = null;
|
||||
for (const sel of selectors) {
|
||||
card = document.querySelector(sel) as HTMLElement | null;
|
||||
if (card) break;
|
||||
}
|
||||
const channelColor = card
|
||||
? (getComputedStyle(card).getPropertyValue('--ch') || '').trim() || _fallbackChannel()
|
||||
: _fallbackChannel();
|
||||
@@ -297,7 +369,7 @@ function _renderModal(): void {
|
||||
: `<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>`;
|
||||
|
||||
// Header — entity type + name, plus inherited hint when applicable.
|
||||
const adapter = _adapters[_ctx.entityType];
|
||||
const adapter = _adapters.get(_ctx.entityType)!;
|
||||
if (eyebrowEl) {
|
||||
eyebrowEl.textContent = adapter.typeLabel(_ctx.entityId);
|
||||
}
|
||||
@@ -414,14 +486,13 @@ async function _applyChange(nextIconId: string, nextColor: string): Promise<void
|
||||
return;
|
||||
}
|
||||
|
||||
const adapter = _adapters[entityType];
|
||||
const adapter = _adapters.get(entityType)!;
|
||||
try {
|
||||
const body: Record<string, unknown> = { icon: nextIconId, icon_color: nextColor };
|
||||
// The output-targets endpoint requires the discriminator field.
|
||||
if (entityType === 'target') {
|
||||
const tgt = (outputTargetsCache.data ?? []).find((x: any) => x.id === entityId);
|
||||
const targetType = (tgt as any)?.target_type ?? 'led';
|
||||
body.target_type = targetType;
|
||||
// Discriminated routes (e.g. output-targets) need extra fields
|
||||
// — adapter declares them via ``bodyExtras``.
|
||||
if (adapter.bodyExtras) {
|
||||
Object.assign(body, adapter.bodyExtras(entityId));
|
||||
}
|
||||
const resp = await fetchWithAuth(adapter.endpoint(entityId), {
|
||||
method: 'PUT',
|
||||
@@ -557,9 +628,9 @@ function _onDocumentClick(e: MouseEvent): void {
|
||||
if (!raw) return;
|
||||
const [typeOrId, id] = raw.includes(':') ? raw.split(':', 2) : ['device', raw];
|
||||
if (!id) return;
|
||||
if (typeOrId !== 'device' && typeOrId !== 'target') return;
|
||||
if (!_adapters.has(typeOrId)) return;
|
||||
e.stopPropagation();
|
||||
openIconPicker(typeOrId as EntityType, id);
|
||||
openIconPicker(typeOrId, id);
|
||||
}
|
||||
|
||||
document.addEventListener('click', _onDocumentClick);
|
||||
|
||||
@@ -12,8 +12,25 @@ import * as P from '../core/icon-paths.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import type { MQTTSource } from '../types.ts';
|
||||
|
||||
registerIconEntityType('mqtt_source', makeSimpleIconAdapter<MQTTSource>({
|
||||
cache: mqttSourcesCache,
|
||||
endpointPrefix: '/mqtt/sources',
|
||||
reload: async () => {
|
||||
if (typeof (window as any).loadIntegrations === 'function') {
|
||||
await (window as any).loadIntegrations();
|
||||
}
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.mqtt_source',
|
||||
typeLabelFallback: 'MQTT source',
|
||||
cardSelectors: (id) => [
|
||||
`[data-card-section="mqtt-sources"] [data-id="${CSS.escape(id)}"]`,
|
||||
],
|
||||
}));
|
||||
|
||||
const ICON_MQTT = `<svg class="icon" viewBox="0 0 24 24">${P.radio}</svg>`;
|
||||
|
||||
// ── Modal ──
|
||||
@@ -250,6 +267,7 @@ export function createMQTTSourceCard(source: MQTTSource) {
|
||||
name: source.name,
|
||||
metaHtml: escapeHtml(`${broker} · ${source.base_topic}`),
|
||||
leds,
|
||||
...makeCardIconFields('mqtt_source', source.id, source),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneMQTTSource('${source.id}')`,
|
||||
hideOnclick: `toggleCardHidden('mqtt-sources','${source.id}')`,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CardSection } from '../core/card-sections.ts';
|
||||
import {
|
||||
ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET, ICON_TRASH, ICON_LINK,
|
||||
} from '../core/icons.ts';
|
||||
import { renderDeviceIcon } from '../core/device-icons.ts';
|
||||
import { renderDeviceIcon, renderDeviceIconSvg } from '../core/device-icons.ts';
|
||||
import { scenePresetsCache, outputTargetsCache, automationsCacheObj, devicesCache } from '../core/state.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { wrapCard, cardColorStyle } from '../core/card-colors.ts';
|
||||
@@ -19,8 +19,24 @@ import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
||||
import { EntityPalette } from '../core/entity-palette.ts';
|
||||
import { navigateToCard } from '../core/navigation.ts';
|
||||
import { isActiveTab } from '../core/tab-registry.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import type { ScenePreset } from '../types.ts';
|
||||
|
||||
registerIconEntityType('scene_preset', makeSimpleIconAdapter<ScenePreset>({
|
||||
cache: scenePresetsCache,
|
||||
endpointPrefix: '/scene-presets',
|
||||
reload: async () => {
|
||||
scenePresetsCache.invalidate();
|
||||
if (typeof (window as any).loadAutomations === 'function') {
|
||||
await (window as any).loadAutomations();
|
||||
}
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.scene_preset',
|
||||
typeLabelFallback: 'Scene preset',
|
||||
cardSelectors: (id) => [`[data-scene-id="${CSS.escape(id)}"]`],
|
||||
}));
|
||||
|
||||
let _editingId: string | null = null;
|
||||
let _allTargets: any[] = []; // fetched on capture open
|
||||
let _sceneTagsInput: TagInput | null = null;
|
||||
@@ -117,6 +133,7 @@ export function createSceneCard(preset: ScenePreset) {
|
||||
name: preset.name,
|
||||
metaHtml,
|
||||
leds,
|
||||
...makeCardIconFields('scene_preset', preset.id, preset),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneScenePreset('${preset.id}')`,
|
||||
hideOnclick: `toggleCardHidden('scenes','${preset.id}')`,
|
||||
@@ -185,8 +202,18 @@ function _renderDashboardPresetCard(preset: ScenePreset): string {
|
||||
const activateLabel = t('scenes.activate') || 'Activate';
|
||||
|
||||
const pStyle = cardColorStyle(preset.id);
|
||||
const iconId = preset.icon || '';
|
||||
const iconColor = preset.icon_color || '';
|
||||
const iconStyle = iconColor
|
||||
? ` style="--ch:${escapeHtml(iconColor)};color:${escapeHtml(iconColor)}"`
|
||||
: '';
|
||||
const iconPlate = iconId
|
||||
? `<div class="mod-icon"${iconStyle}>${renderDeviceIconSvg(iconId, { size: 24 })}</div>`
|
||||
: '';
|
||||
const headCls = iconPlate ? 'mod-head mod-head--with-icon' : 'mod-head';
|
||||
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" data-action="navigate-scene" data-id="${preset.id}"${pStyle ? ` style="${pStyle}"` : ''}>
|
||||
<div class="mod-head">
|
||||
<div class="${headCls}">
|
||||
${iconPlate}
|
||||
<div class="mod-id">
|
||||
<span class="mod-badge">SCN \u00b7 ${escapeHtml(short)}</span>
|
||||
<div class="mod-name"><span>${escapeHtml(preset.name)}</span></div>
|
||||
|
||||
@@ -72,6 +72,81 @@ import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { IconSelect } from '../core/icon-select.ts';
|
||||
import { EntitySelect } from '../core/entity-palette.ts';
|
||||
import { FilterListManager } from '../core/filter-list.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
|
||||
// ── Icon-picker adapter registrations for streams-tab card types ──
|
||||
|
||||
const _reloadStreams = async () => {
|
||||
if (typeof (window as any).loadPictureSources === 'function') {
|
||||
await (window as any).loadPictureSources();
|
||||
}
|
||||
};
|
||||
|
||||
registerIconEntityType('picture_source', makeSimpleIconAdapter<any>({
|
||||
cache: streamsCache,
|
||||
endpointPrefix: '/picture-sources',
|
||||
reload: _reloadStreams,
|
||||
typeLabelKey: 'device.icon.entity.picture_source',
|
||||
typeLabelFallback: 'Picture source',
|
||||
cardSelectors: (id) => [`[data-stream-id="${CSS.escape(id)}"]`],
|
||||
bodyExtras: (rec) => ({ stream_type: (rec as any)?.stream_type ?? 'raw' }),
|
||||
}));
|
||||
|
||||
registerIconEntityType('capture_template', makeSimpleIconAdapter<any>({
|
||||
cache: captureTemplatesCache,
|
||||
endpointPrefix: '/capture-templates',
|
||||
reload: _reloadStreams,
|
||||
typeLabelKey: 'device.icon.entity.capture_template',
|
||||
typeLabelFallback: 'Capture template',
|
||||
cardSelectors: (id) => [`[data-card-section="raw-templates"] [data-template-id="${CSS.escape(id)}"]`],
|
||||
}));
|
||||
|
||||
registerIconEntityType('pp_template', makeSimpleIconAdapter<any>({
|
||||
cache: ppTemplatesCache,
|
||||
endpointPrefix: '/postprocessing-templates',
|
||||
reload: _reloadStreams,
|
||||
typeLabelKey: 'device.icon.entity.pp_template',
|
||||
typeLabelFallback: 'Post-processing template',
|
||||
cardSelectors: (id) => [`[data-pp-template-id="${CSS.escape(id)}"]`],
|
||||
}));
|
||||
|
||||
registerIconEntityType('cspt', makeSimpleIconAdapter<any>({
|
||||
cache: csptCache,
|
||||
endpointPrefix: '/color-strip-processing-templates',
|
||||
reload: _reloadStreams,
|
||||
typeLabelKey: 'device.icon.entity.cspt',
|
||||
typeLabelFallback: 'Color-strip processing template',
|
||||
cardSelectors: (id) => [`[data-cspt-id="${CSS.escape(id)}"]`],
|
||||
}));
|
||||
|
||||
registerIconEntityType('audio_source', makeSimpleIconAdapter<any>({
|
||||
cache: audioSourcesCache,
|
||||
endpointPrefix: '/audio-sources',
|
||||
reload: _reloadStreams,
|
||||
typeLabelKey: 'device.icon.entity.audio_source',
|
||||
typeLabelFallback: 'Audio source',
|
||||
cardSelectors: (id) => [`[data-card-section="audio-sources"] [data-id="${CSS.escape(id)}"]`],
|
||||
bodyExtras: (rec) => ({ source_type: (rec as any)?.source_type ?? 'capture' }),
|
||||
}));
|
||||
|
||||
registerIconEntityType('audio_template', makeSimpleIconAdapter<any>({
|
||||
cache: audioTemplatesCache,
|
||||
endpointPrefix: '/audio-templates',
|
||||
reload: _reloadStreams,
|
||||
typeLabelKey: 'device.icon.entity.audio_template',
|
||||
typeLabelFallback: 'Audio template',
|
||||
cardSelectors: (id) => [`[data-audio-template-id="${CSS.escape(id)}"]`],
|
||||
}));
|
||||
|
||||
registerIconEntityType('gradient', makeSimpleIconAdapter<any>({
|
||||
cache: gradientsCache,
|
||||
endpointPrefix: '/gradients',
|
||||
reload: _reloadStreams,
|
||||
typeLabelKey: 'device.icon.entity.gradient',
|
||||
typeLabelFallback: 'Gradient',
|
||||
cardSelectors: (id) => [`[data-card-section="gradients"] [data-id="${CSS.escape(id)}"]`],
|
||||
}));
|
||||
|
||||
// ── TagInput instances for modals ──
|
||||
let _streamTagsInput: TagInput | null = null;
|
||||
@@ -454,6 +529,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
name: stream.name,
|
||||
metaHtml: details.metaHtml,
|
||||
leds: ['off'],
|
||||
...makeCardIconFields('picture_source', stream.id, stream),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneStream('${stream.id}')`,
|
||||
hideOnclick: `toggleCardHidden('${sectionKey}','${stream.id}')`,
|
||||
@@ -510,6 +586,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
name: template.name,
|
||||
metaHtml: escapeHtml(`${String(template.engine_type).toUpperCase()} · ${configEntries.length} keys`),
|
||||
leds: ['off'],
|
||||
...makeCardIconFields('capture_template', template.id, template),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneCaptureTemplate('${template.id}')`,
|
||||
hideOnclick: `toggleCardHidden('raw-templates','${template.id}')`,
|
||||
@@ -556,6 +633,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
name: tmpl.name,
|
||||
metaHtml: escapeHtml(`${filters.length} ${t('postprocessing.title') || 'filters'}`),
|
||||
leds: ['off'],
|
||||
...makeCardIconFields('pp_template', tmpl.id, tmpl),
|
||||
menu: {
|
||||
duplicateOnclick: `clonePPTemplate('${tmpl.id}')`,
|
||||
hideOnclick: `toggleCardHidden('proc-templates','${tmpl.id}')`,
|
||||
@@ -600,6 +678,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
name: tmpl.name,
|
||||
metaHtml: escapeHtml(`${filters.length} ${t('css_processing.title') || 'strip filters'}`),
|
||||
leds: ['off'],
|
||||
...makeCardIconFields('cspt', tmpl.id, tmpl),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneCSPT('${tmpl.id}')`,
|
||||
hideOnclick: `toggleCardHidden('css-proc-templates','${tmpl.id}')`,
|
||||
@@ -795,6 +874,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
name: src.name,
|
||||
metaHtml: escapeHtml(metaText),
|
||||
leds: ['off'],
|
||||
...makeCardIconFields('audio_source', src.id, src),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneAudioSource('${src.id}')`,
|
||||
hideOnclick: `toggleCardHidden('${sectionKey}','${src.id}')`,
|
||||
@@ -850,6 +930,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
name: template.name,
|
||||
metaHtml: escapeHtml(`${String(template.engine_type).toUpperCase()} · ${configEntries.length} keys`),
|
||||
leds: ['off'],
|
||||
...makeCardIconFields('audio_template', template.id, template),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneAudioTemplate('${template.id}')`,
|
||||
hideOnclick: `toggleCardHidden('audio-templates','${template.id}')`,
|
||||
@@ -896,6 +977,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
name: g.name,
|
||||
metaHtml: escapeHtml(`${g.stops.length} ${t('gradient.stops_label') || 'stops'}`),
|
||||
leds: ['off'],
|
||||
...(g.is_builtin ? {} : makeCardIconFields('gradient', g.id, g)),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneGradient('${g.id}')`,
|
||||
hideOnclick: `toggleCardHidden('gradients','${g.id}')`,
|
||||
|
||||
@@ -11,9 +11,27 @@ import { ICON_CLOCK, ICON_CLONE, ICON_EDIT, ICON_START, ICON_PAUSE } from '../co
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import { loadPictureSources } from './streams.ts';
|
||||
import type { SyncClock } from '../types.ts';
|
||||
|
||||
registerIconEntityType('sync_clock', makeSimpleIconAdapter<SyncClock>({
|
||||
cache: syncClocksCache,
|
||||
endpointPrefix: '/sync-clocks',
|
||||
reload: async () => {
|
||||
syncClocksCache.invalidate();
|
||||
if (typeof (window as any).loadIntegrations === 'function') {
|
||||
await (window as any).loadIntegrations();
|
||||
}
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.sync_clock',
|
||||
typeLabelFallback: 'Sync clock',
|
||||
cardSelectors: (id) => [
|
||||
`[data-card-section="sync-clocks"] [data-id="${CSS.escape(id)}"]`,
|
||||
],
|
||||
}));
|
||||
|
||||
// ── Auto-name ──
|
||||
|
||||
let _scNameManuallyEdited = false;
|
||||
@@ -245,6 +263,7 @@ export function createSyncClockCard(clock: SyncClock) {
|
||||
name: clock.name,
|
||||
metaHtml: escapeHtml(`${statusLabel} · ${clock.speed}x`),
|
||||
leds,
|
||||
...makeCardIconFields('sync_clock', clock.id, clock),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneSyncClock('${clock.id}')`,
|
||||
hideOnclick: `toggleCardHidden('sync-clocks','${clock.id}')`,
|
||||
|
||||
@@ -30,8 +30,27 @@ import {
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import { openAuthedWs } from '../core/ws-auth.ts';
|
||||
import { IconSelect, showTypePicker } from '../core/icon-select.ts';
|
||||
|
||||
registerIconEntityType('value_source', makeSimpleIconAdapter<any>({
|
||||
cache: valueSourcesCache,
|
||||
endpointPrefix: '/value-sources',
|
||||
reload: async () => {
|
||||
valueSourcesCache.invalidate();
|
||||
if (typeof (window as any).loadIntegrations === 'function') {
|
||||
await (window as any).loadIntegrations();
|
||||
}
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.value_source',
|
||||
typeLabelFallback: 'Value source',
|
||||
cardSelectors: (id) => [
|
||||
`[data-card-section="value-sources"] [data-id="${CSS.escape(id)}"]`,
|
||||
],
|
||||
bodyExtras: (rec) => ({ source_type: (rec as any)?.source_type ?? 'static' }),
|
||||
}));
|
||||
import type { IconSelectItem } from '../core/icon-select.ts';
|
||||
import * as P from '../core/icon-paths.ts';
|
||||
import { EntitySelect } from '../core/entity-palette.ts';
|
||||
@@ -1398,6 +1417,7 @@ export function createValueSourceCard(src: ValueSource) {
|
||||
name: src.name,
|
||||
metaHtml: escapeHtml(metaText),
|
||||
leds: ['off'],
|
||||
...makeCardIconFields('value_source', src.id, src),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneValueSource('${src.id}')`,
|
||||
hideOnclick: `toggleCardHidden('value-sources','${src.id}')`,
|
||||
|
||||
@@ -13,8 +13,25 @@ import { IconSelect } from '../core/icon-select.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { makeCardIconFields } from '../core/card-icon.ts';
|
||||
import { registerIconEntityType, makeSimpleIconAdapter } from './icon-picker.ts';
|
||||
import type { WeatherSource } from '../types.ts';
|
||||
|
||||
registerIconEntityType('weather_source', makeSimpleIconAdapter<WeatherSource>({
|
||||
cache: weatherSourcesCache,
|
||||
endpointPrefix: '/weather-sources',
|
||||
reload: async () => {
|
||||
if (typeof (window as any).loadIntegrations === 'function') {
|
||||
await (window as any).loadIntegrations();
|
||||
}
|
||||
},
|
||||
typeLabelKey: 'device.icon.entity.weather_source',
|
||||
typeLabelFallback: 'Weather source',
|
||||
cardSelectors: (id) => [
|
||||
`[data-card-section="weather-sources"] [data-id="${CSS.escape(id)}"]`,
|
||||
],
|
||||
}));
|
||||
|
||||
const ICON_WEATHER = `<svg class="icon" viewBox="0 0 24 24">${P.cloudSun}</svg>`;
|
||||
const _icon = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||
|
||||
@@ -282,6 +299,7 @@ export function createWeatherSourceCard(source: WeatherSource) {
|
||||
name: source.name,
|
||||
metaHtml: escapeHtml(`${providerLabel} · ${coords}`),
|
||||
leds: ['on'],
|
||||
...makeCardIconFields('weather_source', source.id, source),
|
||||
menu: {
|
||||
duplicateOnclick: `cloneWeatherSource('${source.id}')`,
|
||||
hideOnclick: `toggleCardHidden('weather-sources','${source.id}')`,
|
||||
|
||||
@@ -228,6 +228,8 @@ export interface ColorStripSource {
|
||||
tags: string[];
|
||||
overlay_active: boolean;
|
||||
clock_id?: string;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@@ -328,6 +330,8 @@ export interface PatternTemplate {
|
||||
rectangles: KeyColorRectangle[];
|
||||
tags: string[];
|
||||
description?: string;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -358,6 +362,8 @@ interface ValueSourceBase {
|
||||
return_type: 'float' | 'color';
|
||||
description?: string;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -518,6 +524,8 @@ interface AudioSourceBase {
|
||||
source_type: AudioSourceType;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -549,6 +557,8 @@ interface PictureSourceBase {
|
||||
stream_type: PictureSourceType;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -608,6 +618,8 @@ export interface ScenePreset {
|
||||
targets: TargetSnapshot[];
|
||||
order: number;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -622,6 +634,8 @@ export interface SyncClock {
|
||||
tags: string[];
|
||||
is_running: boolean;
|
||||
elapsed_time: number;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -636,6 +650,8 @@ export interface WeatherSource {
|
||||
update_interval: number;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -657,6 +673,8 @@ export interface HomeAssistantSource {
|
||||
entity_count: number;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -694,6 +712,8 @@ export interface MQTTSource {
|
||||
connected: boolean;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -728,6 +748,8 @@ export interface Asset {
|
||||
description?: string;
|
||||
tags: string[];
|
||||
prebuilt: boolean;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -772,6 +794,8 @@ export interface Automation {
|
||||
is_active: boolean;
|
||||
last_activated_at?: string;
|
||||
last_deactivated_at?: string;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -790,6 +814,8 @@ export interface CaptureTemplate {
|
||||
engine_config: Record<string, any>;
|
||||
tags: string[];
|
||||
description?: string;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -800,6 +826,8 @@ export interface PostprocessingTemplate {
|
||||
filters: FilterInstance[];
|
||||
tags: string[];
|
||||
description?: string;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -810,6 +838,8 @@ export interface ColorStripProcessingTemplate {
|
||||
filters: FilterInstance[];
|
||||
tags: string[];
|
||||
description?: string;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -821,6 +851,8 @@ export interface AudioTemplate {
|
||||
engine_config: Record<string, any>;
|
||||
tags: string[];
|
||||
description?: string;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -940,6 +972,8 @@ export interface GameIntegration {
|
||||
enabled: boolean;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@@ -585,6 +585,25 @@
|
||||
"device.icon.entity.device": "Device",
|
||||
"device.icon.entity.target": "LED target",
|
||||
"device.icon.entity.ha_light_target": "HA light target",
|
||||
"device.icon.entity.picture_source": "Picture source",
|
||||
"device.icon.entity.audio_source": "Audio source",
|
||||
"device.icon.entity.weather_source": "Weather source",
|
||||
"device.icon.entity.value_source": "Value source",
|
||||
"device.icon.entity.mqtt_source": "MQTT source",
|
||||
"device.icon.entity.ha_source": "Home Assistant source",
|
||||
"device.icon.entity.automation": "Automation",
|
||||
"device.icon.entity.scene_preset": "Scene preset",
|
||||
"device.icon.entity.sync_clock": "Sync clock",
|
||||
"device.icon.entity.game_integration": "Game integration",
|
||||
"device.icon.entity.audio_processing_template": "Audio processing template",
|
||||
"device.icon.entity.pattern_template": "Pattern template",
|
||||
"device.icon.entity.capture_template": "Capture template",
|
||||
"device.icon.entity.pp_template": "Post-processing template",
|
||||
"device.icon.entity.cspt": "Color-strip processing template",
|
||||
"device.icon.entity.audio_template": "Audio template",
|
||||
"device.icon.entity.gradient": "Gradient",
|
||||
"device.icon.entity.color_strip_source": "Color strip",
|
||||
"device.icon.entity.asset": "Asset",
|
||||
"device.icon.inherited_from": "Inherited from %s",
|
||||
"device.icon.override_inherited": "Override inherited icon…",
|
||||
"device.icon.use_inherited": "Use inherited",
|
||||
|
||||
@@ -603,6 +603,25 @@
|
||||
"device.icon.entity.device": "Устройство",
|
||||
"device.icon.entity.target": "LED-цель",
|
||||
"device.icon.entity.ha_light_target": "HA-светильник",
|
||||
"device.icon.entity.picture_source": "Источник изображения",
|
||||
"device.icon.entity.audio_source": "Источник аудио",
|
||||
"device.icon.entity.weather_source": "Источник погоды",
|
||||
"device.icon.entity.value_source": "Источник значения",
|
||||
"device.icon.entity.mqtt_source": "Источник MQTT",
|
||||
"device.icon.entity.ha_source": "Источник Home Assistant",
|
||||
"device.icon.entity.automation": "Автоматизация",
|
||||
"device.icon.entity.scene_preset": "Сцена",
|
||||
"device.icon.entity.sync_clock": "Часы синхронизации",
|
||||
"device.icon.entity.game_integration": "Игровая интеграция",
|
||||
"device.icon.entity.audio_processing_template": "Шаблон обработки аудио",
|
||||
"device.icon.entity.pattern_template": "Шаблон паттерна",
|
||||
"device.icon.entity.capture_template": "Шаблон захвата",
|
||||
"device.icon.entity.pp_template": "Шаблон постобработки",
|
||||
"device.icon.entity.cspt": "Шаблон обработки полоски",
|
||||
"device.icon.entity.audio_template": "Аудиошаблон",
|
||||
"device.icon.entity.gradient": "Градиент",
|
||||
"device.icon.entity.color_strip_source": "Цветная полоска",
|
||||
"device.icon.entity.asset": "Ассет",
|
||||
"device.icon.inherited_from": "Унаследовано от %s",
|
||||
"device.icon.override_inherited": "Заменить унаследованную иконку…",
|
||||
"device.icon.use_inherited": "Использовать унаследованную",
|
||||
|
||||
@@ -603,6 +603,25 @@
|
||||
"device.icon.entity.device": "设备",
|
||||
"device.icon.entity.target": "LED 目标",
|
||||
"device.icon.entity.ha_light_target": "HA 灯目标",
|
||||
"device.icon.entity.picture_source": "画面源",
|
||||
"device.icon.entity.audio_source": "音频源",
|
||||
"device.icon.entity.weather_source": "天气源",
|
||||
"device.icon.entity.value_source": "数值源",
|
||||
"device.icon.entity.mqtt_source": "MQTT 源",
|
||||
"device.icon.entity.ha_source": "Home Assistant 源",
|
||||
"device.icon.entity.automation": "自动化",
|
||||
"device.icon.entity.scene_preset": "场景预设",
|
||||
"device.icon.entity.sync_clock": "同步时钟",
|
||||
"device.icon.entity.game_integration": "游戏集成",
|
||||
"device.icon.entity.audio_processing_template": "音频处理模板",
|
||||
"device.icon.entity.pattern_template": "图案模板",
|
||||
"device.icon.entity.capture_template": "捕获模板",
|
||||
"device.icon.entity.pp_template": "后处理模板",
|
||||
"device.icon.entity.cspt": "色带处理模板",
|
||||
"device.icon.entity.audio_template": "音频模板",
|
||||
"device.icon.entity.gradient": "渐变",
|
||||
"device.icon.entity.color_strip_source": "色带",
|
||||
"device.icon.entity.asset": "资源",
|
||||
"device.icon.inherited_from": "继承自 %s",
|
||||
"device.icon.override_inherited": "覆盖继承的图标…",
|
||||
"device.icon.use_inherited": "使用继承的",
|
||||
|
||||
@@ -43,9 +43,12 @@ class Asset:
|
||||
tags: List[str] = field(default_factory=list)
|
||||
prebuilt: bool = False # True for shipped assets
|
||||
deleted: bool = False # soft-delete for prebuilt assets
|
||||
# Custom card icon (frontend display only)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
d = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"filename": self.filename,
|
||||
@@ -60,6 +63,11 @@ class Asset:
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Asset":
|
||||
@@ -75,6 +83,8 @@ class Asset:
|
||||
tags=data.get("tags", []),
|
||||
prebuilt=bool(data.get("prebuilt", False)),
|
||||
deleted=bool(data.get("deleted", False)),
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||
)
|
||||
|
||||
@@ -95,6 +95,8 @@ class AssetStore(BaseSqliteStore[Asset]):
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
prebuilt: bool = False,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> Asset:
|
||||
"""Create a new asset from uploaded file data.
|
||||
|
||||
@@ -156,6 +158,8 @@ class AssetStore(BaseSqliteStore[Asset]):
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
prebuilt=prebuilt,
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[asset_id] = asset
|
||||
@@ -171,6 +175,8 @@ class AssetStore(BaseSqliteStore[Asset]):
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> Asset:
|
||||
"""Update asset metadata (not the file itself)."""
|
||||
asset = self.get(asset_id)
|
||||
@@ -182,6 +188,10 @@ class AssetStore(BaseSqliteStore[Asset]):
|
||||
asset.description = description
|
||||
if tags is not None:
|
||||
asset.tags = tags
|
||||
if icon is not None:
|
||||
asset.icon = icon or ""
|
||||
if icon_color is not None:
|
||||
asset.icon_color = icon_color or ""
|
||||
|
||||
asset.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(asset_id, asset)
|
||||
|
||||
@@ -18,10 +18,12 @@ class AudioProcessingTemplate:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert template to dictionary."""
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"filters": [f.to_dict() for f in self.filters],
|
||||
@@ -30,6 +32,11 @@ class AudioProcessingTemplate:
|
||||
"description": self.description,
|
||||
"tags": self.tags,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "AudioProcessingTemplate":
|
||||
@@ -52,4 +59,6 @@ class AudioProcessingTemplate:
|
||||
),
|
||||
description=data.get("description"),
|
||||
tags=data.get("tags", []),
|
||||
icon=data.get("icon", "") or "",
|
||||
icon_color=data.get("icon_color", "") or "",
|
||||
)
|
||||
|
||||
@@ -38,6 +38,8 @@ class AudioProcessingTemplateStore(BaseSqliteStore[AudioProcessingTemplate]):
|
||||
filters: Optional[List[FilterInstance]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> AudioProcessingTemplate:
|
||||
self._check_name_unique(name)
|
||||
|
||||
@@ -60,6 +62,8 @@ class AudioProcessingTemplateStore(BaseSqliteStore[AudioProcessingTemplate]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[template_id] = template
|
||||
@@ -75,6 +79,8 @@ class AudioProcessingTemplateStore(BaseSqliteStore[AudioProcessingTemplate]):
|
||||
filters: Optional[List[FilterInstance]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> AudioProcessingTemplate:
|
||||
template = self.get(template_id)
|
||||
|
||||
@@ -91,6 +97,10 @@ class AudioProcessingTemplateStore(BaseSqliteStore[AudioProcessingTemplate]):
|
||||
template.description = description
|
||||
if tags is not None:
|
||||
template.tags = tags
|
||||
if icon is not None:
|
||||
template.icon = icon
|
||||
if icon_color is not None:
|
||||
template.icon_color = icon_color
|
||||
|
||||
template.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(template_id, template)
|
||||
|
||||
@@ -21,10 +21,12 @@ class AudioSource:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert source to dictionary. Subclasses extend this."""
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"source_type": self.source_type,
|
||||
@@ -39,6 +41,11 @@ class AudioSource:
|
||||
"audio_source_id": None,
|
||||
"audio_processing_template_id": None,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "AudioSource":
|
||||
@@ -72,6 +79,8 @@ def _parse_common_fields(data: dict) -> dict:
|
||||
tags=data.get("tags", []),
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
icon=data.get("icon", "") or "",
|
||||
icon_color=data.get("icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ class AudioSourceStore(BaseSqliteStore[AudioSource]):
|
||||
audio_template_id: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
audio_processing_template_id: Optional[str] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> AudioSource:
|
||||
self._check_name_unique(name)
|
||||
|
||||
@@ -83,6 +85,8 @@ class AudioSourceStore(BaseSqliteStore[AudioSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
audio_source_id=audio_source_id,
|
||||
audio_processing_template_id=audio_processing_template_id,
|
||||
)
|
||||
@@ -95,6 +99,8 @@ class AudioSourceStore(BaseSqliteStore[AudioSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
device_index=device_index if device_index is not None else -1,
|
||||
is_loopback=bool(is_loopback) if is_loopback is not None else True,
|
||||
audio_template_id=audio_template_id,
|
||||
@@ -117,6 +123,8 @@ class AudioSourceStore(BaseSqliteStore[AudioSource]):
|
||||
audio_template_id: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
audio_processing_template_id: Optional[str] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> AudioSource:
|
||||
source = self.get(source_id)
|
||||
|
||||
@@ -128,6 +136,10 @@ class AudioSourceStore(BaseSqliteStore[AudioSource]):
|
||||
source.description = description
|
||||
if tags is not None:
|
||||
source.tags = tags
|
||||
if icon is not None:
|
||||
source.icon = icon
|
||||
if icon_color is not None:
|
||||
source.icon_color = icon_color
|
||||
|
||||
if isinstance(source, CaptureAudioSource):
|
||||
if device_index is not None:
|
||||
|
||||
@@ -17,10 +17,12 @@ class AudioCaptureTemplate:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert template to dictionary."""
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"engine_type": self.engine_type,
|
||||
@@ -30,6 +32,11 @@ class AudioCaptureTemplate:
|
||||
"description": self.description,
|
||||
"tags": self.tags,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "AudioCaptureTemplate":
|
||||
@@ -51,4 +58,6 @@ class AudioCaptureTemplate:
|
||||
),
|
||||
description=data.get("description"),
|
||||
tags=data.get("tags", []),
|
||||
icon=data.get("icon", "") or "",
|
||||
icon_color=data.get("icon_color", "") or "",
|
||||
)
|
||||
|
||||
@@ -77,6 +77,8 @@ class AudioTemplateStore(BaseSqliteStore[AudioCaptureTemplate]):
|
||||
engine_config: Dict[str, Any],
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> AudioCaptureTemplate:
|
||||
self._check_name_unique(name)
|
||||
|
||||
@@ -91,6 +93,8 @@ class AudioTemplateStore(BaseSqliteStore[AudioCaptureTemplate]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[template_id] = template
|
||||
@@ -106,6 +110,8 @@ class AudioTemplateStore(BaseSqliteStore[AudioCaptureTemplate]):
|
||||
engine_config: Optional[Dict[str, Any]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> AudioCaptureTemplate:
|
||||
template = self.get(template_id)
|
||||
|
||||
@@ -120,6 +126,10 @@ class AudioTemplateStore(BaseSqliteStore[AudioCaptureTemplate]):
|
||||
template.description = description
|
||||
if tags is not None:
|
||||
template.tags = tags
|
||||
if icon is not None:
|
||||
template.icon = icon
|
||||
if icon_color is not None:
|
||||
template.icon_color = icon_color
|
||||
|
||||
template.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(template_id, template)
|
||||
|
||||
@@ -243,6 +243,9 @@ class Automation:
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
tags: List[str] = field(default_factory=list)
|
||||
# Custom card icon (frontend display only)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
# Backward-compatible property aliases
|
||||
@property
|
||||
@@ -262,7 +265,7 @@ class Automation:
|
||||
self.rules = value
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
d = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"enabled": self.enabled,
|
||||
@@ -275,6 +278,11 @@ class Automation:
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "Automation":
|
||||
@@ -302,6 +310,8 @@ class Automation:
|
||||
deactivation_mode=data.get("deactivation_mode", "none"),
|
||||
deactivation_scene_preset_id=data.get("deactivation_scene_preset_id"),
|
||||
tags=data.get("tags", []),
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
created_at=datetime.fromisoformat(
|
||||
data.get("created_at", datetime.now(timezone.utc).isoformat())
|
||||
),
|
||||
|
||||
@@ -34,6 +34,8 @@ class AutomationStore(BaseSqliteStore[Automation]):
|
||||
deactivation_mode: str = "none",
|
||||
deactivation_scene_preset_id: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
# Legacy parameter aliases
|
||||
condition_logic: Optional[str] = None,
|
||||
conditions: Optional[List[Rule]] = None,
|
||||
@@ -63,6 +65,8 @@ class AutomationStore(BaseSqliteStore[Automation]):
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[automation_id] = automation
|
||||
@@ -81,6 +85,8 @@ class AutomationStore(BaseSqliteStore[Automation]):
|
||||
deactivation_mode: Optional[str] = None,
|
||||
deactivation_scene_preset_id: str = "__unset__",
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
# Legacy parameter aliases
|
||||
condition_logic: Optional[str] = None,
|
||||
conditions: Optional[List[Rule]] = None,
|
||||
@@ -112,6 +118,10 @@ class AutomationStore(BaseSqliteStore[Automation]):
|
||||
)
|
||||
if tags is not None:
|
||||
automation.tags = tags
|
||||
if icon is not None:
|
||||
automation.icon = icon or ""
|
||||
if icon_color is not None:
|
||||
automation.icon_color = icon_color or ""
|
||||
|
||||
automation.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(automation_id, automation)
|
||||
|
||||
@@ -20,10 +20,12 @@ class ColorStripProcessingTemplate:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert template to dictionary."""
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"filters": [f.to_dict() for f in self.filters],
|
||||
@@ -32,6 +34,11 @@ class ColorStripProcessingTemplate:
|
||||
"description": self.description,
|
||||
"tags": self.tags,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "ColorStripProcessingTemplate":
|
||||
@@ -54,4 +61,6 @@ class ColorStripProcessingTemplate:
|
||||
),
|
||||
description=data.get("description"),
|
||||
tags=data.get("tags", []),
|
||||
icon=data.get("icon", "") or "",
|
||||
icon_color=data.get("icon_color", "") or "",
|
||||
)
|
||||
|
||||
@@ -75,6 +75,8 @@ class ColorStripProcessingTemplateStore(BaseSqliteStore[ColorStripProcessingTemp
|
||||
filters: Optional[List[FilterInstance]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> ColorStripProcessingTemplate:
|
||||
self._check_name_unique(name)
|
||||
|
||||
@@ -94,6 +96,8 @@ class ColorStripProcessingTemplateStore(BaseSqliteStore[ColorStripProcessingTemp
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[template_id] = template
|
||||
@@ -109,6 +113,8 @@ class ColorStripProcessingTemplateStore(BaseSqliteStore[ColorStripProcessingTemp
|
||||
filters: Optional[List[FilterInstance]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> ColorStripProcessingTemplate:
|
||||
template = self.get(template_id)
|
||||
|
||||
@@ -122,6 +128,10 @@ class ColorStripProcessingTemplateStore(BaseSqliteStore[ColorStripProcessingTemp
|
||||
template.description = description
|
||||
if tags is not None:
|
||||
template.tags = tags
|
||||
if icon is not None:
|
||||
template.icon = icon
|
||||
if icon_color is not None:
|
||||
template.icon_color = icon_color
|
||||
|
||||
template.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(template_id, template)
|
||||
|
||||
@@ -63,6 +63,8 @@ class ColorStripSource:
|
||||
description: Optional[str] = None
|
||||
clock_id: Optional[str] = None # optional SyncClock reference
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
@property
|
||||
def sharable(self) -> bool:
|
||||
@@ -75,7 +77,7 @@ class ColorStripSource:
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert source to dictionary. Subclasses extend this with their own fields."""
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"source_type": self.source_type,
|
||||
@@ -85,6 +87,11 @@ class ColorStripSource:
|
||||
"clock_id": self.clock_id,
|
||||
"tags": self.tags,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def create_from_kwargs(
|
||||
@@ -155,6 +162,8 @@ def _parse_css_common(data: dict) -> dict:
|
||||
tags=data.get("tags", []),
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
icon=data.get("icon", "") or "",
|
||||
icon_color=data.get("icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ class ColorStripStore(BaseSqliteStore[ColorStripSource]):
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
tags = kwargs.pop("tags", None) or []
|
||||
icon = kwargs.pop("icon", None) or ""
|
||||
icon_color = kwargs.pop("icon_color", None) or ""
|
||||
|
||||
source = ColorStripSource.create_instance(
|
||||
source_type,
|
||||
@@ -69,6 +71,8 @@ class ColorStripStore(BaseSqliteStore[ColorStripSource]):
|
||||
tags=tags,
|
||||
**kwargs,
|
||||
)
|
||||
source.icon = icon
|
||||
source.icon_color = icon_color
|
||||
|
||||
self._items[source_id] = source
|
||||
self._save_item(source_id, source)
|
||||
@@ -110,6 +114,14 @@ class ColorStripStore(BaseSqliteStore[ColorStripSource]):
|
||||
if tags is not None:
|
||||
source.tags = tags
|
||||
|
||||
icon = kwargs.pop("icon", None)
|
||||
if icon is not None:
|
||||
source.icon = icon
|
||||
|
||||
icon_color = kwargs.pop("icon_color", None)
|
||||
if icon_color is not None:
|
||||
source.icon_color = icon_color
|
||||
|
||||
# -- Type-specific fields --
|
||||
source.apply_update(**kwargs)
|
||||
|
||||
|
||||
@@ -80,9 +80,11 @@ class GameIntegrationConfig:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
d = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"adapter_type": self.adapter_type,
|
||||
@@ -94,6 +96,11 @@ class GameIntegrationConfig:
|
||||
"description": self.description,
|
||||
"tags": list(self.tags),
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "GameIntegrationConfig":
|
||||
@@ -117,6 +124,8 @@ class GameIntegrationConfig:
|
||||
),
|
||||
description=data.get("description"),
|
||||
tags=data.get("tags", []),
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -128,6 +137,8 @@ class GameIntegrationConfig:
|
||||
event_mappings: Optional[List[EventMapping]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> "GameIntegrationConfig":
|
||||
"""Factory method to create a new config with generated ID and timestamps."""
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -142,6 +153,8 @@ class GameIntegrationConfig:
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
def apply_update(
|
||||
@@ -153,6 +166,8 @@ class GameIntegrationConfig:
|
||||
event_mappings: Optional[List[EventMapping]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> "GameIntegrationConfig":
|
||||
"""Return a new config with updated fields (immutable update)."""
|
||||
return GameIntegrationConfig(
|
||||
@@ -166,4 +181,6 @@ class GameIntegrationConfig:
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
description=description if description is not None else self.description,
|
||||
tags=tags if tags is not None else self.tags,
|
||||
icon=icon if icon is not None else self.icon,
|
||||
icon_color=icon_color if icon_color is not None else self.icon_color,
|
||||
)
|
||||
|
||||
@@ -41,6 +41,8 @@ class GameIntegrationStore(BaseSqliteStore[GameIntegrationConfig]):
|
||||
event_mappings: Optional[List[EventMapping]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> GameIntegrationConfig:
|
||||
"""Create a new game integration config.
|
||||
|
||||
@@ -70,6 +72,8 @@ class GameIntegrationStore(BaseSqliteStore[GameIntegrationConfig]):
|
||||
event_mappings=event_mappings,
|
||||
description=description,
|
||||
tags=tags,
|
||||
icon=icon,
|
||||
icon_color=icon_color,
|
||||
)
|
||||
|
||||
self._items[config.id] = config
|
||||
@@ -88,6 +92,8 @@ class GameIntegrationStore(BaseSqliteStore[GameIntegrationConfig]):
|
||||
event_mappings: Optional[List[EventMapping]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> GameIntegrationConfig:
|
||||
"""Update an existing game integration config.
|
||||
|
||||
@@ -122,6 +128,8 @@ class GameIntegrationStore(BaseSqliteStore[GameIntegrationConfig]):
|
||||
event_mappings=event_mappings,
|
||||
description=description,
|
||||
tags=tags,
|
||||
icon=icon,
|
||||
icon_color=icon_color,
|
||||
)
|
||||
|
||||
self._items[integration_id] = updated
|
||||
|
||||
@@ -22,9 +22,11 @@ class Gradient:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"stops": self.stops,
|
||||
@@ -34,6 +36,11 @@ class Gradient:
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Gradient":
|
||||
@@ -46,6 +53,8 @@ class Gradient:
|
||||
tags=data.get("tags", []),
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||
icon=data.get("icon", "") or "",
|
||||
icon_color=data.get("icon_color", "") or "",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -130,6 +130,8 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
||||
stops: list,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> Gradient:
|
||||
self._check_name_unique(name)
|
||||
gid = f"gr_{uuid.uuid4().hex[:8]}"
|
||||
@@ -143,6 +145,8 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
self._items[gid] = gradient
|
||||
self._save_item(gid, gradient)
|
||||
@@ -156,6 +160,8 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
||||
stops: Optional[list] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> Gradient:
|
||||
gradient = self.get(gradient_id)
|
||||
if gradient.is_builtin:
|
||||
@@ -169,6 +175,10 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
||||
gradient.description = description
|
||||
if tags is not None:
|
||||
gradient.tags = tags
|
||||
if icon is not None:
|
||||
gradient.icon = icon
|
||||
if icon_color is not None:
|
||||
gradient.icon_color = icon_color
|
||||
gradient.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(gradient_id, gradient)
|
||||
logger.info(f"Updated gradient: {gradient_id}")
|
||||
|
||||
@@ -50,6 +50,8 @@ class HomeAssistantSource:
|
||||
) # optional allowlist (e.g. ["sensor.*"])
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
@property
|
||||
def ws_url(self) -> str:
|
||||
@@ -72,7 +74,7 @@ class HomeAssistantSource:
|
||||
# Always persist the token in encrypted envelope form. If the field
|
||||
# already contains an envelope, encrypt() is a no-op.
|
||||
stored_token = secret_box.encrypt(self.token) if self.token else ""
|
||||
return {
|
||||
d = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"host": self.host,
|
||||
@@ -84,6 +86,11 @@ class HomeAssistantSource:
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "HomeAssistantSource":
|
||||
@@ -99,4 +106,6 @@ class HomeAssistantSource:
|
||||
token=token,
|
||||
use_ssl=data.get("use_ssl", False),
|
||||
entity_filters=data.get("entity_filters") or [],
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
)
|
||||
|
||||
@@ -73,6 +73,8 @@ class HomeAssistantStore(BaseSqliteStore[HomeAssistantSource]):
|
||||
entity_filters: Optional[List[str]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> HomeAssistantSource:
|
||||
if not host:
|
||||
raise ValueError("host is required")
|
||||
@@ -95,6 +97,8 @@ class HomeAssistantStore(BaseSqliteStore[HomeAssistantSource]):
|
||||
entity_filters=entity_filters or [],
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[sid] = source
|
||||
@@ -112,6 +116,8 @@ class HomeAssistantStore(BaseSqliteStore[HomeAssistantSource]):
|
||||
entity_filters: Optional[List[str]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> HomeAssistantSource:
|
||||
existing = self.get(source_id)
|
||||
|
||||
@@ -131,6 +137,8 @@ class HomeAssistantStore(BaseSqliteStore[HomeAssistantSource]):
|
||||
),
|
||||
description=description if description is not None else existing.description,
|
||||
tags=tags if tags is not None else existing.tags,
|
||||
icon=icon if icon is not None else existing.icon,
|
||||
icon_color=icon_color if icon_color is not None else existing.icon_color,
|
||||
)
|
||||
|
||||
self._items[source_id] = updated
|
||||
|
||||
@@ -48,9 +48,11 @@ class MQTTSource:
|
||||
base_topic: str = "ledgrab"
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
d = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"broker_host": self.broker_host,
|
||||
@@ -64,6 +66,11 @@ class MQTTSource:
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "MQTTSource":
|
||||
@@ -76,4 +83,6 @@ class MQTTSource:
|
||||
password=data.get("password", ""),
|
||||
client_id=data.get("client_id", "ledgrab"),
|
||||
base_topic=data.get("base_topic", "ledgrab"),
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
)
|
||||
|
||||
@@ -37,6 +37,8 @@ class MQTTSourceStore(BaseSqliteStore[MQTTSource]):
|
||||
base_topic: str = "ledgrab",
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> MQTTSource:
|
||||
if not broker_host:
|
||||
raise ValueError("broker_host is required")
|
||||
@@ -59,6 +61,8 @@ class MQTTSourceStore(BaseSqliteStore[MQTTSource]):
|
||||
base_topic=base_topic,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[sid] = source
|
||||
@@ -78,6 +82,8 @@ class MQTTSourceStore(BaseSqliteStore[MQTTSource]):
|
||||
base_topic: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> MQTTSource:
|
||||
existing = self.get(source_id)
|
||||
|
||||
@@ -97,6 +103,8 @@ class MQTTSourceStore(BaseSqliteStore[MQTTSource]):
|
||||
base_topic=base_topic if base_topic is not None else existing.base_topic,
|
||||
description=description if description is not None else existing.description,
|
||||
tags=tags if tags is not None else existing.tags,
|
||||
icon=icon if icon is not None else existing.icon,
|
||||
icon_color=icon_color if icon_color is not None else existing.icon_color,
|
||||
)
|
||||
|
||||
self._items[source_id] = updated
|
||||
|
||||
@@ -46,10 +46,12 @@ class PatternTemplate:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"rectangles": [r.to_dict() for r in self.rectangles],
|
||||
@@ -58,6 +60,11 @@ class PatternTemplate:
|
||||
"description": self.description,
|
||||
"tags": self.tags,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "PatternTemplate":
|
||||
@@ -79,4 +86,6 @@ class PatternTemplate:
|
||||
),
|
||||
description=data.get("description"),
|
||||
tags=data.get("tags", []),
|
||||
icon=data.get("icon", "") or "",
|
||||
icon_color=data.get("icon_color", "") or "",
|
||||
)
|
||||
|
||||
@@ -60,6 +60,8 @@ class PatternTemplateStore(BaseSqliteStore[PatternTemplate]):
|
||||
rectangles: Optional[List[KeyColorRectangle]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> PatternTemplate:
|
||||
self._check_name_unique(name)
|
||||
|
||||
@@ -77,6 +79,8 @@ class PatternTemplateStore(BaseSqliteStore[PatternTemplate]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[template_id] = template
|
||||
@@ -92,6 +96,8 @@ class PatternTemplateStore(BaseSqliteStore[PatternTemplate]):
|
||||
rectangles: Optional[List[KeyColorRectangle]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> PatternTemplate:
|
||||
template = self.get(template_id)
|
||||
|
||||
@@ -104,6 +110,10 @@ class PatternTemplateStore(BaseSqliteStore[PatternTemplate]):
|
||||
template.description = description
|
||||
if tags is not None:
|
||||
template.tags = tags
|
||||
if icon is not None:
|
||||
template.icon = icon
|
||||
if icon_color is not None:
|
||||
template.icon_color = icon_color
|
||||
|
||||
template.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(template_id, template)
|
||||
|
||||
@@ -23,10 +23,12 @@ class PictureSource:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert stream to dictionary. Subclasses extend this."""
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"stream_type": self.stream_type,
|
||||
@@ -50,6 +52,11 @@ class PictureSource:
|
||||
"resolution_limit": None,
|
||||
"clock_id": None,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "PictureSource":
|
||||
@@ -80,6 +87,8 @@ def _parse_common_fields(data: dict) -> dict:
|
||||
tags=data.get("tags", []),
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
icon=data.get("icon", "") or "",
|
||||
icon_color=data.get("icon_color", "") or "",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@ class PictureSourceStore(BaseSqliteStore[PictureSource]):
|
||||
end_time: Optional[float] = None,
|
||||
resolution_limit: Optional[int] = None,
|
||||
clock_id: Optional[str] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> PictureSource:
|
||||
"""Create a new picture source.
|
||||
|
||||
@@ -141,6 +143,8 @@ class PictureSourceStore(BaseSqliteStore[PictureSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
stream: PictureSource
|
||||
@@ -201,6 +205,8 @@ class PictureSourceStore(BaseSqliteStore[PictureSource]):
|
||||
end_time: Optional[float] = None,
|
||||
resolution_limit: Optional[int] = None,
|
||||
clock_id: Optional[str] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> PictureSource:
|
||||
"""Update an existing picture source.
|
||||
|
||||
@@ -228,6 +234,10 @@ class PictureSourceStore(BaseSqliteStore[PictureSource]):
|
||||
stream.description = description
|
||||
if tags is not None:
|
||||
stream.tags = tags
|
||||
if icon is not None:
|
||||
stream.icon = icon
|
||||
if icon_color is not None:
|
||||
stream.icon_color = icon_color
|
||||
|
||||
if isinstance(stream, ScreenCapturePictureSource):
|
||||
if display_index is not None:
|
||||
|
||||
@@ -18,10 +18,12 @@ class PostprocessingTemplate:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert template to dictionary."""
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"filters": [f.to_dict() for f in self.filters],
|
||||
@@ -30,6 +32,11 @@ class PostprocessingTemplate:
|
||||
"description": self.description,
|
||||
"tags": self.tags,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "PostprocessingTemplate":
|
||||
@@ -52,4 +59,6 @@ class PostprocessingTemplate:
|
||||
),
|
||||
description=data.get("description"),
|
||||
tags=data.get("tags", []),
|
||||
icon=data.get("icon", "") or "",
|
||||
icon_color=data.get("icon_color", "") or "",
|
||||
)
|
||||
|
||||
@@ -68,6 +68,8 @@ class PostprocessingTemplateStore(BaseSqliteStore[PostprocessingTemplate]):
|
||||
filters: Optional[List[FilterInstance]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> PostprocessingTemplate:
|
||||
self._check_name_unique(name)
|
||||
|
||||
@@ -90,6 +92,8 @@ class PostprocessingTemplateStore(BaseSqliteStore[PostprocessingTemplate]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[template_id] = template
|
||||
@@ -105,6 +109,8 @@ class PostprocessingTemplateStore(BaseSqliteStore[PostprocessingTemplate]):
|
||||
filters: Optional[List[FilterInstance]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> PostprocessingTemplate:
|
||||
template = self.get(template_id)
|
||||
|
||||
@@ -121,6 +127,10 @@ class PostprocessingTemplateStore(BaseSqliteStore[PostprocessingTemplate]):
|
||||
template.description = description
|
||||
if tags is not None:
|
||||
template.tags = tags
|
||||
if icon is not None:
|
||||
template.icon = icon
|
||||
if icon_color is not None:
|
||||
template.icon_color = icon_color
|
||||
|
||||
template.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(template_id, template)
|
||||
|
||||
@@ -43,13 +43,16 @@ class ScenePreset:
|
||||
name: str
|
||||
description: str = ""
|
||||
tags: List[str] = field(default_factory=list)
|
||||
# Custom card icon (frontend display only)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
targets: List[TargetSnapshot] = field(default_factory=list)
|
||||
order: int = 0
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
d = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
@@ -59,6 +62,11 @@ class ScenePreset:
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "ScenePreset":
|
||||
@@ -67,6 +75,8 @@ class ScenePreset:
|
||||
name=data["name"],
|
||||
description=data.get("description", ""),
|
||||
tags=data.get("tags", []),
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])],
|
||||
order=data.get("order", 0),
|
||||
created_at=datetime.fromisoformat(
|
||||
|
||||
@@ -48,6 +48,8 @@ class ScenePresetStore(BaseSqliteStore[ScenePreset]):
|
||||
order: Optional[int] = None,
|
||||
targets: Optional[List[TargetSnapshot]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> ScenePreset:
|
||||
preset = self.get(preset_id)
|
||||
|
||||
@@ -62,6 +64,10 @@ class ScenePresetStore(BaseSqliteStore[ScenePreset]):
|
||||
preset.targets = targets
|
||||
if tags is not None:
|
||||
preset.tags = tags
|
||||
if icon is not None:
|
||||
preset.icon = icon or ""
|
||||
if icon_color is not None:
|
||||
preset.icon_color = icon_color or ""
|
||||
|
||||
preset.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(preset_id, preset)
|
||||
|
||||
@@ -21,9 +21,11 @@ class SyncClock:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
d = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"speed": self.speed,
|
||||
@@ -32,6 +34,11 @@ class SyncClock:
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "SyncClock":
|
||||
@@ -43,4 +50,6 @@ class SyncClock:
|
||||
tags=data.get("tags", []),
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
)
|
||||
|
||||
@@ -30,6 +30,8 @@ class SyncClockStore(BaseSqliteStore[SyncClock]):
|
||||
speed: float = 1.0,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> SyncClock:
|
||||
self._check_name_unique(name)
|
||||
cid = f"sc_{uuid.uuid4().hex[:8]}"
|
||||
@@ -43,6 +45,8 @@ class SyncClockStore(BaseSqliteStore[SyncClock]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[cid] = clock
|
||||
@@ -57,6 +61,8 @@ class SyncClockStore(BaseSqliteStore[SyncClock]):
|
||||
speed: Optional[float] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> SyncClock:
|
||||
clock = self.get(clock_id)
|
||||
|
||||
@@ -69,6 +75,10 @@ class SyncClockStore(BaseSqliteStore[SyncClock]):
|
||||
clock.description = description
|
||||
if tags is not None:
|
||||
clock.tags = tags
|
||||
if icon is not None:
|
||||
clock.icon = icon
|
||||
if icon_color is not None:
|
||||
clock.icon_color = icon_color
|
||||
|
||||
clock.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(clock_id, clock)
|
||||
|
||||
@@ -17,6 +17,8 @@ class CaptureTemplate:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert template to dictionary.
|
||||
@@ -24,7 +26,7 @@ class CaptureTemplate:
|
||||
Returns:
|
||||
Dictionary representation
|
||||
"""
|
||||
return {
|
||||
d = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"engine_type": self.engine_type,
|
||||
@@ -34,6 +36,11 @@ class CaptureTemplate:
|
||||
"description": self.description,
|
||||
"tags": self.tags,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "CaptureTemplate":
|
||||
@@ -62,4 +69,6 @@ class CaptureTemplate:
|
||||
),
|
||||
description=data.get("description"),
|
||||
tags=data.get("tags", []),
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
)
|
||||
|
||||
@@ -71,6 +71,8 @@ class TemplateStore(BaseSqliteStore[CaptureTemplate]):
|
||||
engine_config: Dict[str, Any],
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> CaptureTemplate:
|
||||
self._check_name_unique(name)
|
||||
|
||||
@@ -85,6 +87,8 @@ class TemplateStore(BaseSqliteStore[CaptureTemplate]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[template_id] = template
|
||||
@@ -101,6 +105,8 @@ class TemplateStore(BaseSqliteStore[CaptureTemplate]):
|
||||
engine_config: Optional[Dict[str, Any]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> CaptureTemplate:
|
||||
template = self.get(template_id)
|
||||
|
||||
@@ -115,6 +121,10 @@ class TemplateStore(BaseSqliteStore[CaptureTemplate]):
|
||||
template.description = description
|
||||
if tags is not None:
|
||||
template.tags = tags
|
||||
if icon is not None:
|
||||
template.icon = icon
|
||||
if icon_color is not None:
|
||||
template.icon_color = icon_color
|
||||
|
||||
template.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(template_id, template)
|
||||
|
||||
@@ -36,10 +36,12 @@ class ValueSource:
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert source to dictionary. Subclasses extend this."""
|
||||
return {
|
||||
d: dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"source_type": self.source_type,
|
||||
@@ -65,6 +67,11 @@ class ValueSource:
|
||||
"latitude": None,
|
||||
"longitude": None,
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "ValueSource":
|
||||
@@ -95,6 +102,8 @@ def _parse_common_fields(data: dict) -> dict:
|
||||
tags=data.get("tags", []),
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +85,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
sensor_label: Optional[str] = None,
|
||||
poll_interval: Optional[float] = None,
|
||||
clock_id: Optional[str] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> ValueSource:
|
||||
_VALID = (
|
||||
"static",
|
||||
@@ -110,6 +112,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
common_tags = tags or []
|
||||
common_icon = icon or ""
|
||||
common_icon_color = icon_color or ""
|
||||
|
||||
if source_type == "static":
|
||||
source: ValueSource = StaticValueSource(
|
||||
@@ -120,6 +124,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
value=value if value is not None else 1.0,
|
||||
)
|
||||
elif source_type == "animated":
|
||||
@@ -131,6 +137,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
waveform=waveform or "sine",
|
||||
speed=speed if speed is not None else 10.0,
|
||||
min_value=min_value if min_value is not None else 0.0,
|
||||
@@ -145,6 +153,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
audio_source_id=audio_source_id or "",
|
||||
mode=mode or "rms",
|
||||
sensitivity=sensitivity if sensitivity is not None else 1.0,
|
||||
@@ -165,6 +175,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
schedule=schedule_data,
|
||||
min_value=min_value if min_value is not None else 0.0,
|
||||
max_value=max_value if max_value is not None else 1.0,
|
||||
@@ -178,6 +190,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
picture_source_id=picture_source_id or "",
|
||||
scene_behavior=scene_behavior or "complement",
|
||||
sensitivity=sensitivity if sensitivity is not None else 1.0,
|
||||
@@ -194,6 +208,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
speed=speed if speed is not None else 1.0,
|
||||
use_real_time=bool(use_real_time) if use_real_time is not None else False,
|
||||
latitude=latitude if latitude is not None else 50.0,
|
||||
@@ -210,6 +226,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
color=color if isinstance(color, list) and len(color) == 3 else [255, 255, 255],
|
||||
)
|
||||
elif source_type == "animated_color":
|
||||
@@ -221,6 +239,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
colors=(
|
||||
colors
|
||||
if isinstance(colors, list) and len(colors) >= 2
|
||||
@@ -242,6 +262,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
schedule=schedule_data,
|
||||
)
|
||||
elif source_type == "ha_entity":
|
||||
@@ -257,6 +279,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
ha_source_id=ha_source_id,
|
||||
entity_id=entity_id,
|
||||
attribute=attribute or "",
|
||||
@@ -275,6 +299,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
value_source_id=value_source_id,
|
||||
gradient_id=gradient_id or "",
|
||||
easing=easing or "linear",
|
||||
@@ -290,6 +316,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
color_strip_source_id=color_strip_source_id,
|
||||
led_start=led_start if led_start is not None else 0,
|
||||
led_end=led_end if led_end is not None else -1,
|
||||
@@ -306,6 +334,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=common_tags,
|
||||
icon=common_icon,
|
||||
icon_color=common_icon_color,
|
||||
metric=m,
|
||||
min_value=min_value if min_value is not None else 0.0,
|
||||
max_value=max_value if max_value is not None else 100.0,
|
||||
@@ -363,6 +393,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
sensor_label: Optional[str] = None,
|
||||
poll_interval: Optional[float] = None,
|
||||
clock_id: Optional[str] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> ValueSource:
|
||||
source = self.get(source_id)
|
||||
|
||||
@@ -374,6 +406,10 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
source.description = description
|
||||
if tags is not None:
|
||||
source.tags = tags
|
||||
if icon is not None:
|
||||
source.icon = icon
|
||||
if icon_color is not None:
|
||||
source.icon_color = icon_color
|
||||
|
||||
if isinstance(source, StaticValueSource):
|
||||
if value is not None:
|
||||
|
||||
@@ -46,9 +46,11 @@ class WeatherSource:
|
||||
update_interval: int = 600 # seconds (10 min default)
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
icon: str = ""
|
||||
icon_color: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
d = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"provider": self.provider,
|
||||
@@ -61,6 +63,11 @@ class WeatherSource:
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
if self.icon:
|
||||
d["icon"] = self.icon
|
||||
if self.icon_color:
|
||||
d["icon_color"] = self.icon_color
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "WeatherSource":
|
||||
@@ -72,4 +79,6 @@ class WeatherSource:
|
||||
latitude=data.get("latitude", 50.0),
|
||||
longitude=data.get("longitude", 0.0),
|
||||
update_interval=data.get("update_interval", 600),
|
||||
icon=data.get("icon", ""),
|
||||
icon_color=data.get("icon_color", ""),
|
||||
)
|
||||
|
||||
@@ -36,6 +36,8 @@ class WeatherSourceStore(BaseSqliteStore[WeatherSource]):
|
||||
update_interval: int = 600,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> WeatherSource:
|
||||
from ledgrab.core.weather.weather_provider import PROVIDER_REGISTRY
|
||||
|
||||
@@ -65,6 +67,8 @@ class WeatherSourceStore(BaseSqliteStore[WeatherSource]):
|
||||
update_interval=update_interval,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
icon=icon or "",
|
||||
icon_color=icon_color or "",
|
||||
)
|
||||
|
||||
self._items[sid] = source
|
||||
@@ -83,6 +87,8 @@ class WeatherSourceStore(BaseSqliteStore[WeatherSource]):
|
||||
update_interval: Optional[int] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
icon: Optional[str] = None,
|
||||
icon_color: Optional[str] = None,
|
||||
) -> WeatherSource:
|
||||
existing = self.get(source_id)
|
||||
|
||||
@@ -118,6 +124,8 @@ class WeatherSourceStore(BaseSqliteStore[WeatherSource]):
|
||||
),
|
||||
description=description if description is not None else existing.description,
|
||||
tags=tags if tags is not None else existing.tags,
|
||||
icon=icon if icon is not None else existing.icon,
|
||||
icon_color=icon_color if icon_color is not None else existing.icon_color,
|
||||
)
|
||||
|
||||
self._items[source_id] = updated
|
||||
|
||||
Reference in New Issue
Block a user