Update docs and screenshots
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s

This commit is contained in:
2026-07-25 10:26:29 +02:00
parent d6cda77aaf
commit 1f110866f5
170 changed files with 24657 additions and 3 deletions
+3
View File
@@ -0,0 +1,3 @@
from app.live.service import LiveDeskService
__all__ = ["LiveDeskService"]
+219
View File
@@ -0,0 +1,219 @@
from __future__ import annotations
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.core.database import SessionLocal
from app.dmx.engine import DmxEngine
from app.dmx.frame import FrameLayer
from app.homeassistant.service import HomeAssistantService
from app.patch.service import PatchService
class LiveDeskService:
def __init__(
self,
engine: DmxEngine,
home_assistant: HomeAssistantService | None = None,
session_factory: async_sessionmaker[AsyncSession] = SessionLocal,
) -> None:
self.engine = engine
self.patch = PatchService(session_factory)
self.home_assistant = home_assistant
self._values_by_patch: dict[int, dict[int, int]] = {}
self._values_by_home_assistant_mapping: dict[int, dict[int, int]] = {}
async def snapshot(self) -> dict[str, object]:
patches = await self.patch.list_instances()
items: list[dict[str, object]] = []
for patch in patches:
relative_values = self._values_by_patch.get(int(patch["id"]), {})
channels: list[dict[str, object]] = []
for channel in patch.get("channels", []):
if not isinstance(channel, dict):
continue
relative_index = int(channel.get("index", 0))
absolute_channel = int(patch["start_address"]) + relative_index - 1
channels.append(
{
"index": relative_index,
"absolute_channel": absolute_channel,
"key": str(channel.get("key", f"channel-{relative_index}")),
"display_name": str(channel.get("display_name", f"Channel {relative_index}")),
"precedence": str(channel.get("precedence", "ltp")),
"resolution": int(channel.get("resolution", 8)),
"value": int(relative_values.get(relative_index, 0)),
}
)
items.append(
{
"id": f"patch:{patch['id']}",
"source_type": "patch",
"source_id": patch["id"],
"patch_id": patch["id"],
"name": patch["name"],
"manufacturer": patch["manufacturer"],
"model": patch["model"],
"mode_key": patch["mode_key"],
"universe": patch["universe"],
"start_address": patch["start_address"],
"end_address": patch["end_address"],
"channel_count": patch["channel_count"],
"entity_id": None,
"status": "active" if patch.get("enabled", True) else "disabled",
"last_sent_summary": None,
"channels": channels,
}
)
if self.home_assistant is not None:
mappings = (await self.home_assistant.list_mappings())["items"]
for mapping in mappings:
if not isinstance(mapping, dict):
continue
mapping_id = int(mapping["id"])
relative_values = self._values_by_home_assistant_mapping.get(mapping_id, {})
channels = []
for channel in self._build_home_assistant_channels(mapping):
relative_index = int(channel["index"])
absolute_channel = int(mapping["start_address"]) + relative_index - 1
channels.append(
{
"index": relative_index,
"absolute_channel": absolute_channel,
"key": str(channel["key"]),
"display_name": str(channel["display_name"]),
"precedence": str(channel["precedence"]),
"resolution": 8,
"value": int(relative_values.get(relative_index, 0)),
}
)
items.append(
{
"id": f"home_assistant:{mapping_id}",
"source_type": "home_assistant",
"source_id": mapping_id,
"patch_id": None,
"name": mapping["name"],
"manufacturer": "Home Assistant",
"model": mapping["fixture_type"],
"mode_key": f"HA {mapping['fixture_type']}",
"universe": mapping["universe"],
"start_address": mapping["start_address"],
"end_address": int(mapping["start_address"]) + int(mapping["channel_span"]) - 1,
"channel_count": mapping["channel_span"],
"entity_id": mapping["entity_id"],
"status": mapping["status"],
"last_sent_summary": mapping["last_sent_summary"],
"channels": channels,
}
)
return {"items": items}
async def set_patch_values(self, patch_id: int, values: dict[int, int]) -> dict[str, object]:
patches = await self.patch.list_instances()
patch = next((item for item in patches if int(item["id"]) == patch_id), None)
if patch is None:
raise LookupError("Patch not found")
channel_map = {
int(channel["index"]): channel
for channel in patch.get("channels", [])
if isinstance(channel, dict) and "index" in channel
}
sanitized_values: dict[int, int] = {}
absolute_values: dict[int, int] = {}
precedence_map: dict[int, str] = {}
for relative_index, raw_value in values.items():
if relative_index not in channel_map:
continue
value = max(0, min(255, int(raw_value)))
sanitized_values[relative_index] = value
absolute_channel = int(patch["start_address"]) + relative_index - 1
absolute_values[absolute_channel] = value
precedence_map[absolute_channel] = str(channel_map[relative_index].get("precedence", "ltp")).lower()
self._values_by_patch[patch_id] = sanitized_values
self.engine.set_layer(
FrameLayer.from_channel_values(
name=f"mixer:patch:{patch_id}",
priority=90,
values=absolute_values,
precedence_map=precedence_map,
universe=int(patch["universe"]),
)
)
return await self.snapshot()
async def clear_patch(self, patch_id: int) -> dict[str, object]:
self._values_by_patch.pop(patch_id, None)
self.engine.remove_layer(f"mixer:patch:{patch_id}")
return await self.snapshot()
async def set_home_assistant_mapping_values(self, mapping_id: int, values: dict[int, int]) -> dict[str, object]:
if self.home_assistant is None:
raise LookupError("Home Assistant live mixer is not available")
mappings = (await self.home_assistant.list_mappings())["items"]
mapping = next((item for item in mappings if int(item["id"]) == mapping_id), None)
if mapping is None:
raise LookupError("Home Assistant mapping not found")
channel_map = {
int(channel["index"]): channel for channel in self._build_home_assistant_channels(mapping)
}
sanitized_values: dict[int, int] = {}
absolute_values: dict[int, int] = {}
precedence_map: dict[int, str] = {}
for relative_index, raw_value in values.items():
if relative_index not in channel_map:
continue
value = max(0, min(255, int(raw_value)))
sanitized_values[relative_index] = value
absolute_channel = int(mapping["start_address"]) + relative_index - 1
absolute_values[absolute_channel] = value
precedence_map[absolute_channel] = str(channel_map[relative_index].get("precedence", "ltp")).lower()
self._values_by_home_assistant_mapping[mapping_id] = sanitized_values
self.engine.set_layer(
FrameLayer.from_channel_values(
name=f"mixer:home-assistant:{mapping_id}",
priority=90,
values=absolute_values,
precedence_map=precedence_map,
universe=int(mapping["universe"]),
)
)
return await self.snapshot()
async def clear_home_assistant_mapping(self, mapping_id: int) -> dict[str, object]:
self._values_by_home_assistant_mapping.pop(mapping_id, None)
self.engine.remove_layer(f"mixer:home-assistant:{mapping_id}")
return await self.snapshot()
def _build_home_assistant_channels(self, mapping: dict[str, object]) -> list[dict[str, object]]:
fixture_type = str(mapping["fixture_type"])
has_master = bool(mapping.get("master_dimmer", False))
if fixture_type == "switch":
return [{"index": 1, "key": "Switch", "display_name": "State", "precedence": "ltp"}]
if fixture_type == "scene":
return [{"index": 1, "key": "Scene", "display_name": "Trigger", "precedence": "ltp"}]
if fixture_type == "automation":
return [{"index": 1, "key": "Automation", "display_name": "Trigger", "precedence": "ltp"}]
if fixture_type == "dimmer":
return [{"index": 1, "key": "Dimmer", "display_name": "Dimmer", "precedence": "ltp"}]
channels: list[dict[str, object]] = []
next_index = 1
if has_master:
channels.append({"index": next_index, "key": "Master", "display_name": "Master", "precedence": "ltp"})
next_index += 1
if fixture_type == "rgb":
names = ["Red", "Green", "Blue"]
elif fixture_type == "rgbw":
names = ["Red", "Green", "Blue", "White"]
elif fixture_type == "cct":
names = ["Warm", "Cold"]
else:
names = ["Channel"]
for name in names:
channels.append({"index": next_index, "key": name, "display_name": name, "precedence": "ltp"})
next_index += 1
return channels