From 178247ddfe1e3a59c875cfb729f0d6d1d2b4959b Mon Sep 17 00:00:00 2001 From: Laurens <43173895+GreenGrassBlueOcean@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:27:58 +0200 Subject: [PATCH 1/2] test(media_player,intercom): add authentic MH202 visitor doorbell and wall control traces (#669) --- .../mh202_sound_intercom_ducking/README.md | 54 +- ...e_trace_MH202_all_2026-10-18T10-27-48.json | 796 ++++++++ ...e_trace_MH202_all_2026-10-18T10-34-13.json | 1816 +++++++++++++++++ tests/test_intercom_ducking_trace_replay.py | 275 +++ 4 files changed, 2926 insertions(+), 15 deletions(-) create mode 100644 tests/fixtures/traces/mh202_sound_intercom_ducking/myhome_trace_MH202_all_2026-10-18T10-27-48.json create mode 100644 tests/fixtures/traces/mh202_sound_intercom_ducking/myhome_trace_MH202_all_2026-10-18T10-34-13.json diff --git a/tests/fixtures/traces/mh202_sound_intercom_ducking/README.md b/tests/fixtures/traces/mh202_sound_intercom_ducking/README.md index 8f8059faf..abd73a402 100644 --- a/tests/fixtures/traces/mh202_sound_intercom_ducking/README.md +++ b/tests/fixtures/traces/mh202_sound_intercom_ducking/README.md @@ -1,20 +1,44 @@ -# MH202 + F441M Intercom Video Stream Audio Ducking & Volume Restoration Trace +# MH202 + F441M Intercom Audio Ducking, Visitor Doorbell Call & Wall Control Traces -Authentic passive bus trace captured on a physical plant with an MH202 gateway, F441M audio matrix, and video intercom entrance panel (Issue #669): +Authentic passive bus traces captured on a physical plant with an MH202 gateway, F441M audio matrix, and video intercom entrance panel ([Issue #669](https://github.com/OpenWebNet-HA/MyHOME/issues/669)): - **Gateway**: BTicino MH202 (Firmware 1.0) -- **Audio Matrix**: Legrand / BTicino F441M (WHO=16 / WHO=22) -- **Amplifiers in Home**: 31, 41, 51, 61, 71, 81 (all powered OFF when trace starts) -- **Video Intercom Event**: Video stream starts from entrance panel, ducking all audio; video stream stops (`*6*9**##`), restoring configured volumes. +- **Audio Matrix**: Legrand / BTicino F441M (dual-dialect WHO=16 / WHO=22) +- **Amplifiers in Home**: `31`, `41`, `51`, `61`, `71`, `81` +- **Video Intercom**: External entrance panel (`WHERE=4000`, caller ID `20`) and indoor video station (`WHERE=11` / unit `1`) -### Validated Protocol Behaviors +--- -1. **Hardware Audio Ducking**: - - Starting a video intercom stream triggers automatic audio ducking on the SCS bus. - - The matrix broadcasts Dimension 1 volume reduction frames across all amplifiers: - - `*#16*41*1*1##`, `*#16*31*1*1##`, `*#16*61*1*1##`, `*#16*51*1*1##`, `*#16*71*1*1##`, `*#16*81*1*1##` - - Interleaved with WHO 22 mirror status reports: `*#22*3#4#1*1*1##`, `*#22*3#3#1*1*1##`, `*#22*3#6#1*1*1##`, `*#22*3#5#1*1*1##`, `*#22*3#7#1*1*1##`, `*#22*3#8#1*1*1##` - - Amplifiers that are currently `MediaPlayerState.OFF` must retain their `OFF` state and **never** wake or auto-join streaming sessions (#669). +## Trace Fixtures + +### 1. `myhome_trace_MH202_sound_intercom_ducking.json` (30 frames) +- **Scenario**: Indoor unit activates entrance panel camera preview while all 6 amplifiers are `OFF`, then closes camera (`*6*9**##`). +- **Validated Behavior**: + - Automatic matrix audio ducking (`*#16**1*1##` + `*#22*3##*1*1##`) and post-call volume restoration (`*#16**1*##`) update `_attr_volume_level` while keeping all powered-off amplifiers strictly in `MediaPlayerState.OFF`. + +### 2. `myhome_trace_MH202_all_2026-10-18T10-27-48.json` (61 frames) +- **Scenario**: Visitor presses the outdoor doorbell button -> indoor unit rings -> call is answered -> door lock is released during conversation -> call ends (`*6*9**##`). +- **On-Wire Sequence**: + 1. **Audio Ducking (`10:27:00.492Z`–`10:27:01.368Z`)**: F441M matrix ducks all 6 amplifiers (`31`, `61`, `51`, `41`, `71`, `81`) to volume `1` via paired WHO 16 (`*#16**1*1##`) and WHO 22 (`*#22*3##*1*1##`) Dimension 1 frames. + 2. **Visitor Doorbell Call (`10:27:01.281Z`–`10:27:01.285Z`)**: + - `*8*1#1#4#20*11##`: 4-component WHO 8 call initiation (`WHAT=1#1#4#20`, `Kind=1` Entrance Panel PE1, `MMType=4` Audio+Video, `Caller=20`, target indoor station `WHERE=11`). + - `*8*9#1#4*20##` (+4 ms): WHO 8 Caller Address Notification (`WHAT=9#1#4`, `WHERE=20`). + 3. **Door Lock Release in Conversation (`10:27:25.455Z`–`10:27:26.201Z`)**: + - `*6*22*1##` (`WHAT=22`, L4686SDK *"Open lock with external unit in conversation"* at unit `1`) followed +26 ms later by `*6*10*4000##` (`WHAT=10`, *"Open lock"* at entrance panel `4000`). + - ~717 ms later, lock relay pulse release frames `*6*24*1##` (`WHAT=24` at `1`) and `*6*23*4000##` (`WHAT=23` at `4000`). + 4. **Call End, Dry Contact & Volume Restoration (`10:27:30.852Z`–`10:27:33.268Z`)**: + - `*6*9**##`: Camera OFF (`WHO=6, WHAT=9, WHERE=*`). + - `*25*32#1*31##`: Dry contact / CEN+ confirmation (`WHO=25, WHAT=32#1, WHERE=31`). + - Matrix restores pre-call amplifier volumes (`31` -> `0`, `41` -> `6`, `61` -> `7`, `51` -> `6`; `71` and `81` remain at `1`). + +### 3. `myhome_trace_MH202_all_2026-10-18T10-34-13.json` (146 frames) +- **Scenario**: User operates the physical wall switch in Zone `41` (Area 4, Point 1): turns amplifier ON -> ramps volume UP and DOWN via physical wall rocker -> turns amplifier OFF. +- **On-Wire Sequence**: + 1. **Dual-Dialect Power-On & Matrix Routing (`10:33:32.011Z`–`10:33:35.201Z`)**: + - WHO 22 commands (`*22*0#4#4*6##`, `*22*1#4#4*2#3##`, `*22*21#4#4*5#2#3##`, `*22*1#4#4*3#4#1##`, `*22*2#4#4*5#2#3##`) interleaved with WHO 16 matrix routing `*16*3*143##` (Environment `4` -> Source `3`), amplifier ON `*16*3*41##`, and source ON `*16*3*103##`. + - WHO 22 Dimension 12 status frames (`*#22*3#4#1*12*1*4##`, `*#22*2#3*12*1*4##`) and full DSP status dump on `3#4#1` (Dim 1 Volume=`6`, Dim 2 High=`30`, Dim 4 Low=`30`, Dim 17 Balance=`10`, Dim 20 Loudness=`1`, Dim 18 3D=`0`, Dim 19 Preset=`3`). + 2. **Wall Rocker Volume Up & Down (`10:33:34.822Z`–`10:33:50.997Z`)**: + - Wall control sends WHO 22 step commands `*22*3#1*3#4#1##` (Volume UP +1) and `*22*4#1*3#4#1##` (Volume DOWN -1). + - Amplifier broadcasts paired `*#16*41*1*##` and `*#22*3#4#1*1*##` volume reports (`6 -> 15 -> 5 -> 12 -> 7`). + 3. **Wall Power-Off (`10:33:51.905Z`–`10:33:52.610Z`)**: + - `*22*0#4#4*3#4#1##` (WHO 22 Speaker `3#4#1` OFF), `*16*13*41##` (WHO 16 Amplifier `41` OFF), and `*#22*3#4#1*12*0*10##` (WHO 22 Dim 12 standby state `0`). -2. **Camera Off & Volume Restoration**: - - When the camera stream ends (`*6*9**##`), the matrix restores individual amplifier volume levels to their pre-intercom configurations (e.g. 6/31, 7/31, 1/31, 0/31). - - Powered-off amplifiers update their stored volume level (`_attr_volume_level`) but remain strictly `MediaPlayerState.OFF`. diff --git a/tests/fixtures/traces/mh202_sound_intercom_ducking/myhome_trace_MH202_all_2026-10-18T10-27-48.json b/tests/fixtures/traces/mh202_sound_intercom_ducking/myhome_trace_MH202_all_2026-10-18T10-27-48.json new file mode 100644 index 000000000..95f7e635c --- /dev/null +++ b/tests/fixtures/traces/mh202_sound_intercom_ducking/myhome_trace_MH202_all_2026-10-18T10-27-48.json @@ -0,0 +1,796 @@ +{ + "capture": { + "kind": "trace", + "started_at": "2026-10-18T10:26:39.233Z", + "filters": { + "who": null, + "where": null, + "direction": null + }, + "window": { + "first": "2026-10-18T10:26:39.429Z", + "last": "2026-10-18T10:27:37.792Z", + "frames": 61, + "buffer_frames": 61, + "buffer_depth": 500, + "truncated": false + } + }, + "environment": { + "home_assistant_version": "2026.10.2", + "integration_version": "2.0.0b15", + "ownd_version": "2.0.0b10", + "exported_at": "2026-10-18T10:27:48.064Z" + }, + "gateway": { + "model": "MH202", + "manufacturer": "BTicino S.p.A.", + "firmware": "1.0", + "mac_prefix": "00:03:50", + "connection_type": "tcp", + "queue_pacing": 0.1, + "is_connected": true, + "identification": { + "model": "MH202", + "source": "ssdp", + "configured_model": "MH202", + "ssdp_model": "MH202", + "who13_code": null, + "who13_model": null, + "who13_model_official": null, + "who13_model_observed": null, + "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": "MH202Profile", + "conflict": null + } + }, + "telemetry": { + "total_rx": 66, + "total_tx": 0, + "captured_in_buffer": 61, + "buffer_depth": 500, + "queue_depth": 0 + }, + "frames": [ + { + "timestamp": 1792319199.429886, + "iso_time": "2026-10-18T10:26:39.429886+00:00", + "direction": "rx", + "raw": "*#18*76#0*113*0##", + "who": "18", + "what": null, + "where": "76", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319200.330243, + "iso_time": "2026-10-18T10:26:40.330243+00:00", + "direction": "rx", + "raw": "*#18*78#0*113*0##", + "who": "18", + "what": null, + "where": "78", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319201.568818, + "iso_time": "2026-10-18T10:26:41.568818+00:00", + "direction": "rx", + "raw": "*9*1*6##", + "who": "9", + "what": "1", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319203.590317, + "iso_time": "2026-10-18T10:26:43.590317+00:00", + "direction": "rx", + "raw": "*#18*77#0*113*335##", + "who": "18", + "what": null, + "where": "77", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319204.261477, + "iso_time": "2026-10-18T10:26:44.261477+00:00", + "direction": "rx", + "raw": "*#18*75#0*113*0##", + "who": "18", + "what": null, + "where": "75", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319205.02734, + "iso_time": "2026-10-18T10:26:45.027340+00:00", + "direction": "rx", + "raw": "*9*0*6##", + "who": "9", + "what": "0", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319205.89069, + "iso_time": "2026-10-18T10:26:45.890690+00:00", + "direction": "rx", + "raw": "*#18*51*113*1137##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319219.601035, + "iso_time": "2026-10-18T10:26:59.601035+00:00", + "direction": "rx", + "raw": "*#18*74#0*113*0##", + "who": "18", + "what": null, + "where": "74", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.058908, + "iso_time": "2026-10-18T10:27:00.058908+00:00", + "direction": "rx", + "raw": "*#18*72#0*113*542##", + "who": "18", + "what": null, + "where": "72", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.492787, + "iso_time": "2026-10-18T10:27:00.492787+00:00", + "direction": "rx", + "raw": "*#16*31*1*1##", + "who": "16", + "what": null, + "where": "31", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.495582, + "iso_time": "2026-10-18T10:27:00.495582+00:00", + "direction": "rx", + "raw": "*#22*3#3#1*1*1##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.527024, + "iso_time": "2026-10-18T10:27:00.527024+00:00", + "direction": "rx", + "raw": "*#13**22*12*27*00*000*00*18*10*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "22", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.546118, + "iso_time": "2026-10-18T10:27:00.546118+00:00", + "direction": "rx", + "raw": "*#16*61*1*1##", + "who": "16", + "what": null, + "where": "61", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.548308, + "iso_time": "2026-10-18T10:27:00.548308+00:00", + "direction": "rx", + "raw": "*#22*3#6#1*1*1##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.576112, + "iso_time": "2026-10-18T10:27:00.576112+00:00", + "direction": "rx", + "raw": "*#16*51*1*1##", + "who": "16", + "what": null, + "where": "51", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.578154, + "iso_time": "2026-10-18T10:27:00.578154+00:00", + "direction": "rx", + "raw": "*#22*3#5#1*1*1##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.606108, + "iso_time": "2026-10-18T10:27:00.606108+00:00", + "direction": "rx", + "raw": "*#16*41*1*1##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319220.608172, + "iso_time": "2026-10-18T10:27:00.608172+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*1##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319221.281188, + "iso_time": "2026-10-18T10:27:01.281188+00:00", + "direction": "rx", + "raw": "*8*1#1#4#20*11##", + "who": "8", + "what": "1", + "where": "11", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319221.28526, + "iso_time": "2026-10-18T10:27:01.285260+00:00", + "direction": "rx", + "raw": "*8*9#1#4*20##", + "who": "8", + "what": "9", + "where": "20", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319221.336592, + "iso_time": "2026-10-18T10:27:01.336592+00:00", + "direction": "rx", + "raw": "*#16*71*1*1##", + "who": "16", + "what": null, + "where": "71", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319221.339075, + "iso_time": "2026-10-18T10:27:01.339075+00:00", + "direction": "rx", + "raw": "*#22*3#7#1*1*1##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319221.366602, + "iso_time": "2026-10-18T10:27:01.366602+00:00", + "direction": "rx", + "raw": "*#16*81*1*1##", + "who": "16", + "what": null, + "where": "81", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319221.368985, + "iso_time": "2026-10-18T10:27:01.368985+00:00", + "direction": "rx", + "raw": "*#22*3#8#1*1*1##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319221.421051, + "iso_time": "2026-10-18T10:27:01.421051+00:00", + "direction": "rx", + "raw": "*#18*79#0*113*0##", + "who": "18", + "what": null, + "where": "79", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319222.820781, + "iso_time": "2026-10-18T10:27:02.820781+00:00", + "direction": "rx", + "raw": "*#18*73#0*113*0##", + "who": "18", + "what": null, + "where": "73", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319227.69062, + "iso_time": "2026-10-18T10:27:07.690620+00:00", + "direction": "rx", + "raw": "*#18*71#0*113*0##", + "who": "18", + "what": null, + "where": "71", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319229.491245, + "iso_time": "2026-10-18T10:27:09.491245+00:00", + "direction": "rx", + "raw": "*#18*76#0*113*0##", + "who": "18", + "what": null, + "where": "76", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319230.380041, + "iso_time": "2026-10-18T10:27:10.380041+00:00", + "direction": "rx", + "raw": "*#18*78#0*113*0##", + "who": "18", + "what": null, + "where": "78", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319233.691214, + "iso_time": "2026-10-18T10:27:13.691214+00:00", + "direction": "rx", + "raw": "*#18*77#0*113*337##", + "who": "18", + "what": null, + "where": "77", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319234.351459, + "iso_time": "2026-10-18T10:27:14.351459+00:00", + "direction": "rx", + "raw": "*#18*75#0*113*0##", + "who": "18", + "what": null, + "where": "75", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319235.801435, + "iso_time": "2026-10-18T10:27:15.801435+00:00", + "direction": "rx", + "raw": "*#18*51*113*1154##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319238.076259, + "iso_time": "2026-10-18T10:27:18.076259+00:00", + "direction": "rx", + "raw": "*9*1*6##", + "who": "9", + "what": "1", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319241.6904, + "iso_time": "2026-10-18T10:27:21.690400+00:00", + "direction": "rx", + "raw": "*#18*72#0*113*556##", + "who": "18", + "what": null, + "where": "72", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319245.455839, + "iso_time": "2026-10-18T10:27:25.455839+00:00", + "direction": "rx", + "raw": "*6*22*1##", + "who": "6", + "what": "22", + "where": "1", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319245.481235, + "iso_time": "2026-10-18T10:27:25.481235+00:00", + "direction": "rx", + "raw": "*6*10*4000##", + "who": "6", + "what": "10", + "where": "4000", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319246.172089, + "iso_time": "2026-10-18T10:27:26.172089+00:00", + "direction": "rx", + "raw": "*6*24*1##", + "who": "6", + "what": "24", + "where": "1", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319246.201258, + "iso_time": "2026-10-18T10:27:26.201258+00:00", + "direction": "rx", + "raw": "*6*23*4000##", + "who": "6", + "what": "23", + "where": "4000", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319246.436946, + "iso_time": "2026-10-18T10:27:26.436946+00:00", + "direction": "rx", + "raw": "*9*0*6##", + "who": "9", + "what": "0", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319246.856245, + "iso_time": "2026-10-18T10:27:26.856245+00:00", + "direction": "rx", + "raw": "*9*1*6##", + "who": "9", + "what": "1", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319247.771396, + "iso_time": "2026-10-18T10:27:27.771396+00:00", + "direction": "rx", + "raw": "*#18*51*113*1265##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319249.61163, + "iso_time": "2026-10-18T10:27:29.611630+00:00", + "direction": "rx", + "raw": "*#18*74#0*113*0##", + "who": "18", + "what": null, + "where": "74", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319249.976663, + "iso_time": "2026-10-18T10:27:29.976663+00:00", + "direction": "rx", + "raw": "*#18*51*113*1470##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319250.852255, + "iso_time": "2026-10-18T10:27:30.852255+00:00", + "direction": "rx", + "raw": "*6*9**##", + "who": "6", + "what": "9", + "where": "*", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319250.873791, + "iso_time": "2026-10-18T10:27:30.873791+00:00", + "direction": "rx", + "raw": "*#13**22*12*27*30*000*00*18*10*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "22", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319251.490332, + "iso_time": "2026-10-18T10:27:31.490332+00:00", + "direction": "rx", + "raw": "*#18*79#0*113*0##", + "who": "18", + "what": null, + "where": "79", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319252.607929, + "iso_time": "2026-10-18T10:27:32.607929+00:00", + "direction": "rx", + "raw": "*25*32#1*31##", + "who": "25", + "what": "32", + "where": "31", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319252.696894, + "iso_time": "2026-10-18T10:27:32.696894+00:00", + "direction": "rx", + "raw": "*#16*31*1*0##", + "who": "16", + "what": null, + "where": "31", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319252.698668, + "iso_time": "2026-10-18T10:27:32.698668+00:00", + "direction": "rx", + "raw": "*#22*3#3#1*1*0##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319252.831247, + "iso_time": "2026-10-18T10:27:32.831247+00:00", + "direction": "rx", + "raw": "*#18*73#0*113*0##", + "who": "18", + "what": null, + "where": "73", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319253.125764, + "iso_time": "2026-10-18T10:27:33.125764+00:00", + "direction": "rx", + "raw": "*#16*41*1*6##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319253.128748, + "iso_time": "2026-10-18T10:27:33.128748+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*6##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319253.17393, + "iso_time": "2026-10-18T10:27:33.173930+00:00", + "direction": "rx", + "raw": "*#16*61*1*7##", + "who": "16", + "what": null, + "where": "61", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319253.177699, + "iso_time": "2026-10-18T10:27:33.177699+00:00", + "direction": "rx", + "raw": "*#22*3#6#1*1*7##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319253.264213, + "iso_time": "2026-10-18T10:27:33.264213+00:00", + "direction": "rx", + "raw": "*#16*51*1*6##", + "who": "16", + "what": null, + "where": "51", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319253.26849, + "iso_time": "2026-10-18T10:27:33.268490+00:00", + "direction": "rx", + "raw": "*#22*3#5#1*1*6##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319255.752015, + "iso_time": "2026-10-18T10:27:35.752015+00:00", + "direction": "rx", + "raw": "*#18*51*113*1284##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319255.915794, + "iso_time": "2026-10-18T10:27:35.915794+00:00", + "direction": "rx", + "raw": "*9*0*6##", + "who": "9", + "what": "0", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319256.333927, + "iso_time": "2026-10-18T10:27:36.333927+00:00", + "direction": "rx", + "raw": "*9*1*6##", + "who": "9", + "what": "1", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319257.732093, + "iso_time": "2026-10-18T10:27:37.732093+00:00", + "direction": "rx", + "raw": "*#18*71#0*113*0##", + "who": "18", + "what": null, + "where": "71", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319257.792526, + "iso_time": "2026-10-18T10:27:37.792526+00:00", + "direction": "rx", + "raw": "*#18*51*113*1149##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + } + ] +} diff --git a/tests/fixtures/traces/mh202_sound_intercom_ducking/myhome_trace_MH202_all_2026-10-18T10-34-13.json b/tests/fixtures/traces/mh202_sound_intercom_ducking/myhome_trace_MH202_all_2026-10-18T10-34-13.json new file mode 100644 index 000000000..6d7b6e20f --- /dev/null +++ b/tests/fixtures/traces/mh202_sound_intercom_ducking/myhome_trace_MH202_all_2026-10-18T10-34-13.json @@ -0,0 +1,1816 @@ +{ + "capture": { + "kind": "trace", + "started_at": "2026-10-18T10:33:19.782Z", + "filters": { + "who": null, + "where": null, + "direction": null + }, + "window": { + "first": "2026-10-18T10:33:19.795Z", + "last": "2026-10-18T10:34:06.094Z", + "frames": 146, + "buffer_frames": 146, + "buffer_depth": 500, + "truncated": false + } + }, + "environment": { + "home_assistant_version": "2026.10.2", + "integration_version": "2.0.0b15", + "ownd_version": "2.0.0b10", + "exported_at": "2026-10-18T10:34:13.884Z" + }, + "gateway": { + "model": "MH202", + "manufacturer": "BTicino S.p.A.", + "firmware": "1.0", + "mac_prefix": "00:03:50", + "connection_type": "tcp", + "queue_pacing": 0.1, + "is_connected": true, + "identification": { + "model": "MH202", + "source": "ssdp", + "configured_model": "MH202", + "ssdp_model": "MH202", + "who13_code": null, + "who13_model": null, + "who13_model_official": null, + "who13_model_observed": null, + "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": "MH202Profile", + "conflict": null + } + }, + "telemetry": { + "total_rx": 160, + "total_tx": 0, + "captured_in_buffer": 146, + "buffer_depth": 500, + "queue_depth": 0 + }, + "frames": [ + { + "timestamp": 1792319599.795591, + "iso_time": "2026-10-18T10:33:19.795591+00:00", + "direction": "rx", + "raw": "*#18*72#0*113*100##", + "who": "18", + "what": null, + "where": "72", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319600.792063, + "iso_time": "2026-10-18T10:33:20.792063+00:00", + "direction": "rx", + "raw": "*#18*51*113*598##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319602.798454, + "iso_time": "2026-10-18T10:33:22.798454+00:00", + "direction": "rx", + "raw": "*9*1*6##", + "who": "9", + "what": "1", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319602.829638, + "iso_time": "2026-10-18T10:33:22.829638+00:00", + "direction": "rx", + "raw": "*#18*72#0*113*111##", + "who": "18", + "what": null, + "where": "72", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319606.238908, + "iso_time": "2026-10-18T10:33:26.238908+00:00", + "direction": "rx", + "raw": "*9*0*6##", + "who": "9", + "what": "0", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319609.925028, + "iso_time": "2026-10-18T10:33:29.925028+00:00", + "direction": "rx", + "raw": "*#18*74#0*113*0##", + "who": "18", + "what": null, + "where": "74", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319610.355342, + "iso_time": "2026-10-18T10:33:30.355342+00:00", + "direction": "rx", + "raw": "*9*1*2##", + "who": "9", + "what": "1", + "where": "2", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319610.394364, + "iso_time": "2026-10-18T10:33:30.394364+00:00", + "direction": "rx", + "raw": "*#13**22*12*33*30*000*00*18*10*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "22", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319611.139759, + "iso_time": "2026-10-18T10:33:31.139759+00:00", + "direction": "rx", + "raw": "*9*1*4##", + "who": "9", + "what": "1", + "where": "4", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.011949, + "iso_time": "2026-10-18T10:33:32.011949+00:00", + "direction": "rx", + "raw": "*22*0#4#4*6##", + "who": "22", + "what": "0", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.074626, + "iso_time": "2026-10-18T10:33:32.074626+00:00", + "direction": "rx", + "raw": "*22*1#4#4*2#3##", + "who": "22", + "what": "1", + "where": "2", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.14525, + "iso_time": "2026-10-18T10:33:32.145250+00:00", + "direction": "rx", + "raw": "*22*21#4#4*5#2#3##", + "who": "22", + "what": "21", + "where": "5", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.221268, + "iso_time": "2026-10-18T10:33:32.221268+00:00", + "direction": "rx", + "raw": "*22*1#4#4*3#4#1##", + "who": "22", + "what": "1", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.385994, + "iso_time": "2026-10-18T10:33:32.385994+00:00", + "direction": "rx", + "raw": "*16*3*143##", + "who": "16", + "what": "3", + "where": "143", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.388895, + "iso_time": "2026-10-18T10:33:32.388895+00:00", + "direction": "rx", + "raw": "*22*2#4#4*5#2#3##", + "who": "22", + "what": "2", + "where": "5", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.710383, + "iso_time": "2026-10-18T10:33:32.710383+00:00", + "direction": "rx", + "raw": "*#18*79#0*113*0##", + "who": "18", + "what": null, + "where": "79", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.783241, + "iso_time": "2026-10-18T10:33:32.783241+00:00", + "direction": "rx", + "raw": "*16*3*41##", + "who": "16", + "what": "3", + "where": "41", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.787722, + "iso_time": "2026-10-18T10:33:32.787722+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*12*1*4##", + "who": "22", + "what": null, + "where": "3", + "dimension": "12", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.882498, + "iso_time": "2026-10-18T10:33:32.882498+00:00", + "direction": "rx", + "raw": "*16*3*103##", + "who": "16", + "what": "3", + "where": "103", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.886084, + "iso_time": "2026-10-18T10:33:32.886084+00:00", + "direction": "rx", + "raw": "*#22*2#3*12*1*4##", + "who": "22", + "what": null, + "where": "2", + "dimension": "12", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319612.939211, + "iso_time": "2026-10-18T10:33:32.939211+00:00", + "direction": "rx", + "raw": "*#18*73#0*113*0##", + "who": "18", + "what": null, + "where": "73", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319613.435988, + "iso_time": "2026-10-18T10:33:33.435988+00:00", + "direction": "rx", + "raw": "*#16*41*1*6##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319613.437523, + "iso_time": "2026-10-18T10:33:33.437523+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*6##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319614.013307, + "iso_time": "2026-10-18T10:33:34.013307+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*2*30##", + "who": "22", + "what": null, + "where": "3", + "dimension": "2", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319614.017135, + "iso_time": "2026-10-18T10:33:34.017135+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*4*30##", + "who": "22", + "what": null, + "where": "3", + "dimension": "4", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319614.21745, + "iso_time": "2026-10-18T10:33:34.217450+00:00", + "direction": "rx", + "raw": "*9*0*2##", + "who": "9", + "what": "0", + "where": "2", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319614.560505, + "iso_time": "2026-10-18T10:33:34.560505+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*17*10##", + "who": "22", + "what": null, + "where": "3", + "dimension": "17", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319614.82224, + "iso_time": "2026-10-18T10:33:34.822240+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.020405, + "iso_time": "2026-10-18T10:33:35.020405+00:00", + "direction": "rx", + "raw": "*#18*77#0*113*253##", + "who": "18", + "what": null, + "where": "77", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.141703, + "iso_time": "2026-10-18T10:33:35.141703+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.194043, + "iso_time": "2026-10-18T10:33:35.194043+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*20*1##", + "who": "22", + "what": null, + "where": "3", + "dimension": "20", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.197717, + "iso_time": "2026-10-18T10:33:35.197717+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*18*0##", + "who": "22", + "what": null, + "where": "3", + "dimension": "18", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.201714, + "iso_time": "2026-10-18T10:33:35.201714+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*19*3##", + "who": "22", + "what": null, + "where": "3", + "dimension": "19", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.365832, + "iso_time": "2026-10-18T10:33:35.365832+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.740384, + "iso_time": "2026-10-18T10:33:35.740384+00:00", + "direction": "rx", + "raw": "*#18*51*113*551##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.860471, + "iso_time": "2026-10-18T10:33:35.860471+00:00", + "direction": "rx", + "raw": "*#18*72#0*113*102##", + "who": "18", + "what": null, + "where": "72", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.917527, + "iso_time": "2026-10-18T10:33:35.917527+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.956433, + "iso_time": "2026-10-18T10:33:35.956433+00:00", + "direction": "rx", + "raw": "*#16*41*1*7##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319615.960404, + "iso_time": "2026-10-18T10:33:35.960404+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*7##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319616.164606, + "iso_time": "2026-10-18T10:33:36.164606+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319616.557089, + "iso_time": "2026-10-18T10:33:36.557089+00:00", + "direction": "rx", + "raw": "*#16*41*1*8##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319616.568776, + "iso_time": "2026-10-18T10:33:36.568776+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*8##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319616.694961, + "iso_time": "2026-10-18T10:33:36.694961+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319616.915667, + "iso_time": "2026-10-18T10:33:36.915667+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*9##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319616.92072, + "iso_time": "2026-10-18T10:33:36.920720+00:00", + "direction": "rx", + "raw": "*#16*41*1*9##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319616.975058, + "iso_time": "2026-10-18T10:33:36.975058+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319617.03202, + "iso_time": "2026-10-18T10:33:37.032020+00:00", + "direction": "rx", + "raw": "*#18*77#0*113*209##", + "who": "18", + "what": null, + "where": "77", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319617.303021, + "iso_time": "2026-10-18T10:33:37.303021+00:00", + "direction": "rx", + "raw": "*#16*41*1*10##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319617.306656, + "iso_time": "2026-10-18T10:33:37.306656+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*10##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319617.753003, + "iso_time": "2026-10-18T10:33:37.753003+00:00", + "direction": "rx", + "raw": "*#16*41*1*11##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319617.759203, + "iso_time": "2026-10-18T10:33:37.759203+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*11##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319618.024386, + "iso_time": "2026-10-18T10:33:38.024386+00:00", + "direction": "rx", + "raw": "*22*4#1*3#4#1##", + "who": "22", + "what": "4", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319618.049021, + "iso_time": "2026-10-18T10:33:38.049021+00:00", + "direction": "rx", + "raw": "*#18*77#0*113*179##", + "who": "18", + "what": null, + "where": "77", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319618.186162, + "iso_time": "2026-10-18T10:33:38.186162+00:00", + "direction": "rx", + "raw": "*#16*41*1*12##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319618.188173, + "iso_time": "2026-10-18T10:33:38.188173+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*12##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319618.854368, + "iso_time": "2026-10-18T10:33:38.854368+00:00", + "direction": "rx", + "raw": "*#18*71#0*113*0##", + "who": "18", + "what": null, + "where": "71", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319618.854604, + "iso_time": "2026-10-18T10:33:38.854604+00:00", + "direction": "rx", + "raw": "*22*4#1*3#4#1##", + "who": "22", + "what": "4", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319618.854971, + "iso_time": "2026-10-18T10:33:38.854971+00:00", + "direction": "rx", + "raw": "*#16*41*1*13##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319618.856036, + "iso_time": "2026-10-18T10:33:38.856036+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*13##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319618.98468, + "iso_time": "2026-10-18T10:33:38.984680+00:00", + "direction": "rx", + "raw": "*#18*72#0*113*113##", + "who": "18", + "what": null, + "where": "72", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319619.101148, + "iso_time": "2026-10-18T10:33:39.101148+00:00", + "direction": "rx", + "raw": "*22*4#1*3#4#1##", + "who": "22", + "what": "4", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319619.226154, + "iso_time": "2026-10-18T10:33:39.226154+00:00", + "direction": "rx", + "raw": "*#16*41*1*14##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319619.228837, + "iso_time": "2026-10-18T10:33:39.228837+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*14##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319619.335488, + "iso_time": "2026-10-18T10:33:39.335488+00:00", + "direction": "rx", + "raw": "*22*4#1*3#4#1##", + "who": "22", + "what": "4", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319619.796043, + "iso_time": "2026-10-18T10:33:39.796043+00:00", + "direction": "rx", + "raw": "*#16*41*1*15##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319619.799611, + "iso_time": "2026-10-18T10:33:39.799611+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*15##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319619.905657, + "iso_time": "2026-10-18T10:33:39.905657+00:00", + "direction": "rx", + "raw": "*22*4#1*3#4#1##", + "who": "22", + "what": "4", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319620.191848, + "iso_time": "2026-10-18T10:33:40.191848+00:00", + "direction": "rx", + "raw": "*9*0*4##", + "who": "9", + "what": "0", + "where": "4", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319620.230387, + "iso_time": "2026-10-18T10:33:40.230387+00:00", + "direction": "rx", + "raw": "*#18*76#0*113*0##", + "who": "18", + "what": null, + "where": "76", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319620.323156, + "iso_time": "2026-10-18T10:33:40.323156+00:00", + "direction": "rx", + "raw": "*#16*41*1*14##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319620.326803, + "iso_time": "2026-10-18T10:33:40.326803+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*14##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319620.435124, + "iso_time": "2026-10-18T10:33:40.435124+00:00", + "direction": "rx", + "raw": "*22*4#1*3#4#1##", + "who": "22", + "what": "4", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319620.665201, + "iso_time": "2026-10-18T10:33:40.665201+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*13##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319620.670444, + "iso_time": "2026-10-18T10:33:40.670444+00:00", + "direction": "rx", + "raw": "*#16*41*1*13##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319620.960509, + "iso_time": "2026-10-18T10:33:40.960509+00:00", + "direction": "rx", + "raw": "*#18*78#0*113*0##", + "who": "18", + "what": null, + "where": "78", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319621.031251, + "iso_time": "2026-10-18T10:33:41.031251+00:00", + "direction": "rx", + "raw": "*9*1*4##", + "who": "9", + "what": "1", + "where": "4", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319621.165319, + "iso_time": "2026-10-18T10:33:41.165319+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*12##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319621.170339, + "iso_time": "2026-10-18T10:33:41.170339+00:00", + "direction": "rx", + "raw": "*#16*41*1*12##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319621.706377, + "iso_time": "2026-10-18T10:33:41.706377+00:00", + "direction": "rx", + "raw": "*#16*41*1*11##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319621.709942, + "iso_time": "2026-10-18T10:33:41.709942+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*11##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319622.256451, + "iso_time": "2026-10-18T10:33:42.256451+00:00", + "direction": "rx", + "raw": "*#16*41*1*10##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319622.260237, + "iso_time": "2026-10-18T10:33:42.260237+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*10##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319622.84619, + "iso_time": "2026-10-18T10:33:42.846190+00:00", + "direction": "rx", + "raw": "*#16*41*1*9##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319622.850138, + "iso_time": "2026-10-18T10:33:42.850138+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*9##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319623.435928, + "iso_time": "2026-10-18T10:33:43.435928+00:00", + "direction": "rx", + "raw": "*#16*41*1*8##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319623.439515, + "iso_time": "2026-10-18T10:33:43.439515+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*8##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319623.695522, + "iso_time": "2026-10-18T10:33:43.695522+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319623.732061, + "iso_time": "2026-10-18T10:33:43.732061+00:00", + "direction": "rx", + "raw": "*#18*51*113*493##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319623.873045, + "iso_time": "2026-10-18T10:33:43.873045+00:00", + "direction": "rx", + "raw": "*#16*41*1*7##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319623.877257, + "iso_time": "2026-10-18T10:33:43.877257+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*7##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319623.981838, + "iso_time": "2026-10-18T10:33:43.981838+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319624.267024, + "iso_time": "2026-10-18T10:33:44.267024+00:00", + "direction": "rx", + "raw": "*#16*41*1*6##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319624.270876, + "iso_time": "2026-10-18T10:33:44.270876+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*6##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319624.496343, + "iso_time": "2026-10-18T10:33:44.496343+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319624.975881, + "iso_time": "2026-10-18T10:33:44.975881+00:00", + "direction": "rx", + "raw": "*#16*41*1*5##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319624.978582, + "iso_time": "2026-10-18T10:33:44.978582+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*5##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319624.979098, + "iso_time": "2026-10-18T10:33:44.979098+00:00", + "direction": "rx", + "raw": "*22*3#1*3#4#1##", + "who": "22", + "what": "3", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319625.386046, + "iso_time": "2026-10-18T10:33:45.386046+00:00", + "direction": "rx", + "raw": "*#16*41*1*6##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319625.389179, + "iso_time": "2026-10-18T10:33:45.389179+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*6##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319625.690569, + "iso_time": "2026-10-18T10:33:45.690569+00:00", + "direction": "rx", + "raw": "*#18*75#0*113*0##", + "who": "18", + "what": null, + "where": "75", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319625.993333, + "iso_time": "2026-10-18T10:33:45.993333+00:00", + "direction": "rx", + "raw": "*#16*41*1*7##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319625.999095, + "iso_time": "2026-10-18T10:33:45.999095+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*7##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319626.293179, + "iso_time": "2026-10-18T10:33:46.293179+00:00", + "direction": "rx", + "raw": "*#16*41*1*8##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319626.296774, + "iso_time": "2026-10-18T10:33:46.296774+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*8##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319626.883494, + "iso_time": "2026-10-18T10:33:46.883494+00:00", + "direction": "rx", + "raw": "*#16*41*1*9##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319626.887307, + "iso_time": "2026-10-18T10:33:46.887307+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*9##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319627.223231, + "iso_time": "2026-10-18T10:33:47.223231+00:00", + "direction": "rx", + "raw": "*#16*41*1*10##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319627.226799, + "iso_time": "2026-10-18T10:33:47.226799+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*10##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319627.54078, + "iso_time": "2026-10-18T10:33:47.540780+00:00", + "direction": "rx", + "raw": "*#16*41*1*11##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319627.556661, + "iso_time": "2026-10-18T10:33:47.556661+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*11##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319627.889579, + "iso_time": "2026-10-18T10:33:47.889579+00:00", + "direction": "rx", + "raw": "*#16*41*1*12##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319627.893145, + "iso_time": "2026-10-18T10:33:47.893145+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*12##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319628.355971, + "iso_time": "2026-10-18T10:33:48.355971+00:00", + "direction": "rx", + "raw": "*22*4#1*3#4#1##", + "who": "22", + "what": "4", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319629.043229, + "iso_time": "2026-10-18T10:33:49.043229+00:00", + "direction": "rx", + "raw": "*#16*41*1*11##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319629.047216, + "iso_time": "2026-10-18T10:33:49.047216+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*11##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319629.155848, + "iso_time": "2026-10-18T10:33:49.155848+00:00", + "direction": "rx", + "raw": "*22*4#1*3#4#1##", + "who": "22", + "what": "4", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319629.718081, + "iso_time": "2026-10-18T10:33:49.718081+00:00", + "direction": "rx", + "raw": "*#16*41*1*10##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319629.72121, + "iso_time": "2026-10-18T10:33:49.721210+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*10##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319630.336698, + "iso_time": "2026-10-18T10:33:50.336698+00:00", + "direction": "rx", + "raw": "*#16*41*1*9##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319630.340334, + "iso_time": "2026-10-18T10:33:50.340334+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*9##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319630.66636, + "iso_time": "2026-10-18T10:33:50.666360+00:00", + "direction": "rx", + "raw": "*#16*41*1*8##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319630.669235, + "iso_time": "2026-10-18T10:33:50.669235+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*8##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319630.890998, + "iso_time": "2026-10-18T10:33:50.890998+00:00", + "direction": "rx", + "raw": "*#18*72#0*113*107##", + "who": "18", + "what": null, + "where": "72", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319630.993073, + "iso_time": "2026-10-18T10:33:50.993073+00:00", + "direction": "rx", + "raw": "*#16*41*1*7##", + "who": "16", + "what": null, + "where": "41", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319630.997727, + "iso_time": "2026-10-18T10:33:50.997727+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*1*7##", + "who": "22", + "what": null, + "where": "3", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319631.260652, + "iso_time": "2026-10-18T10:33:51.260652+00:00", + "direction": "rx", + "raw": "*9*0*4##", + "who": "9", + "what": "0", + "where": "4", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319631.656227, + "iso_time": "2026-10-18T10:33:51.656227+00:00", + "direction": "rx", + "raw": "*9*1*4##", + "who": "9", + "what": "1", + "where": "4", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319631.905904, + "iso_time": "2026-10-18T10:33:51.905904+00:00", + "direction": "rx", + "raw": "*22*0#4#4*3#4#1##", + "who": "22", + "what": "0", + "where": "3", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319632.606347, + "iso_time": "2026-10-18T10:33:52.606347+00:00", + "direction": "rx", + "raw": "*16*13*41##", + "who": "16", + "what": "13", + "where": "41", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319632.610389, + "iso_time": "2026-10-18T10:33:52.610389+00:00", + "direction": "rx", + "raw": "*#22*3#4#1*12*0*10##", + "who": "22", + "what": null, + "where": "3", + "dimension": "12", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319633.886388, + "iso_time": "2026-10-18T10:33:53.886388+00:00", + "direction": "rx", + "raw": "*9*1*2##", + "who": "9", + "what": "1", + "where": "2", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319635.873432, + "iso_time": "2026-10-18T10:33:55.873432+00:00", + "direction": "rx", + "raw": "*9*0*4##", + "who": "9", + "what": "0", + "where": "4", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319637.731467, + "iso_time": "2026-10-18T10:33:57.731467+00:00", + "direction": "rx", + "raw": "*9*0*2##", + "who": "9", + "what": "0", + "where": "2", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319639.929524, + "iso_time": "2026-10-18T10:33:59.929524+00:00", + "direction": "rx", + "raw": "*#18*74#0*113*0##", + "who": "18", + "what": null, + "where": "74", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319640.110055, + "iso_time": "2026-10-18T10:34:00.110055+00:00", + "direction": "rx", + "raw": "*#18*77#0*113*190##", + "who": "18", + "what": null, + "where": "77", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319640.133124, + "iso_time": "2026-10-18T10:34:00.133124+00:00", + "direction": "rx", + "raw": "*#13**22*12*34*00*000*00*18*10*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "22", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319640.920782, + "iso_time": "2026-10-18T10:34:00.920782+00:00", + "direction": "rx", + "raw": "*#18*72#0*113*119##", + "who": "18", + "what": null, + "where": "72", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319641.122992, + "iso_time": "2026-10-18T10:34:01.122992+00:00", + "direction": "rx", + "raw": "*#18*77#0*113*246##", + "who": "18", + "what": null, + "where": "77", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319641.677389, + "iso_time": "2026-10-18T10:34:01.677389+00:00", + "direction": "rx", + "raw": "*#18*51*113*575##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319641.915353, + "iso_time": "2026-10-18T10:34:01.915353+00:00", + "direction": "rx", + "raw": "*9*1*6##", + "who": "9", + "what": "1", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319642.112355, + "iso_time": "2026-10-18T10:34:02.112355+00:00", + "direction": "rx", + "raw": "*#18*77#0*113*299##", + "who": "18", + "what": null, + "where": "77", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319642.811212, + "iso_time": "2026-10-18T10:34:02.811212+00:00", + "direction": "rx", + "raw": "*#18*79#0*113*0##", + "who": "18", + "what": null, + "where": "79", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319642.929825, + "iso_time": "2026-10-18T10:34:02.929825+00:00", + "direction": "rx", + "raw": "*#18*73#0*113*0##", + "who": "18", + "what": null, + "where": "73", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319643.114689, + "iso_time": "2026-10-18T10:34:03.114689+00:00", + "direction": "rx", + "raw": "*#18*77#0*113*340##", + "who": "18", + "what": null, + "where": "77", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319643.669352, + "iso_time": "2026-10-18T10:34:03.669352+00:00", + "direction": "rx", + "raw": "*#18*51*113*665##", + "who": "18", + "what": null, + "where": "51", + "dimension": "113", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1792319646.094606, + "iso_time": "2026-10-18T10:34:06.094606+00:00", + "direction": "rx", + "raw": "*9*0*6##", + "who": "9", + "what": "0", + "where": "6", + "dimension": null, + "is_ack": false, + "is_nack": false + } + ] +} diff --git a/tests/test_intercom_ducking_trace_replay.py b/tests/test_intercom_ducking_trace_replay.py index 6d62424eb..039b26b44 100644 --- a/tests/test_intercom_ducking_trace_replay.py +++ b/tests/test_intercom_ducking_trace_replay.py @@ -272,3 +272,278 @@ async def test_concurrent_intercom_ducking_during_wake_pending_window(hass, mock assert zone.state == MediaPlayerState.ON assert zone.volume_level == pytest.approx(6 / 31.0) + +DOORBELL_TRACE_PATH = ( + Path(__file__).parent + / "fixtures" + / "traces" + / "mh202_sound_intercom_ducking" + / "myhome_trace_MH202_all_2026-10-18T10-27-48.json" +) + +WALL_CONTROLS_TRACE_PATH = ( + Path(__file__).parent + / "fixtures" + / "traces" + / "mh202_sound_intercom_ducking" + / "myhome_trace_MH202_all_2026-10-18T10-34-13.json" +) + + +@pytest.mark.asyncio +async def test_mh202_visitor_doorbell_call_trace_replay(hass, mock_gateway): + """Replay the 61-frame visitor doorbell call + lock release trace (#669). + + Validates: + 1. All 61 frames parse cleanly via OWNMessage.parse, including: + - 4-component WHO 8 call initiation (*8*1#1#4#20*11##) and caller ID (*8*9#1#4*20##) + - WHO 6 door lock release in conversation (*6*22*1##, *6*10*4000##) and + pulse release (*6*24*1##, *6*23*4000##) + - WHO 6 camera OFF (*6*9**##) and WHO 25 dry-contact confirmation (*25*32#1*31##) + 2. All 6 powered-off amplifiers (31, 41, 51, 61, 71, 81) remain strictly OFF + throughout ducking and restoration while an independent group (21 + 22) streams. + 3. Restored amplifier volumes match the on-wire post-call values + (31->0, 41->6, 51->6, 61->7, 71->1, 81->1). + """ + assert DOORBELL_TRACE_PATH.is_file(), f"Missing trace fixture: {DOORBELL_TRACE_PATH}" + + with open(DOORBELL_TRACE_PATH, encoding="utf-8") as f: + trace_data = json.load(f) + + assert trace_data["gateway"]["model"] == "MH202" + assert trace_data["gateway"]["firmware"] == "1.0" + frames = trace_data["frames"] + assert len(frames) == 61 + + # Verify all 61 frames parse cleanly + parsed_frames = [] + for entry in frames: + raw = entry["raw"] + msg = OWNMessage.parse(raw) + assert msg is not None, f"Failed to parse frame: {raw}" + parsed_frames.append((raw, msg)) + + # Spot-check key WHO 8 / WHO 6 / WHO 25 frames in the visitor call sequence + by_raw = dict(parsed_frames) + + call_msg = by_raw["*8*1#1#4#20*11##"] + assert call_msg.who == 8 + assert getattr(call_msg, "what", getattr(call_msg, "_what", None)) == 1 + assert getattr(call_msg, "what_param", getattr(call_msg, "_what_param", None)) == [ + "1", + "4", + "20", + ] + assert call_msg.where == "11" + + caller_id_msg = by_raw["*8*9#1#4*20##"] + assert caller_id_msg.who == 8 + assert getattr(caller_id_msg, "what", getattr(caller_id_msg, "_what", None)) == 9 + assert getattr(caller_id_msg, "what_param", getattr(caller_id_msg, "_what_param", None)) == [ + "1", + "4", + ] + assert caller_id_msg.where == "20" + + lock_conv_msg = by_raw["*6*22*1##"] + assert lock_conv_msg.who == 6 + assert getattr(lock_conv_msg, "what", getattr(lock_conv_msg, "_what", None)) == 22 + assert lock_conv_msg.where == "1" + + lock_ep_msg = by_raw["*6*10*4000##"] + assert lock_ep_msg.who == 6 + assert getattr(lock_ep_msg, "what", getattr(lock_ep_msg, "_what", None)) == 10 + assert lock_ep_msg.where == "4000" + + lock_rel_1 = by_raw["*6*24*1##"] + assert lock_rel_1.who == 6 + assert getattr(lock_rel_1, "what", getattr(lock_rel_1, "_what", None)) == 24 + assert lock_rel_1.where == "1" + + lock_rel_ep = by_raw["*6*23*4000##"] + assert lock_rel_ep.who == 6 + assert getattr(lock_rel_ep, "what", getattr(lock_rel_ep, "_what", None)) == 23 + assert lock_rel_ep.where == "4000" + + cam_off_msg = by_raw["*6*9**##"] + assert cam_off_msg.who == 6 + assert getattr(cam_off_msg, "what", getattr(cam_off_msg, "_what", None)) == 9 + assert cam_off_msg.where == "*" + + dry_contact_msg = by_raw["*25*32#1*31##"] + assert dry_contact_msg.who == 25 + assert dry_contact_msg.where == "31" + + # Replay across active streaming group (21 + 22) and 6 OFF zones (31..81) + runtime = MyHOMERuntimeData(gateway=mock_gateway) + pool = DecoderPool(hass, {"media_player.streamer": 2}) + runtime.decoder_pool = pool + hass.states.async_set("media_player.streamer", MediaPlayerState.PLAYING) + + base_options = { + CONF_SOURCE_NAME.format(2): "Cambridge", + CONF_SOURCE_DEFAULTS: {"2": 2, "3": 2, "4": 2, "5": 2, "6": 2, "7": 2, "8": 2}, + } + mock_gateway.config_entry = MagicMock(options=dict(base_options)) + + leader = _create_test_zone(hass, mock_gateway, runtime, "21", "media_player.living_room") + member = _create_test_zone(hass, mock_gateway, runtime, "22", "media_player.kitchen") + for z in (leader, member): + z._options = lambda: dict(base_options) + z._gateway_handler.config_entry.options = dict(base_options) + z._attr_state = MediaPlayerState.ON + z._attr_source = "Cambridge" + + await pool.claim("media_player.living_room", preferred_source=2) + leader._active_decoder = "media_player.streamer" + await pool.add_member("media_player.living_room", "media_player.kitchen") + + inactive_zones = {} + for where in ("31", "41", "51", "61", "71", "81"): + z = _create_test_zone(hass, mock_gateway, runtime, where, f"media_player.zone_{where}") + z._options = lambda: dict(base_options) + z._gateway_handler.config_entry.options = dict(base_options) + z._attr_state = MediaPlayerState.OFF + inactive_zones[where] = z + + for raw, msg in parsed_frames: + target = _resolve_where(raw) + if target and target in inactive_zones: + sound_evt = OWNSoundEvent.parse(raw) + if sound_evt: + inactive_zones[target].handle_event(sound_evt) + else: + async_dispatcher_send(hass, f"myhome_message_{mock_gateway.mac}", msg) + + await hass.async_block_till_done() + + # Active group remains playing and undisturbed + assert leader.state == MediaPlayerState.PLAYING + assert member.state == MediaPlayerState.PLAYING + assert leader.group_members == ["media_player.living_room", "media_player.kitchen"] + + # All 6 plant zones stayed strictly OFF with exact post-call restored volumes + for where, z in inactive_zones.items(): + assert z.state == MediaPlayerState.OFF, f"Zone {where} woke to {z.state}" + assert z.group_members is None + assert pool.get_leader(z.entity_id) is None + + assert inactive_zones["31"].volume_level == pytest.approx(0.0) + assert inactive_zones["41"].volume_level == pytest.approx(6 / 31.0) + assert inactive_zones["51"].volume_level == pytest.approx(6 / 31.0) + assert inactive_zones["61"].volume_level == pytest.approx(7 / 31.0) + assert inactive_zones["71"].volume_level == pytest.approx(1 / 31.0) + assert inactive_zones["81"].volume_level == pytest.approx(1 / 31.0) + + +@pytest.mark.asyncio +async def test_mh202_wall_controls_trace_zone_41_lifecycle(hass, mock_gateway): + """Replay the 146-frame physical wall switch ON, volume ramp, and OFF trace (#669). + + Validates: + 1. All 146 frames parse cleanly via OWNMessage.parse (dual-dialect WHO 22 + WHO 16). + 2. Matrix routing frame (*16*3*143##) sets Zone 41 source to Source 3 without + prematurely waking Zone 41 before the amplifier ON frame (*16*3*41##) arrives. + 3. Amplifier ON frame (*16*3*41##) wakes Zone 41 and auto-joins the active stream + on Source 3, while source activation (*16*3*103##) is ignored by zone entities. + 4. Physical wall rocker volume steps (*22*3#1*3#4#1## / *22*4#1*3#4#1##) paired with + WHO 16 volume reports (*#16*41*1*##) drive Zone 41 through the exact on-wire + volume trajectory: 6 -> 15 -> 5 -> 12 -> 7 (on the 0..31 scale). + 5. Physical wall switch OFF (*16*13*41##) turns Zone 41 OFF cleanly while all other + zones (31, 51, 61, 71, 81) remain OFF throughout the entire trace. + """ + assert WALL_CONTROLS_TRACE_PATH.is_file(), f"Missing trace fixture: {WALL_CONTROLS_TRACE_PATH}" + + with open(WALL_CONTROLS_TRACE_PATH, encoding="utf-8") as f: + trace_data = json.load(f) + + assert trace_data["gateway"]["model"] == "MH202" + assert trace_data["gateway"]["firmware"] == "1.0" + frames = trace_data["frames"] + assert len(frames) == 146 + + runtime = MyHOMERuntimeData(gateway=mock_gateway) + pool = DecoderPool(hass, {"media_player.streamer_s3": 3}) + runtime.decoder_pool = pool + hass.states.async_set("media_player.streamer_s3", MediaPlayerState.PLAYING) + + base_options = { + CONF_SOURCE_NAME.format(3): "Streamer 3", + CONF_SOURCE_DEFAULTS: {"3": 3, "4": 3, "5": 3, "6": 3, "7": 3, "8": 3}, + } + mock_gateway.config_entry = MagicMock(options=dict(base_options)) + + zones = {} + for where in ("31", "41", "51", "61", "71", "81"): + z = _create_test_zone(hass, mock_gateway, runtime, where, f"media_player.zone_{where}") + z._options = lambda: dict(base_options) + z._gateway_handler.config_entry.options = dict(base_options) + z._attr_state = MediaPlayerState.OFF + zones[where] = z + + zone41 = zones["41"] + observed_states: list[tuple[str, MediaPlayerState | None, float | None, str | None]] = [] + volume_trajectory_hw: list[int] = [] + + for entry in frames: + raw = entry["raw"] + msg = OWNMessage.parse(raw) + assert msg is not None, f"Failed to parse frame: {raw}" + + if isinstance(msg, OWNSoundEvent): + where = msg.where or "" + if where in zones: + zones[where].handle_event(msg) + if where == "41": + if msg.volume is not None: + volume_trajectory_hw.append(msg.volume) + observed_states.append((raw, zone41.state, zone41.volume_level, zone41.source)) + elif len(where) == 3 and where.startswith("1") and where[1] != "0": + # Matrix routing pseudo-zone 1ES (e.g. *16*3*143## routes environment 4 to source 3) + env = where[1] + for z_where, z_ent in zones.items(): + if z_where[0] == env: + z_ent.handle_event(msg) + if z_where == "41": + observed_states.append( + (raw, zone41.state, zone41.volume_level, zone41.source) + ) + elif getattr(msg, "is_source_event", False): + # Source power frame (*16*3*103##) must be ignored by zone entities + zone41.handle_event(msg) + + await hass.async_block_till_done() + + # 1. Verify *16*3*143## set source to "Streamer 3" while keeping Zone 41 OFF until *16*3*41## + routing_step = next(s for s in observed_states if s[0] == "*16*3*143##") + assert routing_step[1] == MediaPlayerState.OFF + assert routing_step[3] == "Streamer 3" + + # 2. Verify *16*3*41## turned Zone 41 ON + wake_step = next(s for s in observed_states if s[0] == "*16*3*41##") + assert wake_step[1] in (MediaPlayerState.ON, MediaPlayerState.PLAYING) + + # 3. Verify the exact hardware volume trajectory reported by *#16*41*1*## + # Starts at 6, ramps UP to peak 15, ramps DOWN to trough 5, ramps UP to 12, ramps DOWN to 7 + assert volume_trajectory_hw[0] == 6 + assert max(volume_trajectory_hw) == 15 + assert min(volume_trajectory_hw) == 5 + assert volume_trajectory_hw[-1] == 7 + assert volume_trajectory_hw == [ + 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, + 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, + 6, 7, 8, 9, 10, 11, 12, + 11, 10, 9, 8, 7, + ] + + # 4. Verify *16*13*41## turned Zone 41 back OFF at final volume 7/31 + assert zone41.state == MediaPlayerState.OFF + assert zone41.volume_level == pytest.approx(7 / 31.0) + assert zone41.source == "Streamer 3" + + # 5. All other zones remained strictly OFF throughout + for where in ("31", "51", "61", "71", "81"): + assert zones[where].state == MediaPlayerState.OFF + + From dbcfacc03ef2adba744f3e898a733e0e84705507 Mon Sep 17 00:00:00 2001 From: Laurens <43173895+GreenGrassBlueOcean@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:02:54 +0200 Subject: [PATCH 2/2] test(media_player,intercom): replay Issue #669 traces through PlatformDiscovery, FrameRouter, and who22_tuner_listener --- .../mh202_sound_intercom_ducking/README.md | 7 +- tests/test_intercom_ducking_trace_replay.py | 325 ++++++++++++------ 2 files changed, 229 insertions(+), 103 deletions(-) diff --git a/tests/fixtures/traces/mh202_sound_intercom_ducking/README.md b/tests/fixtures/traces/mh202_sound_intercom_ducking/README.md index abd73a402..9d8786c2e 100644 --- a/tests/fixtures/traces/mh202_sound_intercom_ducking/README.md +++ b/tests/fixtures/traces/mh202_sound_intercom_ducking/README.md @@ -27,18 +27,17 @@ Authentic passive bus traces captured on a physical plant with an MH202 gateway, - ~717 ms later, lock relay pulse release frames `*6*24*1##` (`WHAT=24` at `1`) and `*6*23*4000##` (`WHAT=23` at `4000`). 4. **Call End, Dry Contact & Volume Restoration (`10:27:30.852Z`–`10:27:33.268Z`)**: - `*6*9**##`: Camera OFF (`WHO=6, WHAT=9, WHERE=*`). - - `*25*32#1*31##`: Dry contact / CEN+ confirmation (`WHO=25, WHAT=32#1, WHERE=31`). + - `*25*32#1*31##`: Dry-contact / IR sensor OFF event (`WHO=25, WHAT=32#1` where `32` = contact OFF / IR not detected and `1` = unsolicited event, `WHERE=31`, `is_on=False`). - Matrix restores pre-call amplifier volumes (`31` -> `0`, `41` -> `6`, `61` -> `7`, `51` -> `6`; `71` and `81` remain at `1`). ### 3. `myhome_trace_MH202_all_2026-10-18T10-34-13.json` (146 frames) - **Scenario**: User operates the physical wall switch in Zone `41` (Area 4, Point 1): turns amplifier ON -> ramps volume UP and DOWN via physical wall rocker -> turns amplifier OFF. - **On-Wire Sequence**: 1. **Dual-Dialect Power-On & Matrix Routing (`10:33:32.011Z`–`10:33:35.201Z`)**: - - WHO 22 commands (`*22*0#4#4*6##`, `*22*1#4#4*2#3##`, `*22*21#4#4*5#2#3##`, `*22*1#4#4*3#4#1##`, `*22*2#4#4*5#2#3##`) interleaved with WHO 16 matrix routing `*16*3*143##` (Environment `4` -> Source `3`), amplifier ON `*16*3*41##`, and source ON `*16*3*103##`. + - WHO 22 commands (`*22*0#4#4*6##` All Sources `6` OFF in Stereo Area 4, `*22*1#4#4*2#3##` Source `2#3` ON in Stereo Area 4, `*22*21#4#4*5#2#3##` and `*22*2#4#4*5#2#3##` General Source `5#2#3` activation in Area 4, `*22*1#4#4*3#4#1##` Speaker `3#4#1` ON in Stereo Area 4) interleaved with WHO 16 matrix routing `*16*3*143##` (Environment `4` -> Source `3`), amplifier ON `*16*3*41##`, and source ON `*16*3*103##`. - WHO 22 Dimension 12 status frames (`*#22*3#4#1*12*1*4##`, `*#22*2#3*12*1*4##`) and full DSP status dump on `3#4#1` (Dim 1 Volume=`6`, Dim 2 High=`30`, Dim 4 Low=`30`, Dim 17 Balance=`10`, Dim 20 Loudness=`1`, Dim 18 3D=`0`, Dim 19 Preset=`3`). 2. **Wall Rocker Volume Up & Down (`10:33:34.822Z`–`10:33:50.997Z`)**: - Wall control sends WHO 22 step commands `*22*3#1*3#4#1##` (Volume UP +1) and `*22*4#1*3#4#1##` (Volume DOWN -1). - - Amplifier broadcasts paired `*#16*41*1*##` and `*#22*3#4#1*1*##` volume reports (`6 -> 15 -> 5 -> 12 -> 7`). + - Gateway broadcasts paired `*#16*41*1*##` and `*#22*3#4#1*1*##` volume reports (`6 -> 15 -> 5 -> 12 -> 7`). 3. **Wall Power-Off (`10:33:51.905Z`–`10:33:52.610Z`)**: - `*22*0#4#4*3#4#1##` (WHO 22 Speaker `3#4#1` OFF), `*16*13*41##` (WHO 16 Amplifier `41` OFF), and `*#22*3#4#1*12*0*10##` (WHO 22 Dim 12 standby state `0`). - diff --git a/tests/test_intercom_ducking_trace_replay.py b/tests/test_intercom_ducking_trace_replay.py index 039b26b44..bc18b101c 100644 --- a/tests/test_intercom_ducking_trace_replay.py +++ b/tests/test_intercom_ducking_trace_replay.py @@ -13,12 +13,21 @@ import pytest from homeassistant.components.media_player.const import MediaPlayerState -from homeassistant.helpers.dispatcher import async_dispatcher_send -from OWNd.message import OWNMessage, OWNSoundEvent +from homeassistant.const import CONF_MAC, Platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect, async_dispatcher_send +from OWNd.message import OWNAuxEvent, OWNDryContactEvent, OWNMessage, OWNSoundEvent from custom_components.myhome.const import CONF_SOURCE_DEFAULTS, CONF_SOURCE_NAME from custom_components.myhome.data import MyHOMERuntimeData from custom_components.myhome.decoder_pool import DecoderPool +from custom_components.myhome.discovery import PlatformDiscovery +from custom_components.myhome.media_player import _sound_route_keys, _zone_address +from custom_components.myhome.media_player_routing import route_pseudo_zones +from custom_components.myhome.sound_source import ( + MyHOMESoundSource, + who22_source_id, + who22_tuner_listener, +) from tests.test_component_media_player import _create_test_zone FIXTURE_PATH = ( @@ -299,11 +308,13 @@ async def test_mh202_visitor_doorbell_call_trace_replay(hass, mock_gateway): - 4-component WHO 8 call initiation (*8*1#1#4#20*11##) and caller ID (*8*9#1#4*20##) - WHO 6 door lock release in conversation (*6*22*1##, *6*10*4000##) and pulse release (*6*24*1##, *6*23*4000##) - - WHO 6 camera OFF (*6*9**##) and WHO 25 dry-contact confirmation (*25*32#1*31##) + - WHO 6 camera OFF (*6*9**##), WHO 25 dry-contact OFF event (*25*32#1*31##), + and WHO 9 auxiliary events (*9*1*6##, *9*0*6##) 2. All 6 powered-off amplifiers (31, 41, 51, 61, 71, 81) remain strictly OFF throughout ducking and restoration while an independent group (21 + 22) streams. 3. Restored amplifier volumes match the on-wire post-call values - (31->0, 41->6, 51->6, 61->7, 71->1, 81->1). + (31->0, 41->6, 51->6, 61->7, 71->1, 81->1) and no phantom media_player + entities are discovered from non-WHO 16 or WHO 22 frames. """ assert DOORBELL_TRACE_PATH.is_file(), f"Missing trace fixture: {DOORBELL_TRACE_PATH}" @@ -323,7 +334,7 @@ async def test_mh202_visitor_doorbell_call_trace_replay(hass, mock_gateway): assert msg is not None, f"Failed to parse frame: {raw}" parsed_frames.append((raw, msg)) - # Spot-check key WHO 8 / WHO 6 / WHO 25 frames in the visitor call sequence + # Spot-check key WHO 8 / WHO 6 / WHO 25 / WHO 9 frames in the visitor call sequence by_raw = dict(parsed_frames) call_msg = by_raw["*8*1#1#4#20*11##"] @@ -371,10 +382,23 @@ async def test_mh202_visitor_doorbell_call_trace_replay(hass, mock_gateway): assert cam_off_msg.where == "*" dry_contact_msg = by_raw["*25*32#1*31##"] + assert isinstance(dry_contact_msg, OWNDryContactEvent) assert dry_contact_msg.who == 25 assert dry_contact_msg.where == "31" + assert dry_contact_msg.is_on is False + + aux_on_msg = by_raw["*9*1*6##"] + assert isinstance(aux_on_msg, OWNAuxEvent) + assert aux_on_msg.where == "6" + assert aux_on_msg.is_on is True + + aux_off_msg = by_raw["*9*0*6##"] + assert isinstance(aux_off_msg, OWNAuxEvent) + assert aux_off_msg.where == "6" + assert aux_off_msg.is_on is False # Replay across active streaming group (21 + 22) and 6 OFF zones (31..81) + # through the real PlatformDiscovery + FrameRouter pipeline. runtime = MyHOMERuntimeData(gateway=mock_gateway) pool = DecoderPool(hass, {"media_player.streamer": 2}) runtime.decoder_pool = pool @@ -384,7 +408,15 @@ async def test_mh202_visitor_doorbell_call_trace_replay(hass, mock_gateway): CONF_SOURCE_NAME.format(2): "Cambridge", CONF_SOURCE_DEFAULTS: {"2": 2, "3": 2, "4": 2, "5": 2, "6": 2, "7": 2, "8": 2}, } - mock_gateway.config_entry = MagicMock(options=dict(base_options)) + unload_cbs = [] + config_entry = MagicMock( + entry_id="test_doorbell_trace_entry", + data={CONF_MAC: mock_gateway.mac}, + options=dict(base_options), + runtime_data=runtime, + ) + config_entry.async_on_unload.side_effect = unload_cbs.append + mock_gateway.config_entry = config_entry leader = _create_test_zone(hass, mock_gateway, runtime, "21", "media_player.living_room") member = _create_test_zone(hass, mock_gateway, runtime, "22", "media_player.kitchen") @@ -406,50 +438,86 @@ async def test_mh202_visitor_doorbell_call_trace_replay(hass, mock_gateway): z._attr_state = MediaPlayerState.OFF inactive_zones[where] = z - for raw, msg in parsed_frames: - target = _resolve_where(raw) - if target and target in inactive_zones: - sound_evt = OWNSoundEvent.parse(raw) - if sound_evt: - inactive_zones[target].handle_event(sound_evt) - else: + added_entities: list = [] + discovery = PlatformDiscovery( + hass, + config_entry, + added_entities.extend, + platform=Platform.MEDIA_PLAYER, + who="16", + event_type=OWNSoundEvent, + build=lambda ctx: None, + address=_zone_address, + pre_message=route_pseudo_zones(runtime.router), + route_keys=_sound_route_keys, + key_suffix="#16", + ) + for where, z in {"21": leader, "22": member, **inactive_zones}.items(): + key = f"{where}#16" + discovery.known.add(key) + unload_cbs.append(runtime.router.subscribe("16", [key], z.handle_event)) + discovery.listen() + + try: + for raw, msg in parsed_frames: async_dispatcher_send(hass, f"myhome_message_{mock_gateway.mac}", msg) - - await hass.async_block_till_done() - - # Active group remains playing and undisturbed - assert leader.state == MediaPlayerState.PLAYING - assert member.state == MediaPlayerState.PLAYING - assert leader.group_members == ["media_player.living_room", "media_player.kitchen"] - - # All 6 plant zones stayed strictly OFF with exact post-call restored volumes - for where, z in inactive_zones.items(): - assert z.state == MediaPlayerState.OFF, f"Zone {where} woke to {z.state}" - assert z.group_members is None - assert pool.get_leader(z.entity_id) is None - - assert inactive_zones["31"].volume_level == pytest.approx(0.0) - assert inactive_zones["41"].volume_level == pytest.approx(6 / 31.0) - assert inactive_zones["51"].volume_level == pytest.approx(6 / 31.0) - assert inactive_zones["61"].volume_level == pytest.approx(7 / 31.0) - assert inactive_zones["71"].volume_level == pytest.approx(1 / 31.0) - assert inactive_zones["81"].volume_level == pytest.approx(1 / 31.0) + # Also feed paired WHO 22 volume frames directly to verify OWNSoundEvent.parse + # resilience if a WHO 22 Dimension 1 frame reaches handle_event. + if raw.startswith("*#22*3#"): + target = _resolve_where(raw) + if target and target in inactive_zones: + who22_evt = OWNSoundEvent.parse(raw) + if who22_evt: + inactive_zones[target].handle_event(who22_evt) + + await hass.async_block_till_done() + + # No phantom media_player entities were discovered + assert added_entities == [] + + # Active group remains playing and undisturbed + assert leader.state == MediaPlayerState.PLAYING + assert member.state == MediaPlayerState.PLAYING + assert leader.group_members == ["media_player.living_room", "media_player.kitchen"] + + # All 6 plant zones stayed strictly OFF with exact post-call restored volumes + for where, z in inactive_zones.items(): + assert z.state == MediaPlayerState.OFF, f"Zone {where} woke to {z.state}" + assert z.group_members is None + assert pool.get_leader(z.entity_id) is None + + assert inactive_zones["31"].volume_level == pytest.approx(0.0) + assert inactive_zones["41"].volume_level == pytest.approx(6 / 31.0) + assert inactive_zones["51"].volume_level == pytest.approx(6 / 31.0) + assert inactive_zones["61"].volume_level == pytest.approx(7 / 31.0) + assert inactive_zones["71"].volume_level == pytest.approx(1 / 31.0) + assert inactive_zones["81"].volume_level == pytest.approx(1 / 31.0) + finally: + for unsub in unload_cbs: + unsub() @pytest.mark.asyncio async def test_mh202_wall_controls_trace_zone_41_lifecycle(hass, mock_gateway): """Replay the 146-frame physical wall switch ON, volume ramp, and OFF trace (#669). - Validates: + Validates through the real PlatformDiscovery + FrameRouter + route_pseudo_zones + + MyHOMESoundSource + who22_tuner_listener pipeline: 1. All 146 frames parse cleanly via OWNMessage.parse (dual-dialect WHO 22 + WHO 16). - 2. Matrix routing frame (*16*3*143##) sets Zone 41 source to Source 3 without - prematurely waking Zone 41 before the amplifier ON frame (*16*3*41##) arrives. + 2. Matrix routing frame (*16*3*143##) is routed via route_pseudo_zones to Zone 41, + setting Zone 41 source to Source 3 ("Streamer 3") without prematurely waking + Zone 41 before the amplifier ON frame (*16*3*41##) arrives, and without + discovering a phantom entity for "143". 3. Amplifier ON frame (*16*3*41##) wakes Zone 41 and auto-joins the active stream - on Source 3, while source activation (*16*3*103##) is ignored by zone entities. - 4. Physical wall rocker volume steps (*22*3#1*3#4#1## / *22*4#1*3#4#1##) paired with + on Source 3, while source activation (*16*3*103##) turns on the declared + MyHOMESoundSource ("103#16") via _sound_route_keys and is ignored by zone entities. + 4. WHO 22 source frames (*22*1#4#4*2#3##, *22*21#4#4*5#2#3##, *22*2#4#4*5#2#3##, + *#22*2#3*12*1*4##) resolve who22_source_id == 3 and are handled cleanly by + who22_tuner_listener without creating phantom entities. + 5. Physical wall rocker volume steps (*22*3#1*3#4#1## / *22*4#1*3#4#1##) paired with WHO 16 volume reports (*#16*41*1*##) drive Zone 41 through the exact on-wire volume trajectory: 6 -> 15 -> 5 -> 12 -> 7 (on the 0..31 scale). - 5. Physical wall switch OFF (*16*13*41##) turns Zone 41 OFF cleanly while all other + 6. Physical wall switch OFF (*16*13*41##) turns Zone 41 OFF cleanly while all other zones (31, 51, 61, 71, 81) remain OFF throughout the entire trace. """ assert WALL_CONTROLS_TRACE_PATH.is_file(), f"Missing trace fixture: {WALL_CONTROLS_TRACE_PATH}" @@ -471,7 +539,15 @@ async def test_mh202_wall_controls_trace_zone_41_lifecycle(hass, mock_gateway): CONF_SOURCE_NAME.format(3): "Streamer 3", CONF_SOURCE_DEFAULTS: {"3": 3, "4": 3, "5": 3, "6": 3, "7": 3, "8": 3}, } - mock_gateway.config_entry = MagicMock(options=dict(base_options)) + unload_cbs = [] + config_entry = MagicMock( + entry_id="test_wall_controls_trace_entry", + data={CONF_MAC: mock_gateway.mac}, + options=dict(base_options), + runtime_data=runtime, + ) + config_entry.async_on_unload.side_effect = unload_cbs.append + mock_gateway.config_entry = config_entry zones = {} for where in ("31", "41", "51", "61", "71", "81"): @@ -481,69 +557,120 @@ async def test_mh202_wall_controls_trace_zone_41_lifecycle(hass, mock_gateway): z._attr_state = MediaPlayerState.OFF zones[where] = z + # Also register a declared MyHOMESoundSource for Source 3 (WHERE=103) + source_103 = MyHOMESoundSource( + hass=hass, + name="Streamer 3", + device_id="103#16", + who="16", + where="103", + manufacturer="BTicino", + model="Audio Source", + gateway=mock_gateway, + ) + source_103.entity_id = "media_player.audio_source_3" + source_103._publish_state = MagicMock() + + added_entities: list = [] + discovery = PlatformDiscovery( + hass, + config_entry, + added_entities.extend, + platform=Platform.MEDIA_PLAYER, + who="16", + event_type=OWNSoundEvent, + build=lambda ctx: None, + address=_zone_address, + pre_message=route_pseudo_zones(runtime.router), + route_keys=_sound_route_keys, + key_suffix="#16", + ) + for where, z in zones.items(): + key = f"{where}#16" + discovery.known.add(key) + unload_cbs.append(runtime.router.subscribe("16", [key], z.handle_event)) + + unload_cbs.append( + runtime.router.subscribe("16", [source_103.device_key], source_103.handle_event) + ) + unload_cbs.append( + async_dispatcher_connect( + hass, + f"myhome_message_{mock_gateway.mac}", + who22_tuner_listener([source_103]), + ) + ) + discovery.listen() + zone41 = zones["41"] observed_states: list[tuple[str, MediaPlayerState | None, float | None, str | None]] = [] volume_trajectory_hw: list[int] = [] + who22_source_frames: list[str] = [] - for entry in frames: - raw = entry["raw"] - msg = OWNMessage.parse(raw) - assert msg is not None, f"Failed to parse frame: {raw}" + try: + for entry in frames: + raw = entry["raw"] + msg = OWNMessage.parse(raw) + assert msg is not None, f"Failed to parse frame: {raw}" - if isinstance(msg, OWNSoundEvent): - where = msg.where or "" - if where in zones: - zones[where].handle_event(msg) - if where == "41": - if msg.volume is not None: - volume_trajectory_hw.append(msg.volume) - observed_states.append((raw, zone41.state, zone41.volume_level, zone41.source)) - elif len(where) == 3 and where.startswith("1") and where[1] != "0": - # Matrix routing pseudo-zone 1ES (e.g. *16*3*143## routes environment 4 to source 3) - env = where[1] - for z_where, z_ent in zones.items(): - if z_where[0] == env: - z_ent.handle_event(msg) - if z_where == "41": - observed_states.append( - (raw, zone41.state, zone41.volume_level, zone41.source) - ) - elif getattr(msg, "is_source_event", False): - # Source power frame (*16*3*103##) must be ignored by zone entities - zone41.handle_event(msg) - - await hass.async_block_till_done() - - # 1. Verify *16*3*143## set source to "Streamer 3" while keeping Zone 41 OFF until *16*3*41## - routing_step = next(s for s in observed_states if s[0] == "*16*3*143##") - assert routing_step[1] == MediaPlayerState.OFF - assert routing_step[3] == "Streamer 3" - - # 2. Verify *16*3*41## turned Zone 41 ON - wake_step = next(s for s in observed_states if s[0] == "*16*3*41##") - assert wake_step[1] in (MediaPlayerState.ON, MediaPlayerState.PLAYING) - - # 3. Verify the exact hardware volume trajectory reported by *#16*41*1*## - # Starts at 6, ramps UP to peak 15, ramps DOWN to trough 5, ramps UP to 12, ramps DOWN to 7 - assert volume_trajectory_hw[0] == 6 - assert max(volume_trajectory_hw) == 15 - assert min(volume_trajectory_hw) == 5 - assert volume_trajectory_hw[-1] == 7 - assert volume_trajectory_hw == [ - 6, - 7, 8, 9, 10, 11, 12, 13, 14, 15, - 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, - 6, 7, 8, 9, 10, 11, 12, - 11, 10, 9, 8, 7, - ] - - # 4. Verify *16*13*41## turned Zone 41 back OFF at final volume 7/31 - assert zone41.state == MediaPlayerState.OFF - assert zone41.volume_level == pytest.approx(7 / 31.0) - assert zone41.source == "Streamer 3" - - # 5. All other zones remained strictly OFF throughout - for where in ("31", "51", "61", "71", "81"): - assert zones[where].state == MediaPlayerState.OFF + if who22_source_id(msg) == 3: + who22_source_frames.append(raw) + async_dispatcher_send(hass, f"myhome_message_{mock_gateway.mac}", msg) + if isinstance(msg, OWNSoundEvent): + where = msg.where or "" + if where == "41" and msg.volume is not None: + volume_trajectory_hw.append(msg.volume) + if where in ("41", "143", "103"): + observed_states.append( + (raw, zone41.state, zone41.volume_level, zone41.source) + ) + + await hass.async_block_till_done() + + # 0. No phantom entities created for 143, 103, or WHO 22 addresses + assert added_entities == [] + + # 1. Verify *16*3*143## was routed by route_pseudo_zones to Zone 41, setting + # source to "Streamer 3" while keeping Zone 41 OFF until *16*3*41## + routing_step = next(s for s in observed_states if s[0] == "*16*3*143##") + assert routing_step[1] == MediaPlayerState.OFF + assert routing_step[3] == "Streamer 3" + + # 2. Verify *16*3*41## turned Zone 41 ON and *16*3*103## turned Source 103 ON + wake_step = next(s for s in observed_states if s[0] == "*16*3*41##") + assert wake_step[1] in (MediaPlayerState.ON, MediaPlayerState.PLAYING) + assert source_103.state == MediaPlayerState.ON + assert who22_source_frames == [ + "*22*1#4#4*2#3##", + "*22*21#4#4*5#2#3##", + "*22*2#4#4*5#2#3##", + "*#22*2#3*12*1*4##", + ] + + # 3. Verify the exact hardware volume trajectory reported by *#16*41*1*## + # Starts at 6, ramps UP to peak 15, ramps DOWN to trough 5, ramps UP to 12, ramps DOWN to 7 + assert volume_trajectory_hw[0] == 6 + assert max(volume_trajectory_hw) == 15 + assert min(volume_trajectory_hw) == 5 + assert volume_trajectory_hw[-1] == 7 + assert volume_trajectory_hw == [ + 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, + 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, + 6, 7, 8, 9, 10, 11, 12, + 11, 10, 9, 8, 7, + ] + + # 4. Verify *16*13*41## turned Zone 41 back OFF at final volume 7/31 + assert zone41.state == MediaPlayerState.OFF + assert zone41.volume_level == pytest.approx(7 / 31.0) + assert zone41.source == "Streamer 3" + + # 5. All other zones remained strictly OFF throughout + for where in ("31", "51", "61", "71", "81"): + assert zones[where].state == MediaPlayerState.OFF + finally: + for unsub in unload_cbs: + unsub()