Add Art-Net / sACN (E1.31) DMX device support
Full-stack implementation of DMX output for stage lighting and LED controllers: - DMXClient with Art-Net and sACN packet builders, multi-universe splitting - DMXDeviceProvider with manual_led_count capability and URL parsing - Device store, API schemas, routes wired with dmx_protocol/start_universe/start_channel - Frontend: add/settings modals with DMX fields, IconSelect protocol picker - Fix add device modal dirty check on type change (re-snapshot after switch) - i18n keys for DMX in en/ru/zh locales Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,10 @@ class Device:
|
||||
rgbw: bool = False,
|
||||
zone_mode: str = "combined",
|
||||
tags: List[str] = None,
|
||||
# DMX (Art-Net / sACN) fields
|
||||
dmx_protocol: str = "artnet",
|
||||
dmx_start_universe: int = 0,
|
||||
dmx_start_channel: int = 1,
|
||||
created_at: Optional[datetime] = None,
|
||||
updated_at: Optional[datetime] = None,
|
||||
):
|
||||
@@ -50,6 +54,9 @@ class Device:
|
||||
self.rgbw = rgbw
|
||||
self.zone_mode = zone_mode
|
||||
self.tags = tags or []
|
||||
self.dmx_protocol = dmx_protocol
|
||||
self.dmx_start_universe = dmx_start_universe
|
||||
self.dmx_start_channel = dmx_start_channel
|
||||
self.created_at = created_at or datetime.now(timezone.utc)
|
||||
self.updated_at = updated_at or datetime.now(timezone.utc)
|
||||
|
||||
@@ -79,6 +86,12 @@ class Device:
|
||||
d["zone_mode"] = self.zone_mode
|
||||
if self.tags:
|
||||
d["tags"] = self.tags
|
||||
if self.dmx_protocol != "artnet":
|
||||
d["dmx_protocol"] = self.dmx_protocol
|
||||
if self.dmx_start_universe != 0:
|
||||
d["dmx_start_universe"] = self.dmx_start_universe
|
||||
if self.dmx_start_channel != 1:
|
||||
d["dmx_start_channel"] = self.dmx_start_channel
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
@@ -98,6 +111,9 @@ class Device:
|
||||
rgbw=data.get("rgbw", False),
|
||||
zone_mode=data.get("zone_mode", "combined"),
|
||||
tags=data.get("tags", []),
|
||||
dmx_protocol=data.get("dmx_protocol", "artnet"),
|
||||
dmx_start_universe=data.get("dmx_start_universe", 0),
|
||||
dmx_start_channel=data.get("dmx_start_channel", 1),
|
||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.now(timezone.utc).isoformat())),
|
||||
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.now(timezone.utc).isoformat())),
|
||||
)
|
||||
@@ -183,6 +199,9 @@ class DeviceStore:
|
||||
rgbw: bool = False,
|
||||
zone_mode: str = "combined",
|
||||
tags: Optional[List[str]] = None,
|
||||
dmx_protocol: str = "artnet",
|
||||
dmx_start_universe: int = 0,
|
||||
dmx_start_channel: int = 1,
|
||||
) -> Device:
|
||||
"""Create a new device."""
|
||||
device_id = f"device_{uuid.uuid4().hex[:8]}"
|
||||
@@ -203,6 +222,9 @@ class DeviceStore:
|
||||
rgbw=rgbw,
|
||||
zone_mode=zone_mode,
|
||||
tags=tags or [],
|
||||
dmx_protocol=dmx_protocol,
|
||||
dmx_start_universe=dmx_start_universe,
|
||||
dmx_start_channel=dmx_start_channel,
|
||||
)
|
||||
|
||||
self._devices[device_id] = device
|
||||
@@ -232,6 +254,9 @@ class DeviceStore:
|
||||
rgbw: Optional[bool] = None,
|
||||
zone_mode: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
dmx_protocol: Optional[str] = None,
|
||||
dmx_start_universe: Optional[int] = None,
|
||||
dmx_start_channel: Optional[int] = None,
|
||||
) -> Device:
|
||||
"""Update device."""
|
||||
device = self._devices.get(device_id)
|
||||
@@ -258,6 +283,12 @@ class DeviceStore:
|
||||
device.zone_mode = zone_mode
|
||||
if tags is not None:
|
||||
device.tags = tags
|
||||
if dmx_protocol is not None:
|
||||
device.dmx_protocol = dmx_protocol
|
||||
if dmx_start_universe is not None:
|
||||
device.dmx_start_universe = dmx_start_universe
|
||||
if dmx_start_channel is not None:
|
||||
device.dmx_start_channel = dmx_start_channel
|
||||
|
||||
device.updated_at = datetime.now(timezone.utc)
|
||||
self.save()
|
||||
|
||||
Reference in New Issue
Block a user