chore(release): beta-1.03

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-10 16:55:07 +12:00
parent 822d9d8e01
commit 0ae39ab94b
22 changed files with 1816 additions and 184 deletions

View File

@@ -38,6 +38,29 @@ def normalize_mac(mac):
return None
def resolve_device_mac_for_select_routing(devices, name_key):
"""
Map a v1 ``select`` map key to device storage id (MAC).
Matches the registry **name**, or ``led-<12hex>`` as a MAC hint (default driver
name form) so routing still works after the device is renamed in the registry.
"""
k = str(name_key or "").strip()
if not k:
return None
for did in devices.list():
doc = devices.read(did) or {}
if str(doc.get("name") or "").strip() == k:
m = normalize_mac(did)
if m:
return m
if k.startswith("led-"):
m = normalize_mac(k[4:])
if m and devices.read(m):
return m
return None
def derive_device_mac(mac=None, address=None, transport="espnow"):
"""
Resolve the device MAC used as storage id.

View File

@@ -1,14 +1,66 @@
from models.model import Model
class Group(Model):
"""Device groups (members + optional WiFi driver defaults); also pattern fields for sequences."""
def __init__(self):
super().__init__()
def load(self):
super().load()
changed = False
for gid, doc in list(self.items()):
if not isinstance(doc, dict):
continue
if self._migrate_record(doc):
changed = True
if changed:
self.save()
def _migrate_record(self, doc):
changed = False
raw_dev = doc.get("devices")
if raw_dev is None:
doc["devices"] = []
changed = True
elif isinstance(raw_dev, list):
norm = []
for x in raw_dev:
if x is None:
continue
s = str(x).strip().lower().replace(":", "").replace("-", "")
if len(s) == 12 and all(c in "0123456789abcdef" for c in s):
norm.append(s)
else:
norm.append(str(x).strip())
if norm != raw_dev:
doc["devices"] = norm
changed = True
for key in (
"wifi_driver_display_name",
"wifi_driver_num_leds",
"wifi_color_order",
"wifi_startup_mode",
):
if key not in doc:
doc[key] = None
changed = True
if "output_brightness" not in doc:
doc["output_brightness"] = 255
changed = True
return changed
def create(self, name=""):
next_id = self.get_next_id()
self[next_id] = {
"name": name,
"devices": [],
"wifi_driver_display_name": None,
"wifi_driver_num_leds": None,
"wifi_color_order": None,
"wifi_startup_mode": None,
"output_brightness": 255,
"pattern": "on",
"colors": ["000000", "FF0000"],
"brightness": 100,
@@ -22,7 +74,7 @@ class Group(Model):
"n5": 0,
"n6": 0,
"n7": 0,
"n8": 0
"n8": 0,
}
self.save()
return next_id

View File

@@ -261,33 +261,33 @@ async def _driver_connection_loop(ip: str) -> None:
retry_interval_s = 2.0
retry_interval_s = max(0.2, retry_interval_s)
try:
retry_window_s = float(_settings.get("wifi_driver_connect_retry_window_s", 120.0))
max_boot_attempts = int(_settings.get("wifi_driver_initial_connect_attempts", 4))
except (TypeError, ValueError):
retry_window_s = 120.0
retry_window_s = max(5.0, retry_window_s)
max_boot_attempts = 4
max_boot_attempts = max(1, max_boot_attempts)
try:
open_timeout = float(_settings.get("wifi_driver_ws_open_timeout", 45.0))
except (TypeError, ValueError):
open_timeout = 45.0
open_timeout = max(5.0, open_timeout)
loop = asyncio.get_running_loop()
stagger = _stagger_delay_s_for_ip(ip)
if stagger > 0:
await asyncio.sleep(stagger)
# Only bound boot-time: after we have connected once, keep retrying (Wi-Fi drops, reboots).
connected_once = False
deadline = loop.time() + retry_window_s
boot_attempts = 0
try:
while True:
now = loop.time()
if not connected_once and now >= deadline:
print(
f"[WS] driver {ip} still unreachable after {int(retry_window_s)}s "
f"(initial window); stopping until next UDP hello / registry prime"
)
break
if not connected_once:
if boot_attempts >= max_boot_attempts:
print(
f"[WS] driver {ip} still unreachable after {max_boot_attempts} "
f"initial dial attempt(s); stopping until next UDP hello / registry prime"
)
break
boot_attempts += 1
try:
print(f"[WS] connecting to {uri!r}")
async with websockets.connect(

View File

@@ -27,11 +27,27 @@ class Zone(Model):
Zone._migration_checked = True
super().__init__()
def create(self, name="", names=None, presets=None):
def load(self):
super().load()
changed = False
for zid, doc in list(self.items()):
if not isinstance(doc, dict):
continue
if "group_ids" not in doc:
doc["group_ids"] = []
changed = True
if changed:
self.save()
def create(self, name="", names=None, presets=None, group_ids=None):
next_id = self.get_next_id()
gid_list = []
if isinstance(group_ids, list):
gid_list = [str(x) for x in group_ids if x is not None]
self[next_id] = {
"name": name,
"names": names if names else [],
"group_ids": gid_list,
"presets": presets if presets else [],
"default_preset": None,
"brightness": 255,