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
|
# 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
|
## Device Event Notifications
|
||||||
|
|
||||||
Notify the user when LED devices come online/go offline (configured targets), and when new
|
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,
|
name=body.name,
|
||||||
description=body.description,
|
description=body.description,
|
||||||
tags=body.tags,
|
tags=body.tags,
|
||||||
|
icon=body.icon,
|
||||||
|
icon_color=body.icon_color,
|
||||||
)
|
)
|
||||||
except EntityNotFoundError:
|
except EntityNotFoundError:
|
||||||
raise HTTPException(status_code=404, detail=f"Asset not found: {asset_id}")
|
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,
|
updated_at=t.updated_at,
|
||||||
description=t.description,
|
description=t.description,
|
||||||
tags=t.tags,
|
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,
|
filters=filters,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("audio_processing_template", "created", template.id)
|
fire_entity_event("audio_processing_template", "created", template.id)
|
||||||
return _apt_to_response(template)
|
return _apt_to_response(template)
|
||||||
@@ -129,6 +133,8 @@ async def update_audio_processing_template(
|
|||||||
filters=filters,
|
filters=filters,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("audio_processing_template", "updated", template_id)
|
fire_entity_event("audio_processing_template", "updated", template_id)
|
||||||
# Hot-update: rebuild filter pipelines for running streams using this template
|
# Hot-update: rebuild filter pipelines for running streams using this template
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ _RESPONSE_MAP = {
|
|||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
device_index=s.device_index,
|
device_index=s.device_index,
|
||||||
is_loopback=s.is_loopback,
|
is_loopback=s.is_loopback,
|
||||||
audio_template_id=s.audio_template_id,
|
audio_template_id=s.audio_template_id,
|
||||||
@@ -57,6 +59,8 @@ _RESPONSE_MAP = {
|
|||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_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_source_id=s.audio_source_id,
|
||||||
audio_processing_template_id=s.audio_processing_template_id,
|
audio_processing_template_id=s.audio_processing_template_id,
|
||||||
),
|
),
|
||||||
@@ -75,6 +79,8 @@ def _to_response(source: AudioSource) -> AudioSourceResponse:
|
|||||||
tags=source.tags,
|
tags=source.tags,
|
||||||
created_at=source.created_at,
|
created_at=source.created_at,
|
||||||
updated_at=source.updated_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),
|
device_index=getattr(source, "device_index", -1),
|
||||||
is_loopback=getattr(source, "is_loopback", True),
|
is_loopback=getattr(source, "is_loopback", True),
|
||||||
audio_template_id=getattr(source, "audio_template_id", None),
|
audio_template_id=getattr(source, "audio_template_id", None),
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ async def list_audio_templates(
|
|||||||
created_at=t.created_at,
|
created_at=t.created_at,
|
||||||
updated_at=t.updated_at,
|
updated_at=t.updated_at,
|
||||||
description=t.description,
|
description=t.description,
|
||||||
|
icon=getattr(t, "icon", "") or "",
|
||||||
|
icon_color=getattr(t, "icon_color", "") or "",
|
||||||
)
|
)
|
||||||
for t in templates
|
for t in templates
|
||||||
]
|
]
|
||||||
@@ -81,6 +83,8 @@ async def create_audio_template(
|
|||||||
engine_config=data.engine_config,
|
engine_config=data.engine_config,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("audio_template", "created", template.id)
|
fire_entity_event("audio_template", "created", template.id)
|
||||||
return AudioTemplateResponse(
|
return AudioTemplateResponse(
|
||||||
@@ -92,6 +96,8 @@ async def create_audio_template(
|
|||||||
created_at=template.created_at,
|
created_at=template.created_at,
|
||||||
updated_at=template.updated_at,
|
updated_at=template.updated_at,
|
||||||
description=template.description,
|
description=template.description,
|
||||||
|
icon=getattr(template, "icon", "") or "",
|
||||||
|
icon_color=getattr(template, "icon_color", "") or "",
|
||||||
)
|
)
|
||||||
except EntityNotFoundError as e:
|
except EntityNotFoundError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
@@ -127,6 +133,8 @@ async def get_audio_template(
|
|||||||
created_at=t.created_at,
|
created_at=t.created_at,
|
||||||
updated_at=t.updated_at,
|
updated_at=t.updated_at,
|
||||||
description=t.description,
|
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,
|
engine_config=data.engine_config,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("audio_template", "updated", template_id)
|
fire_entity_event("audio_template", "updated", template_id)
|
||||||
return AudioTemplateResponse(
|
return AudioTemplateResponse(
|
||||||
@@ -161,6 +171,8 @@ async def update_audio_template(
|
|||||||
created_at=t.created_at,
|
created_at=t.created_at,
|
||||||
updated_at=t.updated_at,
|
updated_at=t.updated_at,
|
||||||
description=t.description,
|
description=t.description,
|
||||||
|
icon=getattr(t, "icon", "") or "",
|
||||||
|
icon_color=getattr(t, "icon_color", "") or "",
|
||||||
)
|
)
|
||||||
except EntityNotFoundError as e:
|
except EntityNotFoundError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(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_activated_at=state.get("last_activated_at"),
|
||||||
last_deactivated_at=state.get("last_deactivated_at"),
|
last_deactivated_at=state.get("last_deactivated_at"),
|
||||||
tags=automation.tags,
|
tags=automation.tags,
|
||||||
|
icon=getattr(automation, "icon", "") or "",
|
||||||
|
icon_color=getattr(automation, "icon_color", "") or "",
|
||||||
created_at=automation.created_at,
|
created_at=automation.created_at,
|
||||||
updated_at=automation.updated_at,
|
updated_at=automation.updated_at,
|
||||||
)
|
)
|
||||||
@@ -191,6 +193,8 @@ async def create_automation(
|
|||||||
deactivation_mode=data.deactivation_mode,
|
deactivation_mode=data.deactivation_mode,
|
||||||
deactivation_scene_preset_id=data.deactivation_scene_preset_id,
|
deactivation_scene_preset_id=data.deactivation_scene_preset_id,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
if automation.enabled:
|
if automation.enabled:
|
||||||
@@ -285,6 +289,8 @@ async def update_automation(
|
|||||||
rules=rules,
|
rules=rules,
|
||||||
deactivation_mode=data.deactivation_mode,
|
deactivation_mode=data.deactivation_mode,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
if data.scene_preset_id is not None:
|
if data.scene_preset_id is not None:
|
||||||
update_kwargs["scene_preset_id"] = data.scene_preset_id
|
update_kwargs["scene_preset_id"] = data.scene_preset_id
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ def _cspt_to_response(t) -> ColorStripProcessingTemplateResponse:
|
|||||||
updated_at=t.updated_at,
|
updated_at=t.updated_at,
|
||||||
description=t.description,
|
description=t.description,
|
||||||
tags=t.tags,
|
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,
|
filters=filters,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("cspt", "created", template.id)
|
fire_entity_event("cspt", "created", template.id)
|
||||||
return _cspt_to_response(template)
|
return _cspt_to_response(template)
|
||||||
@@ -141,6 +145,8 @@ async def update_cspt(
|
|||||||
filters=filters,
|
filters=filters,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("cspt", "updated", template_id)
|
fire_entity_event("cspt", "updated", template_id)
|
||||||
return _cspt_to_response(template)
|
return _cspt_to_response(template)
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ def _common_response_kwargs(source, overlay_active: bool = False) -> dict:
|
|||||||
tags=source.tags,
|
tags=source.tags,
|
||||||
created_at=source.created_at,
|
created_at=source.created_at,
|
||||||
updated_at=source.updated_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,
|
updated_at=config.updated_at,
|
||||||
description=config.description,
|
description=config.description,
|
||||||
tags=config.tags,
|
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,
|
event_mappings=mappings,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
fire_entity_event("game_integration", "created", config.id)
|
fire_entity_event("game_integration", "created", config.id)
|
||||||
@@ -323,6 +327,8 @@ async def update_integration(
|
|||||||
event_mappings=mappings,
|
event_mappings=mappings,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
fire_entity_event("game_integration", "updated", integration_id)
|
fire_entity_event("game_integration", "updated", integration_id)
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ def _to_response(gradient: Gradient) -> GradientResponse:
|
|||||||
tags=gradient.tags,
|
tags=gradient.tags,
|
||||||
created_at=gradient.created_at,
|
created_at=gradient.created_at,
|
||||||
updated_at=gradient.updated_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],
|
stops=[s.model_dump() for s in data.stops],
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("gradient", "created", gradient.id)
|
fire_entity_event("gradient", "created", gradient.id)
|
||||||
return _to_response(gradient)
|
return _to_response(gradient)
|
||||||
@@ -103,6 +107,8 @@ async def update_gradient(
|
|||||||
stops=stops,
|
stops=stops,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("gradient", "updated", gradient_id)
|
fire_entity_event("gradient", "updated", gradient_id)
|
||||||
return _to_response(gradient)
|
return _to_response(gradient)
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ def _to_response(
|
|||||||
entity_count=len(runtime.get_all_states()) if runtime else 0,
|
entity_count=len(runtime.get_all_states()) if runtime else 0,
|
||||||
description=source.description,
|
description=source.description,
|
||||||
tags=source.tags,
|
tags=source.tags,
|
||||||
|
icon=getattr(source, "icon", "") or "",
|
||||||
|
icon_color=getattr(source, "icon_color", "") or "",
|
||||||
created_at=source.created_at,
|
created_at=source.created_at,
|
||||||
updated_at=source.updated_at,
|
updated_at=source.updated_at,
|
||||||
token=token_field,
|
token=token_field,
|
||||||
@@ -105,6 +107,8 @@ async def create_ha_source(
|
|||||||
entity_filters=data.entity_filters,
|
entity_filters=data.entity_filters,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
@@ -158,6 +162,8 @@ async def update_ha_source(
|
|||||||
entity_filters=data.entity_filters,
|
entity_filters=data.entity_filters,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
except EntityNotFoundError:
|
except EntityNotFoundError:
|
||||||
raise HTTPException(status_code=404, detail=f"Home Assistant source {source_id} not found")
|
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,
|
connected=runtime.is_connected if runtime else False,
|
||||||
description=source.description,
|
description=source.description,
|
||||||
tags=source.tags,
|
tags=source.tags,
|
||||||
|
icon=getattr(source, "icon", "") or "",
|
||||||
|
icon_color=getattr(source, "icon_color", "") or "",
|
||||||
created_at=source.created_at,
|
created_at=source.created_at,
|
||||||
updated_at=source.updated_at,
|
updated_at=source.updated_at,
|
||||||
)
|
)
|
||||||
@@ -90,6 +92,8 @@ async def create_mqtt_source(
|
|||||||
base_topic=data.base_topic,
|
base_topic=data.base_topic,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
@@ -139,6 +143,8 @@ async def update_mqtt_source(
|
|||||||
base_topic=data.base_topic,
|
base_topic=data.base_topic,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
except EntityNotFoundError:
|
except EntityNotFoundError:
|
||||||
raise HTTPException(status_code=404, detail=f"MQTT source {source_id} not found")
|
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,
|
updated_at=t.updated_at,
|
||||||
description=t.description,
|
description=t.description,
|
||||||
tags=t.tags,
|
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,
|
rectangles=rectangles,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("pattern_template", "created", template.id)
|
fire_entity_event("pattern_template", "created", template.id)
|
||||||
return _pat_template_to_response(template)
|
return _pat_template_to_response(template)
|
||||||
@@ -139,6 +143,8 @@ async def update_pattern_template(
|
|||||||
rectangles=rectangles,
|
rectangles=rectangles,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("pattern_template", "updated", template_id)
|
fire_entity_event("pattern_template", "updated", template_id)
|
||||||
return _pat_template_to_response(template)
|
return _pat_template_to_response(template)
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ _RESPONSE_MAP = {
|
|||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
display_index=s.display_index,
|
display_index=s.display_index,
|
||||||
capture_template_id=s.capture_template_id,
|
capture_template_id=s.capture_template_id,
|
||||||
target_fps=s.target_fps,
|
target_fps=s.target_fps,
|
||||||
@@ -76,6 +78,8 @@ _RESPONSE_MAP = {
|
|||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_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,
|
source_stream_id=s.source_stream_id,
|
||||||
postprocessing_template_id=s.postprocessing_template_id,
|
postprocessing_template_id=s.postprocessing_template_id,
|
||||||
),
|
),
|
||||||
@@ -86,6 +90,8 @@ _RESPONSE_MAP = {
|
|||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_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,
|
image_asset_id=s.image_asset_id,
|
||||||
),
|
),
|
||||||
VideoCaptureSource: lambda s: VideoPictureSourceResponse(
|
VideoCaptureSource: lambda s: VideoPictureSourceResponse(
|
||||||
@@ -95,6 +101,8 @@ _RESPONSE_MAP = {
|
|||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_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,
|
video_asset_id=s.video_asset_id,
|
||||||
loop=s.loop,
|
loop=s.loop,
|
||||||
playback_speed=s.playback_speed,
|
playback_speed=s.playback_speed,
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ def _pp_template_to_response(t) -> PostprocessingTemplateResponse:
|
|||||||
updated_at=t.updated_at,
|
updated_at=t.updated_at,
|
||||||
description=t.description,
|
description=t.description,
|
||||||
tags=t.tags,
|
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,
|
filters=filters,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("pp_template", "created", template.id)
|
fire_entity_event("pp_template", "created", template.id)
|
||||||
return _pp_template_to_response(template)
|
return _pp_template_to_response(template)
|
||||||
@@ -143,6 +147,8 @@ async def update_pp_template(
|
|||||||
filters=filters,
|
filters=filters,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("pp_template", "updated", template_id)
|
fire_entity_event("pp_template", "updated", template_id)
|
||||||
return _pp_template_to_response(template)
|
return _pp_template_to_response(template)
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ def _preset_to_response(preset: ScenePreset) -> ScenePresetResponse:
|
|||||||
],
|
],
|
||||||
order=preset.order,
|
order=preset.order,
|
||||||
tags=preset.tags,
|
tags=preset.tags,
|
||||||
|
icon=getattr(preset, "icon", "") or "",
|
||||||
|
icon_color=getattr(preset, "icon_color", "") or "",
|
||||||
created_at=preset.created_at,
|
created_at=preset.created_at,
|
||||||
updated_at=preset.updated_at,
|
updated_at=preset.updated_at,
|
||||||
)
|
)
|
||||||
@@ -84,6 +86,8 @@ async def create_scene_preset(
|
|||||||
targets=targets,
|
targets=targets,
|
||||||
order=store.count(),
|
order=store.count(),
|
||||||
tags=data.tags if data.tags is not None else [],
|
tags=data.tags if data.tags is not None else [],
|
||||||
|
icon=data.icon or "",
|
||||||
|
icon_color=data.icon_color or "",
|
||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=now,
|
updated_at=now,
|
||||||
)
|
)
|
||||||
@@ -182,6 +186,8 @@ async def update_scene_preset(
|
|||||||
order=data.order,
|
order=data.order,
|
||||||
targets=new_targets,
|
targets=new_targets,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ def _to_response(clock: SyncClock, manager: SyncClockManager) -> SyncClockRespon
|
|||||||
speed=rt.speed if rt else clock.speed,
|
speed=rt.speed if rt else clock.speed,
|
||||||
description=clock.description,
|
description=clock.description,
|
||||||
tags=clock.tags,
|
tags=clock.tags,
|
||||||
|
icon=getattr(clock, "icon", "") or "",
|
||||||
|
icon_color=getattr(clock, "icon_color", "") or "",
|
||||||
is_running=rt.is_running if rt else True,
|
is_running=rt.is_running if rt else True,
|
||||||
elapsed_time=rt.get_time() if rt else 0.0,
|
elapsed_time=rt.get_time() if rt else 0.0,
|
||||||
created_at=clock.created_at,
|
created_at=clock.created_at,
|
||||||
@@ -75,6 +77,8 @@ async def create_sync_clock(
|
|||||||
speed=data.speed,
|
speed=data.speed,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
fire_entity_event("sync_clock", "created", clock.id)
|
fire_entity_event("sync_clock", "created", clock.id)
|
||||||
return _to_response(clock, manager)
|
return _to_response(clock, manager)
|
||||||
@@ -120,6 +124,8 @@ async def update_sync_clock(
|
|||||||
speed=data.speed,
|
speed=data.speed,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
# Hot-update runtime speed
|
# Hot-update runtime speed
|
||||||
if data.speed is not None:
|
if data.speed is not None:
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ logger = get_logger(__name__)
|
|||||||
router = APIRouter()
|
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 =====
|
# ===== CAPTURE TEMPLATE ENDPOINTS =====
|
||||||
|
|
||||||
|
|
||||||
@@ -57,19 +72,7 @@ async def list_templates(
|
|||||||
try:
|
try:
|
||||||
templates = template_store.get_all_templates()
|
templates = template_store.get_all_templates()
|
||||||
|
|
||||||
template_responses = [
|
template_responses = [_template_to_response(t) for t in templates]
|
||||||
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
|
|
||||||
]
|
|
||||||
|
|
||||||
return TemplateListResponse(
|
return TemplateListResponse(
|
||||||
templates=template_responses,
|
templates=template_responses,
|
||||||
@@ -100,19 +103,12 @@ async def create_template(
|
|||||||
engine_config=template_data.engine_config,
|
engine_config=template_data.engine_config,
|
||||||
description=template_data.description,
|
description=template_data.description,
|
||||||
tags=template_data.tags,
|
tags=template_data.tags,
|
||||||
|
icon=template_data.icon,
|
||||||
|
icon_color=template_data.icon_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
fire_entity_event("capture_template", "created", template.id)
|
fire_entity_event("capture_template", "created", template.id)
|
||||||
return TemplateResponse(
|
return _template_to_response(template)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
except EntityNotFoundError as e:
|
except EntityNotFoundError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
@@ -138,16 +134,7 @@ async def get_template(
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail=f"Template {template_id} not found")
|
raise HTTPException(status_code=404, detail=f"Template {template_id} not found")
|
||||||
|
|
||||||
return TemplateResponse(
|
return _template_to_response(template)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
@@ -168,19 +155,12 @@ async def update_template(
|
|||||||
engine_config=update_data.engine_config,
|
engine_config=update_data.engine_config,
|
||||||
description=update_data.description,
|
description=update_data.description,
|
||||||
tags=update_data.tags,
|
tags=update_data.tags,
|
||||||
|
icon=update_data.icon,
|
||||||
|
icon_color=update_data.icon_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
fire_entity_event("capture_template", "updated", template_id)
|
fire_entity_event("capture_template", "updated", template_id)
|
||||||
return TemplateResponse(
|
return _template_to_response(template)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
except EntityNotFoundError as e:
|
except EntityNotFoundError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
value=s.value,
|
value=s.value,
|
||||||
@@ -73,6 +75,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
waveform=s.waveform,
|
waveform=s.waveform,
|
||||||
@@ -85,6 +89,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
audio_source_id=s.audio_source_id,
|
audio_source_id=s.audio_source_id,
|
||||||
@@ -100,6 +106,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
speed=s.speed,
|
speed=s.speed,
|
||||||
@@ -114,6 +122,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
color=list(s.color),
|
color=list(s.color),
|
||||||
@@ -123,6 +133,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
colors=[list(c) for c in s.colors],
|
colors=[list(c) for c in s.colors],
|
||||||
@@ -135,6 +147,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
schedule=s.schedule,
|
schedule=s.schedule,
|
||||||
@@ -144,6 +158,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
ha_source_id=s.ha_source_id,
|
ha_source_id=s.ha_source_id,
|
||||||
@@ -158,6 +174,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
value_source_id=s.value_source_id,
|
value_source_id=s.value_source_id,
|
||||||
@@ -169,6 +187,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
color_strip_source_id=s.color_strip_source_id,
|
color_strip_source_id=s.color_strip_source_id,
|
||||||
@@ -180,6 +200,8 @@ _RESPONSE_MAP = {
|
|||||||
name=s.name,
|
name=s.name,
|
||||||
description=s.description,
|
description=s.description,
|
||||||
tags=s.tags,
|
tags=s.tags,
|
||||||
|
icon=getattr(s, "icon", "") or "",
|
||||||
|
icon_color=getattr(s, "icon_color", "") or "",
|
||||||
created_at=s.created_at,
|
created_at=s.created_at,
|
||||||
updated_at=s.updated_at,
|
updated_at=s.updated_at,
|
||||||
metric=s.metric,
|
metric=s.metric,
|
||||||
@@ -204,6 +226,8 @@ def _to_response(source: ValueSource) -> ValueSourceResponse:
|
|||||||
name=source.name,
|
name=source.name,
|
||||||
description=source.description,
|
description=source.description,
|
||||||
tags=source.tags,
|
tags=source.tags,
|
||||||
|
icon=getattr(source, "icon", "") or "",
|
||||||
|
icon_color=getattr(source, "icon_color", "") or "",
|
||||||
created_at=source.created_at,
|
created_at=source.created_at,
|
||||||
updated_at=source.updated_at,
|
updated_at=source.updated_at,
|
||||||
picture_source_id=source.picture_source_id,
|
picture_source_id=source.picture_source_id,
|
||||||
@@ -218,6 +242,8 @@ def _to_response(source: ValueSource) -> ValueSourceResponse:
|
|||||||
name=source.name,
|
name=source.name,
|
||||||
description=source.description,
|
description=source.description,
|
||||||
tags=source.tags,
|
tags=source.tags,
|
||||||
|
icon=getattr(source, "icon", "") or "",
|
||||||
|
icon_color=getattr(source, "icon_color", "") or "",
|
||||||
created_at=source.created_at,
|
created_at=source.created_at,
|
||||||
updated_at=source.updated_at,
|
updated_at=source.updated_at,
|
||||||
schedule=source.schedule,
|
schedule=source.schedule,
|
||||||
@@ -233,6 +259,8 @@ def _to_response(source: ValueSource) -> ValueSourceResponse:
|
|||||||
name=source.name,
|
name=source.name,
|
||||||
description=source.description,
|
description=source.description,
|
||||||
tags=source.tags,
|
tags=source.tags,
|
||||||
|
icon=getattr(source, "icon", "") or "",
|
||||||
|
icon_color=getattr(source, "icon_color", "") or "",
|
||||||
created_at=source.created_at,
|
created_at=source.created_at,
|
||||||
updated_at=source.updated_at,
|
updated_at=source.updated_at,
|
||||||
value=getattr(source, "value", 1.0),
|
value=getattr(source, "value", 1.0),
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ def _to_response(source: WeatherSource) -> WeatherSourceResponse:
|
|||||||
update_interval=d["update_interval"],
|
update_interval=d["update_interval"],
|
||||||
description=d.get("description"),
|
description=d.get("description"),
|
||||||
tags=d.get("tags", []),
|
tags=d.get("tags", []),
|
||||||
|
icon=getattr(source, "icon", "") or "",
|
||||||
|
icon_color=getattr(source, "icon_color", "") or "",
|
||||||
created_at=source.created_at,
|
created_at=source.created_at,
|
||||||
updated_at=source.updated_at,
|
updated_at=source.updated_at,
|
||||||
)
|
)
|
||||||
@@ -79,6 +81,8 @@ async def create_weather_source(
|
|||||||
update_interval=data.update_interval,
|
update_interval=data.update_interval,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
@@ -125,6 +129,8 @@ async def update_weather_source(
|
|||||||
update_interval=data.update_interval,
|
update_interval=data.update_interval,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
tags=data.tags,
|
tags=data.tags,
|
||||||
|
icon=data.icon,
|
||||||
|
icon_color=data.icon_color,
|
||||||
)
|
)
|
||||||
except EntityNotFoundError:
|
except EntityNotFoundError:
|
||||||
raise HTTPException(status_code=404, detail=f"Weather source {source_id} not found")
|
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")
|
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")
|
description: Optional[str] = Field(None, max_length=500, description="Optional description")
|
||||||
tags: Optional[List[str]] = Field(None, description="User-defined tags")
|
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):
|
class AssetResponse(BaseModel):
|
||||||
@@ -26,6 +36,16 @@ class AssetResponse(BaseModel):
|
|||||||
description: Optional[str] = Field(None, description="Description")
|
description: Optional[str] = Field(None, description="Description")
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
prebuilt: bool = Field(False, description="Whether this is a shipped prebuilt asset")
|
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")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class AudioProcessingTemplateUpdate(BaseModel):
|
||||||
@@ -28,6 +38,16 @@ class AudioProcessingTemplateUpdate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class AudioProcessingTemplateResponse(BaseModel):
|
||||||
@@ -42,6 +62,16 @@ class AudioProcessingTemplateResponse(BaseModel):
|
|||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
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):
|
class AudioProcessingTemplateListResponse(BaseModel):
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ class _AudioSourceResponseBase(BaseModel):
|
|||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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):
|
class CaptureAudioSourceResponse(_AudioSourceResponseBase):
|
||||||
@@ -53,6 +63,16 @@ class _AudioSourceCreateBase(BaseModel):
|
|||||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class CaptureAudioSourceCreate(_AudioSourceCreateBase):
|
||||||
@@ -87,6 +107,16 @@ class _AudioSourceUpdateBase(BaseModel):
|
|||||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
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)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class CaptureAudioSourceUpdate(_AudioSourceUpdateBase):
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ class AudioTemplateCreate(BaseModel):
|
|||||||
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class AudioTemplateUpdate(BaseModel):
|
||||||
@@ -26,6 +36,16 @@ class AudioTemplateUpdate(BaseModel):
|
|||||||
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class AudioTemplateResponse(BaseModel):
|
||||||
@@ -39,6 +59,16 @@ class AudioTemplateResponse(BaseModel):
|
|||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
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):
|
class AudioTemplateListResponse(BaseModel):
|
||||||
|
|||||||
@@ -67,6 +67,16 @@ class AutomationCreate(BaseModel):
|
|||||||
None, description="Scene preset for fallback deactivation"
|
None, description="Scene preset for fallback deactivation"
|
||||||
)
|
)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class AutomationUpdate(BaseModel):
|
||||||
@@ -84,6 +94,16 @@ class AutomationUpdate(BaseModel):
|
|||||||
None, description="Scene preset for fallback deactivation"
|
None, description="Scene preset for fallback deactivation"
|
||||||
)
|
)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class AutomationResponse(BaseModel):
|
||||||
@@ -108,6 +128,16 @@ class AutomationResponse(BaseModel):
|
|||||||
last_deactivated_at: Optional[datetime] = Field(
|
last_deactivated_at: Optional[datetime] = Field(
|
||||||
None, description="Last time this automation was deactivated"
|
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")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class ColorStripProcessingTemplateUpdate(BaseModel):
|
||||||
@@ -28,6 +38,16 @@ class ColorStripProcessingTemplateUpdate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class ColorStripProcessingTemplateResponse(BaseModel):
|
||||||
@@ -40,6 +60,16 @@ class ColorStripProcessingTemplateResponse(BaseModel):
|
|||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
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):
|
class ColorStripProcessingTemplateListResponse(BaseModel):
|
||||||
|
|||||||
@@ -95,6 +95,16 @@ class _CSSResponseBase(BaseModel):
|
|||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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):
|
class PictureCSSResponse(_CSSResponseBase):
|
||||||
@@ -266,6 +276,16 @@ class _CSSCreateBase(BaseModel):
|
|||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class PictureCSSCreate(_CSSCreateBase):
|
||||||
@@ -450,6 +470,16 @@ class _CSSUpdateBase(BaseModel):
|
|||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class PictureCSSUpdate(_CSSUpdateBase):
|
||||||
|
|||||||
@@ -42,6 +42,16 @@ class GameIntegrationCreate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Integration description", max_length=500)
|
description: Optional[str] = Field(None, description="Integration description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class GameIntegrationUpdate(BaseModel):
|
||||||
@@ -56,6 +66,16 @@ class GameIntegrationUpdate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Integration description", max_length=500)
|
description: Optional[str] = Field(None, description="Integration description", max_length=500)
|
||||||
tags: Optional[List[str]] = Field(None, description="User-defined tags")
|
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):
|
class GameIntegrationResponse(BaseModel):
|
||||||
@@ -71,6 +91,16 @@ class GameIntegrationResponse(BaseModel):
|
|||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Integration description")
|
description: Optional[str] = Field(None, description="Integration description")
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class GameIntegrationListResponse(BaseModel):
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ class GradientCreate(BaseModel):
|
|||||||
stops: List[GradientStopSchema] = Field(description="Color stops", min_length=2)
|
stops: List[GradientStopSchema] = Field(description="Color stops", min_length=2)
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class GradientUpdate(BaseModel):
|
||||||
@@ -29,6 +39,16 @@ class GradientUpdate(BaseModel):
|
|||||||
stops: Optional[List[GradientStopSchema]] = Field(None, description="Color stops", min_length=2)
|
stops: Optional[List[GradientStopSchema]] = Field(None, description="Color stops", min_length=2)
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class GradientResponse(BaseModel):
|
||||||
@@ -42,6 +62,16 @@ class GradientResponse(BaseModel):
|
|||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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):
|
class GradientListResponse(BaseModel):
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ class HomeAssistantSourceCreate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class HomeAssistantSourceUpdate(BaseModel):
|
||||||
@@ -30,6 +40,16 @@ class HomeAssistantSourceUpdate(BaseModel):
|
|||||||
entity_filters: Optional[List[str]] = Field(None, description="Entity ID filter patterns")
|
entity_filters: Optional[List[str]] = Field(None, description="Entity ID filter patterns")
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class HomeAssistantSourceResponse(BaseModel):
|
||||||
@@ -44,6 +64,16 @@ class HomeAssistantSourceResponse(BaseModel):
|
|||||||
entity_count: int = Field(default=0, description="Number of cached entities")
|
entity_count: int = Field(default=0, description="Number of cached entities")
|
||||||
description: Optional[str] = Field(None, description="Description")
|
description: Optional[str] = Field(None, description="Description")
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
token: Optional[str] = Field(
|
token: Optional[str] = Field(
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ class MQTTSourceCreate(BaseModel):
|
|||||||
base_topic: str = Field(default="ledgrab", description="Base topic prefix")
|
base_topic: str = Field(default="ledgrab", description="Base topic prefix")
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class MQTTSourceUpdate(BaseModel):
|
||||||
@@ -32,6 +42,16 @@ class MQTTSourceUpdate(BaseModel):
|
|||||||
base_topic: Optional[str] = Field(None, description="Base topic prefix")
|
base_topic: Optional[str] = Field(None, description="Base topic prefix")
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class MQTTSourceResponse(BaseModel):
|
||||||
@@ -48,6 +68,16 @@ class MQTTSourceResponse(BaseModel):
|
|||||||
connected: bool = Field(default=False, description="Whether the broker connection is active")
|
connected: bool = Field(default=False, description="Whether the broker connection is active")
|
||||||
description: Optional[str] = Field(None, description="Description")
|
description: Optional[str] = Field(None, description="Description")
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class PatternTemplateUpdate(BaseModel):
|
||||||
@@ -28,6 +38,16 @@ class PatternTemplateUpdate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class PatternTemplateResponse(BaseModel):
|
||||||
@@ -40,6 +60,16 @@ class PatternTemplateResponse(BaseModel):
|
|||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
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):
|
class PatternTemplateListResponse(BaseModel):
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ class _PictureSourceResponseBase(BaseModel):
|
|||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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):
|
class RawPictureSourceResponse(_PictureSourceResponseBase):
|
||||||
@@ -72,6 +82,16 @@ class _PictureSourceCreateBase(BaseModel):
|
|||||||
name: str = Field(description="Stream name", min_length=1, max_length=100)
|
name: str = Field(description="Stream name", min_length=1, max_length=100)
|
||||||
description: Optional[str] = Field(None, description="Stream description", max_length=500)
|
description: Optional[str] = Field(None, description="Stream description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class RawPictureSourceCreate(_PictureSourceCreateBase):
|
||||||
@@ -127,6 +147,16 @@ class _PictureSourceUpdateBase(BaseModel):
|
|||||||
name: Optional[str] = Field(None, description="Stream name", min_length=1, max_length=100)
|
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)
|
description: Optional[str] = Field(None, description="Stream description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class RawPictureSourceUpdate(_PictureSourceUpdateBase):
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ class PostprocessingTemplateCreate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class PostprocessingTemplateUpdate(BaseModel):
|
||||||
@@ -28,6 +38,16 @@ class PostprocessingTemplateUpdate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class PostprocessingTemplateResponse(BaseModel):
|
||||||
@@ -40,6 +60,16 @@ class PostprocessingTemplateResponse(BaseModel):
|
|||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
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):
|
class PostprocessingTemplateListResponse(BaseModel):
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ class ScenePresetCreate(BaseModel):
|
|||||||
None, description="Target IDs to capture (all if omitted)"
|
None, description="Target IDs to capture (all if omitted)"
|
||||||
)
|
)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class ScenePresetUpdate(BaseModel):
|
||||||
@@ -36,6 +46,16 @@ class ScenePresetUpdate(BaseModel):
|
|||||||
description="Update target list: keep state for existing, capture fresh for new, drop removed",
|
description="Update target list: keep state for existing, capture fresh for new, drop removed",
|
||||||
)
|
)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class ScenePresetResponse(BaseModel):
|
||||||
@@ -47,6 +67,16 @@ class ScenePresetResponse(BaseModel):
|
|||||||
targets: List[TargetSnapshotSchema]
|
targets: List[TargetSnapshotSchema]
|
||||||
order: int
|
order: int
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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
|
created_at: datetime
|
||||||
updated_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)
|
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)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
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)
|
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)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class SyncClockResponse(BaseModel):
|
||||||
@@ -32,6 +52,16 @@ class SyncClockResponse(BaseModel):
|
|||||||
speed: float = Field(description="Speed multiplier")
|
speed: float = Field(description="Speed multiplier")
|
||||||
description: Optional[str] = Field(None, description="Description")
|
description: Optional[str] = Field(None, description="Description")
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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")
|
is_running: bool = Field(True, description="Whether clock is currently running")
|
||||||
elapsed_time: float = Field(0.0, description="Current elapsed time in seconds")
|
elapsed_time: float = Field(0.0, description="Current elapsed time in seconds")
|
||||||
created_at: datetime = Field(description="Creation timestamp")
|
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")
|
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class TemplateUpdate(BaseModel):
|
||||||
@@ -24,6 +34,16 @@ class TemplateUpdate(BaseModel):
|
|||||||
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
||||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class TemplateResponse(BaseModel):
|
||||||
@@ -37,6 +57,12 @@ class TemplateResponse(BaseModel):
|
|||||||
created_at: datetime = Field(description="Creation timestamp")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update timestamp")
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
description: Optional[str] = Field(None, description="Template description")
|
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):
|
class TemplateListResponse(BaseModel):
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ class _ValueSourceResponseBase(BaseModel):
|
|||||||
name: str = Field(description="Source name")
|
name: str = Field(description="Source name")
|
||||||
description: Optional[str] = Field(None, description="Description")
|
description: Optional[str] = Field(None, description="Description")
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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)
|
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class StaticValueSourceCreate(_ValueSourceCreateBase):
|
||||||
@@ -320,6 +340,16 @@ class _ValueSourceUpdateBase(BaseModel):
|
|||||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
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)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class StaticValueSourceUpdate(_ValueSourceUpdateBase):
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ class WeatherSourceCreate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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):
|
class WeatherSourceUpdate(BaseModel):
|
||||||
@@ -44,6 +54,16 @@ class WeatherSourceUpdate(BaseModel):
|
|||||||
)
|
)
|
||||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||||
tags: Optional[List[str]] = None
|
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):
|
class WeatherSourceResponse(BaseModel):
|
||||||
@@ -60,6 +80,16 @@ class WeatherSourceResponse(BaseModel):
|
|||||||
update_interval: int = Field(description="API poll interval in seconds")
|
update_interval: int = Field(description="API poll interval in seconds")
|
||||||
description: Optional[str] = Field(None, description="Description")
|
description: Optional[str] = Field(None, description="Description")
|
||||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
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")
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
updated_at: datetime = Field(description="Last update 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;
|
is_builtin: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const gradientsCache = new DataCache<GradientEntity[]>({
|
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 { wrapCard } from '../core/card-colors.ts';
|
||||||
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.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 { loadPictureSources } from './streams.ts';
|
||||||
import type { Asset } from '../types.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 = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||||
const ICON_PLAY_SOUND = _icon(P.play);
|
const ICON_PLAY_SOUND = _icon(P.play);
|
||||||
const ICON_UPLOAD = _icon(P.fileUp);
|
const ICON_UPLOAD = _icon(P.fileUp);
|
||||||
@@ -162,6 +176,7 @@ export function createAssetCard(asset: Asset): string {
|
|||||||
name: asset.name,
|
name: asset.name,
|
||||||
metaHtml: escapeHtml(`${typeLabel} · ${sizeStr}`),
|
metaHtml: escapeHtml(`${typeLabel} · ${sizeStr}`),
|
||||||
leds: ['on'],
|
leds: ['on'],
|
||||||
|
...makeCardIconFields('asset', asset.id, asset),
|
||||||
menu: {
|
menu: {
|
||||||
hideOnclick: `toggleCardHidden('assets','${asset.id}')`,
|
hideOnclick: `toggleCardHidden('assets','${asset.id}')`,
|
||||||
deleteOnclick: `deleteAsset('${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 { FilterListManager } from '../core/filter-list.ts';
|
||||||
import { wrapCard } from '../core/card-colors.ts';
|
import { wrapCard } from '../core/card-colors.ts';
|
||||||
import type { ModCardOpts } from '../core/mod-card.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';
|
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 ─────────────────────────────────────────────
|
// ── Module state ─────────────────────────────────────────────
|
||||||
|
|
||||||
let _aptTagsInput: TagInput | null = null;
|
let _aptTagsInput: TagInput | null = null;
|
||||||
@@ -286,6 +300,7 @@ export function createAudioProcessingTemplateCard(tmpl: any): string {
|
|||||||
name: tmpl.name,
|
name: tmpl.name,
|
||||||
metaHtml: escapeHtml(`${filters.length} ${t('audio_processing.title') || 'filters'}`),
|
metaHtml: escapeHtml(`${filters.length} ${t('audio_processing.title') || 'filters'}`),
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...makeCardIconFields('audio_processing_template', tmpl.id, tmpl),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneAudioProcessingTemplate('${tmpl.id}')`,
|
duplicateOnclick: `cloneAudioProcessingTemplate('${tmpl.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('audio-processing-templates','${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 { wrapCard } from '../core/card-colors.ts';
|
||||||
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.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 { getBaseOrigin } from './settings.ts';
|
||||||
import { IconSelect } from '../core/icon-select.ts';
|
import { IconSelect } from '../core/icon-select.ts';
|
||||||
import { EntitySelect } from '../core/entity-palette.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 { csScenes, createSceneCard, initScenePresetDelegation } from './scene-presets.ts';
|
||||||
import type { Automation } from '../types.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 ──
|
// ── HA rule entity cache ──
|
||||||
let _haRuleEntities: any[] = [];
|
let _haRuleEntities: any[] = [];
|
||||||
|
|
||||||
@@ -401,6 +417,7 @@ function createAutomationCard(automation: Automation, sceneMap = new Map()) {
|
|||||||
name: automation.name,
|
name: automation.name,
|
||||||
metaHtml,
|
metaHtml,
|
||||||
leds: [ledState],
|
leds: [ledState],
|
||||||
|
...makeCardIconFields('automation', automation.id, automation),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneAutomation('${automation.id}')`,
|
duplicateOnclick: `cloneAutomation('${automation.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('automations','${automation.id}')`,
|
hideOnclick: `toggleCardHidden('automations','${automation.id}')`,
|
||||||
|
|||||||
@@ -23,6 +23,23 @@ import type { ColorStripSource } from '../../types.ts';
|
|||||||
import { bindableValue, bindableColor } from '../../types.ts';
|
import { bindableValue, bindableColor } from '../../types.ts';
|
||||||
import { renderTagChips } from '../../core/tag-input.ts';
|
import { renderTagChips } from '../../core/tag-input.ts';
|
||||||
import { rgbArrayToHex } from '../css-gradient-editor.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 ────────────────────────────────────────────────────── */
|
/* ── Types ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -332,6 +349,7 @@ export function createColorStripCard(source: ColorStripSource, pictureSourceMap:
|
|||||||
name: source.name,
|
name: source.name,
|
||||||
metaHtml: escapeHtml(metaText),
|
metaHtml: escapeHtml(metaText),
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...makeCardIconFields('color_strip_source', source.id, source),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneColorStrip('${source.id}')`,
|
duplicateOnclick: `cloneColorStrip('${source.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('color-strips','${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 btnLabel = clock.is_running ? (t('sync_clock.action.pause') || 'Pause') : (t('sync_clock.action.resume') || 'Resume');
|
||||||
|
|
||||||
const scStyle = cardColorStyle(clock.id);
|
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}"` : ''}>
|
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">
|
<div class="mod-id">
|
||||||
<span class="mod-badge">CLK · ${escapeHtml(short)}</span>
|
<span class="mod-badge">CLK · ${escapeHtml(short)}</span>
|
||||||
<div class="mod-name"><span>${escapeHtml(clock.name)}</span></div>
|
<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.
|
/** Resolve the effective custom icon for a dashboard target card.
|
||||||
* LED targets inherit from their referenced device when no own icon is
|
* LED targets inherit from their referenced device when no own icon is
|
||||||
* set; HA-light targets have no inheritance source. Returns the HTML
|
* 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}`);
|
metaLines.push(`${ICON_SCENE} ${sceneName}`);
|
||||||
|
|
||||||
const aStyle = cardColorStyle(automation.id);
|
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}"` : ''}>
|
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">
|
<div class="mod-id">
|
||||||
<span class="mod-badge">AUTO · ${escapeHtml(short)}</span>
|
<span class="mod-badge">AUTO · ${escapeHtml(short)}</span>
|
||||||
<div class="mod-name"><span>${escapeHtml(automation.name)}</span></div>
|
<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 { wrapCard } from '../core/card-colors.ts';
|
||||||
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.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 { IconSelect, type IconSelectItem } from '../core/icon-select.ts';
|
||||||
import {
|
import {
|
||||||
ICON_GAMEPAD, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_TRASH,
|
ICON_GAMEPAD, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_TRASH,
|
||||||
@@ -25,6 +27,22 @@ import type {
|
|||||||
EffectPreset,
|
EffectPreset,
|
||||||
} from '../types.ts';
|
} 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>`;
|
const _icon = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||||
|
|
||||||
// ── Bulk actions ──
|
// ── Bulk actions ──
|
||||||
@@ -566,6 +584,7 @@ export function createGameIntegrationCard(gi: GameIntegration): string {
|
|||||||
name: gi.name,
|
name: gi.name,
|
||||||
metaHtml: escapeHtml(`${adapterName} · ${mappingCount} ${t('game_integration.mappings') || 'events'}`),
|
metaHtml: escapeHtml(`${adapterName} · ${mappingCount} ${t('game_integration.mappings') || 'events'}`),
|
||||||
leds,
|
leds,
|
||||||
|
...makeCardIconFields('game_integration', gi.id, gi),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneGameIntegration('${gi.id}')`,
|
duplicateOnclick: `cloneGameIntegration('${gi.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('game-integrations','${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 { wrapCard } from '../core/card-colors.ts';
|
||||||
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.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';
|
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>`;
|
const ICON_HA = `<svg class="icon" viewBox="0 0 24 24">${P.home}</svg>`;
|
||||||
|
|
||||||
// ── Modal ──
|
// ── Modal ──
|
||||||
@@ -240,6 +257,7 @@ export function createHASourceCard(source: HomeAssistantSource) {
|
|||||||
name: source.name,
|
name: source.name,
|
||||||
metaHtml: escapeHtml(`${source.host}${isConnected ? ` · ${source.entity_count} entities` : ''}`),
|
metaHtml: escapeHtml(`${source.host}${isConnected ? ` · ${source.entity_count} entities` : ''}`),
|
||||||
leds,
|
leds,
|
||||||
|
...makeCardIconFields('ha_source', source.id, source),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneHASource('${source.id}')`,
|
duplicateOnclick: `cloneHASource('${source.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('ha-sources','${source.id}')`,
|
hideOnclick: `toggleCardHidden('ha-sources','${source.id}')`,
|
||||||
|
|||||||
@@ -34,17 +34,22 @@ const RECENT_MAX = 10;
|
|||||||
// ────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────
|
||||||
// Entity-type registry
|
// 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;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
icon_color: string;
|
icon_color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InheritedIcon {
|
export interface InheritedIcon {
|
||||||
/** The icon id we'd render if this entity has no own icon. */
|
/** The icon id we'd render if this entity has no own icon. */
|
||||||
iconId: string;
|
iconId: string;
|
||||||
/** Effective color for the inherited icon. */
|
/** Effective color for the inherited icon. */
|
||||||
@@ -53,20 +58,83 @@ interface InheritedIcon {
|
|||||||
fromName: string;
|
fromName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntityTypeAdapter {
|
export interface EntityTypeAdapter {
|
||||||
/** Look up the entity by id. Returns null when missing from the cache. */
|
/** Look up the entity by id. Returns null when missing from the cache. */
|
||||||
lookup(id: string): EntityRecord | null;
|
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;
|
endpoint(id: string): string;
|
||||||
/** Invalidate the cache and reload the relevant view. */
|
/** Invalidate the cache and reload the relevant view. */
|
||||||
reload(): Promise<void>;
|
reload(): Promise<void>;
|
||||||
/** Optional fallback icon (e.g. LED target → parent device). */
|
/** Optional fallback icon (e.g. LED target → parent device). */
|
||||||
inheritedFrom(id: string): InheritedIcon | null;
|
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;
|
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 _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 {
|
||||||
|
lookup: (id: string) => {
|
||||||
|
const rec = _find(id);
|
||||||
|
if (!rec) return null;
|
||||||
|
return {
|
||||||
|
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) ?? '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
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);
|
const dev = (devicesCache.data ?? []).find((d: any) => d.id === id);
|
||||||
if (!dev) return null;
|
if (!dev) return null;
|
||||||
return {
|
return {
|
||||||
@@ -75,9 +143,19 @@ function _readDevice(id: string): EntityRecord | null {
|
|||||||
icon: (dev.icon as string | undefined) ?? '',
|
icon: (dev.icon as string | undefined) ?? '',
|
||||||
icon_color: (dev.icon_color 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)}"]`],
|
||||||
|
});
|
||||||
|
|
||||||
function _readTarget(id: string): EntityRecord | null {
|
registerIconEntityType('target', {
|
||||||
|
lookup: (id: string) => {
|
||||||
const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
|
const tgt = (outputTargetsCache.data ?? []).find((t: any) => t.id === id);
|
||||||
if (!tgt) return null;
|
if (!tgt) return null;
|
||||||
return {
|
return {
|
||||||
@@ -86,21 +164,7 @@ function _readTarget(id: string): EntityRecord | null {
|
|||||||
icon: (tgt.icon as string | undefined) ?? '',
|
icon: (tgt.icon as string | undefined) ?? '',
|
||||||
icon_color: (tgt.icon_color 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}`,
|
endpoint: (id) => `/output-targets/${id}`,
|
||||||
reload: async () => {
|
reload: async () => {
|
||||||
outputTargetsCache.invalidate();
|
outputTargetsCache.invalidate();
|
||||||
@@ -128,8 +192,15 @@ const _adapters: Record<EntityType, EntityTypeAdapter> = {
|
|||||||
}
|
}
|
||||||
return t('device.icon.entity.target') || 'LED 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
|
// Picker state
|
||||||
@@ -188,7 +259,7 @@ function _pushRecent(iconId: string): void {
|
|||||||
/** Open the picker for the given entity. Reads current icon from cache. */
|
/** Open the picker for the given entity. Reads current icon from cache. */
|
||||||
export function openIconPicker(entityType: EntityType, entityId: string): void {
|
export function openIconPicker(entityType: EntityType, entityId: string): void {
|
||||||
if (!entityId) return;
|
if (!entityId) return;
|
||||||
const adapter = _adapters[entityType];
|
const adapter = _adapters.get(entityType);
|
||||||
if (!adapter) return;
|
if (!adapter) return;
|
||||||
|
|
||||||
const rec = adapter.lookup(entityId);
|
const rec = adapter.lookup(entityId);
|
||||||
@@ -197,13 +268,14 @@ export function openIconPicker(entityType: EntityType, entityId: string): void {
|
|||||||
const inherited = adapter.inheritedFrom(entityId);
|
const inherited = adapter.inheritedFrom(entityId);
|
||||||
|
|
||||||
// Resolve channel color from the live card so the preview matches.
|
// Resolve channel color from the live card so the preview matches.
|
||||||
// LED-target cards use ``data-target-id``; HA-light-target cards use
|
// Adapters provide the candidate selectors; we try them in order
|
||||||
// ``data-ha-target-id``. Try the LED selector first and fall back to
|
// and fall back to the global accent when no card is found.
|
||||||
// the HA-light one when the entity is a target.
|
const selectors = adapter.cardSelectors?.(entityId) ?? [];
|
||||||
const card = entityType === 'device'
|
let card: HTMLElement | null = null;
|
||||||
? document.querySelector(`[data-device-id="${CSS.escape(entityId)}"]`) as HTMLElement | null
|
for (const sel of selectors) {
|
||||||
: (document.querySelector(`[data-target-id="${CSS.escape(entityId)}"]`)
|
card = document.querySelector(sel) as HTMLElement | null;
|
||||||
?? document.querySelector(`[data-ha-target-id="${CSS.escape(entityId)}"]`)) as HTMLElement | null;
|
if (card) break;
|
||||||
|
}
|
||||||
const channelColor = card
|
const channelColor = card
|
||||||
? (getComputedStyle(card).getPropertyValue('--ch') || '').trim() || _fallbackChannel()
|
? (getComputedStyle(card).getPropertyValue('--ch') || '').trim() || _fallbackChannel()
|
||||||
: _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>`;
|
: `<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.
|
// Header — entity type + name, plus inherited hint when applicable.
|
||||||
const adapter = _adapters[_ctx.entityType];
|
const adapter = _adapters.get(_ctx.entityType)!;
|
||||||
if (eyebrowEl) {
|
if (eyebrowEl) {
|
||||||
eyebrowEl.textContent = adapter.typeLabel(_ctx.entityId);
|
eyebrowEl.textContent = adapter.typeLabel(_ctx.entityId);
|
||||||
}
|
}
|
||||||
@@ -414,14 +486,13 @@ async function _applyChange(nextIconId: string, nextColor: string): Promise<void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const adapter = _adapters[entityType];
|
const adapter = _adapters.get(entityType)!;
|
||||||
try {
|
try {
|
||||||
const body: Record<string, unknown> = { icon: nextIconId, icon_color: nextColor };
|
const body: Record<string, unknown> = { icon: nextIconId, icon_color: nextColor };
|
||||||
// The output-targets endpoint requires the discriminator field.
|
// Discriminated routes (e.g. output-targets) need extra fields
|
||||||
if (entityType === 'target') {
|
// — adapter declares them via ``bodyExtras``.
|
||||||
const tgt = (outputTargetsCache.data ?? []).find((x: any) => x.id === entityId);
|
if (adapter.bodyExtras) {
|
||||||
const targetType = (tgt as any)?.target_type ?? 'led';
|
Object.assign(body, adapter.bodyExtras(entityId));
|
||||||
body.target_type = targetType;
|
|
||||||
}
|
}
|
||||||
const resp = await fetchWithAuth(adapter.endpoint(entityId), {
|
const resp = await fetchWithAuth(adapter.endpoint(entityId), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -557,9 +628,9 @@ function _onDocumentClick(e: MouseEvent): void {
|
|||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
const [typeOrId, id] = raw.includes(':') ? raw.split(':', 2) : ['device', raw];
|
const [typeOrId, id] = raw.includes(':') ? raw.split(':', 2) : ['device', raw];
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
if (typeOrId !== 'device' && typeOrId !== 'target') return;
|
if (!_adapters.has(typeOrId)) return;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openIconPicker(typeOrId as EntityType, id);
|
openIconPicker(typeOrId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('click', _onDocumentClick);
|
document.addEventListener('click', _onDocumentClick);
|
||||||
|
|||||||
@@ -12,8 +12,25 @@ import * as P from '../core/icon-paths.ts';
|
|||||||
import { wrapCard } from '../core/card-colors.ts';
|
import { wrapCard } from '../core/card-colors.ts';
|
||||||
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.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';
|
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>`;
|
const ICON_MQTT = `<svg class="icon" viewBox="0 0 24 24">${P.radio}</svg>`;
|
||||||
|
|
||||||
// ── Modal ──
|
// ── Modal ──
|
||||||
@@ -250,6 +267,7 @@ export function createMQTTSourceCard(source: MQTTSource) {
|
|||||||
name: source.name,
|
name: source.name,
|
||||||
metaHtml: escapeHtml(`${broker} · ${source.base_topic}`),
|
metaHtml: escapeHtml(`${broker} · ${source.base_topic}`),
|
||||||
leds,
|
leds,
|
||||||
|
...makeCardIconFields('mqtt_source', source.id, source),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneMQTTSource('${source.id}')`,
|
duplicateOnclick: `cloneMQTTSource('${source.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('mqtt-sources','${source.id}')`,
|
hideOnclick: `toggleCardHidden('mqtt-sources','${source.id}')`,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { CardSection } from '../core/card-sections.ts';
|
|||||||
import {
|
import {
|
||||||
ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET, ICON_TRASH, ICON_LINK,
|
ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET, ICON_TRASH, ICON_LINK,
|
||||||
} from '../core/icons.ts';
|
} 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 { scenePresetsCache, outputTargetsCache, automationsCacheObj, devicesCache } from '../core/state.ts';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||||
import { wrapCard, cardColorStyle } from '../core/card-colors.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 { EntityPalette } from '../core/entity-palette.ts';
|
||||||
import { navigateToCard } from '../core/navigation.ts';
|
import { navigateToCard } from '../core/navigation.ts';
|
||||||
import { isActiveTab } from '../core/tab-registry.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';
|
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 _editingId: string | null = null;
|
||||||
let _allTargets: any[] = []; // fetched on capture open
|
let _allTargets: any[] = []; // fetched on capture open
|
||||||
let _sceneTagsInput: TagInput | null = null;
|
let _sceneTagsInput: TagInput | null = null;
|
||||||
@@ -117,6 +133,7 @@ export function createSceneCard(preset: ScenePreset) {
|
|||||||
name: preset.name,
|
name: preset.name,
|
||||||
metaHtml,
|
metaHtml,
|
||||||
leds,
|
leds,
|
||||||
|
...makeCardIconFields('scene_preset', preset.id, preset),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneScenePreset('${preset.id}')`,
|
duplicateOnclick: `cloneScenePreset('${preset.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('scenes','${preset.id}')`,
|
hideOnclick: `toggleCardHidden('scenes','${preset.id}')`,
|
||||||
@@ -185,8 +202,18 @@ function _renderDashboardPresetCard(preset: ScenePreset): string {
|
|||||||
const activateLabel = t('scenes.activate') || 'Activate';
|
const activateLabel = t('scenes.activate') || 'Activate';
|
||||||
|
|
||||||
const pStyle = cardColorStyle(preset.id);
|
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}"` : ''}>
|
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">
|
<div class="mod-id">
|
||||||
<span class="mod-badge">SCN \u00b7 ${escapeHtml(short)}</span>
|
<span class="mod-badge">SCN \u00b7 ${escapeHtml(short)}</span>
|
||||||
<div class="mod-name"><span>${escapeHtml(preset.name)}</span></div>
|
<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 { IconSelect } from '../core/icon-select.ts';
|
||||||
import { EntitySelect } from '../core/entity-palette.ts';
|
import { EntitySelect } from '../core/entity-palette.ts';
|
||||||
import { FilterListManager } from '../core/filter-list.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 ──
|
// ── TagInput instances for modals ──
|
||||||
let _streamTagsInput: TagInput | null = null;
|
let _streamTagsInput: TagInput | null = null;
|
||||||
@@ -454,6 +529,7 @@ function renderPictureSourcesList(streams: any) {
|
|||||||
name: stream.name,
|
name: stream.name,
|
||||||
metaHtml: details.metaHtml,
|
metaHtml: details.metaHtml,
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...makeCardIconFields('picture_source', stream.id, stream),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneStream('${stream.id}')`,
|
duplicateOnclick: `cloneStream('${stream.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('${sectionKey}','${stream.id}')`,
|
hideOnclick: `toggleCardHidden('${sectionKey}','${stream.id}')`,
|
||||||
@@ -510,6 +586,7 @@ function renderPictureSourcesList(streams: any) {
|
|||||||
name: template.name,
|
name: template.name,
|
||||||
metaHtml: escapeHtml(`${String(template.engine_type).toUpperCase()} · ${configEntries.length} keys`),
|
metaHtml: escapeHtml(`${String(template.engine_type).toUpperCase()} · ${configEntries.length} keys`),
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...makeCardIconFields('capture_template', template.id, template),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneCaptureTemplate('${template.id}')`,
|
duplicateOnclick: `cloneCaptureTemplate('${template.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('raw-templates','${template.id}')`,
|
hideOnclick: `toggleCardHidden('raw-templates','${template.id}')`,
|
||||||
@@ -556,6 +633,7 @@ function renderPictureSourcesList(streams: any) {
|
|||||||
name: tmpl.name,
|
name: tmpl.name,
|
||||||
metaHtml: escapeHtml(`${filters.length} ${t('postprocessing.title') || 'filters'}`),
|
metaHtml: escapeHtml(`${filters.length} ${t('postprocessing.title') || 'filters'}`),
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...makeCardIconFields('pp_template', tmpl.id, tmpl),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `clonePPTemplate('${tmpl.id}')`,
|
duplicateOnclick: `clonePPTemplate('${tmpl.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('proc-templates','${tmpl.id}')`,
|
hideOnclick: `toggleCardHidden('proc-templates','${tmpl.id}')`,
|
||||||
@@ -600,6 +678,7 @@ function renderPictureSourcesList(streams: any) {
|
|||||||
name: tmpl.name,
|
name: tmpl.name,
|
||||||
metaHtml: escapeHtml(`${filters.length} ${t('css_processing.title') || 'strip filters'}`),
|
metaHtml: escapeHtml(`${filters.length} ${t('css_processing.title') || 'strip filters'}`),
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...makeCardIconFields('cspt', tmpl.id, tmpl),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneCSPT('${tmpl.id}')`,
|
duplicateOnclick: `cloneCSPT('${tmpl.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('css-proc-templates','${tmpl.id}')`,
|
hideOnclick: `toggleCardHidden('css-proc-templates','${tmpl.id}')`,
|
||||||
@@ -795,6 +874,7 @@ function renderPictureSourcesList(streams: any) {
|
|||||||
name: src.name,
|
name: src.name,
|
||||||
metaHtml: escapeHtml(metaText),
|
metaHtml: escapeHtml(metaText),
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...makeCardIconFields('audio_source', src.id, src),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneAudioSource('${src.id}')`,
|
duplicateOnclick: `cloneAudioSource('${src.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('${sectionKey}','${src.id}')`,
|
hideOnclick: `toggleCardHidden('${sectionKey}','${src.id}')`,
|
||||||
@@ -850,6 +930,7 @@ function renderPictureSourcesList(streams: any) {
|
|||||||
name: template.name,
|
name: template.name,
|
||||||
metaHtml: escapeHtml(`${String(template.engine_type).toUpperCase()} · ${configEntries.length} keys`),
|
metaHtml: escapeHtml(`${String(template.engine_type).toUpperCase()} · ${configEntries.length} keys`),
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...makeCardIconFields('audio_template', template.id, template),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneAudioTemplate('${template.id}')`,
|
duplicateOnclick: `cloneAudioTemplate('${template.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('audio-templates','${template.id}')`,
|
hideOnclick: `toggleCardHidden('audio-templates','${template.id}')`,
|
||||||
@@ -896,6 +977,7 @@ function renderPictureSourcesList(streams: any) {
|
|||||||
name: g.name,
|
name: g.name,
|
||||||
metaHtml: escapeHtml(`${g.stops.length} ${t('gradient.stops_label') || 'stops'}`),
|
metaHtml: escapeHtml(`${g.stops.length} ${t('gradient.stops_label') || 'stops'}`),
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...(g.is_builtin ? {} : makeCardIconFields('gradient', g.id, g)),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneGradient('${g.id}')`,
|
duplicateOnclick: `cloneGradient('${g.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('gradients','${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 { wrapCard } from '../core/card-colors.ts';
|
||||||
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
import type { ModCardOpts, ModChipOpts, LedState } from '../core/mod-card.ts';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.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 { loadPictureSources } from './streams.ts';
|
||||||
import type { SyncClock } from '../types.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 ──
|
// ── Auto-name ──
|
||||||
|
|
||||||
let _scNameManuallyEdited = false;
|
let _scNameManuallyEdited = false;
|
||||||
@@ -245,6 +263,7 @@ export function createSyncClockCard(clock: SyncClock) {
|
|||||||
name: clock.name,
|
name: clock.name,
|
||||||
metaHtml: escapeHtml(`${statusLabel} · ${clock.speed}x`),
|
metaHtml: escapeHtml(`${statusLabel} · ${clock.speed}x`),
|
||||||
leds,
|
leds,
|
||||||
|
...makeCardIconFields('sync_clock', clock.id, clock),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneSyncClock('${clock.id}')`,
|
duplicateOnclick: `cloneSyncClock('${clock.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('sync-clocks','${clock.id}')`,
|
hideOnclick: `toggleCardHidden('sync-clocks','${clock.id}')`,
|
||||||
|
|||||||
@@ -30,8 +30,27 @@ import {
|
|||||||
import { wrapCard } from '../core/card-colors.ts';
|
import { wrapCard } from '../core/card-colors.ts';
|
||||||
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.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 { openAuthedWs } from '../core/ws-auth.ts';
|
||||||
import { IconSelect, showTypePicker } from '../core/icon-select.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 type { IconSelectItem } from '../core/icon-select.ts';
|
||||||
import * as P from '../core/icon-paths.ts';
|
import * as P from '../core/icon-paths.ts';
|
||||||
import { EntitySelect } from '../core/entity-palette.ts';
|
import { EntitySelect } from '../core/entity-palette.ts';
|
||||||
@@ -1398,6 +1417,7 @@ export function createValueSourceCard(src: ValueSource) {
|
|||||||
name: src.name,
|
name: src.name,
|
||||||
metaHtml: escapeHtml(metaText),
|
metaHtml: escapeHtml(metaText),
|
||||||
leds: ['off'],
|
leds: ['off'],
|
||||||
|
...makeCardIconFields('value_source', src.id, src),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneValueSource('${src.id}')`,
|
duplicateOnclick: `cloneValueSource('${src.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('value-sources','${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 { wrapCard } from '../core/card-colors.ts';
|
||||||
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
import type { ModCardOpts, ModChipOpts } from '../core/mod-card.ts';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.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';
|
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_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>`;
|
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,
|
name: source.name,
|
||||||
metaHtml: escapeHtml(`${providerLabel} · ${coords}`),
|
metaHtml: escapeHtml(`${providerLabel} · ${coords}`),
|
||||||
leds: ['on'],
|
leds: ['on'],
|
||||||
|
...makeCardIconFields('weather_source', source.id, source),
|
||||||
menu: {
|
menu: {
|
||||||
duplicateOnclick: `cloneWeatherSource('${source.id}')`,
|
duplicateOnclick: `cloneWeatherSource('${source.id}')`,
|
||||||
hideOnclick: `toggleCardHidden('weather-sources','${source.id}')`,
|
hideOnclick: `toggleCardHidden('weather-sources','${source.id}')`,
|
||||||
|
|||||||
@@ -228,6 +228,8 @@ export interface ColorStripSource {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
overlay_active: boolean;
|
overlay_active: boolean;
|
||||||
clock_id?: string;
|
clock_id?: string;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|
||||||
@@ -328,6 +330,8 @@ export interface PatternTemplate {
|
|||||||
rectangles: KeyColorRectangle[];
|
rectangles: KeyColorRectangle[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -358,6 +362,8 @@ interface ValueSourceBase {
|
|||||||
return_type: 'float' | 'color';
|
return_type: 'float' | 'color';
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -518,6 +524,8 @@ interface AudioSourceBase {
|
|||||||
source_type: AudioSourceType;
|
source_type: AudioSourceType;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -549,6 +557,8 @@ interface PictureSourceBase {
|
|||||||
stream_type: PictureSourceType;
|
stream_type: PictureSourceType;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -608,6 +618,8 @@ export interface ScenePreset {
|
|||||||
targets: TargetSnapshot[];
|
targets: TargetSnapshot[];
|
||||||
order: number;
|
order: number;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -622,6 +634,8 @@ export interface SyncClock {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
is_running: boolean;
|
is_running: boolean;
|
||||||
elapsed_time: number;
|
elapsed_time: number;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -636,6 +650,8 @@ export interface WeatherSource {
|
|||||||
update_interval: number;
|
update_interval: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -657,6 +673,8 @@ export interface HomeAssistantSource {
|
|||||||
entity_count: number;
|
entity_count: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -694,6 +712,8 @@ export interface MQTTSource {
|
|||||||
connected: boolean;
|
connected: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -728,6 +748,8 @@ export interface Asset {
|
|||||||
description?: string;
|
description?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
prebuilt: boolean;
|
prebuilt: boolean;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -772,6 +794,8 @@ export interface Automation {
|
|||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
last_activated_at?: string;
|
last_activated_at?: string;
|
||||||
last_deactivated_at?: string;
|
last_deactivated_at?: string;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -790,6 +814,8 @@ export interface CaptureTemplate {
|
|||||||
engine_config: Record<string, any>;
|
engine_config: Record<string, any>;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -800,6 +826,8 @@ export interface PostprocessingTemplate {
|
|||||||
filters: FilterInstance[];
|
filters: FilterInstance[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -810,6 +838,8 @@ export interface ColorStripProcessingTemplate {
|
|||||||
filters: FilterInstance[];
|
filters: FilterInstance[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -821,6 +851,8 @@ export interface AudioTemplate {
|
|||||||
engine_config: Record<string, any>;
|
engine_config: Record<string, any>;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -940,6 +972,8 @@ export interface GameIntegration {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
icon?: string;
|
||||||
|
icon_color?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -585,6 +585,25 @@
|
|||||||
"device.icon.entity.device": "Device",
|
"device.icon.entity.device": "Device",
|
||||||
"device.icon.entity.target": "LED target",
|
"device.icon.entity.target": "LED target",
|
||||||
"device.icon.entity.ha_light_target": "HA light 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.inherited_from": "Inherited from %s",
|
||||||
"device.icon.override_inherited": "Override inherited icon…",
|
"device.icon.override_inherited": "Override inherited icon…",
|
||||||
"device.icon.use_inherited": "Use inherited",
|
"device.icon.use_inherited": "Use inherited",
|
||||||
|
|||||||
@@ -603,6 +603,25 @@
|
|||||||
"device.icon.entity.device": "Устройство",
|
"device.icon.entity.device": "Устройство",
|
||||||
"device.icon.entity.target": "LED-цель",
|
"device.icon.entity.target": "LED-цель",
|
||||||
"device.icon.entity.ha_light_target": "HA-светильник",
|
"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.inherited_from": "Унаследовано от %s",
|
||||||
"device.icon.override_inherited": "Заменить унаследованную иконку…",
|
"device.icon.override_inherited": "Заменить унаследованную иконку…",
|
||||||
"device.icon.use_inherited": "Использовать унаследованную",
|
"device.icon.use_inherited": "Использовать унаследованную",
|
||||||
|
|||||||
@@ -603,6 +603,25 @@
|
|||||||
"device.icon.entity.device": "设备",
|
"device.icon.entity.device": "设备",
|
||||||
"device.icon.entity.target": "LED 目标",
|
"device.icon.entity.target": "LED 目标",
|
||||||
"device.icon.entity.ha_light_target": "HA 灯目标",
|
"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.inherited_from": "继承自 %s",
|
||||||
"device.icon.override_inherited": "覆盖继承的图标…",
|
"device.icon.override_inherited": "覆盖继承的图标…",
|
||||||
"device.icon.use_inherited": "使用继承的",
|
"device.icon.use_inherited": "使用继承的",
|
||||||
|
|||||||
@@ -43,9 +43,12 @@ class Asset:
|
|||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
prebuilt: bool = False # True for shipped assets
|
prebuilt: bool = False # True for shipped assets
|
||||||
deleted: bool = False # soft-delete for prebuilt 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:
|
def to_dict(self) -> dict:
|
||||||
return {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"filename": self.filename,
|
"filename": self.filename,
|
||||||
@@ -60,6 +63,11 @@ class Asset:
|
|||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_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
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "Asset":
|
def from_dict(data: dict) -> "Asset":
|
||||||
@@ -75,6 +83,8 @@ class Asset:
|
|||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
prebuilt=bool(data.get("prebuilt", False)),
|
prebuilt=bool(data.get("prebuilt", False)),
|
||||||
deleted=bool(data.get("deleted", False)),
|
deleted=bool(data.get("deleted", False)),
|
||||||
|
icon=data.get("icon", ""),
|
||||||
|
icon_color=data.get("icon_color", ""),
|
||||||
created_at=datetime.fromisoformat(data["created_at"]),
|
created_at=datetime.fromisoformat(data["created_at"]),
|
||||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ class AssetStore(BaseSqliteStore[Asset]):
|
|||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
prebuilt: bool = False,
|
prebuilt: bool = False,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> Asset:
|
) -> Asset:
|
||||||
"""Create a new asset from uploaded file data.
|
"""Create a new asset from uploaded file data.
|
||||||
|
|
||||||
@@ -156,6 +158,8 @@ class AssetStore(BaseSqliteStore[Asset]):
|
|||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
prebuilt=prebuilt,
|
prebuilt=prebuilt,
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[asset_id] = asset
|
self._items[asset_id] = asset
|
||||||
@@ -171,6 +175,8 @@ class AssetStore(BaseSqliteStore[Asset]):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> Asset:
|
) -> Asset:
|
||||||
"""Update asset metadata (not the file itself)."""
|
"""Update asset metadata (not the file itself)."""
|
||||||
asset = self.get(asset_id)
|
asset = self.get(asset_id)
|
||||||
@@ -182,6 +188,10 @@ class AssetStore(BaseSqliteStore[Asset]):
|
|||||||
asset.description = description
|
asset.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
asset.tags = tags
|
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)
|
asset.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(asset_id, asset)
|
self._save_item(asset_id, asset)
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ class AudioProcessingTemplate:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert template to dictionary."""
|
"""Convert template to dictionary."""
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"filters": [f.to_dict() for f in self.filters],
|
"filters": [f.to_dict() for f in self.filters],
|
||||||
@@ -30,6 +32,11 @@ class AudioProcessingTemplate:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "AudioProcessingTemplate":
|
def from_dict(cls, data: dict) -> "AudioProcessingTemplate":
|
||||||
@@ -52,4 +59,6 @@ class AudioProcessingTemplate:
|
|||||||
),
|
),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
tags=data.get("tags", []),
|
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,
|
filters: Optional[List[FilterInstance]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> AudioProcessingTemplate:
|
) -> AudioProcessingTemplate:
|
||||||
self._check_name_unique(name)
|
self._check_name_unique(name)
|
||||||
|
|
||||||
@@ -60,6 +62,8 @@ class AudioProcessingTemplateStore(BaseSqliteStore[AudioProcessingTemplate]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[template_id] = template
|
self._items[template_id] = template
|
||||||
@@ -75,6 +79,8 @@ class AudioProcessingTemplateStore(BaseSqliteStore[AudioProcessingTemplate]):
|
|||||||
filters: Optional[List[FilterInstance]] = None,
|
filters: Optional[List[FilterInstance]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> AudioProcessingTemplate:
|
) -> AudioProcessingTemplate:
|
||||||
template = self.get(template_id)
|
template = self.get(template_id)
|
||||||
|
|
||||||
@@ -91,6 +97,10 @@ class AudioProcessingTemplateStore(BaseSqliteStore[AudioProcessingTemplate]):
|
|||||||
template.description = description
|
template.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
template.tags = tags
|
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)
|
template.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(template_id, template)
|
self._save_item(template_id, template)
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ class AudioSource:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert source to dictionary. Subclasses extend this."""
|
"""Convert source to dictionary. Subclasses extend this."""
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"source_type": self.source_type,
|
"source_type": self.source_type,
|
||||||
@@ -39,6 +41,11 @@ class AudioSource:
|
|||||||
"audio_source_id": None,
|
"audio_source_id": None,
|
||||||
"audio_processing_template_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
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "AudioSource":
|
def from_dict(data: dict) -> "AudioSource":
|
||||||
@@ -72,6 +79,8 @@ def _parse_common_fields(data: dict) -> dict:
|
|||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
created_at=created_at,
|
created_at=created_at,
|
||||||
updated_at=updated_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,
|
audio_template_id: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
audio_processing_template_id: Optional[str] = None,
|
audio_processing_template_id: Optional[str] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> AudioSource:
|
) -> AudioSource:
|
||||||
self._check_name_unique(name)
|
self._check_name_unique(name)
|
||||||
|
|
||||||
@@ -83,6 +85,8 @@ class AudioSourceStore(BaseSqliteStore[AudioSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
audio_source_id=audio_source_id,
|
audio_source_id=audio_source_id,
|
||||||
audio_processing_template_id=audio_processing_template_id,
|
audio_processing_template_id=audio_processing_template_id,
|
||||||
)
|
)
|
||||||
@@ -95,6 +99,8 @@ class AudioSourceStore(BaseSqliteStore[AudioSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
device_index=device_index if device_index is not None else -1,
|
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,
|
is_loopback=bool(is_loopback) if is_loopback is not None else True,
|
||||||
audio_template_id=audio_template_id,
|
audio_template_id=audio_template_id,
|
||||||
@@ -117,6 +123,8 @@ class AudioSourceStore(BaseSqliteStore[AudioSource]):
|
|||||||
audio_template_id: Optional[str] = None,
|
audio_template_id: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
audio_processing_template_id: Optional[str] = None,
|
audio_processing_template_id: Optional[str] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> AudioSource:
|
) -> AudioSource:
|
||||||
source = self.get(source_id)
|
source = self.get(source_id)
|
||||||
|
|
||||||
@@ -128,6 +136,10 @@ class AudioSourceStore(BaseSqliteStore[AudioSource]):
|
|||||||
source.description = description
|
source.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
source.tags = tags
|
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 isinstance(source, CaptureAudioSource):
|
||||||
if device_index is not None:
|
if device_index is not None:
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ class AudioCaptureTemplate:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert template to dictionary."""
|
"""Convert template to dictionary."""
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"engine_type": self.engine_type,
|
"engine_type": self.engine_type,
|
||||||
@@ -30,6 +32,11 @@ class AudioCaptureTemplate:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "AudioCaptureTemplate":
|
def from_dict(cls, data: dict) -> "AudioCaptureTemplate":
|
||||||
@@ -51,4 +58,6 @@ class AudioCaptureTemplate:
|
|||||||
),
|
),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
tags=data.get("tags", []),
|
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],
|
engine_config: Dict[str, Any],
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> AudioCaptureTemplate:
|
) -> AudioCaptureTemplate:
|
||||||
self._check_name_unique(name)
|
self._check_name_unique(name)
|
||||||
|
|
||||||
@@ -91,6 +93,8 @@ class AudioTemplateStore(BaseSqliteStore[AudioCaptureTemplate]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[template_id] = template
|
self._items[template_id] = template
|
||||||
@@ -106,6 +110,8 @@ class AudioTemplateStore(BaseSqliteStore[AudioCaptureTemplate]):
|
|||||||
engine_config: Optional[Dict[str, Any]] = None,
|
engine_config: Optional[Dict[str, Any]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> AudioCaptureTemplate:
|
) -> AudioCaptureTemplate:
|
||||||
template = self.get(template_id)
|
template = self.get(template_id)
|
||||||
|
|
||||||
@@ -120,6 +126,10 @@ class AudioTemplateStore(BaseSqliteStore[AudioCaptureTemplate]):
|
|||||||
template.description = description
|
template.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
template.tags = tags
|
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)
|
template.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(template_id, template)
|
self._save_item(template_id, template)
|
||||||
|
|||||||
@@ -243,6 +243,9 @@ class Automation:
|
|||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
# Custom card icon (frontend display only)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
# Backward-compatible property aliases
|
# Backward-compatible property aliases
|
||||||
@property
|
@property
|
||||||
@@ -262,7 +265,7 @@ class Automation:
|
|||||||
self.rules = value
|
self.rules = value
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"enabled": self.enabled,
|
"enabled": self.enabled,
|
||||||
@@ -275,6 +278,11 @@ class Automation:
|
|||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_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
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "Automation":
|
def from_dict(cls, data: dict) -> "Automation":
|
||||||
@@ -302,6 +310,8 @@ class Automation:
|
|||||||
deactivation_mode=data.get("deactivation_mode", "none"),
|
deactivation_mode=data.get("deactivation_mode", "none"),
|
||||||
deactivation_scene_preset_id=data.get("deactivation_scene_preset_id"),
|
deactivation_scene_preset_id=data.get("deactivation_scene_preset_id"),
|
||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
|
icon=data.get("icon", ""),
|
||||||
|
icon_color=data.get("icon_color", ""),
|
||||||
created_at=datetime.fromisoformat(
|
created_at=datetime.fromisoformat(
|
||||||
data.get("created_at", datetime.now(timezone.utc).isoformat())
|
data.get("created_at", datetime.now(timezone.utc).isoformat())
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class AutomationStore(BaseSqliteStore[Automation]):
|
|||||||
deactivation_mode: str = "none",
|
deactivation_mode: str = "none",
|
||||||
deactivation_scene_preset_id: Optional[str] = None,
|
deactivation_scene_preset_id: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
# Legacy parameter aliases
|
# Legacy parameter aliases
|
||||||
condition_logic: Optional[str] = None,
|
condition_logic: Optional[str] = None,
|
||||||
conditions: Optional[List[Rule]] = None,
|
conditions: Optional[List[Rule]] = None,
|
||||||
@@ -63,6 +65,8 @@ class AutomationStore(BaseSqliteStore[Automation]):
|
|||||||
created_at=now,
|
created_at=now,
|
||||||
updated_at=now,
|
updated_at=now,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[automation_id] = automation
|
self._items[automation_id] = automation
|
||||||
@@ -81,6 +85,8 @@ class AutomationStore(BaseSqliteStore[Automation]):
|
|||||||
deactivation_mode: Optional[str] = None,
|
deactivation_mode: Optional[str] = None,
|
||||||
deactivation_scene_preset_id: str = "__unset__",
|
deactivation_scene_preset_id: str = "__unset__",
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
# Legacy parameter aliases
|
# Legacy parameter aliases
|
||||||
condition_logic: Optional[str] = None,
|
condition_logic: Optional[str] = None,
|
||||||
conditions: Optional[List[Rule]] = None,
|
conditions: Optional[List[Rule]] = None,
|
||||||
@@ -112,6 +118,10 @@ class AutomationStore(BaseSqliteStore[Automation]):
|
|||||||
)
|
)
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
automation.tags = tags
|
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)
|
automation.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(automation_id, automation)
|
self._save_item(automation_id, automation)
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ class ColorStripProcessingTemplate:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert template to dictionary."""
|
"""Convert template to dictionary."""
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"filters": [f.to_dict() for f in self.filters],
|
"filters": [f.to_dict() for f in self.filters],
|
||||||
@@ -32,6 +34,11 @@ class ColorStripProcessingTemplate:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "ColorStripProcessingTemplate":
|
def from_dict(cls, data: dict) -> "ColorStripProcessingTemplate":
|
||||||
@@ -54,4 +61,6 @@ class ColorStripProcessingTemplate:
|
|||||||
),
|
),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
tags=data.get("tags", []),
|
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,
|
filters: Optional[List[FilterInstance]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> ColorStripProcessingTemplate:
|
) -> ColorStripProcessingTemplate:
|
||||||
self._check_name_unique(name)
|
self._check_name_unique(name)
|
||||||
|
|
||||||
@@ -94,6 +96,8 @@ class ColorStripProcessingTemplateStore(BaseSqliteStore[ColorStripProcessingTemp
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[template_id] = template
|
self._items[template_id] = template
|
||||||
@@ -109,6 +113,8 @@ class ColorStripProcessingTemplateStore(BaseSqliteStore[ColorStripProcessingTemp
|
|||||||
filters: Optional[List[FilterInstance]] = None,
|
filters: Optional[List[FilterInstance]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> ColorStripProcessingTemplate:
|
) -> ColorStripProcessingTemplate:
|
||||||
template = self.get(template_id)
|
template = self.get(template_id)
|
||||||
|
|
||||||
@@ -122,6 +128,10 @@ class ColorStripProcessingTemplateStore(BaseSqliteStore[ColorStripProcessingTemp
|
|||||||
template.description = description
|
template.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
template.tags = tags
|
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)
|
template.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(template_id, template)
|
self._save_item(template_id, template)
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class ColorStripSource:
|
|||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
clock_id: Optional[str] = None # optional SyncClock reference
|
clock_id: Optional[str] = None # optional SyncClock reference
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sharable(self) -> bool:
|
def sharable(self) -> bool:
|
||||||
@@ -75,7 +77,7 @@ class ColorStripSource:
|
|||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert source to dictionary. Subclasses extend this with their own fields."""
|
"""Convert source to dictionary. Subclasses extend this with their own fields."""
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"source_type": self.source_type,
|
"source_type": self.source_type,
|
||||||
@@ -85,6 +87,11 @@ class ColorStripSource:
|
|||||||
"clock_id": self.clock_id,
|
"clock_id": self.clock_id,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from_kwargs(
|
def create_from_kwargs(
|
||||||
@@ -155,6 +162,8 @@ def _parse_css_common(data: dict) -> dict:
|
|||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
created_at=created_at,
|
created_at=created_at,
|
||||||
updated_at=updated_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)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
tags = kwargs.pop("tags", None) or []
|
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 = ColorStripSource.create_instance(
|
||||||
source_type,
|
source_type,
|
||||||
@@ -69,6 +71,8 @@ class ColorStripStore(BaseSqliteStore[ColorStripSource]):
|
|||||||
tags=tags,
|
tags=tags,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
source.icon = icon
|
||||||
|
source.icon_color = icon_color
|
||||||
|
|
||||||
self._items[source_id] = source
|
self._items[source_id] = source
|
||||||
self._save_item(source_id, source)
|
self._save_item(source_id, source)
|
||||||
@@ -110,6 +114,14 @@ class ColorStripStore(BaseSqliteStore[ColorStripSource]):
|
|||||||
if tags is not None:
|
if tags is not None:
|
||||||
source.tags = tags
|
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 --
|
# -- Type-specific fields --
|
||||||
source.apply_update(**kwargs)
|
source.apply_update(**kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,11 @@ class GameIntegrationConfig:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"adapter_type": self.adapter_type,
|
"adapter_type": self.adapter_type,
|
||||||
@@ -94,6 +96,11 @@ class GameIntegrationConfig:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"tags": list(self.tags),
|
"tags": list(self.tags),
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "GameIntegrationConfig":
|
def from_dict(cls, data: dict) -> "GameIntegrationConfig":
|
||||||
@@ -117,6 +124,8 @@ class GameIntegrationConfig:
|
|||||||
),
|
),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
|
icon=data.get("icon", ""),
|
||||||
|
icon_color=data.get("icon_color", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -128,6 +137,8 @@ class GameIntegrationConfig:
|
|||||||
event_mappings: Optional[List[EventMapping]] = None,
|
event_mappings: Optional[List[EventMapping]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> "GameIntegrationConfig":
|
) -> "GameIntegrationConfig":
|
||||||
"""Factory method to create a new config with generated ID and timestamps."""
|
"""Factory method to create a new config with generated ID and timestamps."""
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@@ -142,6 +153,8 @@ class GameIntegrationConfig:
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
def apply_update(
|
def apply_update(
|
||||||
@@ -153,6 +166,8 @@ class GameIntegrationConfig:
|
|||||||
event_mappings: Optional[List[EventMapping]] = None,
|
event_mappings: Optional[List[EventMapping]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> "GameIntegrationConfig":
|
) -> "GameIntegrationConfig":
|
||||||
"""Return a new config with updated fields (immutable update)."""
|
"""Return a new config with updated fields (immutable update)."""
|
||||||
return GameIntegrationConfig(
|
return GameIntegrationConfig(
|
||||||
@@ -166,4 +181,6 @@ class GameIntegrationConfig:
|
|||||||
updated_at=datetime.now(timezone.utc),
|
updated_at=datetime.now(timezone.utc),
|
||||||
description=description if description is not None else self.description,
|
description=description if description is not None else self.description,
|
||||||
tags=tags if tags is not None else self.tags,
|
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,
|
event_mappings: Optional[List[EventMapping]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> GameIntegrationConfig:
|
) -> GameIntegrationConfig:
|
||||||
"""Create a new game integration config.
|
"""Create a new game integration config.
|
||||||
|
|
||||||
@@ -70,6 +72,8 @@ class GameIntegrationStore(BaseSqliteStore[GameIntegrationConfig]):
|
|||||||
event_mappings=event_mappings,
|
event_mappings=event_mappings,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
|
icon=icon,
|
||||||
|
icon_color=icon_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[config.id] = config
|
self._items[config.id] = config
|
||||||
@@ -88,6 +92,8 @@ class GameIntegrationStore(BaseSqliteStore[GameIntegrationConfig]):
|
|||||||
event_mappings: Optional[List[EventMapping]] = None,
|
event_mappings: Optional[List[EventMapping]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> GameIntegrationConfig:
|
) -> GameIntegrationConfig:
|
||||||
"""Update an existing game integration config.
|
"""Update an existing game integration config.
|
||||||
|
|
||||||
@@ -122,6 +128,8 @@ class GameIntegrationStore(BaseSqliteStore[GameIntegrationConfig]):
|
|||||||
event_mappings=event_mappings,
|
event_mappings=event_mappings,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
|
icon=icon,
|
||||||
|
icon_color=icon_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[integration_id] = updated
|
self._items[integration_id] = updated
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ class Gradient:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"stops": self.stops,
|
"stops": self.stops,
|
||||||
@@ -34,6 +36,11 @@ class Gradient:
|
|||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_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
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "Gradient":
|
def from_dict(data: dict) -> "Gradient":
|
||||||
@@ -46,6 +53,8 @@ class Gradient:
|
|||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
created_at=datetime.fromisoformat(data["created_at"]),
|
created_at=datetime.fromisoformat(data["created_at"]),
|
||||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||||
|
icon=data.get("icon", "") or "",
|
||||||
|
icon_color=data.get("icon_color", "") or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
|||||||
stops: list,
|
stops: list,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> Gradient:
|
) -> Gradient:
|
||||||
self._check_name_unique(name)
|
self._check_name_unique(name)
|
||||||
gid = f"gr_{uuid.uuid4().hex[:8]}"
|
gid = f"gr_{uuid.uuid4().hex[:8]}"
|
||||||
@@ -143,6 +145,8 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
self._items[gid] = gradient
|
self._items[gid] = gradient
|
||||||
self._save_item(gid, gradient)
|
self._save_item(gid, gradient)
|
||||||
@@ -156,6 +160,8 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
|||||||
stops: Optional[list] = None,
|
stops: Optional[list] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> Gradient:
|
) -> Gradient:
|
||||||
gradient = self.get(gradient_id)
|
gradient = self.get(gradient_id)
|
||||||
if gradient.is_builtin:
|
if gradient.is_builtin:
|
||||||
@@ -169,6 +175,10 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
|||||||
gradient.description = description
|
gradient.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
gradient.tags = tags
|
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)
|
gradient.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(gradient_id, gradient)
|
self._save_item(gradient_id, gradient)
|
||||||
logger.info(f"Updated gradient: {gradient_id}")
|
logger.info(f"Updated gradient: {gradient_id}")
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class HomeAssistantSource:
|
|||||||
) # optional allowlist (e.g. ["sensor.*"])
|
) # optional allowlist (e.g. ["sensor.*"])
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ws_url(self) -> str:
|
def ws_url(self) -> str:
|
||||||
@@ -72,7 +74,7 @@ class HomeAssistantSource:
|
|||||||
# Always persist the token in encrypted envelope form. If the field
|
# Always persist the token in encrypted envelope form. If the field
|
||||||
# already contains an envelope, encrypt() is a no-op.
|
# already contains an envelope, encrypt() is a no-op.
|
||||||
stored_token = secret_box.encrypt(self.token) if self.token else ""
|
stored_token = secret_box.encrypt(self.token) if self.token else ""
|
||||||
return {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"host": self.host,
|
"host": self.host,
|
||||||
@@ -84,6 +86,11 @@ class HomeAssistantSource:
|
|||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_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
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "HomeAssistantSource":
|
def from_dict(data: dict) -> "HomeAssistantSource":
|
||||||
@@ -99,4 +106,6 @@ class HomeAssistantSource:
|
|||||||
token=token,
|
token=token,
|
||||||
use_ssl=data.get("use_ssl", False),
|
use_ssl=data.get("use_ssl", False),
|
||||||
entity_filters=data.get("entity_filters") or [],
|
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,
|
entity_filters: Optional[List[str]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> HomeAssistantSource:
|
) -> HomeAssistantSource:
|
||||||
if not host:
|
if not host:
|
||||||
raise ValueError("host is required")
|
raise ValueError("host is required")
|
||||||
@@ -95,6 +97,8 @@ class HomeAssistantStore(BaseSqliteStore[HomeAssistantSource]):
|
|||||||
entity_filters=entity_filters or [],
|
entity_filters=entity_filters or [],
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[sid] = source
|
self._items[sid] = source
|
||||||
@@ -112,6 +116,8 @@ class HomeAssistantStore(BaseSqliteStore[HomeAssistantSource]):
|
|||||||
entity_filters: Optional[List[str]] = None,
|
entity_filters: Optional[List[str]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> HomeAssistantSource:
|
) -> HomeAssistantSource:
|
||||||
existing = self.get(source_id)
|
existing = self.get(source_id)
|
||||||
|
|
||||||
@@ -131,6 +137,8 @@ class HomeAssistantStore(BaseSqliteStore[HomeAssistantSource]):
|
|||||||
),
|
),
|
||||||
description=description if description is not None else existing.description,
|
description=description if description is not None else existing.description,
|
||||||
tags=tags if tags is not None else existing.tags,
|
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
|
self._items[source_id] = updated
|
||||||
|
|||||||
@@ -48,9 +48,11 @@ class MQTTSource:
|
|||||||
base_topic: str = "ledgrab"
|
base_topic: str = "ledgrab"
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"broker_host": self.broker_host,
|
"broker_host": self.broker_host,
|
||||||
@@ -64,6 +66,11 @@ class MQTTSource:
|
|||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_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
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "MQTTSource":
|
def from_dict(data: dict) -> "MQTTSource":
|
||||||
@@ -76,4 +83,6 @@ class MQTTSource:
|
|||||||
password=data.get("password", ""),
|
password=data.get("password", ""),
|
||||||
client_id=data.get("client_id", "ledgrab"),
|
client_id=data.get("client_id", "ledgrab"),
|
||||||
base_topic=data.get("base_topic", "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",
|
base_topic: str = "ledgrab",
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> MQTTSource:
|
) -> MQTTSource:
|
||||||
if not broker_host:
|
if not broker_host:
|
||||||
raise ValueError("broker_host is required")
|
raise ValueError("broker_host is required")
|
||||||
@@ -59,6 +61,8 @@ class MQTTSourceStore(BaseSqliteStore[MQTTSource]):
|
|||||||
base_topic=base_topic,
|
base_topic=base_topic,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[sid] = source
|
self._items[sid] = source
|
||||||
@@ -78,6 +82,8 @@ class MQTTSourceStore(BaseSqliteStore[MQTTSource]):
|
|||||||
base_topic: Optional[str] = None,
|
base_topic: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> MQTTSource:
|
) -> MQTTSource:
|
||||||
existing = self.get(source_id)
|
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,
|
base_topic=base_topic if base_topic is not None else existing.base_topic,
|
||||||
description=description if description is not None else existing.description,
|
description=description if description is not None else existing.description,
|
||||||
tags=tags if tags is not None else existing.tags,
|
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
|
self._items[source_id] = updated
|
||||||
|
|||||||
@@ -46,10 +46,12 @@ class PatternTemplate:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert to dictionary."""
|
"""Convert to dictionary."""
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"rectangles": [r.to_dict() for r in self.rectangles],
|
"rectangles": [r.to_dict() for r in self.rectangles],
|
||||||
@@ -58,6 +60,11 @@ class PatternTemplate:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "PatternTemplate":
|
def from_dict(cls, data: dict) -> "PatternTemplate":
|
||||||
@@ -79,4 +86,6 @@ class PatternTemplate:
|
|||||||
),
|
),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
tags=data.get("tags", []),
|
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,
|
rectangles: Optional[List[KeyColorRectangle]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> PatternTemplate:
|
) -> PatternTemplate:
|
||||||
self._check_name_unique(name)
|
self._check_name_unique(name)
|
||||||
|
|
||||||
@@ -77,6 +79,8 @@ class PatternTemplateStore(BaseSqliteStore[PatternTemplate]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[template_id] = template
|
self._items[template_id] = template
|
||||||
@@ -92,6 +96,8 @@ class PatternTemplateStore(BaseSqliteStore[PatternTemplate]):
|
|||||||
rectangles: Optional[List[KeyColorRectangle]] = None,
|
rectangles: Optional[List[KeyColorRectangle]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> PatternTemplate:
|
) -> PatternTemplate:
|
||||||
template = self.get(template_id)
|
template = self.get(template_id)
|
||||||
|
|
||||||
@@ -104,6 +110,10 @@ class PatternTemplateStore(BaseSqliteStore[PatternTemplate]):
|
|||||||
template.description = description
|
template.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
template.tags = tags
|
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)
|
template.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(template_id, template)
|
self._save_item(template_id, template)
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ class PictureSource:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert stream to dictionary. Subclasses extend this."""
|
"""Convert stream to dictionary. Subclasses extend this."""
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"stream_type": self.stream_type,
|
"stream_type": self.stream_type,
|
||||||
@@ -50,6 +52,11 @@ class PictureSource:
|
|||||||
"resolution_limit": None,
|
"resolution_limit": None,
|
||||||
"clock_id": None,
|
"clock_id": None,
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "PictureSource":
|
def from_dict(data: dict) -> "PictureSource":
|
||||||
@@ -80,6 +87,8 @@ def _parse_common_fields(data: dict) -> dict:
|
|||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
created_at=created_at,
|
created_at=created_at,
|
||||||
updated_at=updated_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,
|
end_time: Optional[float] = None,
|
||||||
resolution_limit: Optional[int] = None,
|
resolution_limit: Optional[int] = None,
|
||||||
clock_id: Optional[str] = None,
|
clock_id: Optional[str] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> PictureSource:
|
) -> PictureSource:
|
||||||
"""Create a new picture source.
|
"""Create a new picture source.
|
||||||
|
|
||||||
@@ -141,6 +143,8 @@ class PictureSourceStore(BaseSqliteStore[PictureSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
stream: PictureSource
|
stream: PictureSource
|
||||||
@@ -201,6 +205,8 @@ class PictureSourceStore(BaseSqliteStore[PictureSource]):
|
|||||||
end_time: Optional[float] = None,
|
end_time: Optional[float] = None,
|
||||||
resolution_limit: Optional[int] = None,
|
resolution_limit: Optional[int] = None,
|
||||||
clock_id: Optional[str] = None,
|
clock_id: Optional[str] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> PictureSource:
|
) -> PictureSource:
|
||||||
"""Update an existing picture source.
|
"""Update an existing picture source.
|
||||||
|
|
||||||
@@ -228,6 +234,10 @@ class PictureSourceStore(BaseSqliteStore[PictureSource]):
|
|||||||
stream.description = description
|
stream.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
stream.tags = tags
|
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 isinstance(stream, ScreenCapturePictureSource):
|
||||||
if display_index is not None:
|
if display_index is not None:
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ class PostprocessingTemplate:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert template to dictionary."""
|
"""Convert template to dictionary."""
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"filters": [f.to_dict() for f in self.filters],
|
"filters": [f.to_dict() for f in self.filters],
|
||||||
@@ -30,6 +32,11 @@ class PostprocessingTemplate:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "PostprocessingTemplate":
|
def from_dict(cls, data: dict) -> "PostprocessingTemplate":
|
||||||
@@ -52,4 +59,6 @@ class PostprocessingTemplate:
|
|||||||
),
|
),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
tags=data.get("tags", []),
|
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,
|
filters: Optional[List[FilterInstance]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> PostprocessingTemplate:
|
) -> PostprocessingTemplate:
|
||||||
self._check_name_unique(name)
|
self._check_name_unique(name)
|
||||||
|
|
||||||
@@ -90,6 +92,8 @@ class PostprocessingTemplateStore(BaseSqliteStore[PostprocessingTemplate]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[template_id] = template
|
self._items[template_id] = template
|
||||||
@@ -105,6 +109,8 @@ class PostprocessingTemplateStore(BaseSqliteStore[PostprocessingTemplate]):
|
|||||||
filters: Optional[List[FilterInstance]] = None,
|
filters: Optional[List[FilterInstance]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> PostprocessingTemplate:
|
) -> PostprocessingTemplate:
|
||||||
template = self.get(template_id)
|
template = self.get(template_id)
|
||||||
|
|
||||||
@@ -121,6 +127,10 @@ class PostprocessingTemplateStore(BaseSqliteStore[PostprocessingTemplate]):
|
|||||||
template.description = description
|
template.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
template.tags = tags
|
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)
|
template.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(template_id, template)
|
self._save_item(template_id, template)
|
||||||
|
|||||||
@@ -43,13 +43,16 @@ class ScenePreset:
|
|||||||
name: str
|
name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
tags: List[str] = field(default_factory=list)
|
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)
|
targets: List[TargetSnapshot] = field(default_factory=list)
|
||||||
order: int = 0
|
order: int = 0
|
||||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_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:
|
def to_dict(self) -> dict:
|
||||||
return {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
@@ -59,6 +62,11 @@ class ScenePreset:
|
|||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_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
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "ScenePreset":
|
def from_dict(cls, data: dict) -> "ScenePreset":
|
||||||
@@ -67,6 +75,8 @@ class ScenePreset:
|
|||||||
name=data["name"],
|
name=data["name"],
|
||||||
description=data.get("description", ""),
|
description=data.get("description", ""),
|
||||||
tags=data.get("tags", []),
|
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", [])],
|
targets=[TargetSnapshot.from_dict(t) for t in data.get("targets", [])],
|
||||||
order=data.get("order", 0),
|
order=data.get("order", 0),
|
||||||
created_at=datetime.fromisoformat(
|
created_at=datetime.fromisoformat(
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ class ScenePresetStore(BaseSqliteStore[ScenePreset]):
|
|||||||
order: Optional[int] = None,
|
order: Optional[int] = None,
|
||||||
targets: Optional[List[TargetSnapshot]] = None,
|
targets: Optional[List[TargetSnapshot]] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> ScenePreset:
|
) -> ScenePreset:
|
||||||
preset = self.get(preset_id)
|
preset = self.get(preset_id)
|
||||||
|
|
||||||
@@ -62,6 +64,10 @@ class ScenePresetStore(BaseSqliteStore[ScenePreset]):
|
|||||||
preset.targets = targets
|
preset.targets = targets
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
preset.tags = tags
|
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)
|
preset.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(preset_id, preset)
|
self._save_item(preset_id, preset)
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ class SyncClock:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"speed": self.speed,
|
"speed": self.speed,
|
||||||
@@ -32,6 +34,11 @@ class SyncClock:
|
|||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_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
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "SyncClock":
|
def from_dict(data: dict) -> "SyncClock":
|
||||||
@@ -43,4 +50,6 @@ class SyncClock:
|
|||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
created_at=datetime.fromisoformat(data["created_at"]),
|
created_at=datetime.fromisoformat(data["created_at"]),
|
||||||
updated_at=datetime.fromisoformat(data["updated_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,
|
speed: float = 1.0,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> SyncClock:
|
) -> SyncClock:
|
||||||
self._check_name_unique(name)
|
self._check_name_unique(name)
|
||||||
cid = f"sc_{uuid.uuid4().hex[:8]}"
|
cid = f"sc_{uuid.uuid4().hex[:8]}"
|
||||||
@@ -43,6 +45,8 @@ class SyncClockStore(BaseSqliteStore[SyncClock]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[cid] = clock
|
self._items[cid] = clock
|
||||||
@@ -57,6 +61,8 @@ class SyncClockStore(BaseSqliteStore[SyncClock]):
|
|||||||
speed: Optional[float] = None,
|
speed: Optional[float] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> SyncClock:
|
) -> SyncClock:
|
||||||
clock = self.get(clock_id)
|
clock = self.get(clock_id)
|
||||||
|
|
||||||
@@ -69,6 +75,10 @@ class SyncClockStore(BaseSqliteStore[SyncClock]):
|
|||||||
clock.description = description
|
clock.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
clock.tags = tags
|
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)
|
clock.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(clock_id, clock)
|
self._save_item(clock_id, clock)
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class CaptureTemplate:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert template to dictionary.
|
"""Convert template to dictionary.
|
||||||
@@ -24,7 +26,7 @@ class CaptureTemplate:
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary representation
|
Dictionary representation
|
||||||
"""
|
"""
|
||||||
return {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"engine_type": self.engine_type,
|
"engine_type": self.engine_type,
|
||||||
@@ -34,6 +36,11 @@ class CaptureTemplate:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict) -> "CaptureTemplate":
|
def from_dict(cls, data: dict) -> "CaptureTemplate":
|
||||||
@@ -62,4 +69,6 @@ class CaptureTemplate:
|
|||||||
),
|
),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
tags=data.get("tags", []),
|
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],
|
engine_config: Dict[str, Any],
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> CaptureTemplate:
|
) -> CaptureTemplate:
|
||||||
self._check_name_unique(name)
|
self._check_name_unique(name)
|
||||||
|
|
||||||
@@ -85,6 +87,8 @@ class TemplateStore(BaseSqliteStore[CaptureTemplate]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[template_id] = template
|
self._items[template_id] = template
|
||||||
@@ -101,6 +105,8 @@ class TemplateStore(BaseSqliteStore[CaptureTemplate]):
|
|||||||
engine_config: Optional[Dict[str, Any]] = None,
|
engine_config: Optional[Dict[str, Any]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> CaptureTemplate:
|
) -> CaptureTemplate:
|
||||||
template = self.get(template_id)
|
template = self.get(template_id)
|
||||||
|
|
||||||
@@ -115,6 +121,10 @@ class TemplateStore(BaseSqliteStore[CaptureTemplate]):
|
|||||||
template.description = description
|
template.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
template.tags = tags
|
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)
|
template.updated_at = datetime.now(timezone.utc)
|
||||||
self._save_item(template_id, template)
|
self._save_item(template_id, template)
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ class ValueSource:
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert source to dictionary. Subclasses extend this."""
|
"""Convert source to dictionary. Subclasses extend this."""
|
||||||
return {
|
d: dict = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"source_type": self.source_type,
|
"source_type": self.source_type,
|
||||||
@@ -65,6 +67,11 @@ class ValueSource:
|
|||||||
"latitude": None,
|
"latitude": None,
|
||||||
"longitude": None,
|
"longitude": None,
|
||||||
}
|
}
|
||||||
|
if self.icon:
|
||||||
|
d["icon"] = self.icon
|
||||||
|
if self.icon_color:
|
||||||
|
d["icon_color"] = self.icon_color
|
||||||
|
return d
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "ValueSource":
|
def from_dict(data: dict) -> "ValueSource":
|
||||||
@@ -95,6 +102,8 @@ def _parse_common_fields(data: dict) -> dict:
|
|||||||
tags=data.get("tags", []),
|
tags=data.get("tags", []),
|
||||||
created_at=created_at,
|
created_at=created_at,
|
||||||
updated_at=updated_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,
|
sensor_label: Optional[str] = None,
|
||||||
poll_interval: Optional[float] = None,
|
poll_interval: Optional[float] = None,
|
||||||
clock_id: Optional[str] = None,
|
clock_id: Optional[str] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> ValueSource:
|
) -> ValueSource:
|
||||||
_VALID = (
|
_VALID = (
|
||||||
"static",
|
"static",
|
||||||
@@ -110,6 +112,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
common_tags = tags or []
|
common_tags = tags or []
|
||||||
|
common_icon = icon or ""
|
||||||
|
common_icon_color = icon_color or ""
|
||||||
|
|
||||||
if source_type == "static":
|
if source_type == "static":
|
||||||
source: ValueSource = StaticValueSource(
|
source: ValueSource = StaticValueSource(
|
||||||
@@ -120,6 +124,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
value=value if value is not None else 1.0,
|
value=value if value is not None else 1.0,
|
||||||
)
|
)
|
||||||
elif source_type == "animated":
|
elif source_type == "animated":
|
||||||
@@ -131,6 +137,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
waveform=waveform or "sine",
|
waveform=waveform or "sine",
|
||||||
speed=speed if speed is not None else 10.0,
|
speed=speed if speed is not None else 10.0,
|
||||||
min_value=min_value if min_value is not None else 0.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,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
audio_source_id=audio_source_id or "",
|
audio_source_id=audio_source_id or "",
|
||||||
mode=mode or "rms",
|
mode=mode or "rms",
|
||||||
sensitivity=sensitivity if sensitivity is not None else 1.0,
|
sensitivity=sensitivity if sensitivity is not None else 1.0,
|
||||||
@@ -165,6 +175,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
schedule=schedule_data,
|
schedule=schedule_data,
|
||||||
min_value=min_value if min_value is not None else 0.0,
|
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,
|
max_value=max_value if max_value is not None else 1.0,
|
||||||
@@ -178,6 +190,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
picture_source_id=picture_source_id or "",
|
picture_source_id=picture_source_id or "",
|
||||||
scene_behavior=scene_behavior or "complement",
|
scene_behavior=scene_behavior or "complement",
|
||||||
sensitivity=sensitivity if sensitivity is not None else 1.0,
|
sensitivity=sensitivity if sensitivity is not None else 1.0,
|
||||||
@@ -194,6 +208,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
speed=speed if speed is not None else 1.0,
|
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,
|
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,
|
latitude=latitude if latitude is not None else 50.0,
|
||||||
@@ -210,6 +226,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
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],
|
color=color if isinstance(color, list) and len(color) == 3 else [255, 255, 255],
|
||||||
)
|
)
|
||||||
elif source_type == "animated_color":
|
elif source_type == "animated_color":
|
||||||
@@ -221,6 +239,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
colors=(
|
colors=(
|
||||||
colors
|
colors
|
||||||
if isinstance(colors, list) and len(colors) >= 2
|
if isinstance(colors, list) and len(colors) >= 2
|
||||||
@@ -242,6 +262,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
schedule=schedule_data,
|
schedule=schedule_data,
|
||||||
)
|
)
|
||||||
elif source_type == "ha_entity":
|
elif source_type == "ha_entity":
|
||||||
@@ -257,6 +279,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
ha_source_id=ha_source_id,
|
ha_source_id=ha_source_id,
|
||||||
entity_id=entity_id,
|
entity_id=entity_id,
|
||||||
attribute=attribute or "",
|
attribute=attribute or "",
|
||||||
@@ -275,6 +299,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
value_source_id=value_source_id,
|
value_source_id=value_source_id,
|
||||||
gradient_id=gradient_id or "",
|
gradient_id=gradient_id or "",
|
||||||
easing=easing or "linear",
|
easing=easing or "linear",
|
||||||
@@ -290,6 +316,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
color_strip_source_id=color_strip_source_id,
|
color_strip_source_id=color_strip_source_id,
|
||||||
led_start=led_start if led_start is not None else 0,
|
led_start=led_start if led_start is not None else 0,
|
||||||
led_end=led_end if led_end is not None else -1,
|
led_end=led_end if led_end is not None else -1,
|
||||||
@@ -306,6 +334,8 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
updated_at=now,
|
updated_at=now,
|
||||||
description=description,
|
description=description,
|
||||||
tags=common_tags,
|
tags=common_tags,
|
||||||
|
icon=common_icon,
|
||||||
|
icon_color=common_icon_color,
|
||||||
metric=m,
|
metric=m,
|
||||||
min_value=min_value if min_value is not None else 0.0,
|
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,
|
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,
|
sensor_label: Optional[str] = None,
|
||||||
poll_interval: Optional[float] = None,
|
poll_interval: Optional[float] = None,
|
||||||
clock_id: Optional[str] = None,
|
clock_id: Optional[str] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> ValueSource:
|
) -> ValueSource:
|
||||||
source = self.get(source_id)
|
source = self.get(source_id)
|
||||||
|
|
||||||
@@ -374,6 +406,10 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
|||||||
source.description = description
|
source.description = description
|
||||||
if tags is not None:
|
if tags is not None:
|
||||||
source.tags = tags
|
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 isinstance(source, StaticValueSource):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
|||||||
@@ -46,9 +46,11 @@ class WeatherSource:
|
|||||||
update_interval: int = 600 # seconds (10 min default)
|
update_interval: int = 600 # seconds (10 min default)
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = field(default_factory=list)
|
tags: List[str] = field(default_factory=list)
|
||||||
|
icon: str = ""
|
||||||
|
icon_color: str = ""
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"provider": self.provider,
|
"provider": self.provider,
|
||||||
@@ -61,6 +63,11 @@ class WeatherSource:
|
|||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
"updated_at": self.updated_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
|
@staticmethod
|
||||||
def from_dict(data: dict) -> "WeatherSource":
|
def from_dict(data: dict) -> "WeatherSource":
|
||||||
@@ -72,4 +79,6 @@ class WeatherSource:
|
|||||||
latitude=data.get("latitude", 50.0),
|
latitude=data.get("latitude", 50.0),
|
||||||
longitude=data.get("longitude", 0.0),
|
longitude=data.get("longitude", 0.0),
|
||||||
update_interval=data.get("update_interval", 600),
|
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,
|
update_interval: int = 600,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> WeatherSource:
|
) -> WeatherSource:
|
||||||
from ledgrab.core.weather.weather_provider import PROVIDER_REGISTRY
|
from ledgrab.core.weather.weather_provider import PROVIDER_REGISTRY
|
||||||
|
|
||||||
@@ -65,6 +67,8 @@ class WeatherSourceStore(BaseSqliteStore[WeatherSource]):
|
|||||||
update_interval=update_interval,
|
update_interval=update_interval,
|
||||||
description=description,
|
description=description,
|
||||||
tags=tags or [],
|
tags=tags or [],
|
||||||
|
icon=icon or "",
|
||||||
|
icon_color=icon_color or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._items[sid] = source
|
self._items[sid] = source
|
||||||
@@ -83,6 +87,8 @@ class WeatherSourceStore(BaseSqliteStore[WeatherSource]):
|
|||||||
update_interval: Optional[int] = None,
|
update_interval: Optional[int] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tags: Optional[List[str]] = None,
|
tags: Optional[List[str]] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
icon_color: Optional[str] = None,
|
||||||
) -> WeatherSource:
|
) -> WeatherSource:
|
||||||
existing = self.get(source_id)
|
existing = self.get(source_id)
|
||||||
|
|
||||||
@@ -118,6 +124,8 @@ class WeatherSourceStore(BaseSqliteStore[WeatherSource]):
|
|||||||
),
|
),
|
||||||
description=description if description is not None else existing.description,
|
description=description if description is not None else existing.description,
|
||||||
tags=tags if tags is not None else existing.tags,
|
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
|
self._items[source_id] = updated
|
||||||
|
|||||||
Reference in New Issue
Block a user