Send presets and select on broadcast with groups; unicast only for per-device settings. V1 select as [preset_id, step?]. Sequence steps use beat counts; manual presets get select each beat, auto only on step change. Bridge downlink router, Pi envelope delivery, and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
174 lines
4.6 KiB
Python
174 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for bridge devices envelope (Pi + espnow-sender downlink)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
|
|
from util.bridge_envelope import ( # noqa: E402
|
|
BROADCAST_MAC,
|
|
build_devices_envelope,
|
|
build_groups_envelope,
|
|
build_v1_body,
|
|
envelope_payload_size,
|
|
format_mac_key,
|
|
is_broadcast_mac,
|
|
split_v1_body_for_espnow,
|
|
v1_body_size,
|
|
)
|
|
|
|
|
|
def test_unicast_mac_keys_per_device():
|
|
from util.driver_delivery import _unicast_mac_keys
|
|
|
|
keys = _unicast_mac_keys(["188b0e1560a8", "e8f60a16ea10"])
|
|
assert len(keys) == 2
|
|
assert keys[0] == "18:8b:0e:15:60:a8"
|
|
assert keys[1] == "e8:f6:0a:16:ea:10"
|
|
assert _unicast_mac_keys(["188b0e1560a8"]) == ["18:8b:0e:15:60:a8"]
|
|
assert _unicast_mac_keys(None) == [BROADCAST_MAC]
|
|
|
|
|
|
def test_deliver_json_messages_defaults_broadcast():
|
|
from util.driver_delivery import deliver_json_messages
|
|
|
|
class _Sender:
|
|
def __init__(self):
|
|
self.keys = []
|
|
|
|
async def send(self, envelope):
|
|
devs = envelope.get("dv") or envelope.get("devices") or {}
|
|
self.keys.extend(devs.keys())
|
|
return True
|
|
|
|
async def _run():
|
|
sender = _Sender()
|
|
await deliver_json_messages(
|
|
sender,
|
|
[json.dumps({"v": "1", "select": ["2"]})],
|
|
["188b0e1560a8", "e8f60a16ea10"],
|
|
None,
|
|
)
|
|
return sender.keys
|
|
|
|
keys = __import__("asyncio").run(_run())
|
|
assert keys == [BROADCAST_MAC]
|
|
|
|
|
|
def is_devices_envelope(raw: bytes) -> bool:
|
|
if not raw or raw[0:1] != b"{":
|
|
return False
|
|
try:
|
|
data = json.loads(raw)
|
|
except (ValueError, TypeError):
|
|
return False
|
|
devs = data.get("devices") if isinstance(data, dict) else None
|
|
if devs is None and isinstance(data, dict):
|
|
devs = data.get("dv")
|
|
return isinstance(data, dict) and data.get("v") == "1" and isinstance(devs, dict)
|
|
|
|
|
|
def build_driver_payload(body: dict) -> bytes:
|
|
out = {"v": "1", **{k: body[k] for k in body if k != "v"}}
|
|
raw = json.dumps(out)
|
|
if len(raw) > 250:
|
|
raise ValueError("too large")
|
|
return raw.encode("utf-8")
|
|
|
|
|
|
def test_build_groups_envelope():
|
|
env = build_groups_envelope("e8f60a16ea10", ["5", "18"])
|
|
assert env["v"] == "1"
|
|
key = format_mac_key("e8f60a16ea10")
|
|
devs = env.get("dv") or env.get("devices")
|
|
body = devs[key]
|
|
assert body["sg"] is True
|
|
assert body["g"] == ["5", "18"]
|
|
|
|
|
|
def test_is_broadcast_mac():
|
|
assert is_broadcast_mac("ff:ff:ff:ff:ff:ff")
|
|
assert is_broadcast_mac("ffffffffffff")
|
|
assert not is_broadcast_mac("e8f60a16ea10")
|
|
|
|
|
|
def test_is_devices_envelope():
|
|
env = build_devices_envelope(
|
|
{
|
|
BROADCAST_MAC: build_v1_body(
|
|
presets={"1": {"p": "on", "c": ["#FFFFFF"], "a": True}},
|
|
groups=["5"],
|
|
set_groups=False,
|
|
)
|
|
}
|
|
)
|
|
raw = json.dumps(env).encode("utf-8")
|
|
assert is_devices_envelope(raw)
|
|
assert not is_devices_envelope(b'{"v":"1","s":{}}')
|
|
|
|
|
|
def test_build_driver_payload_size():
|
|
body = build_v1_body(
|
|
presets={"x": {"pattern": "on", "colors": ["#FF0000"], "auto": True}},
|
|
select=["x", 0],
|
|
save=True,
|
|
)
|
|
payload = build_driver_payload(body)
|
|
assert len(payload) <= 250
|
|
data = json.loads(payload)
|
|
assert data["v"] == "1"
|
|
assert data["s"] == ["x", 0]
|
|
|
|
|
|
def test_split_preset_and_select():
|
|
body = build_v1_body(
|
|
presets={
|
|
"2": {
|
|
"p": "on",
|
|
"c": ["#FFFFFF"],
|
|
"bg": "#000000",
|
|
"d": 100,
|
|
"b": 255,
|
|
"a": True,
|
|
"n1": 0,
|
|
"n2": 0,
|
|
}
|
|
},
|
|
select=["2", 0],
|
|
save=True,
|
|
)
|
|
if v1_body_size(body) <= 250:
|
|
chunks = split_v1_body_for_espnow(body)
|
|
assert len(chunks) == 1
|
|
else:
|
|
chunks = split_v1_body_for_espnow(body)
|
|
assert len(chunks) >= 2
|
|
assert all(v1_body_size(c) <= 250 for c in chunks)
|
|
assert "p" in chunks[0]
|
|
assert any("s" in c for c in chunks)
|
|
|
|
|
|
def test_envelope_fits_espnow_limit():
|
|
env = build_devices_envelope(
|
|
{
|
|
BROADCAST_MAC: build_v1_body(
|
|
presets={
|
|
"2": {
|
|
"pattern": "on",
|
|
"colors": ["#FFFFFF"],
|
|
"auto": True,
|
|
}
|
|
},
|
|
select=["2"],
|
|
)
|
|
}
|
|
)
|
|
assert envelope_payload_size(env) <= 250
|