"""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)