diff --git a/README.md b/README.md index ccac9631..25958bdf 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ We now maintain a comprehensive, community-curated **[GitHub Wiki](https://githu | **MH200N** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | ✅ | | **MH201** | ✅ | ✅ | ✅ | ✅ | | | ✅ | ✅ | ✅ | ✅ | | ✅ | | ✅ | | | | | **MH202** | | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | | | | ✅ | | ✅ | | ✅ | | -| **MyHomeServer1** | | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | | ✅ | | ✅ | | ✅ | ✅ | ✅ | | +| **MyHomeServer1** | | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | | ✅ | ✅ | ✅ | | *Checkmarks (✅) indicate that at least one `diagnostic_summary.json` or `.txt` bus capture in our test corpus contains frames for that subsystem from the specified gateway model. This matrix is automatically updated from the fixtures repository.* diff --git a/docs/trace-availability.md b/docs/trace-availability.md index bfceec63..06a53600 100644 --- a/docs/trace-availability.md +++ b/docs/trace-availability.md @@ -12,7 +12,7 @@ This matrix documents the real-world physical gateways for which we have capture | **MH200N** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | ✅ | | **MH201** | ✅ | ✅ | ✅ | ✅ | | | ✅ | ✅ | ✅ | ✅ | | ✅ | | ✅ | | | | | **MH202** | | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | | | | ✅ | | ✅ | | ✅ | | -| **MyHomeServer1** | | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | | ✅ | | ✅ | | ✅ | ✅ | ✅ | | +| **MyHomeServer1** | | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ | | ✅ | ✅ | ✅ | | *Checkmarks (✅) indicate that at least one `diagnostic_summary.json` or `.txt` bus capture in our test corpus contains frames for that subsystem from the specified gateway model.* diff --git a/tests/fixtures/traces/issue_541/README.md b/tests/fixtures/traces/issue_541/README.md new file mode 100644 index 00000000..174da518 --- /dev/null +++ b/tests/fixtures/traces/issue_541/README.md @@ -0,0 +1,18 @@ +# #541 Classic CEN (WHO 15) short press on a shared MH200N + MyHomeServer1 bus + +Authentic on-wire bus traces contributed by **@wave68runner** on +[#541 (comment 5894041457)](https://github.com/OpenWebNet-HA/MyHOME/issues/541#issuecomment-5894041457). +Both gateways sit on the same SCS bus, so each captured the same physical button press. + +## Contributed Files + +| File | Gateway | Frames | Description | +|---|---|---|---| +| `myhome_trace_MH200N_cen_short_press_2026-09-29T16-03-43.json` | MH200N (fw 1.0, SSDP) | 3 | Classic CEN press + release-after-short-press, plus one gateway clock frame. | +| `myhome_trace_MyHomeServer1_cen_short_press_2026-09-29T16-03-55.json` | MyHomeServer1 | 56 | The same press + release, surrounded by the `*#13**#0` / `*#13**#1` clock frames this gateway emits every second. | + +## Empirical Protocol Discoveries + +- One short press of CEN button 1 on object 73 is `*15*01*73##` (pressed) followed by `*15*01#1*73##` + (released after short press). The two-digit `01` WHAT is parsed as pushbutton 1. +- Fills the **MyHomeServer1 / WHO 15** cell of the trace matrix. diff --git a/tests/fixtures/traces/issue_541/myhome_trace_MH200N_cen_short_press_2026-09-29T16-03-43.json b/tests/fixtures/traces/issue_541/myhome_trace_MH200N_cen_short_press_2026-09-29T16-03-43.json new file mode 100644 index 00000000..e7a7a7ed --- /dev/null +++ b/tests/fixtures/traces/issue_541/myhome_trace_MH200N_cen_short_press_2026-09-29T16-03-43.json @@ -0,0 +1,100 @@ +{ + "capture": { + "kind": "trace", + "started_at": "2026-09-29T16:03:31.191Z", + "filters": { + "who": null, + "where": null, + "direction": null + }, + "window": { + "first": "2026-09-29T16:03:33.511Z", + "last": "2026-09-29T16:03:33.636Z", + "frames": 3, + "buffer_frames": 3, + "buffer_depth": 200, + "truncated": false + } + }, + "environment": { + "home_assistant_version": "2026.9.3", + "integration_version": "2.0.0b13", + "ownd_version": "2.0.0b8", + "exported_at": "2026-09-29T16:03:43.528Z" + }, + "gateway": { + "model": "MH200N", + "manufacturer": "BTicino S.p.A.", + "firmware": "1.0", + "mac_prefix": "00:03:50", + "connection_type": "tcp", + "queue_pacing": 0.15, + "is_connected": true, + "identification": { + "model": "MH200N", + "source": "ssdp", + "configured_model": "MH200N", + "ssdp_model": "MH200N", + "who13_code": 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": "MH200NProfile", + "conflict": null + } + }, + "telemetry": { + "total_rx": 3, + "total_tx": 0, + "captured_in_buffer": 3, + "buffer_depth": 200, + "queue_depth": 0 + }, + "frames": [ + { + "timestamp": 1790697813.511285, + "iso_time": "2026-09-29T16:03:33.511285+00:00", + "direction": "rx", + "raw": "*15*01*73##", + "who": "15", + "what": "1", + "where": "73", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697813.620138, + "iso_time": "2026-09-29T16:03:33.620138+00:00", + "direction": "rx", + "raw": "*#13**22*18*03*32*001*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "22", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697813.636738, + "iso_time": "2026-09-29T16:03:33.636738+00:00", + "direction": "rx", + "raw": "*15*01#1*73##", + "who": "15", + "what": "1", + "where": "73", + "dimension": null, + "is_ack": false, + "is_nack": false + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/traces/issue_541/myhome_trace_MyHomeServer1_cen_short_press_2026-09-29T16-03-55.json b/tests/fixtures/traces/issue_541/myhome_trace_MyHomeServer1_cen_short_press_2026-09-29T16-03-55.json new file mode 100644 index 00000000..4c1c33a7 --- /dev/null +++ b/tests/fixtures/traces/issue_541/myhome_trace_MyHomeServer1_cen_short_press_2026-09-29T16-03-55.json @@ -0,0 +1,736 @@ +{ + "capture": { + "kind": "trace", + "started_at": "2026-09-29T16:03:29.137Z", + "filters": { + "who": null, + "where": null, + "direction": null + }, + "window": { + "first": "2026-09-29T16:03:29.942Z", + "last": "2026-09-29T16:03:55.264Z", + "frames": 56, + "buffer_frames": 56, + "buffer_depth": 200, + "truncated": false + } + }, + "environment": { + "home_assistant_version": "2026.9.3", + "integration_version": "2.0.0b13", + "ownd_version": "2.0.0b8", + "exported_at": "2026-09-29T16:03:55.924Z" + }, + "gateway": { + "model": "MyHomeServer1", + "manufacturer": "BTicino S.p.A.", + "firmware": "Unknown", + "mac_prefix": "00:03:50", + "connection_type": "tcp", + "queue_pacing": 0.02, + "is_connected": true, + "identification": { + "model": "MyHomeServer1", + "source": "manual", + "configured_model": "MyHomeServer1", + "ssdp_model": null, + "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": "MyHomeServer1Profile", + "conflict": null + } + }, + "telemetry": { + "total_rx": 56, + "total_tx": 0, + "captured_in_buffer": 56, + "buffer_depth": 200, + "queue_depth": 0 + }, + "frames": [ + { + "timestamp": 1790697809.9423, + "iso_time": "2026-09-29T16:03:29.942300+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697809.945597, + "iso_time": "2026-09-29T16:03:29.945597+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*29*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697810.562147, + "iso_time": "2026-09-29T16:03:30.562147+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697810.565607, + "iso_time": "2026-09-29T16:03:30.565607+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*30*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697811.185587, + "iso_time": "2026-09-29T16:03:31.185587+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697811.189107, + "iso_time": "2026-09-29T16:03:31.189107+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*31*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697812.949847, + "iso_time": "2026-09-29T16:03:32.949847+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697812.953563, + "iso_time": "2026-09-29T16:03:32.953563+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*32*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697813.524718, + "iso_time": "2026-09-29T16:03:33.524718+00:00", + "direction": "rx", + "raw": "*15*01*73##", + "who": "15", + "what": "1", + "where": "73", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697813.529288, + "iso_time": "2026-09-29T16:03:33.529288+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697813.532603, + "iso_time": "2026-09-29T16:03:33.532603+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*33*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697813.830884, + "iso_time": "2026-09-29T16:03:33.830884+00:00", + "direction": "rx", + "raw": "*15*01#1*73##", + "who": "15", + "what": "1", + "where": "73", + "dimension": null, + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697814.183263, + "iso_time": "2026-09-29T16:03:34.183263+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697814.191609, + "iso_time": "2026-09-29T16:03:34.191609+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*34*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697815.964277, + "iso_time": "2026-09-29T16:03:35.964277+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697815.967576, + "iso_time": "2026-09-29T16:03:35.967576+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*35*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697816.583399, + "iso_time": "2026-09-29T16:03:36.583399+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697816.587168, + "iso_time": "2026-09-29T16:03:36.587168+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*36*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697817.204788, + "iso_time": "2026-09-29T16:03:37.204788+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697817.211801, + "iso_time": "2026-09-29T16:03:37.211801+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*37*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697818.974377, + "iso_time": "2026-09-29T16:03:38.974377+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697818.977639, + "iso_time": "2026-09-29T16:03:38.977639+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*38*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697819.593431, + "iso_time": "2026-09-29T16:03:39.593431+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697819.600958, + "iso_time": "2026-09-29T16:03:39.600958+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*39*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697820.211731, + "iso_time": "2026-09-29T16:03:40.211731+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697820.215066, + "iso_time": "2026-09-29T16:03:40.215066+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*40*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697821.982303, + "iso_time": "2026-09-29T16:03:41.982303+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697821.985591, + "iso_time": "2026-09-29T16:03:41.985591+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*41*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697822.604935, + "iso_time": "2026-09-29T16:03:42.604935+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697822.608365, + "iso_time": "2026-09-29T16:03:42.608365+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*42*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697823.227152, + "iso_time": "2026-09-29T16:03:43.227152+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697823.230996, + "iso_time": "2026-09-29T16:03:43.230996+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*43*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697824.989288, + "iso_time": "2026-09-29T16:03:44.989288+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697824.992639, + "iso_time": "2026-09-29T16:03:44.992639+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*44*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697825.607886, + "iso_time": "2026-09-29T16:03:45.607886+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697825.611669, + "iso_time": "2026-09-29T16:03:45.611669+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*45*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697826.230546, + "iso_time": "2026-09-29T16:03:46.230546+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697826.233834, + "iso_time": "2026-09-29T16:03:46.233834+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*46*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697827.996094, + "iso_time": "2026-09-29T16:03:47.996094+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697827.999614, + "iso_time": "2026-09-29T16:03:47.999614+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*47*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697828.615158, + "iso_time": "2026-09-29T16:03:48.615158+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697828.618701, + "iso_time": "2026-09-29T16:03:48.618701+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*48*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697829.237198, + "iso_time": "2026-09-29T16:03:49.237198+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697829.240655, + "iso_time": "2026-09-29T16:03:49.240655+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*49*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697831.00311, + "iso_time": "2026-09-29T16:03:51.003110+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697831.006763, + "iso_time": "2026-09-29T16:03:51.006763+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*50*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697831.618195, + "iso_time": "2026-09-29T16:03:51.618195+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697831.625724, + "iso_time": "2026-09-29T16:03:51.625724+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*51*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697832.244393, + "iso_time": "2026-09-29T16:03:52.244393+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697832.24771, + "iso_time": "2026-09-29T16:03:52.247710+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*52*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697834.019503, + "iso_time": "2026-09-29T16:03:54.019503+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697834.022821, + "iso_time": "2026-09-29T16:03:54.022821+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*53*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697834.639177, + "iso_time": "2026-09-29T16:03:54.639177+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697834.642433, + "iso_time": "2026-09-29T16:03:54.642433+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*54*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697835.261546, + "iso_time": "2026-09-29T16:03:55.261546+00:00", + "direction": "rx", + "raw": "*#13**#1*02*29*09*2026##", + "who": "13", + "what": null, + "where": "", + "dimension": "1", + "is_ack": false, + "is_nack": false + }, + { + "timestamp": 1790697835.26483, + "iso_time": "2026-09-29T16:03:55.264830+00:00", + "direction": "rx", + "raw": "*#13**#0*18*03*55*##", + "who": "13", + "what": null, + "where": "", + "dimension": "0", + "is_ack": false, + "is_nack": false + } + ] +} \ No newline at end of file diff --git a/tests/test_issue_541_cen_trace.py b/tests/test_issue_541_cen_trace.py new file mode 100644 index 00000000..791e127d --- /dev/null +++ b/tests/test_issue_541_cen_trace.py @@ -0,0 +1,109 @@ +"""Tests for #541: classic CEN (WHO 15) short press replayed from authentic traces. + +Traces contributed by @wave68runner on a shared MH200N + MyHomeServer1 bus. +""" + +from __future__ import annotations + +import asyncio +import json +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from homeassistant.const import ( + CONF_FRIENDLY_NAME, + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, +) +from OWNd.message import OWNCENEvent, OWNMessage + +from custom_components.myhome.const import ( + CONF_DEVICE_TYPE, + CONF_FIRMWARE, + CONF_MANUFACTURER, + CONF_MANUFACTURER_URL, + CONF_SHORT_PRESS, + CONF_SHORT_RELEASE, + CONF_SSDP_LOCATION, + CONF_SSDP_ST, + CONF_UDN, +) +from custom_components.myhome.gateway import MyHOMEGatewayHandler + +TRACES_DIR = Path(__file__).resolve().parent / "fixtures" / "traces" / "issue_541" +TRACE_FILES = sorted(TRACES_DIR.glob("myhome_trace_*.json")) + + +def _cen_frames(trace_file: Path) -> list[str]: + data = json.loads(trace_file.read_text(encoding="utf-8")) + return [f["raw"] for f in data["frames"] if f["raw"].startswith("*15*")] + + +@pytest.fixture +def gateway_handler() -> MyHOMEGatewayHandler: + entry = MagicMock() + entry.entry_id = "entry_541" + entry.data = { + CONF_HOST: "192.168.1.5", + CONF_PORT: 20000, + CONF_PASSWORD: "open", + CONF_SSDP_LOCATION: "", + CONF_SSDP_ST: "", + CONF_DEVICE_TYPE: "Gateway", + CONF_FRIENDLY_NAME: "GW", + CONF_MANUFACTURER: "Bticino", + CONF_MANUFACTURER_URL: "", + CONF_NAME: "MYHOME", + CONF_FIRMWARE: "1.0", + CONF_MAC: "00:11:22:33:44:55", + CONF_UDN: "1234", + } + hass = MagicMock() + hass.data = {} + handler = MyHOMEGatewayHandler(hass, entry) + handler.device_registry_id = "gateway_device" + return handler + + +def test_trace_files_present() -> None: + assert len(TRACE_FILES) == 2, TRACE_FILES + + +@pytest.mark.parametrize("trace_file", TRACE_FILES, ids=lambda p: p.name) +def test_cen_frames_parse_as_short_press_then_release(trace_file: Path) -> None: + frames = _cen_frames(trace_file) + assert frames == ["*15*01*73##", "*15*01#1*73##"] + pressed, released = (OWNMessage.parse(f) for f in frames) + assert isinstance(pressed, OWNCENEvent) and isinstance(released, OWNCENEvent) + assert (pressed.object, pressed.push_button) == ("73", 1) + assert pressed.is_pressed and not pressed.is_released_after_short_press + assert released.is_released_after_short_press and not released.is_pressed + + +@pytest.mark.asyncio +@pytest.mark.parametrize("trace_file", TRACE_FILES, ids=lambda p: p.name) +async def test_cen_trace_fires_myhome_cen_event(gateway_handler: MyHOMEGatewayHandler, trace_file: Path) -> None: + messages = [OWNMessage.parse(f) for f in _cen_frames(trace_file)] + + with ( + patch("custom_components.myhome.gateway.OWNEventSession") as session_class, + patch("homeassistant.helpers.device_registry.async_get", return_value=MagicMock()), + ): + session = MagicMock() + session.connect = AsyncMock(return_value={"Success": True}) + session.get_next = AsyncMock(side_effect=[*messages, asyncio.CancelledError()]) + session_class.return_value = session + gateway_handler.send_status_request = AsyncMock() + with patch.object(gateway_handler.hass.bus, "async_fire") as fire: + try: + await gateway_handler.listening_loop() + except asyncio.CancelledError: + pass + + expected = {"object": 73, "pushbutton": 1, "where": "73", "gateway_mac": gateway_handler.mac, "entry_id": "entry_541"} + fire.assert_any_call("myhome_cen_event", {**expected, "event": CONF_SHORT_PRESS}) + fire.assert_any_call("myhome_cen_event", {**expected, "event": CONF_SHORT_RELEASE})