Files
led-controller/src/util/driver_delivery.py
2026-05-28 00:38:21 +12:00

149 lines
4.2 KiB
Python

"""Deliver v1 JSON to drivers via bridge devices envelope."""
import asyncio
import json
from typing import Any, Dict, List, Optional, Set, Union
from util.bridge_envelope import (
BROADCAST_MAC,
build_devices_envelope,
format_mac_key,
normalize_mac_key,
split_v1_body_for_espnow,
)
from util.espnow_message import build_message
_MAX_JSON_ESPNOW = 240
def _body_from_message(msg: Union[str, bytes, bytearray, Dict[str, Any]]) -> Optional[Dict[str, Any]]:
if isinstance(msg, dict):
if msg.get("v") == "1" and "devices" not in msg:
return dict(msg)
return None
if isinstance(msg, str):
try:
data = json.loads(msg)
except (ValueError, TypeError):
return None
return data if isinstance(data, dict) else None
if isinstance(msg, (bytes, bytearray)):
raw = bytes(msg)
if not raw or raw[0] != ord("{"):
return None
try:
data = json.loads(raw.decode("utf-8"))
except (UnicodeError, ValueError, TypeError):
return None
return data if isinstance(data, dict) else None
return None
async def _deliver_v1_body(bridge, mac_key: str, body: Dict[str, Any], delay_s: float) -> int:
deliveries = 0
try:
chunks = split_v1_body_for_espnow(body)
except ValueError:
return 0
for chunk in chunks:
env = build_devices_envelope({mac_key: chunk})
if await bridge.send(env):
deliveries += 1
if delay_s > 0:
await asyncio.sleep(delay_s)
return deliveries
def build_preset_json_chunks(
presets_by_name: Dict[str, Any],
*,
save: bool = False,
default: Optional[str] = None,
max_payload: int = _MAX_JSON_ESPNOW,
) -> List[str]:
entries = list(presets_by_name.items())
chunks: List[str] = []
batch: Dict[str, Any] = {}
def _msg_for(presets_map: Dict[str, Any], *, final_save: bool, def_id: Optional[str]) -> str:
return build_message(
presets=presets_map,
save=final_save,
default=def_id,
)
for name, preset_obj in entries:
trial = dict(batch)
trial[name] = preset_obj
try:
msg = _msg_for(trial, final_save=False, def_id=None)
except (TypeError, ValueError):
msg = ""
if len(msg.encode("utf-8")) <= max_payload or not batch:
batch = trial
else:
chunks.append(_msg_for(batch, final_save=False, def_id=None))
batch = {name: preset_obj}
if batch:
chunks.append(
_msg_for(
batch,
final_save=save,
def_id=str(default) if default else None,
)
)
return [c for c in chunks if c]
def _unicast_mac_keys(target_macs: Optional[List[str]]) -> List[str]:
"""One formatted MAC per target; empty list means broadcast."""
if not target_macs:
return [BROADCAST_MAC]
keys: List[str] = []
seen: set = set()
for raw in target_macs:
h = normalize_mac_key(raw)
if h and h not in seen:
seen.add(h)
keys.append(format_mac_key(h))
return keys if keys else [BROADCAST_MAC]
async def deliver_json_messages(
bridge,
messages,
target_macs,
devices_model,
delay_s=0.1,
*,
unicast: bool = False,
):
"""
Deliver v1 JSON to drivers. Default: ESP-NOW broadcast (``ff:ff:…``); drivers
filter on ``groups`` in the body. Set ``unicast=True`` only for per-device settings
or single-device identify.
Uses the current bridge connection only (per-group bridge assignment is disabled).
"""
del devices_model
from models.transport import get_current_bridge
active = get_current_bridge() or bridge
if active is None:
raise RuntimeError("Transport not configured")
if unicast and target_macs:
mac_keys = _unicast_mac_keys(target_macs)
else:
mac_keys = [BROADCAST_MAC]
deliveries = 0
for mac_key in mac_keys:
for msg in messages:
body = _body_from_message(msg)
if not body:
continue
deliveries += await _deliver_v1_body(active, mac_key, body, delay_s)
return deliveries, len(messages)