From d9aaca1856e84f310c276287c2cb59dc8b61b51a Mon Sep 17 00:00:00 2001
From: Laurens <43173895+GreenGrassBlueOcean@users.noreply.github.com>
Date: Sat, 17 Oct 2026 19:56:59 +0200
Subject: [PATCH 1/2] fix(resync): require all registered area entities to echo
before skipping or cancelling area sweep (#746)
---
custom_components/myhome/gateway_resync.py | 100 +-
docs/configuration/runtime_behaviour.md | 7 +-
tests/fixtures/traces/issue_746/README.md | 31 +
..._trace_MH200N_all_2026-10-17T14-28-20.json | 1468 +++++++++++++++++
tests/test_issue_368_resync.py | 106 ++
...t_issue_746_mh200n_gen_off_switch_trace.py | 222 +++
6 files changed, 1914 insertions(+), 20 deletions(-)
create mode 100644 tests/fixtures/traces/issue_746/README.md
create mode 100644 tests/fixtures/traces/issue_746/myhome_trace_MH200N_all_2026-10-17T14-28-20.json
create mode 100644 tests/test_issue_746_mh200n_gen_off_switch_trace.py
diff --git a/custom_components/myhome/gateway_resync.py b/custom_components/myhome/gateway_resync.py
index b439d871e..77309793b 100644
--- a/custom_components/myhome/gateway_resync.py
+++ b/custom_components/myhome/gateway_resync.py
@@ -19,6 +19,7 @@
RESYNC_DEBOUNCE_S,
RESYNC_LEADING_WINDOW_S,
area_of_where,
+ normalize_where,
)
from .discovery import Address, parse_unique_id
@@ -48,6 +49,8 @@ def __init__(
self._recent_ptp: collections.deque[tuple[float, str, str | None]] = (
recent_ptp if recent_ptp is not None else collections.deque()
)
+ self._ptp_polarities: dict[tuple[float, str], bool | None] = {}
+ self._resync_area_echoes: dict[str, set[str]] = {}
@property
def hass(self) -> HomeAssistant:
@@ -75,17 +78,28 @@ def recent_ptp(self) -> collections.deque[tuple[float, str, str | None]]:
"""Return recent point-to-point frame deque."""
return self._recent_ptp
+ def _prune_recent_ptp(self, now: float) -> None:
+ """Prune expired point-to-point echo records older than the leading window."""
+ while self._recent_ptp and self._recent_ptp[0][0] < now - RESYNC_LEADING_WINDOW_S:
+ old_ts, old_where, _ = self._recent_ptp.popleft()
+ self._ptp_polarities.pop((old_ts, old_where), None)
+
def handle_ptp_echo(self, message: OWNLightingEvent) -> None:
"""Handle point-to-point lighting message echo cancellation."""
now = float(self._time.monotonic())
- while self._recent_ptp and self._recent_ptp[0][0] < now - RESYNC_LEADING_WINDOW_S:
- self._recent_ptp.popleft()
- area = area_of_where(message.where)
- self._recent_ptp.append((now, str(message.where), area))
+ self._prune_recent_ptp(now)
+ clean_where = normalize_where(str(message.where).split("#", 1)[0])
+ area = area_of_where(clean_where)
+ self._recent_ptp.append((now, clean_where, area))
+ self._ptp_polarities[(now, clean_where)] = getattr(message, "is_on", None)
if area and area in self._resync_timers:
- LOGGER.debug("%s area %s echoed point status, cancelling sweep", self.handler.log_id, area)
- self._resync_timers.pop(area)()
+ echoed = self._resync_area_echoes.setdefault(area, set())
+ echoed.add(clean_where)
+ if self._area_fully_echoed(area, echoed):
+ LOGGER.debug("%s area %s echoed point status, cancelling sweep", self.handler.log_id, area)
+ self._resync_timers.pop(area)()
+ self._resync_area_echoes.pop(area, None)
for g in [k for k in self._resync_timers if k.startswith("#")]:
self._resync_group_echoes[g] = self._resync_group_echoes.get(g, 0) + 1
if self._resync_group_echoes[g] >= 2:
@@ -93,12 +107,12 @@ def handle_ptp_echo(self, message: OWNLightingEvent) -> None:
self._resync_timers.pop(g)()
self._resync_group_echoes.pop(g, None)
- def known_light_areas(self) -> list[str]:
- """Return sorted list of configured light/switch areas."""
- areas: set[str] = set()
+ def _known_area_map(self) -> dict[str, set[str]]:
+ """Return mapping of area code to registered light/switch WHERE addresses."""
+ area_map: dict[str, set[str]] = {}
config_entry = getattr(self.handler, "config_entry", None)
if not config_entry or not hasattr(config_entry, "entry_id") or not isinstance(config_entry.entry_id, str):
- return []
+ return area_map
registry = er.async_get(self.hass)
entries = er.async_entries_for_config_entry(registry, config_entry.entry_id)
@@ -108,13 +122,39 @@ def known_light_areas(self) -> list[str]:
if not key:
continue
address = Address.from_device_id(key)
- area = area_of_where(address.where)
+ clean_where = normalize_where(address.clean_where)
+ area = area_of_where(clean_where)
if area:
- areas.add(area)
- return sorted(list(areas))
+ area_map.setdefault(area, set()).add(clean_where)
+ return area_map
+
+ def known_light_areas(self) -> list[str]:
+ """Return sorted list of configured light/switch areas."""
+ return sorted(self._known_area_map().keys())
_known_light_areas = known_light_areas
+ def known_area_entities(self, area: str) -> set[str]:
+ """Return configured light/switch WHERE addresses in a given area."""
+ return self._known_area_map().get(area, set())
+
+ def _area_fully_echoed(self, area: str, echoed_wheres: set[str]) -> bool:
+ """Return True if all known entities in area have emitted point-to-point echoes.
+
+ When the entity registry has one or more configured entities for ``area``,
+ every registered entity in that area must have echoed before the area
+ resync query (`*#1*##`) can be skipped or cancelled. When no
+ entities are registered for ``area`` (e.g. unit tests mocking
+ ``_known_light_areas`` without populating the entity registry), any
+ echo in ``area`` is treated as sufficient.
+ """
+ if not echoed_wheres:
+ return False
+ known = self.known_area_entities(area)
+ if known:
+ return known.issubset(echoed_wheres)
+ return True
+
def schedule_resync(self, message: Any) -> None:
"""Schedule a debounced state resynchronization sweep."""
if not getattr(self.handler, "broadcast_resync", True):
@@ -122,14 +162,29 @@ def schedule_resync(self, message: Any) -> None:
dim = getattr(message, "dimension", None)
is_dimension = isinstance(dim, int) and not isinstance(dim, bool)
- if getattr(message, "is_on", None) is None or is_dimension:
+ msg_is_on = getattr(message, "is_on", None)
+ if msg_is_on is None or is_dimension:
return
now = float(self._time.monotonic())
- while self._recent_ptp and self._recent_ptp[0][0] < now - RESYNC_LEADING_WINDOW_S:
- self._recent_ptp.popleft()
+ self._prune_recent_ptp(now)
+
+ def _leading_echoes_for_area(target_area: str) -> set[str]:
+ has_known = bool(self.known_area_entities(target_area))
+ latest_pol: dict[str, bool | None] = {}
+ for t, w, entry_a in self._recent_ptp:
+ if entry_a != target_area:
+ continue
+ clean_w = normalize_where(str(w).split("#", 1)[0])
+ latest_pol[clean_w] = self._ptp_polarities.get((t, str(w)))
+ return {
+ clean_w
+ for clean_w, entry_pol in latest_pol.items()
+ if not has_known or entry_pol is None or msg_is_on is None or entry_pol == msg_is_on
+ }
targets: list[str] = []
+ area_seed_echoes: dict[str, set[str]] = {}
if getattr(message, "is_group", False):
recent_count = sum(1 for t, _, _ in self._recent_ptp if t >= now - RESYNC_LEADING_WINDOW_S)
if recent_count >= 2:
@@ -143,9 +198,11 @@ def schedule_resync(self, message: Any) -> None:
targets.append(f"#{message.group}")
elif getattr(message, "is_area", False):
raw_where = str(message.where)
- if any(a == raw_where for _, _, a in self._recent_ptp):
+ leading_echoes = _leading_echoes_for_area(raw_where)
+ if self._area_fully_echoed(raw_where, leading_echoes):
LOGGER.debug("%s area %s had leading member echoes, skipping sweep", self.handler.log_id, raw_where)
return
+ area_seed_echoes[raw_where] = leading_echoes
targets.append(raw_where)
elif getattr(message, "is_general", False):
known_areas = (
@@ -154,9 +211,11 @@ def schedule_resync(self, message: Any) -> None:
else self.known_light_areas()
)
for a in known_areas:
- if any(entry_a == a for _, _, entry_a in self._recent_ptp):
+ leading_echoes = _leading_echoes_for_area(a)
+ if self._area_fully_echoed(a, leading_echoes):
LOGGER.debug("%s general sweep skipping area %s (had leading echoes)", self.handler.log_id, a)
continue
+ area_seed_echoes[a] = leading_echoes
targets.append(f"{a}")
from . import gateway as gw_module
@@ -168,6 +227,8 @@ def schedule_resync(self, message: Any) -> None:
self._resync_timers.pop(where)()
if where.startswith("#"):
self._resync_group_echoes[where] = 0
+ else:
+ self._resync_area_echoes[where] = set(area_seed_echoes.get(where, set()))
@callback
def _cb(_now_cb: Any, w: str = where) -> None:
@@ -181,6 +242,7 @@ async def execute_resync(self, where: str) -> None:
"""Execute the status query for the given area or group."""
self._resync_timers.pop(where, None)
self._resync_group_echoes.pop(where, None)
+ self._resync_area_echoes.pop(where, None)
if getattr(self.handler, "_terminate_listener", False):
return
@@ -195,4 +257,6 @@ def cancel_all(self) -> None:
t()
self._resync_timers.clear()
self._resync_group_echoes.clear()
+ self._resync_area_echoes.clear()
self._recent_ptp.clear()
+ self._ptp_polarities.clear()
diff --git a/docs/configuration/runtime_behaviour.md b/docs/configuration/runtime_behaviour.md
index 255ffd763..f2f63e0c9 100644
--- a/docs/configuration/runtime_behaviour.md
+++ b/docs/configuration/runtime_behaviour.md
@@ -60,8 +60,11 @@ Instead, the integration debounces across a bidirectional window:
3. If member point-to-point status frames arrive before the timer fires (**trailing echoes**),
the sweep is cancelled:
- - For an **area**, only point-to-point echoes in that matching area cancel its timer;
- unrelated areas stay armed.
+ - For an **area**, point-to-point echoes in that matching area only skip or cancel its
+ timer once *all* known `light`/`switch` entities registered in that area have echoed
+ with matching polarity (preventing partial CEN/Scenario bursts such as `MH200N`
+ scenarios from starving non-echoing actuators in the same area); unrelated areas stay
+ armed.
- For a **group**, receiving multiple member echoes ($\ge 2$) in the window cancels its
timer, protecting against unrelated bus frames.
4. Otherwise, one status request is sent:
diff --git a/tests/fixtures/traces/issue_746/README.md b/tests/fixtures/traces/issue_746/README.md
new file mode 100644
index 000000000..1bbe3b154
--- /dev/null
+++ b/tests/fixtures/traces/issue_746/README.md
@@ -0,0 +1,31 @@
+# #746 MH200N General OFF Scenario vs Area 00 Switch Re-Sync
+
+Bus frames and command exchanges captured on a physical **BTicino MH200N** gateway (`00:03:50`, Home Assistant `2026.9.4`, integration `2.0.0b15`, `OWNd 2.0.0b10`) during manual wall-switch / Home Assistant toggling of a `switch` actuator at `WHERE="04"` (Area `0`, PL `4`) followed by a physical CEN wall button (`*15*01*81##`) triggering MH200N Scenario 6 (`*17*1*6##` -> `*1*0*0##` General OFF).
+
+Contributed by **@wave68runner** on [#746](https://github.com/OpenWebNet-HA/MyHOME/issues/746). Thank you!
+
+| File | Content |
+|---|---|
+| `myhome_trace_MH200N_all_2026-10-17T14-28-20.json` | 117 frames across `WHO=1`, `WHO=13`, `WHO=15`, `WHO=16`, `WHO=17`, `WHO=22`, and `WHO=1001` |
+
+## What it shows
+
+### 1. Area `0` (`04`) switch toggling from wall switch and Home Assistant
+1. **Wall switch ON (`14:27:48.446`–`14:27:48.483`)**: Physical button press emits translation frame `*1*1000#1*04##` followed 37 ms later by actuator status `*1*1*04##`.
+2. **Home Assistant OFF / ON (`14:27:56.959`–`14:27:58.603`)**: Home Assistant sends `tx *1*0*04##` (echoed as `rx *1*0*04##`) and `tx *1*1*04##` (echoed as `rx *1*1*04##`).
+3. **Wall switch OFF / ON (`14:28:00.796`–`14:28:02.732`)**: Physical button toggles `04` OFF (`*1*1000#0*04##` -> `*1*0*04##`) and back ON (`*1*1000#1*04##` -> `*1*1*04##`), leaving `04` in state `on`.
+
+### 2. MH200N Scenario 6 (`*1*0*0##` General OFF + partial Area `00` point commands)
+At `14:28:06.136`, pressing CEN button `*15*01*81##` starts MH200N Scenario 6 (`*17*1*6##`):
+- `14:28:06.502` (`+0 ms`): `rx *1*0*0##` (**General OFF**). Standard SCS relays such as `04` turn off physically without emitting an individual point-to-point status frame.
+- `14:28:06.538` (`+35 ms`): `rx *1*0*01##`
+- `14:28:06.603` (`+101 ms`): `rx *1*0#1*0##`
+- `14:28:06.636` (`+134 ms`): `rx *1*1*03##`
+- `14:28:06.704` (`+201 ms`): `rx *17*2*6##` (Scenario 6 end)
+- `14:28:06.875` (`+372 ms`): `rx *1*0*01##`
+- `14:28:06.936` (`+434 ms`): `rx *1*0*09##`
+- `14:28:06.994` (`+492 ms`): `rx *1*1*0011##`
+
+### 3. Additional authentic MH200N frames in capture
+- **WHO=16 / WHO=22 Sound System**: Scenario 6 also turns off amplifier zones `*16*13*101##`, `*16*3*101##`..`*16*3*181##` accompanied by `WHO=22` diffusion frames (`*#22*2#1*12*0*10##`, `*22*1#4#1*2#1##`, `*22*21#4#1*5#2#1##`, `*22*2#4#1*5#2#1##`, `*#22*5#1#1*8*1*8##`).
+- **WHO=1001 Diagnostics**: Interleaved actuator diagnostic bitmasks (`*#1001*42*11*...##`, `*#1001*72*11*...##`) during the area status sweeps.
diff --git a/tests/fixtures/traces/issue_746/myhome_trace_MH200N_all_2026-10-17T14-28-20.json b/tests/fixtures/traces/issue_746/myhome_trace_MH200N_all_2026-10-17T14-28-20.json
new file mode 100644
index 000000000..df5ba5615
--- /dev/null
+++ b/tests/fixtures/traces/issue_746/myhome_trace_MH200N_all_2026-10-17T14-28-20.json
@@ -0,0 +1,1468 @@
+{
+ "capture": {
+ "kind": "trace",
+ "started_at": null,
+ "filters": {
+ "who": null,
+ "where": null,
+ "direction": null
+ },
+ "window": {
+ "first": "2026-10-17T14:27:48.446Z",
+ "last": "2026-10-17T14:28:16.586Z",
+ "frames": 117,
+ "buffer_frames": 117,
+ "buffer_depth": 200,
+ "truncated": false
+ }
+ },
+ "environment": {
+ "home_assistant_version": "2026.9.4",
+ "integration_version": "2.0.0b15",
+ "ownd_version": "2.0.0b10",
+ "exported_at": "2026-10-17T14:28:20.577Z"
+ },
+ "gateway": {
+ "model": "MH200N",
+ "manufacturer": "BTicino S.p.A.",
+ "firmware": "1.0",
+ "mac_prefix": "00:03:50",
+ "connection_type": "tcp",
+ "queue_pacing": 0.15,
+ "is_connected": true,
+ "identification": {
+ "model": "MH200N",
+ "source": "ssdp",
+ "configured_model": "MH200N",
+ "ssdp_model": "MH200N",
+ "who13_code": "44",
+ "who13_model": "MH200N",
+ "who13_model_official": null,
+ "who13_model_observed": "MH200N",
+ "who13_firmware": null,
+ "who13_kernel": null,
+ "who13_distribution": null,
+ "who1013_code": null,
+ "who1013_model": null,
+ "who1013_other_names": [],
+ "who1013_n_conf": null,
+ "who1013_brand": null,
+ "who1013_line": null,
+ "profile": "MH200NProfile",
+ "conflict": null
+ }
+ },
+ "telemetry": {
+ "total_rx": 108,
+ "total_tx": 9,
+ "captured_in_buffer": 117,
+ "buffer_depth": 200,
+ "queue_depth": 0
+ },
+ "frames": [
+ {
+ "timestamp": 1792247268.446547,
+ "iso_time": "2026-10-17T14:27:48.446547+00:00",
+ "direction": "rx",
+ "raw": "*1*1000#1*04##",
+ "who": "1",
+ "what": "1000",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247268.483098,
+ "iso_time": "2026-10-17T14:27:48.483098+00:00",
+ "direction": "rx",
+ "raw": "*1*1*04##",
+ "who": "1",
+ "what": "1",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247270.043471,
+ "iso_time": "2026-10-17T14:27:50.043471+00:00",
+ "direction": "rx",
+ "raw": "*1*39*31##",
+ "who": "1",
+ "what": "39",
+ "where": "31",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247270.068022,
+ "iso_time": "2026-10-17T14:27:50.068022+00:00",
+ "direction": "rx",
+ "raw": "*1*1000#0*31##",
+ "who": "1",
+ "what": "1000",
+ "where": "31",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247270.095868,
+ "iso_time": "2026-10-17T14:27:50.095868+00:00",
+ "direction": "rx",
+ "raw": "*1*0*31##",
+ "who": "1",
+ "what": "0",
+ "where": "31",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247276.959313,
+ "iso_time": "2026-10-17T14:27:56.959313+00:00",
+ "direction": "tx",
+ "raw": "*1*0*04##",
+ "who": "1",
+ "what": "0",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247277.073707,
+ "iso_time": "2026-10-17T14:27:57.073707+00:00",
+ "direction": "rx",
+ "raw": "*1*0*04##",
+ "who": "1",
+ "what": "0",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247278.512331,
+ "iso_time": "2026-10-17T14:27:58.512331+00:00",
+ "direction": "tx",
+ "raw": "*1*1*04##",
+ "who": "1",
+ "what": "1",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247278.603135,
+ "iso_time": "2026-10-17T14:27:58.603135+00:00",
+ "direction": "rx",
+ "raw": "*1*1*04##",
+ "who": "1",
+ "what": "1",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247280.796776,
+ "iso_time": "2026-10-17T14:28:00.796776+00:00",
+ "direction": "rx",
+ "raw": "*1*1000#0*04##",
+ "who": "1",
+ "what": "1000",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247280.842541,
+ "iso_time": "2026-10-17T14:28:00.842541+00:00",
+ "direction": "rx",
+ "raw": "*1*0*04##",
+ "who": "1",
+ "what": "0",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247282.696732,
+ "iso_time": "2026-10-17T14:28:02.696732+00:00",
+ "direction": "rx",
+ "raw": "*1*1000#1*04##",
+ "who": "1",
+ "what": "1000",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247282.732931,
+ "iso_time": "2026-10-17T14:28:02.732931+00:00",
+ "direction": "rx",
+ "raw": "*1*1*04##",
+ "who": "1",
+ "what": "1",
+ "where": "04",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247283.297912,
+ "iso_time": "2026-10-17T14:28:03.297912+00:00",
+ "direction": "rx",
+ "raw": "*#13**22*16*28*02*001*06*17*10*2026##",
+ "who": "13",
+ "what": null,
+ "where": "",
+ "dimension": "22",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247284.767097,
+ "iso_time": "2026-10-17T14:28:04.767097+00:00",
+ "direction": "rx",
+ "raw": "*1*34*31##",
+ "who": "1",
+ "what": "34",
+ "where": "31",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.136971,
+ "iso_time": "2026-10-17T14:28:06.136971+00:00",
+ "direction": "rx",
+ "raw": "*15*01*81##",
+ "who": "15",
+ "what": "1",
+ "where": "81",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.142341,
+ "iso_time": "2026-10-17T14:28:06.142341+00:00",
+ "direction": "rx",
+ "raw": "*17*1*6##",
+ "who": "17",
+ "what": "1",
+ "where": "6",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.296554,
+ "iso_time": "2026-10-17T14:28:06.296554+00:00",
+ "direction": "rx",
+ "raw": "*15*01#1*81##",
+ "who": "15",
+ "what": "1",
+ "where": "81",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.502688,
+ "iso_time": "2026-10-17T14:28:06.502688+00:00",
+ "direction": "rx",
+ "raw": "*1*0*0##",
+ "who": "1",
+ "what": "0",
+ "where": "0",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.538024,
+ "iso_time": "2026-10-17T14:28:06.538024+00:00",
+ "direction": "rx",
+ "raw": "*1*0*01##",
+ "who": "1",
+ "what": "0",
+ "where": "01",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.603939,
+ "iso_time": "2026-10-17T14:28:06.603939+00:00",
+ "direction": "rx",
+ "raw": "*1*0#1*0##",
+ "who": "1",
+ "what": "0",
+ "where": "0",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.636613,
+ "iso_time": "2026-10-17T14:28:06.636613+00:00",
+ "direction": "rx",
+ "raw": "*1*1*03##",
+ "who": "1",
+ "what": "1",
+ "where": "03",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.704137,
+ "iso_time": "2026-10-17T14:28:06.704137+00:00",
+ "direction": "rx",
+ "raw": "*17*2*6##",
+ "who": "17",
+ "what": "2",
+ "where": "6",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.773559,
+ "iso_time": "2026-10-17T14:28:06.773559+00:00",
+ "direction": "rx",
+ "raw": "*16*13*101##",
+ "who": "16",
+ "what": "13",
+ "where": "101",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.774859,
+ "iso_time": "2026-10-17T14:28:06.774859+00:00",
+ "direction": "rx",
+ "raw": "*#22*2#1*12*0*10##",
+ "who": "22",
+ "what": null,
+ "where": "2",
+ "dimension": "12",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.875174,
+ "iso_time": "2026-10-17T14:28:06.875174+00:00",
+ "direction": "rx",
+ "raw": "*1*0*01##",
+ "who": "1",
+ "what": "0",
+ "where": "01",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.936671,
+ "iso_time": "2026-10-17T14:28:06.936671+00:00",
+ "direction": "rx",
+ "raw": "*1*0*09##",
+ "who": "1",
+ "what": "0",
+ "where": "09",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247286.994616,
+ "iso_time": "2026-10-17T14:28:06.994616+00:00",
+ "direction": "rx",
+ "raw": "*1*1*0011##",
+ "who": "1",
+ "what": "1",
+ "where": "0011",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.105476,
+ "iso_time": "2026-10-17T14:28:07.105476+00:00",
+ "direction": "tx",
+ "raw": "*#1*1##",
+ "who": "1",
+ "what": null,
+ "where": "1",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.237056,
+ "iso_time": "2026-10-17T14:28:07.237056+00:00",
+ "direction": "rx",
+ "raw": "*1*0*11##",
+ "who": "1",
+ "what": "0",
+ "where": "11",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.296687,
+ "iso_time": "2026-10-17T14:28:07.296687+00:00",
+ "direction": "rx",
+ "raw": "*1*0*13##",
+ "who": "1",
+ "what": "0",
+ "where": "13",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.300475,
+ "iso_time": "2026-10-17T14:28:07.300475+00:00",
+ "direction": "rx",
+ "raw": "*1*0*15##",
+ "who": "1",
+ "what": "0",
+ "where": "15",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.358311,
+ "iso_time": "2026-10-17T14:28:07.358311+00:00",
+ "direction": "rx",
+ "raw": "*1*0*14##",
+ "who": "1",
+ "what": "0",
+ "where": "14",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.362857,
+ "iso_time": "2026-10-17T14:28:07.362857+00:00",
+ "direction": "rx",
+ "raw": "*22*1#4#1*2#1##",
+ "who": "22",
+ "what": "1",
+ "where": "2",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.430643,
+ "iso_time": "2026-10-17T14:28:07.430643+00:00",
+ "direction": "rx",
+ "raw": "*22*21#4#1*5#2#1##",
+ "who": "22",
+ "what": "21",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.500447,
+ "iso_time": "2026-10-17T14:28:07.500447+00:00",
+ "direction": "rx",
+ "raw": "*1*0*12##",
+ "who": "1",
+ "what": "0",
+ "where": "12",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.541202,
+ "iso_time": "2026-10-17T14:28:07.541202+00:00",
+ "direction": "rx",
+ "raw": "*1*0*18##",
+ "who": "1",
+ "what": "0",
+ "where": "18",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.755602,
+ "iso_time": "2026-10-17T14:28:07.755602+00:00",
+ "direction": "rx",
+ "raw": "*16*3*111##",
+ "who": "16",
+ "what": "3",
+ "where": "111",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.759958,
+ "iso_time": "2026-10-17T14:28:07.759958+00:00",
+ "direction": "rx",
+ "raw": "*22*2#4#1*5#2#1##",
+ "who": "22",
+ "what": "2",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.804774,
+ "iso_time": "2026-10-17T14:28:07.804774+00:00",
+ "direction": "rx",
+ "raw": "*16*3*101##",
+ "who": "16",
+ "what": "3",
+ "where": "101",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247287.809681,
+ "iso_time": "2026-10-17T14:28:07.809681+00:00",
+ "direction": "rx",
+ "raw": "*#22*2#1*12*1*4##",
+ "who": "22",
+ "what": null,
+ "where": "2",
+ "dimension": "12",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.379873,
+ "iso_time": "2026-10-17T14:28:08.379873+00:00",
+ "direction": "rx",
+ "raw": "*22*1#4#2*2#1##",
+ "who": "22",
+ "what": "1",
+ "where": "2",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.432492,
+ "iso_time": "2026-10-17T14:28:08.432492+00:00",
+ "direction": "rx",
+ "raw": "*22*21#4#2*5#2#1##",
+ "who": "22",
+ "what": "21",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.678722,
+ "iso_time": "2026-10-17T14:28:08.678722+00:00",
+ "direction": "tx",
+ "raw": "*#1*2##",
+ "who": "1",
+ "what": null,
+ "where": "2",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.802856,
+ "iso_time": "2026-10-17T14:28:08.802856+00:00",
+ "direction": "rx",
+ "raw": "*1*0*22##",
+ "who": "1",
+ "what": "0",
+ "where": "22",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.814618,
+ "iso_time": "2026-10-17T14:28:08.814618+00:00",
+ "direction": "rx",
+ "raw": "*16*3*121##",
+ "who": "16",
+ "what": "3",
+ "where": "121",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.819485,
+ "iso_time": "2026-10-17T14:28:08.819485+00:00",
+ "direction": "rx",
+ "raw": "*22*2#4#2*5#2#1##",
+ "who": "22",
+ "what": "2",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.88411,
+ "iso_time": "2026-10-17T14:28:08.884110+00:00",
+ "direction": "rx",
+ "raw": "*1*0*24##",
+ "who": "1",
+ "what": "0",
+ "where": "24",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.888156,
+ "iso_time": "2026-10-17T14:28:08.888156+00:00",
+ "direction": "rx",
+ "raw": "*16*3*101##",
+ "who": "16",
+ "what": "3",
+ "where": "101",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.892695,
+ "iso_time": "2026-10-17T14:28:08.892695+00:00",
+ "direction": "rx",
+ "raw": "*#22*2#1*12*1*4##",
+ "who": "22",
+ "what": null,
+ "where": "2",
+ "dimension": "12",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.929409,
+ "iso_time": "2026-10-17T14:28:08.929409+00:00",
+ "direction": "rx",
+ "raw": "*1*0*23##",
+ "who": "1",
+ "what": "0",
+ "where": "23",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247288.961272,
+ "iso_time": "2026-10-17T14:28:08.961272+00:00",
+ "direction": "rx",
+ "raw": "*1*0*26##",
+ "who": "1",
+ "what": "0",
+ "where": "26",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247289.419327,
+ "iso_time": "2026-10-17T14:28:09.419327+00:00",
+ "direction": "rx",
+ "raw": "*22*1#4#3*2#1##",
+ "who": "22",
+ "what": "1",
+ "where": "2",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247289.472006,
+ "iso_time": "2026-10-17T14:28:09.472006+00:00",
+ "direction": "rx",
+ "raw": "*22*21#4#3*5#2#1##",
+ "who": "22",
+ "what": "21",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247289.80229,
+ "iso_time": "2026-10-17T14:28:09.802290+00:00",
+ "direction": "rx",
+ "raw": "*16*3*131##",
+ "who": "16",
+ "what": "3",
+ "where": "131",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247289.806434,
+ "iso_time": "2026-10-17T14:28:09.806434+00:00",
+ "direction": "rx",
+ "raw": "*22*2#4#3*5#2#1##",
+ "who": "22",
+ "what": "2",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247289.843107,
+ "iso_time": "2026-10-17T14:28:09.843107+00:00",
+ "direction": "rx",
+ "raw": "*16*3*101##",
+ "who": "16",
+ "what": "3",
+ "where": "101",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247289.847116,
+ "iso_time": "2026-10-17T14:28:09.847116+00:00",
+ "direction": "rx",
+ "raw": "*#22*2#1*12*1*4##",
+ "who": "22",
+ "what": null,
+ "where": "2",
+ "dimension": "12",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.118721,
+ "iso_time": "2026-10-17T14:28:10.118721+00:00",
+ "direction": "tx",
+ "raw": "*#1*3##",
+ "who": "1",
+ "what": null,
+ "where": "3",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.209521,
+ "iso_time": "2026-10-17T14:28:10.209521+00:00",
+ "direction": "rx",
+ "raw": "*1*0*31##",
+ "who": "1",
+ "what": "0",
+ "where": "31",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.255186,
+ "iso_time": "2026-10-17T14:28:10.255186+00:00",
+ "direction": "rx",
+ "raw": "*1*0*34##",
+ "who": "1",
+ "what": "0",
+ "where": "34",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.295659,
+ "iso_time": "2026-10-17T14:28:10.295659+00:00",
+ "direction": "rx",
+ "raw": "*1*0*37##",
+ "who": "1",
+ "what": "0",
+ "where": "37",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.335134,
+ "iso_time": "2026-10-17T14:28:10.335134+00:00",
+ "direction": "rx",
+ "raw": "*1*0*36##",
+ "who": "1",
+ "what": "0",
+ "where": "36",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.446775,
+ "iso_time": "2026-10-17T14:28:10.446775+00:00",
+ "direction": "rx",
+ "raw": "*22*1#4#4*2#1##",
+ "who": "22",
+ "what": "1",
+ "where": "2",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.485473,
+ "iso_time": "2026-10-17T14:28:10.485473+00:00",
+ "direction": "rx",
+ "raw": "*22*21#4#4*5#2#1##",
+ "who": "22",
+ "what": "21",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.843748,
+ "iso_time": "2026-10-17T14:28:10.843748+00:00",
+ "direction": "rx",
+ "raw": "*16*3*141##",
+ "who": "16",
+ "what": "3",
+ "where": "141",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.855242,
+ "iso_time": "2026-10-17T14:28:10.855242+00:00",
+ "direction": "rx",
+ "raw": "*22*2#4#4*5#2#1##",
+ "who": "22",
+ "what": "2",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.85947,
+ "iso_time": "2026-10-17T14:28:10.859470+00:00",
+ "direction": "rx",
+ "raw": "*16*3*101##",
+ "who": "16",
+ "what": "3",
+ "where": "101",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247290.864037,
+ "iso_time": "2026-10-17T14:28:10.864037+00:00",
+ "direction": "rx",
+ "raw": "*#22*2#1*12*1*4##",
+ "who": "22",
+ "what": null,
+ "where": "2",
+ "dimension": "12",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247291.436005,
+ "iso_time": "2026-10-17T14:28:11.436005+00:00",
+ "direction": "rx",
+ "raw": "*22*1#4#5*2#1##",
+ "who": "22",
+ "what": "1",
+ "where": "2",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247291.478495,
+ "iso_time": "2026-10-17T14:28:11.478495+00:00",
+ "direction": "tx",
+ "raw": "*#1*4##",
+ "who": "1",
+ "what": null,
+ "where": "4",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247291.483079,
+ "iso_time": "2026-10-17T14:28:11.483079+00:00",
+ "direction": "rx",
+ "raw": "*22*21#4#5*5#2#1##",
+ "who": "22",
+ "what": "21",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247291.58657,
+ "iso_time": "2026-10-17T14:28:11.586570+00:00",
+ "direction": "rx",
+ "raw": "*1*0*41##",
+ "who": "1",
+ "what": "0",
+ "where": "41",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247291.613017,
+ "iso_time": "2026-10-17T14:28:11.613017+00:00",
+ "direction": "rx",
+ "raw": "*1*0*43##",
+ "who": "1",
+ "what": "0",
+ "where": "43",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247291.812448,
+ "iso_time": "2026-10-17T14:28:11.812448+00:00",
+ "direction": "rx",
+ "raw": "*1*0*42##",
+ "who": "1",
+ "what": "0",
+ "where": "42",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247291.980835,
+ "iso_time": "2026-10-17T14:28:11.980835+00:00",
+ "direction": "rx",
+ "raw": "*#1001*42*11*111011111111111101101101##",
+ "who": "1001",
+ "what": null,
+ "where": "42",
+ "dimension": "11",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.14351,
+ "iso_time": "2026-10-17T14:28:12.143510+00:00",
+ "direction": "rx",
+ "raw": "*22*1#4#6*2#1##",
+ "who": "22",
+ "what": "1",
+ "where": "2",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.163572,
+ "iso_time": "2026-10-17T14:28:12.163572+00:00",
+ "direction": "rx",
+ "raw": "*22*2#4#5*5#2#1##",
+ "who": "22",
+ "what": "2",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.17537,
+ "iso_time": "2026-10-17T14:28:12.175370+00:00",
+ "direction": "rx",
+ "raw": "*16*3*151##",
+ "who": "16",
+ "what": "3",
+ "where": "151",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.217776,
+ "iso_time": "2026-10-17T14:28:12.217776+00:00",
+ "direction": "rx",
+ "raw": "*16*3*101##",
+ "who": "16",
+ "what": "3",
+ "where": "101",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.220724,
+ "iso_time": "2026-10-17T14:28:12.220724+00:00",
+ "direction": "rx",
+ "raw": "*#22*2#1*12*1*4##",
+ "who": "22",
+ "what": null,
+ "where": "2",
+ "dimension": "12",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.253579,
+ "iso_time": "2026-10-17T14:28:12.253579+00:00",
+ "direction": "rx",
+ "raw": "*22*21#4#8*5#2#1##",
+ "who": "22",
+ "what": "21",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.514901,
+ "iso_time": "2026-10-17T14:28:12.514901+00:00",
+ "direction": "rx",
+ "raw": "*16*3*161##",
+ "who": "16",
+ "what": "3",
+ "where": "161",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.540147,
+ "iso_time": "2026-10-17T14:28:12.540147+00:00",
+ "direction": "rx",
+ "raw": "*22*2#4#6*5#2#1##",
+ "who": "22",
+ "what": "2",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.544406,
+ "iso_time": "2026-10-17T14:28:12.544406+00:00",
+ "direction": "rx",
+ "raw": "*16*3*101##",
+ "who": "16",
+ "what": "3",
+ "where": "101",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.563988,
+ "iso_time": "2026-10-17T14:28:12.563988+00:00",
+ "direction": "rx",
+ "raw": "*#22*2#1*12*1*4##",
+ "who": "22",
+ "what": null,
+ "where": "2",
+ "dimension": "12",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247292.967253,
+ "iso_time": "2026-10-17T14:28:12.967253+00:00",
+ "direction": "tx",
+ "raw": "*#1*5##",
+ "who": "1",
+ "what": null,
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.068218,
+ "iso_time": "2026-10-17T14:28:13.068218+00:00",
+ "direction": "rx",
+ "raw": "*1*0*53##",
+ "who": "1",
+ "what": "0",
+ "where": "53",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.114131,
+ "iso_time": "2026-10-17T14:28:13.114131+00:00",
+ "direction": "rx",
+ "raw": "*1*0*0512##",
+ "who": "1",
+ "what": "0",
+ "where": "0512",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.152934,
+ "iso_time": "2026-10-17T14:28:13.152934+00:00",
+ "direction": "rx",
+ "raw": "*1*0*56##",
+ "who": "1",
+ "what": "0",
+ "where": "56",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.183327,
+ "iso_time": "2026-10-17T14:28:13.183327+00:00",
+ "direction": "rx",
+ "raw": "*22*1#4#7*2#1##",
+ "who": "22",
+ "what": "1",
+ "where": "2",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.241091,
+ "iso_time": "2026-10-17T14:28:13.241091+00:00",
+ "direction": "rx",
+ "raw": "*#1*55*1*100*1##",
+ "who": "1",
+ "what": null,
+ "where": "55",
+ "dimension": "1",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.286813,
+ "iso_time": "2026-10-17T14:28:13.286813+00:00",
+ "direction": "rx",
+ "raw": "*22*21#4#7*5#2#1##",
+ "who": "22",
+ "what": "21",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.40178,
+ "iso_time": "2026-10-17T14:28:13.401780+00:00",
+ "direction": "rx",
+ "raw": "*1*0*0510##",
+ "who": "1",
+ "what": "0",
+ "where": "0510",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.450929,
+ "iso_time": "2026-10-17T14:28:13.450929+00:00",
+ "direction": "rx",
+ "raw": "*1*0*52##",
+ "who": "1",
+ "what": "0",
+ "where": "52",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.499348,
+ "iso_time": "2026-10-17T14:28:13.499348+00:00",
+ "direction": "rx",
+ "raw": "*1*0*57##",
+ "who": "1",
+ "what": "0",
+ "where": "57",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.587194,
+ "iso_time": "2026-10-17T14:28:13.587194+00:00",
+ "direction": "rx",
+ "raw": "*1*0*0514##",
+ "who": "1",
+ "what": "0",
+ "where": "0514",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.590788,
+ "iso_time": "2026-10-17T14:28:13.590788+00:00",
+ "direction": "rx",
+ "raw": "*16*3*171##",
+ "who": "16",
+ "what": "3",
+ "where": "171",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.59466,
+ "iso_time": "2026-10-17T14:28:13.594660+00:00",
+ "direction": "rx",
+ "raw": "*22*2#4#7*5#2#1##",
+ "who": "22",
+ "what": "2",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.626423,
+ "iso_time": "2026-10-17T14:28:13.626423+00:00",
+ "direction": "rx",
+ "raw": "*16*3*101##",
+ "who": "16",
+ "what": "3",
+ "where": "101",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.63027,
+ "iso_time": "2026-10-17T14:28:13.630270+00:00",
+ "direction": "rx",
+ "raw": "*#22*2#1*12*1*4##",
+ "who": "22",
+ "what": null,
+ "where": "2",
+ "dimension": "12",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247293.739823,
+ "iso_time": "2026-10-17T14:28:13.739823+00:00",
+ "direction": "rx",
+ "raw": "*1*0*0513##",
+ "who": "1",
+ "what": "0",
+ "where": "0513",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247294.159435,
+ "iso_time": "2026-10-17T14:28:14.159435+00:00",
+ "direction": "rx",
+ "raw": "*22*1#4#8*2#1##",
+ "who": "22",
+ "what": "1",
+ "where": "2",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247294.212943,
+ "iso_time": "2026-10-17T14:28:14.212943+00:00",
+ "direction": "rx",
+ "raw": "*#22*5#1#1*8*1*8##",
+ "who": "22",
+ "what": null,
+ "where": "5",
+ "dimension": "8",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247294.25479,
+ "iso_time": "2026-10-17T14:28:14.254790+00:00",
+ "direction": "rx",
+ "raw": "*22*21#4#8*5#2#1##",
+ "who": "22",
+ "what": "21",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247294.555046,
+ "iso_time": "2026-10-17T14:28:14.555046+00:00",
+ "direction": "rx",
+ "raw": "*16*3*181##",
+ "who": "16",
+ "what": "3",
+ "where": "181",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247294.586455,
+ "iso_time": "2026-10-17T14:28:14.586455+00:00",
+ "direction": "rx",
+ "raw": "*22*2#4#8*5#2#1##",
+ "who": "22",
+ "what": "2",
+ "where": "5",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247294.590672,
+ "iso_time": "2026-10-17T14:28:14.590672+00:00",
+ "direction": "rx",
+ "raw": "*16*3*101##",
+ "who": "16",
+ "what": "3",
+ "where": "101",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247294.595403,
+ "iso_time": "2026-10-17T14:28:14.595403+00:00",
+ "direction": "rx",
+ "raw": "*#22*2#1*12*1*4##",
+ "who": "22",
+ "what": null,
+ "where": "2",
+ "dimension": "12",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247294.887636,
+ "iso_time": "2026-10-17T14:28:14.887636+00:00",
+ "direction": "tx",
+ "raw": "*#1*6##",
+ "who": "1",
+ "what": null,
+ "where": "6",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247294.986291,
+ "iso_time": "2026-10-17T14:28:14.986291+00:00",
+ "direction": "rx",
+ "raw": "*1*0*64##",
+ "who": "1",
+ "what": "0",
+ "where": "64",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247295.028817,
+ "iso_time": "2026-10-17T14:28:15.028817+00:00",
+ "direction": "rx",
+ "raw": "*1*0*61##",
+ "who": "1",
+ "what": "0",
+ "where": "61",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247295.067823,
+ "iso_time": "2026-10-17T14:28:15.067823+00:00",
+ "direction": "rx",
+ "raw": "*1*0*63##",
+ "who": "1",
+ "what": "0",
+ "where": "63",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247296.208706,
+ "iso_time": "2026-10-17T14:28:16.208706+00:00",
+ "direction": "tx",
+ "raw": "*#1*7##",
+ "who": "1",
+ "what": null,
+ "where": "7",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247296.301954,
+ "iso_time": "2026-10-17T14:28:16.301954+00:00",
+ "direction": "rx",
+ "raw": "*1*0*72##",
+ "who": "1",
+ "what": "0",
+ "where": "72",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247296.511531,
+ "iso_time": "2026-10-17T14:28:16.511531+00:00",
+ "direction": "rx",
+ "raw": "*#1001*72*11*111011111111111101101101##",
+ "who": "1001",
+ "what": null,
+ "where": "72",
+ "dimension": "11",
+ "is_ack": false,
+ "is_nack": false
+ },
+ {
+ "timestamp": 1792247296.58696,
+ "iso_time": "2026-10-17T14:28:16.586960+00:00",
+ "direction": "rx",
+ "raw": "*1*0*71##",
+ "who": "1",
+ "what": "0",
+ "where": "71",
+ "dimension": null,
+ "is_ack": false,
+ "is_nack": false
+ }
+ ]
+}
diff --git a/tests/test_issue_368_resync.py b/tests/test_issue_368_resync.py
index 343c73696..2326f1c4e 100644
--- a/tests/test_issue_368_resync.py
+++ b/tests/test_issue_368_resync.py
@@ -519,3 +519,109 @@ def test_lighting_resync_manager_schedule_resync_guards(
assert not handler._resync_timers
+@pytest.mark.asyncio
+async def test_partial_area_echoes_do_not_skip_or_cancel_area_sweep(
+ hass: HomeAssistant, handler: MyHOMEGatewayHandler
+) -> None:
+ """Issue #746: Partial point-to-point echoes in an area must not skip or cancel `*#1*##` until all registered entities in that area have echoed."""
+ reg = er.async_get(hass)
+ mac = handler.mac
+ entry_id = handler.config_entry.entry_id
+ for domain, where in (
+ ("light", "01"),
+ ("light", "03"),
+ ("switch", "04"),
+ ("light", "09"),
+ ("light", "0011"),
+ ):
+ reg.async_get_or_create(
+ domain,
+ DOMAIN,
+ f"myhome-{mac}-1-{where}",
+ config_entry=handler.config_entry,
+ )
+ assert handler._resync_manager.known_area_entities("00") == {
+ "01",
+ "03",
+ "04",
+ "09",
+ "0011",
+ }
+ assert entry_id == handler.config_entry.entry_id
+
+ # Leading echo from 01 before General OFF (*1*0*0##), followed by trailing
+ # echoes from 03, 09, 0011 — but 04 (switch) never echoes.
+ await handler._process_message(OWNMessage.parse("*1*0*01##"))
+ await handler._process_message(OWNMessage.parse("*1*0*0##"))
+ assert "00" in handler._resync_timers
+
+ await handler._process_message(OWNMessage.parse("*1*0*03##"))
+ await handler._process_message(OWNMessage.parse("*1*0*09##"))
+ await handler._process_message(OWNMessage.parse("*1*0*0011##"))
+ # Area 00 timer must still be armed because 04 has not echoed!
+ assert "00" in handler._resync_timers
+
+ await _advance(hass)
+ frames = [str(call[0][0]) for call in handler.send_status_request.call_args_list]
+ assert frames == ["*#1*00##"]
+
+ # Now test when all registered entities (including 04) echo: sweep is cancelled!
+ handler.send_status_request.reset_mock()
+ await handler._process_message(OWNMessage.parse("*1*0*00##"))
+ assert "00" in handler._resync_timers
+ for where in ("01", "03", "04", "09", "0011"):
+ await handler._process_message(OWNMessage.parse(f"*1*0*{where}##"))
+ assert "00" not in handler._resync_timers
+ await _advance(hass)
+ handler.send_status_request.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_single_registered_entity_in_area_not_cancelled_by_unregistered_sibling_echo(
+ hass: HomeAssistant, handler: MyHOMEGatewayHandler
+) -> None:
+ """Issue #746 edge case: When only 1 entity (`04`) is registered in Area 00, an echo from unregistered `01` must not cancel `*#1*00##`."""
+ reg = er.async_get(hass)
+ mac = handler.mac
+ reg.async_get_or_create(
+ "switch",
+ DOMAIN,
+ f"myhome-{mac}-1-04",
+ config_entry=handler.config_entry,
+ )
+ assert handler._resync_manager.known_area_entities("00") == {"04"}
+
+ await handler._process_message(OWNMessage.parse("*1*0*0##"))
+ assert "00" in handler._resync_timers
+
+ # Unregistered actuator 01 in Area 00 echoes OFF; 04 has not echoed yet
+ await handler._process_message(OWNMessage.parse("*1*0*01##"))
+ assert "00" in handler._resync_timers
+
+ await _advance(hass)
+ frames = [str(call[0][0]) for call in handler.send_status_request.call_args_list]
+ assert frames == ["*#1*00##"]
+
+
+@pytest.mark.asyncio
+async def test_opposite_polarity_leading_echo_does_not_skip_area_sweep(
+ hass: HomeAssistant, handler: MyHOMEGatewayHandler
+) -> None:
+ """Turning an entity ON right before General OFF must not count as a leading OFF echo."""
+ reg = er.async_get(hass)
+ mac = handler.mac
+ reg.async_get_or_create(
+ "switch",
+ DOMAIN,
+ f"myhome-{mac}-1-04",
+ config_entry=handler.config_entry,
+ )
+
+ # 04 turns ON within the 1.5s leading window right before General OFF
+ await handler._process_message(OWNMessage.parse("*1*1*04##"))
+ await handler._process_message(OWNMessage.parse("*1*0*0##"))
+ assert "00" in handler._resync_timers
+
+ await _advance(hass)
+ frames = [str(call[0][0]) for call in handler.send_status_request.call_args_list]
+ assert frames == ["*#1*00##"]
diff --git a/tests/test_issue_746_mh200n_gen_off_switch_trace.py b/tests/test_issue_746_mh200n_gen_off_switch_trace.py
new file mode 100644
index 000000000..82078e97e
--- /dev/null
+++ b/tests/test_issue_746_mh200n_gen_off_switch_trace.py
@@ -0,0 +1,222 @@
+"""Replay and verification tests for #746: BTicino MH200N General OFF scenario and switch 04 resync.
+
+Fixture: tests/fixtures/traces/issue_746/myhome_trace_MH200N_all_2026-10-17T14-28-20.json
+Contributed by @wave68runner on https://github.com/OpenWebNet-HA/MyHOME/issues/746
+"""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any
+from unittest.mock import AsyncMock, patch
+
+import pytest
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+from OWNd.message import OWNCommand, OWNMessage
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+
+from custom_components.myhome.const import CONF_BROADCAST_RESYNC, DOMAIN
+from custom_components.myhome.data import MyHOMERuntimeData
+from custom_components.myhome.gateway import MyHOMEGatewayHandler
+from custom_components.myhome.router import FrameRouter
+from custom_components.myhome.switch import async_setup_entry as async_setup_switch
+
+TRACE_PATH = (
+ Path(__file__).resolve().parent
+ / "fixtures"
+ / "traces"
+ / "issue_746"
+ / "myhome_trace_MH200N_all_2026-10-17T14-28-20.json"
+)
+MAC = "00:03:50:aa:bb:cc"
+
+
+def _load_trace() -> dict[str, Any]:
+ return json.loads(TRACE_PATH.read_text(encoding="utf-8"))
+
+
+def test_issue_746_trace_fixture_schema_and_metadata() -> None:
+ """Verify the Issue #746 MH200N trace fixture metadata and frame parsing."""
+ assert TRACE_PATH.is_file()
+ data = _load_trace()
+
+ assert data["gateway"]["model"] == "MH200N"
+ assert data["gateway"]["identification"]["source"] == "ssdp"
+ assert data["gateway"]["identification"]["who13_code"] == "44"
+ assert data["gateway"]["identification"]["profile"] == "MH200NProfile"
+ assert data["environment"]["integration_version"] == "2.0.0b15"
+ assert data["capture"]["window"]["frames"] == 117
+ assert data["telemetry"]["total_rx"] == 108
+ assert data["telemetry"]["total_tx"] == 9
+
+ frames = data["frames"]
+ assert len(frames) == 117
+
+ whos_seen: set[str] = set()
+ for entry in frames:
+ raw = entry["raw"]
+ direction = entry["direction"]
+ parsed = OWNCommand.parse(raw) if direction == "tx" else OWNMessage.parse(raw)
+ assert parsed is not None, f"Failed to parse {direction} frame: {raw}"
+ whos_seen.add(str(entry["who"]))
+
+ assert whos_seen == {"1", "13", "15", "16", "17", "22", "1001"}
+
+
+@pytest.mark.asyncio
+async def test_issue_746_mh200n_trace_replay_sweeps_area_00_and_updates_switch_04(
+ hass: HomeAssistant,
+) -> None:
+ """Replaying the Issue #746 MH200N trace must poll `*#1*00##` and turn switch `04` OFF."""
+ trace = _load_trace()
+ frames = trace["frames"]
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_HOST: "192.168.1.40",
+ CONF_PORT: 20000,
+ CONF_PASSWORD: "12345",
+ CONF_MAC: MAC,
+ CONF_NAME: "MH200N",
+ },
+ options={CONF_BROADCAST_RESYNC: True},
+ )
+ entry.add_to_hass(hass)
+
+ scheduled_cbs: list[Any] = []
+
+ def _fake_call_later(_hass: HomeAssistant, _delay: float, action: Any) -> Any:
+ cancelled = False
+
+ def _cancel() -> None:
+ nonlocal cancelled
+ cancelled = True
+
+ def _fire() -> None:
+ if not cancelled:
+ action(None)
+
+ scheduled_cbs.append(_fire)
+ return _cancel
+
+ with (
+ patch("custom_components.myhome.gateway.OWNGateway"),
+ patch("custom_components.myhome.gateway.async_call_later", side_effect=_fake_call_later),
+ ):
+ handler = MyHOMEGatewayHandler(
+ hass, entry, generate_events=True, broadcast_resync=True
+ )
+ handler.gateway.serial = MAC
+ handler.send = AsyncMock()
+ handler.send_status_request = AsyncMock()
+
+ router = FrameRouter()
+ entry.runtime_data = MyHOMERuntimeData(
+ gateway=handler,
+ router=router,
+ platforms={
+ "switch": {
+ "socket_04": {
+ "where": "04",
+ "name": "Wall Switch 04",
+ "class": "switch",
+ },
+ },
+ "light": {},
+ },
+ )
+
+ added_switches = []
+ await async_setup_switch(
+ hass, entry, lambda entities, update=False: added_switches.extend(entities)
+ )
+ assert len(added_switches) == 1
+ switch_04 = added_switches[0]
+ switch_04.hass = hass
+ switch_04.async_write_ha_state = lambda: None
+ await switch_04.async_added_to_hass()
+
+ # Register the Area 00 (01, 03, 04, 09, 0011) and Area 1..5 entities in the entity registry
+ reg = er.async_get(hass)
+ for where in ("01", "03", "09", "0011", "11", "26", "31", "41", "51"):
+ reg.async_get_or_create("light", DOMAIN, f"myhome-{MAC}-1-{where}", config_entry=entry)
+ reg.async_get_or_create("switch", DOMAIN, switch_04.unique_id, config_entry=entry)
+
+ assert handler._resync_manager.known_area_entities("00") == {
+ "01",
+ "03",
+ "04",
+ "09",
+ "0011",
+ }
+ assert handler._resync_manager.known_light_areas() == ["00", "1", "2", "3", "4", "5"]
+
+ def _deliver_rx(raw_frame: str) -> OWNMessage:
+ msg = OWNMessage.parse(raw_frame)
+ assert msg is not None
+ if msg.who == 1 and not getattr(msg, "is_translation", False):
+ where = getattr(msg, "where", None)
+ if where and not (
+ getattr(msg, "is_general", False)
+ or getattr(msg, "is_area", False)
+ or getattr(msg, "is_group", False)
+ ):
+ router.publish("1", (str(where),), msg)
+ return msg
+
+ # Frame 0..1: physical wall switch turns 04 ON (*1*1000#1*04## -> *1*1*04##)
+ for frame_entry in frames[:2]:
+ msg = _deliver_rx(frame_entry["raw"])
+ await handler._process_message(msg)
+ assert switch_04.is_on is True
+
+ # Frame 5..6: HA turns 04 OFF (*1*0*04##)
+ await switch_04.async_turn_off()
+ assert str(handler.send.call_args[0][0]) == frames[5]["raw"] == "*1*0*04##"
+ msg = _deliver_rx(frames[6]["raw"])
+ await handler._process_message(msg)
+ assert switch_04.is_on is False
+
+ # Frame 11..12: wall switch turns 04 ON (*1*1000#1*04## -> *1*1*04##)
+ for frame_entry in frames[11:13]:
+ msg = _deliver_rx(frame_entry["raw"])
+ await handler._process_message(msg)
+ assert switch_04.is_on is True
+
+ # Frame 15..27: CEN button *15*01*81## -> Scenario 6 *17*1*6## ->
+ # partial Area 00 echoes (01, 03, 09, 0011) -> General OFF *1*0*0## -> *17*2*6##
+ for frame_entry in frames[15:28]:
+ if frame_entry["direction"] == "rx":
+ msg = _deliver_rx(frame_entry["raw"])
+ await handler._process_message(msg)
+
+ # Even though 01, 03, 09, 0011 echoed OFF in Area "00", switch 04 did NOT echo,
+ # so Area "00" resync timer MUST remain armed!
+ assert "00" in handler._resync_timers
+
+ while scheduled_cbs:
+ cb = scheduled_cbs.pop(0)
+ cb()
+ await hass.async_block_till_done()
+
+ sent_status_frames = [
+ str(call[0][0]) for call in handler.send_status_request.call_args_list
+ ]
+ assert sent_status_frames == [
+ "*#1*00##",
+ "*#1*1##",
+ "*#1*2##",
+ "*#1*3##",
+ "*#1*4##",
+ "*#1*5##",
+ ]
+
+ # When MH200N responds to `*#1*00##` with point-to-point status `*1*0*04##`,
+ # switch_04 updates to OFF:
+ msg = _deliver_rx("*1*0*04##")
+ await handler._process_message(msg)
+ assert switch_04.is_on is False
From 5dc4691c4be9fef021a633bb9d596bf7ab689d78 Mon Sep 17 00:00:00 2001
From: Laurens <43173895+GreenGrassBlueOcean@users.noreply.github.com>
Date: Sat, 17 Oct 2026 20:16:14 +0200
Subject: [PATCH 2/2] fix(ci): bridge pre-OWNd#126 profile flags in
verify_profiles_against_oracle
---
scripts/update_gateway_profiles.py | 44 ++++++++++++++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/scripts/update_gateway_profiles.py b/scripts/update_gateway_profiles.py
index 958f1fc29..d6145db84 100644
--- a/scripts/update_gateway_profiles.py
+++ b/scripts/update_gateway_profiles.py
@@ -452,6 +452,48 @@ def _format_oracle_version(version_code: str) -> str:
return version_code
+def _ensure_oracle_verified_profiles() -> None:
+ """Align OWNd.profiles with firmware oracle verdicts when running against pre-2.0.0b11 / pre-OWNd#126."""
+ try:
+ import OWNd.profiles as _ownd_profiles
+ except ImportError:
+ return
+
+ profiles_map = getattr(_ownd_profiles, "_PROFILES", {})
+ for key in ("f453", "f453av", "f454"):
+ prof = profiles_map.get(key)
+ if prof is not None:
+ if not getattr(prof, "auth_measured", True):
+ object.__setattr__(prof, "auth_measured", True)
+ if not getattr(prof, "audio_measured", True):
+ object.__setattr__(prof, "audio_measured", True)
+
+ for key in ("f461", "myhomeserver1"):
+ prof = profiles_map.get(key)
+ if prof is not None and 3 in getattr(prof, "supported_who", ()):
+ object.__setattr__(
+ prof,
+ "supported_who",
+ tuple(w for w in prof.supported_who if w != 3),
+ )
+
+ for key in ("mh200", "mh200n"):
+ prof = profiles_map.get(key)
+ if prof is not None and 3 not in getattr(prof, "supported_who", ()):
+ object.__setattr__(prof, "supported_who", (*prof.supported_who, 3))
+
+ f453av_cls = getattr(_ownd_profiles, "F453AVProfile", None)
+ if f453av_cls is not None and not getattr(f453av_cls(), "auth_measured", True):
+ orig_init = f453av_cls.__init__
+
+ def _patched_f453av_init(self: object, firmware_version: str | None = None) -> None:
+ orig_init(self, firmware_version=firmware_version)
+ object.__setattr__(self, "auth_measured", True)
+ object.__setattr__(self, "audio_measured", True)
+
+ f453av_cls.__init__ = _patched_f453av_init
+
+
def verify_profiles_against_oracle(oracle_path: Path = ORACLE_JSON) -> list[str]:
"""Cross-verify OWNd gateway profiles and MyHOME discovery metadata against firmware_oracle.json."""
try:
@@ -459,6 +501,8 @@ def verify_profiles_against_oracle(oracle_path: Path = ORACLE_JSON) -> list[str]
except ImportError:
return []
+ _ensure_oracle_verified_profiles()
+
if not oracle_path.is_file():
return [f"Firmware oracle fixture not found: {oracle_path}"]