diff --git a/tests/fixtures/traces/issue_649/README.md b/tests/fixtures/traces/issue_649/README.md new file mode 100644 index 000000000..ba646a64a --- /dev/null +++ b/tests/fixtures/traces/issue_649/README.md @@ -0,0 +1,112 @@ +# #649 / MyHomeServer1 Physical Plant Trace (Thermostat Zone 60, Energy Meter 51, Light 43) + +Verbatim physical plant bus monitor trace captured from an authentic BTicino MyHomeServer1 gateway by **@gdluck** on [#649 (comment 6046117228)](https://github.com/OpenWebNet-HA/MyHOME/pull/649#issuecomment-6046117228). +Captured live on 2026-10-07 between 20:06:54 and 20:13:22 UTC. In MyHOME, physical traces are immutable ground truths: never edit a raw monitor frame. + +## Fixture Status & Provenance + +> [!IMPORTANT] +> **Ground Truth vs. Derived Fixture**: +> - `live_test_monitor_2026-10-07.log` is the **sole authentic ground truth** artifact: 107 monitor lines (`1791403614.835`–`1791404002.913`) captured verbatim from the gateway's monitor session. It contains no passwords or secrets. +> - `myhome_trace_MyHomeServer1_live_2026-10-07.json` is a **derived archive** generated from the raw monitor log under proposed OWNd WHO 4 season-decoding semantics (OWNd #94 / #97). It is **not** a direct Home Assistant runtime diagnostics export (`exported_at` is synthetic, `total_tx` is 0, and all 107 frames are monitor `rx`). Released OWNd 2.0.0b10 still decodes `*4*1*Z##` as `hvac_mode`. + +## Hardware Profile + +- **Gateway Model**: BTicino MyHomeServer1 (Server MyHOME_Up) +- **Firmware Version**: 3.87.13 (reported by operator inspection; not encoded in WHO 13 frame) +- **IP Address**: 192.168.0.218 (port 20000) +- **WHO 13 Identification**: Code `130` (`*#130**1*1*0*3*1*8##`) +- **Plant Devices Observed in Session**: + - **Thermostat Zone 60**: Heating zone with no central unit (`#0` absent). Normal operating temperature 23.0 °C. + - **Thermostat Zones 39 & 50**: Additional zones reporting external probe, mode, and actuator status on the bus. + - **Energy Meter 51**: BTicino F520 single-phase electricity meter (polled via command session; absent from monitor log). + - **Lighting Actuator 43**: Standard relay actuator (non-dimmer). +- **Environment**: + - Home Assistant: 2026.9.4 + - Integration: 2.0.0b15 + - OWNd Protocol Engine: 2.0.0b10 (synthesized with proposed season-decoder branch) +- **Session Capture**: + - 107 frames captured on MONITOR session (`live_test_monitor_2026-10-07.log`). + - 37 frames transmitted by operator on separate COMMAND session (absent from monitor capture; see comment evidence below). + +## Files in this Fixture + +| File | Format | Description | +|---|---|---| +| `live_test_monitor_2026-10-07.log` | Raw Monitor Log (107 frames) | **Authentic Ground Truth**. 107 monitor log lines (` OWN `) captured from the physical gateway session. | +| `myhome_trace_MyHomeServer1_live_2026-10-07.json` | Derived Trace Archive (107 frames) | **Derived Archive**. Structured trace archive synthesized from the raw log under proposed OWNd season decoding. Every frame is monitor `rx`. | + +## Plant Observations & Comment Evidence vs. Firmware Emulator Replay + +> [!NOTE] +> **Comment Evidence vs. Fixture Evidence**: +> The 37 command-session frames, ACKs, and NACKs in this table are reported from `@gdluck`'s commentary on [#649](https://github.com/OpenWebNet-HA/MyHOME/pull/649#issuecomment-6046117228). +> They are **NOT** present in the raw monitor fixture (`live_test_monitor_2026-10-07.log`), because OpenWebNet monitor sessions do not capture command session ACKs/NACKs. +> Specifically: +> - **Command session frames**: The 37 command transmissions and their ACK/NACK responses were on a separate TCP command connection. +> - **Energy meter (WHO 18)**: Rows for F520 (`*#18*51*1200##` NACK, `510#9`, `513#9`) are comment evidence only; no WHO 18 frames appear on the monitor log. +> - **Timed relay command**: The command `*#1*43*#2*0*0*5##` is not in the file. The monitor log shows relay 43 ON at `1791403915.726` and OFF at `1791403920.864` (5.138 s), which is consistent with a 5 s timer, but the +0.1 s / +5.2 s delta relative to command transmission is comment evidence. +> - **Mode digit 3**: That digit 3 "leaves the season alone" is observed as the absence of a season frame between polls on the monitor log, not a paired command and report. + +| Frame sent (Comment Evidence) | Live reply (Comment Evidence) | Live report on the monitor (Fixture Evidence) | Emulator | Match | +|---|---|---|---|---| +| `*#4*60##` | ACK, 5 frames | dim 0 (`0235`), dim 12 (`0230*3`), `*4*1*60##`, dim 14 (`0230*3`), dim 13 (`00`) | NACK, **no bus frame** | **differs** (see 1 below) | +| `*#4*60*11##` | NACK | – | request sent, no device | = (no fan coil on 60) | +| `*#4*60*19##` | ACK `*#4*60*19*5*0##` | – | request sent | = | +| `*#4*60*20##` | ACK `*#4*60#1*20*0##` | – | request sent | = | +| `*#4*#60##` | NACK | – | not replayed | plant has no central unit | +| `*#4*60*15##`, `15#1` | NACK | – | request sent, no device | = (no slave probe) | +| `*#4*60*14##`, `13##` | ACK with value | – | request sent | = | +| `*#4*60*#14*0230*3##` | ACK | dim 12 `0230*3` + `*4*1*60##` (season **unchanged**) | ACK, `92 17 30 2C` | = | +| `*#4*60*#14*0230*2##` | ACK | `*4*0*60##` + dim 12 (season → conditioning) | ACK, `91 17 30 2C` | = | +| `*#4*60*#14*0230*1##` | ACK | `*4*1*60##` + dim 12 (season → heating), actuator `60#2*20*5` | ACK, `90 17 30 2C` | = | +| `*#4*60*#11*2##`, `#11*0` | ACK | nothing (no fan coil) | ACK | = | +| `*4*303*60##` | ACK | `*4*303*60##` + dim 12 | ACK, `93 17 30 0F` | = | +| `*4*311*60##` | **NACK** | – | NACK, nothing | = | +| `*4*311*#60##` | ACK | **nothing**, zone unchanged | ACK, `D1 00 03 02 C2 17 00 00` | = on acceptance (see 2 below) | +| `*4*102*60##` | ACK | `*4*102*60##` + dim 12 `0070*3` (antifreeze, 7.0 °C) | ACK | = | +| `*4*303*#60##` | ACK | nothing | ACK | = on acceptance (see 2 below) | +| `*4*311*#60##` (again) | ACK | nothing; `*#4*60##` 2 min later still antifreeze | ACK | see 2 below | +| `*#18*51*51##`, `54`, `53`, `113` | ACK | – | ACK / "silent" (waiting for the bus) | = | +| `*#18*51*#1200#1*255##` | ACK | – | ACK | = | +| `*#18*51*1200##` | **NACK** | – | NACK, nothing | = | +| `*18*510#9*51##`, `*#18*51*513#9##`, `*18*59#9*51##`, `*#18*51*516##` | ACK | – | ACK / silent | = | +| `*#1*43##`, `*#1*43*1##` | ACK `*1*0*43##` | – | request sent | = | +| `*1*11*43##` | ACK | `*1*1*43##` (light on; switched off by operator 6 s later) | ACK, `11 00 12 16` | = | +| `*#1*43*#2*0*0*5##` | ACK | `*1*1*43##` at +0.1 s, `*1*0*43##` at **+5.2 s** | ACK, `D1 11 01 42 06 …` | = (5-s timer ran) | +| `*#1*43*2##` | ACK `*#1*43*2*0*0*0##` | – | request sent | = | +| `*1*2*43##` | ACK | nothing (relay, not a dimmer) | ACK, `11 00 12 1D` | = | + +## Empirical Findings & Fixture Validation + +### 1. What the Monitor Log Confirms + +- **Bare zone poll cache replay**: Polling `*#4*60##` yields immediate sequential monitor frames: `*#4*60*0*0235##`, `*#4*60*12*0230*3##`, `*4*1*60##`, `*#4*60*14*0230*3##`, `*#4*60*13*00##`. +- **Season change on the bus**: Season flip to conditioning is recorded at `1791403701.897` (`*4*0*60##`), and return to heating at `1791403714.186` (`*4*1*60##`), with active setpoint dimension 12 maintaining `0230*3`. +- **Actuator state tracking**: `*#4*60#2*20*6##` reports following the conditioning flip, and `*#4*60#2*20*5##` reports following the return to heating. On this plant, actuator 2 is a heating valve actuator, not a fan coil fan speed (even though OWNd describes `20*5` as fan speed 0). +- **Antifreeze persistence**: `*4*102*60##` and `*#4*60*12*0070*3##` appear at `1791403787`, persist across polls at `1791403946`, and are cleared only at `1791403994` by `*4*1*60##` and setpoint write `0230*3`. +- **Relay 43 timed behavior**: Relay 43 reports ON at `1791403915.726` and OFF at `1791403920.864` (a duration of 5.138 s, consistent with a 5 s timer). + +### 2. Additional Unmentioned Bus Traffic in the Log + +The capture includes broader plant activity not covered in the original three-device discussion: +- **Zone 39 traffic**: + - `*4*4002#39*0#1##` at 1791403682.224 (external probe / mode report; parsed by OWNd as zone 0 due to `0#1` address format). + - `*4*4002*39##` at 1791403682.224. + - `*#4*39#1*#20*0##` and `*#4*39#1*20*0##` at 1791403684 (actuator 1 status). +- **Zone 50 traffic**: + - `*4*4002#50*0#3##` at 1791403874.276. + - `*4*4002*50##` at 1791403874.340. + - `*#4*50#1*#20*0##` and `*#4*50#1*20*0##` at 1791403876 (actuator 1 status). +- **Unexplained Dimension Frames on Zone 60**: + - `*#4*59*60*80##` at 1791403744.277 and `*#4*59*60*86##` at 1791404001.928 (WHO 4 dimension 59 query/status). + - `*#4*40*60*42##` at 1791403814.839 (WHO 4 dimension 40 status). + - `*#4*48*60*53##` at 1791403829.228 (WHO 4 dimension 48 status). +- **Gateway identity frame**: + - `*#130**1*1*0*3*1*8##` at 1791403740.765 (WHO 13 model 130 = MyHomeServer1). Firmware `3.87.13` is an operator note, not encoded in this frame. + +### 3. What the Fixture Does Not Prove + +- **Command session transmissions**: The 37 command frames and their ACK/NACK responses are absent from the capture. +- **WHO 18 energy meter**: F520 meter frames (`*#18*...`) do not appear on the monitor log; statements regarding F520 behavior rest entirely on comment evidence. +- **Digit 3 season preservation**: Mode digit 3 leaving the season unchanged is inferred from the absence of a season broadcast after setpoint writes, not an atomic command-reply pair in this capture. diff --git a/tests/fixtures/traces/issue_649/live_test_monitor_2026-10-07.log b/tests/fixtures/traces/issue_649/live_test_monitor_2026-10-07.log new file mode 100644 index 000000000..12b049103 --- /dev/null +++ b/tests/fixtures/traces/issue_649/live_test_monitor_2026-10-07.log @@ -0,0 +1,107 @@ +1791403614.835 OWN *#4*60*0*0235## +1791403615.145 OWN *#4*60*12*0230*3## +1791403615.148 OWN *4*1*60## +1791403615.443 OWN *#4*60*14*0230*3## +1791403615.543 OWN *#4*60*13*00## +1791403632.480 OWN *#4*60*19*5*0## +1791403639.838 OWN *#4*60#1*20*0## +1791403639.891 OWN *#4*60#2*20*5## +1791403674.864 OWN *#4*60*14*0230*3## +1791403682.219 OWN *#4*60*13*00## +1791403682.224 OWN *4*4002#39*0#1## +1791403682.224 OWN *4*4002*39## +1791403684.328 OWN *#4*39#1*#20*0## +1791403684.331 OWN *#4*39#1*20*0## +1791403689.621 OWN *#4*60*0*0235## +1791403690.104 OWN *#4*60*0*0235## +1791403690.397 OWN *#4*60*12*0230*3## +1791403690.401 OWN *4*1*60## +1791403690.693 OWN *#4*60*14*0230*3## +1791403691.001 OWN *#4*60*13*00## +1791403691.397 OWN *#4*60*0*0235## +1791403691.700 OWN *#4*60*12*0230*3## +1791403691.702 OWN *4*1*60## +1791403692.001 OWN *#4*60*14*0230*3## +1791403692.297 OWN *#4*60*13*00## +1791403701.897 OWN *4*0*60## +1791403701.954 OWN *#4*60*12*0230*3## +1791403701.961 OWN *4*0*60## +1791403702.010 OWN *#4*60*0*0235## +1791403702.281 OWN *#4*60*0*0235## +1791403702.598 OWN *#4*60*12*0230*3## +1791403702.601 OWN *4*0*60## +1791403702.880 OWN *#4*60*14*0230*3## +1791403703.179 OWN *#4*60*13*00## +1791403703.606 OWN *#4*60*0*0235## +1791403703.875 OWN *#4*60*12*0230*3## +1791403703.877 OWN *4*0*60## +1791403704.176 OWN *#4*60*14*0230*3## +1791403704.478 OWN *#4*60*13*00## +1791403705.954 OWN *#4*60#2*#20*6## +1791403706.001 OWN *#4*60#2*20*6## +1791403714.186 OWN *4*1*60## +1791403714.247 OWN *#4*60*12*0230*3## +1791403714.251 OWN *4*1*60## +1791403714.285 OWN *#4*60*0*0235## +1791403714.588 OWN *#4*60*0*0235## +1791403714.892 OWN *#4*60*12*0230*3## +1791403714.896 OWN *4*1*60## +1791403715.194 OWN *#4*60*14*0230*3## +1791403715.475 OWN *#4*60*13*00## +1791403715.884 OWN *#4*60*0*0235## +1791403716.048 OWN *#4*60#2*#20*5## +1791403716.126 OWN *#4*60#2*20*5## +1791403716.595 OWN *#4*60*12*0230*3## +1791403716.607 OWN *4*1*60## +1791403716.622 OWN *#4*60*14*0230*3## +1791403716.757 OWN *#4*60*13*00## +1791403740.765 OWN *#130**1*1*0*3*1*8## +1791403744.277 OWN *#4*59*60*80## +1791403750.839 OWN *4*303*60## +1791403750.888 OWN *#4*60*12*0230*3## +1791403750.891 OWN *4*303*60## +1791403750.962 OWN *#4*60*0*0235## +1791403787.587 OWN *4*102*60## +1791403787.628 OWN *#4*60*12*0070*3## +1791403787.637 OWN *4*102*60## +1791403787.681 OWN *#4*60*0*0235## +1791403814.839 OWN *#4*40*60*42## +1791403829.228 OWN *#4*48*60*53## +1791403874.276 OWN *4*4002#50*0#3## +1791403874.340 OWN *4*4002*50## +1791403876.280 OWN *#4*50#1*#20*0## +1791403876.312 OWN *#4*50#1*20*0## +1791403890.522 OWN *1*0*43## +1791403897.000 OWN *1*0*43## +1791403903.400 OWN *1*1*43## +1791403909.556 OWN *1*0*43## +1791403915.726 OWN *1*1*43## +1791403920.864 OWN *1*0*43## +1791403922.004 OWN *#1*43*2*0*0*0## +1791403922.009 OWN *#1*43*2*0*0*0## +1791403934.429 OWN *1*0*43## +1791403946.281 OWN *#4*60*0*0235## +1791403946.577 OWN *#4*60*12*0070*3## +1791403946.579 OWN *4*102*60## +1791403946.871 OWN *#4*60*14*0230*3## +1791403946.987 OWN *#4*60*13*00## +1791403994.376 OWN *4*1*60## +1791403994.413 OWN *#4*60*12*0230*3## +1791403994.419 OWN *4*1*60## +1791403994.458 OWN *#4*60*0*0235## +1791403994.990 OWN *#4*60*0*0235## +1791403995.300 OWN *#4*60*12*0230*3## +1791403995.304 OWN *4*1*60## +1791403995.582 OWN *#4*60*14*0230*3## +1791403995.889 OWN *#4*60*13*00## +1791403996.293 OWN *#4*60*0*0235## +1791403996.590 OWN *#4*60*12*0230*3## +1791403996.593 OWN *4*1*60## +1791403996.894 OWN *#4*60*14*0230*3## +1791403997.172 OWN *#4*60*13*00## +1791404001.928 OWN *#4*59*60*86## +1791404002.208 OWN *#4*60*0*0235## +1791404002.505 OWN *#4*60*12*0230*3## +1791404002.507 OWN *4*1*60## +1791404002.793 OWN *#4*60*14*0230*3## +1791404002.913 OWN *#4*60*13*00## diff --git a/tests/fixtures/traces/issue_649/myhome_trace_MyHomeServer1_live_2026-10-07.json b/tests/fixtures/traces/issue_649/myhome_trace_MyHomeServer1_live_2026-10-07.json new file mode 100644 index 000000000..301260a60 --- /dev/null +++ b/tests/fixtures/traces/issue_649/myhome_trace_MyHomeServer1_live_2026-10-07.json @@ -0,0 +1,917 @@ +{ + "capture": { + "kind": "derived_trace_archive", + "source_file": "live_test_monitor_2026-10-07.log", + "notes": "Derived archive synthesized from authentic raw monitor log lines. Not an authentic Home Assistant runtime diagnostics export (exported_at timestamp is synthetic, total_tx is 0, and all frames are rx). Descriptions are generated using proposed OWNd WHO 4 season decoding (OWNd #94 / #97); pinned OWNd 2.0.0b10 reports *4*1*Z## as hvac_mode.", + "started_at": "2026-10-07T20:06:54.835000+00:00", + "filters": { + "who": null, + "where": null, + "direction": null + }, + "window": { + "first": "2026-10-07T20:06:54.835000+00:00", + "last": "2026-10-07T20:13:22.913000+00:00", + "frames": 107, + "buffer_frames": 107, + "buffer_depth": 500, + "truncated": false + } + }, + "environment": { + "home_assistant_version": "2026.9.4", + "integration_version": "2.0.0b15", + "ownd_version": "2.0.0b10 (synthesized with proposed WHO 4 season decoder branch)", + "exported_at": "2026-10-07T20:20:00.000000+00:00", + "archive_type": "derived_monitor_archive" + }, + "gateway": { + "model": "MyHomeServer1", + "manufacturer": "BTicino", + "firmware": "3.87.13", + "mac_prefix": "00:03:50", + "connection_type": "tcp", + "queue_pacing": 0.05, + "is_connected": true, + "identification": { + "model": "MyHomeServer1", + "source": "who13", + "configured_model": "MyHomeServer1", + "ssdp_model": null, + "who13_code": 130, + "who13_model": "MyHomeServer1", + "who13_model_official": "MyHomeServer1", + "who13_model_observed": "MyHomeServer1", + "who13_firmware": "3.87.13", + "who13_kernel": null, + "who13_distribution": null, + "profile": "MyHomeServer1Profile", + "conflict": null + } + }, + "telemetry": { + "total_rx": 107, + "total_tx": 0, + "captured_in_buffer": 107, + "buffer_depth": 500, + "queue_depth": 0 + }, + "frames": [ + { + "timestamp": 1791403614.835, + "iso_time": "2026-10-07T20:06:54.835000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403615.145, + "iso_time": "2026-10-07T20:06:55.145000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403615.148, + "iso_time": "2026-10-07T20:06:55.148000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403615.443, + "iso_time": "2026-10-07T20:06:55.443000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403615.543, + "iso_time": "2026-10-07T20:06:55.543000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403632.48, + "iso_time": "2026-10-07T20:07:12.480000+00:00", + "direction": "rx", + "raw": "*#4*60*19*5*0##", + "type": "OWNHeatingEvent", + "description": "Zone 60's cooling fan is off; heating valve is off." + }, + { + "timestamp": 1791403639.838, + "iso_time": "2026-10-07T20:07:19.838000+00:00", + "direction": "rx", + "raw": "*#4*60#1*20*0##", + "type": "OWNHeatingEvent", + "description": "Zone 60's actuator 1 is off." + }, + { + "timestamp": 1791403639.891, + "iso_time": "2026-10-07T20:07:19.891000+00:00", + "direction": "rx", + "raw": "*#4*60#2*20*5##", + "type": "OWNHeatingEvent", + "description": "Zone 60 actuator 2 is off (tracks heating actuator on this plant; decoded as fan speed 0 by OWNd)." + }, + { + "timestamp": 1791403674.864, + "iso_time": "2026-10-07T20:07:54.864000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403682.219, + "iso_time": "2026-10-07T20:08:02.219000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403682.224, + "iso_time": "2026-10-07T20:08:02.224000+00:00", + "direction": "rx", + "raw": "*4*4002#39*0#1##", + "type": "OWNHeatingEvent", + "description": "Zone 39 external probe/mode report (parsed as zone 0 by OWNd where-param parser)." + }, + { + "timestamp": 1791403682.224, + "iso_time": "2026-10-07T20:08:02.224000+00:00", + "direction": "rx", + "raw": "*4*4002*39##", + "type": "OWNHeatingEvent", + "description": "Zone 39's mode is unknown." + }, + { + "timestamp": 1791403684.328, + "iso_time": "2026-10-07T20:08:04.328000+00:00", + "direction": "rx", + "raw": "*#4*39#1*#20*0##", + "type": "OWNHeatingCommand", + "description": "*#4*39#1*#20*0##" + }, + { + "timestamp": 1791403684.331, + "iso_time": "2026-10-07T20:08:04.331000+00:00", + "direction": "rx", + "raw": "*#4*39#1*20*0##", + "type": "OWNHeatingEvent", + "description": "Zone 39's actuator 1 is off." + }, + { + "timestamp": 1791403689.621, + "iso_time": "2026-10-07T20:08:09.621000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403690.104, + "iso_time": "2026-10-07T20:08:10.104000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403690.397, + "iso_time": "2026-10-07T20:08:10.397000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403690.401, + "iso_time": "2026-10-07T20:08:10.401000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403690.693, + "iso_time": "2026-10-07T20:08:10.693000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403691.001, + "iso_time": "2026-10-07T20:08:11.001000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403691.397, + "iso_time": "2026-10-07T20:08:11.397000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403691.7, + "iso_time": "2026-10-07T20:08:11.700000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403691.702, + "iso_time": "2026-10-07T20:08:11.702000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403692.001, + "iso_time": "2026-10-07T20:08:12.001000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403692.297, + "iso_time": "2026-10-07T20:08:12.297000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403701.897, + "iso_time": "2026-10-07T20:08:21.897000+00:00", + "direction": "rx", + "raw": "*4*0*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is conditioning." + }, + { + "timestamp": 1791403701.954, + "iso_time": "2026-10-07T20:08:21.954000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403701.961, + "iso_time": "2026-10-07T20:08:21.961000+00:00", + "direction": "rx", + "raw": "*4*0*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is conditioning." + }, + { + "timestamp": 1791403702.01, + "iso_time": "2026-10-07T20:08:22.010000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403702.281, + "iso_time": "2026-10-07T20:08:22.281000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403702.598, + "iso_time": "2026-10-07T20:08:22.598000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403702.601, + "iso_time": "2026-10-07T20:08:22.601000+00:00", + "direction": "rx", + "raw": "*4*0*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is conditioning." + }, + { + "timestamp": 1791403702.88, + "iso_time": "2026-10-07T20:08:22.880000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403703.179, + "iso_time": "2026-10-07T20:08:23.179000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403703.606, + "iso_time": "2026-10-07T20:08:23.606000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403703.875, + "iso_time": "2026-10-07T20:08:23.875000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403703.877, + "iso_time": "2026-10-07T20:08:23.877000+00:00", + "direction": "rx", + "raw": "*4*0*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is conditioning." + }, + { + "timestamp": 1791403704.176, + "iso_time": "2026-10-07T20:08:24.176000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403704.478, + "iso_time": "2026-10-07T20:08:24.478000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403705.954, + "iso_time": "2026-10-07T20:08:25.954000+00:00", + "direction": "rx", + "raw": "*#4*60#2*#20*6##", + "type": "OWNHeatingCommand", + "description": "*#4*60#2*#20*6##" + }, + { + "timestamp": 1791403706.001, + "iso_time": "2026-10-07T20:08:26.001000+00:00", + "direction": "rx", + "raw": "*#4*60#2*20*6##", + "type": "OWNHeatingEvent", + "description": "Zone 60 actuator 2 is active (tracks heating actuator on this plant; decoded as fan speed 1 by OWNd)." + }, + { + "timestamp": 1791403714.186, + "iso_time": "2026-10-07T20:08:34.186000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403714.247, + "iso_time": "2026-10-07T20:08:34.247000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403714.251, + "iso_time": "2026-10-07T20:08:34.251000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403714.285, + "iso_time": "2026-10-07T20:08:34.285000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403714.588, + "iso_time": "2026-10-07T20:08:34.588000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403714.892, + "iso_time": "2026-10-07T20:08:34.892000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403714.896, + "iso_time": "2026-10-07T20:08:34.896000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403715.194, + "iso_time": "2026-10-07T20:08:35.194000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403715.475, + "iso_time": "2026-10-07T20:08:35.475000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403715.884, + "iso_time": "2026-10-07T20:08:35.884000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403716.048, + "iso_time": "2026-10-07T20:08:36.048000+00:00", + "direction": "rx", + "raw": "*#4*60#2*#20*5##", + "type": "OWNHeatingCommand", + "description": "*#4*60#2*#20*5##" + }, + { + "timestamp": 1791403716.126, + "iso_time": "2026-10-07T20:08:36.126000+00:00", + "direction": "rx", + "raw": "*#4*60#2*20*5##", + "type": "OWNHeatingEvent", + "description": "Zone 60 actuator 2 is off (tracks heating actuator on this plant; decoded as fan speed 0 by OWNd)." + }, + { + "timestamp": 1791403716.595, + "iso_time": "2026-10-07T20:08:36.595000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403716.607, + "iso_time": "2026-10-07T20:08:36.607000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403716.622, + "iso_time": "2026-10-07T20:08:36.622000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403716.757, + "iso_time": "2026-10-07T20:08:36.757000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403740.765, + "iso_time": "2026-10-07T20:09:00.765000+00:00", + "direction": "rx", + "raw": "*#130**1*1*0*3*1*8##", + "type": "OWNEvent", + "description": "*#130**1*1*0*3*1*8##" + }, + { + "timestamp": 1791403744.277, + "iso_time": "2026-10-07T20:09:04.277000+00:00", + "direction": "rx", + "raw": "*#4*59*60*80##", + "type": "OWNHeatingEvent", + "description": "Zone 59's main sensor is reporting a humidity of 80.0%." + }, + { + "timestamp": 1791403750.839, + "iso_time": "2026-10-07T20:09:10.839000+00:00", + "direction": "rx", + "raw": "*4*303*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's mode is set to 'off'." + }, + { + "timestamp": 1791403750.888, + "iso_time": "2026-10-07T20:09:10.888000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403750.891, + "iso_time": "2026-10-07T20:09:10.891000+00:00", + "direction": "rx", + "raw": "*4*303*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's mode is set to 'off'." + }, + { + "timestamp": 1791403750.962, + "iso_time": "2026-10-07T20:09:10.962000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403787.587, + "iso_time": "2026-10-07T20:09:47.587000+00:00", + "direction": "rx", + "raw": "*4*102*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's mode is set to 'off'." + }, + { + "timestamp": 1791403787.628, + "iso_time": "2026-10-07T20:09:47.628000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0070*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 7.0\u00b0C." + }, + { + "timestamp": 1791403787.637, + "iso_time": "2026-10-07T20:09:47.637000+00:00", + "direction": "rx", + "raw": "*4*102*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's mode is set to 'off'." + }, + { + "timestamp": 1791403787.681, + "iso_time": "2026-10-07T20:09:47.681000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403814.839, + "iso_time": "2026-10-07T20:10:14.839000+00:00", + "direction": "rx", + "raw": "*#4*40*60*42##", + "type": "OWNHeatingEvent", + "description": "Zone 40's main sensor is reporting a humidity of 42.0%." + }, + { + "timestamp": 1791403829.228, + "iso_time": "2026-10-07T20:10:29.228000+00:00", + "direction": "rx", + "raw": "*#4*48*60*53##", + "type": "OWNHeatingEvent", + "description": "Zone 48's main sensor is reporting a humidity of 53.0%." + }, + { + "timestamp": 1791403874.276, + "iso_time": "2026-10-07T20:11:14.276000+00:00", + "direction": "rx", + "raw": "*4*4002#50*0#3##", + "type": "OWNHeatingEvent", + "description": "Zone 0's mode is unknown." + }, + { + "timestamp": 1791403874.34, + "iso_time": "2026-10-07T20:11:14.340000+00:00", + "direction": "rx", + "raw": "*4*4002*50##", + "type": "OWNHeatingEvent", + "description": "Zone 50's mode is unknown." + }, + { + "timestamp": 1791403876.28, + "iso_time": "2026-10-07T20:11:16.280000+00:00", + "direction": "rx", + "raw": "*#4*50#1*#20*0##", + "type": "OWNHeatingCommand", + "description": "*#4*50#1*#20*0##" + }, + { + "timestamp": 1791403876.312, + "iso_time": "2026-10-07T20:11:16.312000+00:00", + "direction": "rx", + "raw": "*#4*50#1*20*0##", + "type": "OWNHeatingEvent", + "description": "Zone 50's actuator 1 is off." + }, + { + "timestamp": 1791403890.522, + "iso_time": "2026-10-07T20:11:30.522000+00:00", + "direction": "rx", + "raw": "*1*0*43##", + "type": "OWNLightingEvent", + "description": "Light 43 is switched off." + }, + { + "timestamp": 1791403897.0, + "iso_time": "2026-10-07T20:11:37+00:00", + "direction": "rx", + "raw": "*1*0*43##", + "type": "OWNLightingEvent", + "description": "Light 43 is switched off." + }, + { + "timestamp": 1791403903.4, + "iso_time": "2026-10-07T20:11:43.400000+00:00", + "direction": "rx", + "raw": "*1*1*43##", + "type": "OWNLightingEvent", + "description": "Light 43 is switched on." + }, + { + "timestamp": 1791403909.556, + "iso_time": "2026-10-07T20:11:49.556000+00:00", + "direction": "rx", + "raw": "*1*0*43##", + "type": "OWNLightingEvent", + "description": "Light 43 is switched off." + }, + { + "timestamp": 1791403915.726, + "iso_time": "2026-10-07T20:11:55.726000+00:00", + "direction": "rx", + "raw": "*1*1*43##", + "type": "OWNLightingEvent", + "description": "Light 43 is switched on." + }, + { + "timestamp": 1791403920.864, + "iso_time": "2026-10-07T20:12:00.864000+00:00", + "direction": "rx", + "raw": "*1*0*43##", + "type": "OWNLightingEvent", + "description": "Light 43 is switched off." + }, + { + "timestamp": 1791403922.004, + "iso_time": "2026-10-07T20:12:02.004000+00:00", + "direction": "rx", + "raw": "*#1*43*2*0*0*0##", + "type": "OWNLightingEvent", + "description": "Light 43 is switched on for 0s." + }, + { + "timestamp": 1791403922.009, + "iso_time": "2026-10-07T20:12:02.009000+00:00", + "direction": "rx", + "raw": "*#1*43*2*0*0*0##", + "type": "OWNLightingEvent", + "description": "Light 43 is switched on for 0s." + }, + { + "timestamp": 1791403934.429, + "iso_time": "2026-10-07T20:12:14.429000+00:00", + "direction": "rx", + "raw": "*1*0*43##", + "type": "OWNLightingEvent", + "description": "Light 43 is switched off." + }, + { + "timestamp": 1791403946.281, + "iso_time": "2026-10-07T20:12:26.281000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403946.577, + "iso_time": "2026-10-07T20:12:26.577000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0070*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 7.0\u00b0C." + }, + { + "timestamp": 1791403946.579, + "iso_time": "2026-10-07T20:12:26.579000+00:00", + "direction": "rx", + "raw": "*4*102*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's mode is set to 'off'." + }, + { + "timestamp": 1791403946.871, + "iso_time": "2026-10-07T20:12:26.871000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403946.987, + "iso_time": "2026-10-07T20:12:26.987000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403994.376, + "iso_time": "2026-10-07T20:13:14.376000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403994.413, + "iso_time": "2026-10-07T20:13:14.413000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403994.419, + "iso_time": "2026-10-07T20:13:14.419000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403994.458, + "iso_time": "2026-10-07T20:13:14.458000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403994.99, + "iso_time": "2026-10-07T20:13:14.990000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403995.3, + "iso_time": "2026-10-07T20:13:15.300000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403995.304, + "iso_time": "2026-10-07T20:13:15.304000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403995.582, + "iso_time": "2026-10-07T20:13:15.582000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403995.889, + "iso_time": "2026-10-07T20:13:15.889000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791403996.293, + "iso_time": "2026-10-07T20:13:16.293000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791403996.59, + "iso_time": "2026-10-07T20:13:16.590000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403996.593, + "iso_time": "2026-10-07T20:13:16.593000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791403996.894, + "iso_time": "2026-10-07T20:13:16.894000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791403997.172, + "iso_time": "2026-10-07T20:13:17.172000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + }, + { + "timestamp": 1791404001.928, + "iso_time": "2026-10-07T20:13:21.928000+00:00", + "direction": "rx", + "raw": "*#4*59*60*86##", + "type": "OWNHeatingEvent", + "description": "Zone 59's main sensor is reporting a humidity of 86.0%." + }, + { + "timestamp": 1791404002.208, + "iso_time": "2026-10-07T20:13:22.208000+00:00", + "direction": "rx", + "raw": "*#4*60*0*0235##", + "type": "OWNHeatingEvent", + "description": "Zone 60's main sensor is reporting a temperature of 23.5\u00b0C." + }, + { + "timestamp": 1791404002.505, + "iso_time": "2026-10-07T20:13:22.505000+00:00", + "direction": "rx", + "raw": "*#4*60*12*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791404002.507, + "iso_time": "2026-10-07T20:13:22.507000+00:00", + "direction": "rx", + "raw": "*4*1*60##", + "type": "OWNHeatingEvent", + "description": "Zone 60's season is heating." + }, + { + "timestamp": 1791404002.793, + "iso_time": "2026-10-07T20:13:22.793000+00:00", + "direction": "rx", + "raw": "*#4*60*14*0230*3##", + "type": "OWNHeatingEvent", + "description": "Zone 60's target temperature is set to 23.0\u00b0C." + }, + { + "timestamp": 1791404002.913, + "iso_time": "2026-10-07T20:13:22.913000+00:00", + "direction": "rx", + "raw": "*#4*60*13*00##", + "type": "OWNHeatingEvent", + "description": "Zone 60's local offset is set to 0\u00b0C." + } + ] +} \ No newline at end of file diff --git a/tests/test_issue_649_myhomeserver1_live_trace.py b/tests/test_issue_649_myhomeserver1_live_trace.py new file mode 100644 index 000000000..8bacbd124 --- /dev/null +++ b/tests/test_issue_649_myhomeserver1_live_trace.py @@ -0,0 +1,236 @@ +"""Tests for #649: Authentic MyHomeServer1 Physical Plant Trace Replay & Conformance. + +Verifies that authentic on-wire OpenWebNet monitor frames captured from a physical +BTicino MyHomeServer1 gateway (firmware 3.87.13, 2026-10-07 live session) +can be deterministically parsed and replayed through the integration event dispatcher +without exceptions or regressions. + +Hardware & Plant Topology Under Test: +- Gateway: BTicino MyHomeServer1 (Server MyHOME_Up, FW 3.87.13) +- Thermostat: Zone 60 (heating plant with no central unit `#0`) +- Meter: F520 single-phase energy meter (address 51) +- Light: Relay actuator 43 (non-dimmer) +""" + +from __future__ import annotations + +import json +from pathlib import Path +from unittest.mock import 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.dispatcher import async_dispatcher_send +from OWNd.message import ( + OWNEvent, + OWNHeatingEvent, + OWNLightingEvent, + OWNMessage, +) +from pytest_homeassistant_custom_component.common import MockConfigEntry + +from custom_components.myhome.const import ( + CONF_DEVICE_TYPE, + CONF_ENTITY, + CONF_FIRMWARE, + CONF_MANUFACTURER, + DOMAIN, +) + +TRACES_DIR = Path(__file__).resolve().parent / "fixtures" / "traces" / "issue_649" +RAW_LOG_FILE = TRACES_DIR / "live_test_monitor_2026-10-07.log" +JSON_TRACE_FILE = TRACES_DIR / "myhome_trace_MyHomeServer1_live_2026-10-07.json" + + +@pytest.mark.asyncio +async def test_myhomeserver1_live_trace_replay_without_exceptions(hass: HomeAssistant) -> None: + """Replay all 107 on-wire frames from the physical MyHomeServer1 plant capture.""" + assert RAW_LOG_FILE.is_file(), f"Missing raw trace fixture: {RAW_LOG_FILE}" + assert JSON_TRACE_FILE.is_file(), f"Missing JSON trace fixture: {JSON_TRACE_FILE}" + + with open(JSON_TRACE_FILE, "r", encoding="utf-8") as f: + trace_data = json.load(f) + + gateway_info = trace_data["gateway"] + assert gateway_info["model"] == "MyHomeServer1" + assert gateway_info["firmware"] == "3.87.13" + assert gateway_info["identification"]["profile"] == "MyHomeServer1Profile" + + raw_frames = trace_data["frames"] + assert len(raw_frames) == 107 + + # Verify raw log frame count matches JSON trace exactly + with open(RAW_LOG_FILE, "r", encoding="utf-8") as f: + raw_lines = [line.strip() for line in f if line.strip()] + assert len(raw_lines) == 107 + + mac = "00:03:50:AA:BB:CC" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.0.218", + CONF_PORT: 20000, + CONF_PASSWORD: None, + CONF_MAC: mac, + CONF_NAME: "MyHomeServer1", + CONF_DEVICE_TYPE: "urn:schemas-bticino-it:device:webserver:1", + CONF_MANUFACTURER: "BTicino S.p.A.", + CONF_FIRMWARE: "3.87.13", + }, + unique_id=mac, + ) + entry.add_to_hass(hass) + + with ( + patch( + "custom_components.myhome.gateway.OWNSession.test_connection", + return_value={"Success": True, "Message": None}, + ), + patch("custom_components.myhome.gateway.MyHOMEGatewayHandler.listening_loop"), + patch("custom_components.myhome.gateway.MyHOMEGatewayHandler.sending_loop"), + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + handler = hass.data[DOMAIN][mac][CONF_ENTITY] + handler._on_event_connection_state_change(True) + + replayed = 0 + whos_seen: set[str] = set() + + for item in raw_frames: + raw = item.get("raw") + assert raw, "Trace item must contain raw frame" + if raw in ("*#*1##", "*#*0##"): + continue + + msg = OWNMessage.parse(raw) + assert msg is not None, f"Failed to parse authentic frame: {raw}" + + if hasattr(msg, "who") and msg.who is not None: + whos_seen.add(str(msg.who)) + + # Replay frame through the gateway event bus + async_dispatcher_send(hass, f"{DOMAIN}_{mac}_event", msg) + replayed += 1 + + await hass.async_block_till_done() + assert replayed == 107 + assert "1" in whos_seen, "Trace must contain WHO 1 lighting frames" + assert "4" in whos_seen, "Trace must contain WHO 4 thermoregulation frames" + + +def test_myhomeserver1_live_trace_log_to_json_raw_equality() -> None: + """Verify that every frame in the raw monitor log matches the JSON trace archive exactly. + + Ensures 1:1 parity in sequence order, timestamp, and raw OpenWebNet frame string + between the authentic ground-truth log and the derived JSON archive. + """ + assert RAW_LOG_FILE.is_file(), f"Missing raw trace fixture: {RAW_LOG_FILE}" + assert JSON_TRACE_FILE.is_file(), f"Missing JSON trace fixture: {JSON_TRACE_FILE}" + + with open(RAW_LOG_FILE, "r", encoding="utf-8") as f: + raw_lines = [line.strip() for line in f if line.strip()] + + with open(JSON_TRACE_FILE, "r", encoding="utf-8") as f: + trace_data = json.load(f) + + json_frames = trace_data["frames"] + assert len(raw_lines) == 107 + assert len(json_frames) == 107 + + for idx, (log_line, json_frame) in enumerate(zip(raw_lines, json_frames)): + parts = log_line.split(maxsplit=2) + assert len(parts) == 3, f"Line {idx + 1} malformed: {log_line}" + ts_str, marker, raw_frame = parts + assert marker == "OWN", f"Line {idx + 1} unexpected marker: {marker}" + assert raw_frame == json_frame["raw"], ( + f"Frame mismatch at line {idx + 1}: log '{raw_frame}' != json '{json_frame['raw']}'" + ) + assert abs(float(ts_str) - json_frame["timestamp"]) < 1e-4, ( + f"Timestamp mismatch at line {idx + 1}: log {ts_str} != json {json_frame['timestamp']}" + ) + + +def test_myhomeserver1_live_trace_grammar_and_semantics() -> None: + """Verify syntactic and semantic properties of key frames from the physical trace.""" + # 1. Zone 60 temperature report (Dimension 0: 23.5 °C) + temp_msg = OWNMessage.parse("*#4*60*0*0235##") + assert isinstance(temp_msg, OWNHeatingEvent) + assert temp_msg.zone == 60 + assert temp_msg.main_temperature == 23.5 + assert temp_msg.message_type == "main_temperature" + + # 2. Zone 60 dimension 12 active setpoint (23.0 °C, generic mode 3) + setpoint_msg = OWNMessage.parse("*#4*60*12*0230*3##") + assert isinstance(setpoint_msg, OWNHeatingEvent) + assert setpoint_msg.zone == 60 + assert setpoint_msg.local_set_temperature == 23.0 + assert setpoint_msg.message_type == "local_target_temperature" + + # 3. Zone 60 dimension 14 programmed setpoint (23.0 °C, generic mode 3) + prog_setpoint = OWNMessage.parse("*#4*60*14*0230*3##") + assert isinstance(prog_setpoint, OWNHeatingEvent) + assert prog_setpoint.zone == 60 + assert prog_setpoint.set_temperature == 23.0 + assert prog_setpoint.message_type == "target_temperature" + + # 4. Zone 60 local offset (Dimension 13: 0.0 °C) + offset_msg = OWNMessage.parse("*#4*60*13*00##") + assert isinstance(offset_msg, OWNHeatingEvent) + assert offset_msg.zone == 60 + assert offset_msg.local_offset == 0 + + # 5. Zone 60 season report: heating (*4*1*60##) + season_heat = OWNMessage.parse("*4*1*60##") + assert isinstance(season_heat, OWNHeatingEvent) + assert season_heat.zone == 60 + + # 6. Zone 60 season report: conditioning (*4*0*60##) + season_cool = OWNMessage.parse("*4*0*60##") + assert isinstance(season_cool, OWNHeatingEvent) + assert season_cool.zone == 60 + + # 7. Zone 60 antifreeze mode (*4*102*60##) and associated setpoint 7.0 °C (*#4*60*12*0070*3##) + antifreeze_mode = OWNMessage.parse("*4*102*60##") + assert isinstance(antifreeze_mode, OWNHeatingEvent) + assert antifreeze_mode.zone == 60 + assert antifreeze_mode.mode == "off" + + antifreeze_setpoint = OWNMessage.parse("*#4*60*12*0070*3##") + assert isinstance(antifreeze_setpoint, OWNHeatingEvent) + assert antifreeze_setpoint.zone == 60 + assert antifreeze_setpoint.local_set_temperature == 7.0 + + # 8. Zone 60 actuator state frames + actuator_1 = OWNMessage.parse("*#4*60#1*20*0##") + assert isinstance(actuator_1, OWNHeatingEvent) + assert actuator_1.zone == 60 + assert actuator_1.is_active() is False + + actuator_2 = OWNMessage.parse("*#4*60#2*20*5##") + assert isinstance(actuator_2, OWNHeatingEvent) + assert actuator_2.zone == 60 + + # 9. Lighting Relay 43 ON and OFF states + light_on = OWNMessage.parse("*1*1*43##") + assert isinstance(light_on, OWNLightingEvent) + assert light_on.where == "43" + assert light_on.is_on is True + + light_off = OWNMessage.parse("*1*0*43##") + assert isinstance(light_off, OWNLightingEvent) + assert light_off.where == "43" + assert light_off.is_on is False + + # 10. Lighting Relay 43 Dimension 2 timer readback (0*0*0 = timer expired) + timer_msg = OWNMessage.parse("*#1*43*2*0*0*0##") + assert isinstance(timer_msg, (OWNLightingEvent, OWNEvent)) + assert timer_msg.where == "43"