ramses-rf 0.52.4__py3-none-any.whl → 0.53.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ramses_cli/client.py +168 -54
- ramses_cli/debug.py +1 -1
- ramses_cli/py.typed +0 -0
- ramses_cli/utils/convert.py +2 -2
- ramses_rf/__init__.py +2 -0
- ramses_rf/database.py +40 -17
- ramses_rf/device/base.py +14 -3
- ramses_rf/device/heat.py +1 -1
- ramses_rf/device/hvac.py +24 -21
- ramses_rf/entity_base.py +9 -7
- ramses_rf/gateway.py +214 -27
- ramses_rf/schemas.py +2 -1
- ramses_rf/system/zones.py +22 -2
- ramses_rf/version.py +1 -1
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.53.0.dist-info}/METADATA +1 -1
- ramses_rf-0.53.0.dist-info/RECORD +56 -0
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.53.0.dist-info}/WHEEL +1 -1
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.53.0.dist-info}/licenses/LICENSE +1 -1
- ramses_tx/address.py +21 -6
- ramses_tx/command.py +19 -3
- ramses_tx/const.py +110 -23
- ramses_tx/helpers.py +30 -10
- ramses_tx/message.py +11 -5
- ramses_tx/packet.py +13 -5
- ramses_tx/parsers.py +1039 -16
- ramses_tx/protocol.py +112 -23
- ramses_tx/protocol_fsm.py +28 -10
- ramses_tx/schemas.py +2 -2
- ramses_tx/transport.py +529 -47
- ramses_tx/version.py +1 -1
- ramses_rf-0.52.4.dist-info/RECORD +0 -55
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.53.0.dist-info}/entry_points.txt +0 -0
ramses_tx/parsers.py
CHANGED
|
@@ -190,6 +190,16 @@ _LOGGER = _PKT_LOGGER = logging.getLogger(__name__)
|
|
|
190
190
|
|
|
191
191
|
# rf_unknown
|
|
192
192
|
def parser_0001(payload: str, msg: Message) -> Mapping[str, bool | str | None]:
|
|
193
|
+
"""Parse the 0001 (rf_unknown) packet.
|
|
194
|
+
|
|
195
|
+
:param payload: The raw hex payload
|
|
196
|
+
:type payload: str
|
|
197
|
+
:param msg: The message object containing context
|
|
198
|
+
:type msg: Message
|
|
199
|
+
:return: A mapping of parsed slot and parameter data
|
|
200
|
+
:rtype: Mapping[str, bool | str | None]
|
|
201
|
+
:raises AssertionError: If the payload format does not match expected constants.
|
|
202
|
+
"""
|
|
193
203
|
# When in test mode, a 12: will send a W ?every 6 seconds:
|
|
194
204
|
# 12:39:56.099 061 W --- 12:010740 --:------ 12:010740 0001 005 0000000501
|
|
195
205
|
# 12:40:02.098 061 W --- 12:010740 --:------ 12:010740 0001 005 0000000501
|
|
@@ -265,6 +275,15 @@ def parser_0001(payload: str, msg: Message) -> Mapping[str, bool | str | None]:
|
|
|
265
275
|
|
|
266
276
|
# outdoor_sensor (outdoor_weather / outdoor_temperature)
|
|
267
277
|
def parser_0002(payload: str, msg: Message) -> dict[str, Any]:
|
|
278
|
+
"""Parse the 0002 (outdoor_sensor) packet.
|
|
279
|
+
|
|
280
|
+
:param payload: The raw hex payload
|
|
281
|
+
:type payload: str
|
|
282
|
+
:param msg: The message object
|
|
283
|
+
:type msg: Message
|
|
284
|
+
:return: A dictionary containing the outdoor temperature
|
|
285
|
+
:rtype: dict[str, Any]
|
|
286
|
+
"""
|
|
268
287
|
if payload[6:] == "02": # or: msg.src.type == DEV_TYPE_MAP.OUT:
|
|
269
288
|
return {
|
|
270
289
|
SZ_TEMPERATURE: hex_to_temp(payload[2:6]),
|
|
@@ -276,6 +295,15 @@ def parser_0002(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
276
295
|
|
|
277
296
|
# zone_name
|
|
278
297
|
def parser_0004(payload: str, msg: Message) -> PayDictT._0004:
|
|
298
|
+
"""Parse the 0004 (zone_name) packet.
|
|
299
|
+
|
|
300
|
+
:param payload: The raw hex payload
|
|
301
|
+
:type payload: str
|
|
302
|
+
:param msg: The message object
|
|
303
|
+
:type msg: Message
|
|
304
|
+
:return: A dictionary containing the zone name
|
|
305
|
+
:rtype: PayDictT._0004
|
|
306
|
+
"""
|
|
279
307
|
# RQ payload is zz00; limited to 12 chars in evohome UI? if "7F"*20: not a zone
|
|
280
308
|
|
|
281
309
|
return {} if payload[4:] == "7F" * 20 else {SZ_NAME: hex_to_str(payload[4:])}
|
|
@@ -283,6 +311,16 @@ def parser_0004(payload: str, msg: Message) -> PayDictT._0004:
|
|
|
283
311
|
|
|
284
312
|
# system_zones (add/del a zone?) # TODO: needs a cleanup
|
|
285
313
|
def parser_0005(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
314
|
+
"""Parse the 0005 (system_zones) packet to identify zone types and masks.
|
|
315
|
+
|
|
316
|
+
:param payload: The raw hex payload
|
|
317
|
+
:type payload: str
|
|
318
|
+
:param msg: The message object
|
|
319
|
+
:type msg: Message
|
|
320
|
+
:return: A list or dictionary of zone classes and masks
|
|
321
|
+
:rtype: dict | list[dict]
|
|
322
|
+
:raises AssertionError: If the message source is not a recognized device type.
|
|
323
|
+
"""
|
|
286
324
|
# .I --- 01:145038 --:------ 01:145038 0005 004 00000100
|
|
287
325
|
# RP --- 02:017205 18:073736 --:------ 0005 004 0009001F
|
|
288
326
|
# .I --- 34:064023 --:------ 34:064023 0005 012 000A0000-000F0000-00100000
|
|
@@ -317,10 +355,15 @@ def parser_0005(payload: str, msg: Message) -> dict | list[dict]: # TODO: only
|
|
|
317
355
|
|
|
318
356
|
# schedule_sync (any changes?)
|
|
319
357
|
def parser_0006(payload: str, msg: Message) -> PayDictT._0006:
|
|
320
|
-
"""Return the total number of changes to the schedules
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
358
|
+
"""Return the total number of changes to the system schedules.
|
|
359
|
+
|
|
360
|
+
:param payload: The raw hex payload
|
|
361
|
+
:type payload: str
|
|
362
|
+
:param msg: The message object
|
|
363
|
+
:type msg: Message
|
|
364
|
+
:return: A dictionary containing the schedule change counter
|
|
365
|
+
:rtype: PayDictT._0006
|
|
366
|
+
:raises AssertionError: If the payload header is invalid.
|
|
324
367
|
"""
|
|
325
368
|
# 16:10:34.288 053 RQ --- 30:071715 01:145038 --:------ 0006 001 00
|
|
326
369
|
# 16:10:34.291 053 RP --- 01:145038 30:071715 --:------ 0006 004 00050008
|
|
@@ -337,6 +380,16 @@ def parser_0006(payload: str, msg: Message) -> PayDictT._0006:
|
|
|
337
380
|
|
|
338
381
|
# relay_demand (domain/zone/device)
|
|
339
382
|
def parser_0008(payload: str, msg: Message) -> PayDictT._0008:
|
|
383
|
+
"""Parse the 0008 (relay_demand) packet.
|
|
384
|
+
|
|
385
|
+
:param payload: The raw hex payload
|
|
386
|
+
:type payload: str
|
|
387
|
+
:param msg: The message object
|
|
388
|
+
:type msg: Message
|
|
389
|
+
:return: A dictionary containing the relay demand percentage
|
|
390
|
+
:rtype: PayDictT._0008
|
|
391
|
+
:raises AssertionError: If the message length is invalid for specific device types.
|
|
392
|
+
"""
|
|
340
393
|
# https://www.domoticaforum.eu/viewtopic.php?f=7&t=5806&start=105#p73681
|
|
341
394
|
# e.g. Electric Heat Zone
|
|
342
395
|
|
|
@@ -360,7 +413,8 @@ def parser_0008(payload: str, msg: Message) -> PayDictT._0008:
|
|
|
360
413
|
|
|
361
414
|
# relay_failsafe
|
|
362
415
|
def parser_0009(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
363
|
-
"""
|
|
416
|
+
"""Parse the 0009 (relay_failsafe) packet.
|
|
417
|
+
The relay failsafe mode.
|
|
364
418
|
|
|
365
419
|
The failsafe mode defines the relay behaviour if the RF communication is lost (e.g.
|
|
366
420
|
when a room thermostat stops communicating due to discharged batteries):
|
|
@@ -369,6 +423,14 @@ def parser_0009(payload: str, msg: Message) -> dict | list[dict]: # TODO: only
|
|
|
369
423
|
- True (enabled) - if RF comms are lost, relay will cycle at 20% ON, 80% OFF
|
|
370
424
|
|
|
371
425
|
This setting may need to be enabled to ensure frost protect mode.
|
|
426
|
+
|
|
427
|
+
:param payload: The raw hex payload
|
|
428
|
+
:type payload: str
|
|
429
|
+
:param msg: The message object
|
|
430
|
+
:type msg: Message
|
|
431
|
+
:return: A dictionary defining if failsafe mode is enabled
|
|
432
|
+
:rtype: dict | list[dict]
|
|
433
|
+
:raises AssertionError: If the domain ID in the payload is invalid.
|
|
372
434
|
"""
|
|
373
435
|
# can get: 003 or 006, e.g.: FC01FF-F901FF or FC00FF-F900FF
|
|
374
436
|
# .I --- 23:100224 --:------ 23:100224 0009 003 0100FF # 2-zone ST9520C
|
|
@@ -395,6 +457,17 @@ def parser_0009(payload: str, msg: Message) -> dict | list[dict]: # TODO: only
|
|
|
395
457
|
def parser_000a(
|
|
396
458
|
payload: str, msg: Message
|
|
397
459
|
) -> PayDictT._000A | list[PayDictT._000A] | PayDictT.EMPTY:
|
|
460
|
+
"""Parse the 000a (zone_params) packet.
|
|
461
|
+
|
|
462
|
+
:param payload: The raw hex payload
|
|
463
|
+
:type payload: str
|
|
464
|
+
:param msg: The message object
|
|
465
|
+
:type msg: Message
|
|
466
|
+
:return: A dictionary of zone parameters including min/max temps
|
|
467
|
+
:rtype: PayDictT._000A | list[PayDictT._000A] | PayDictT.EMPTY
|
|
468
|
+
:raises AssertionError: If the message length is unexpected.
|
|
469
|
+
"""
|
|
470
|
+
|
|
398
471
|
def _parser(seqx: str) -> PayDictT._000A: # null_rp: "007FFF7FFF"
|
|
399
472
|
bitmap = int(seqx[2:4], 16)
|
|
400
473
|
return {
|
|
@@ -424,6 +497,17 @@ def parser_000a(
|
|
|
424
497
|
|
|
425
498
|
# zone_devices
|
|
426
499
|
def parser_000c(payload: str, msg: Message) -> dict[str, Any]:
|
|
500
|
+
"""Parse the 000c (zone_devices) packet.
|
|
501
|
+
|
|
502
|
+
:param payload: The raw hex payload
|
|
503
|
+
:type payload: str
|
|
504
|
+
:param msg: The message object
|
|
505
|
+
:type msg: Message
|
|
506
|
+
:return: A dictionary mapping device IDs to zone indices
|
|
507
|
+
:rtype: dict[str, Any]
|
|
508
|
+
:raises PacketPayloadInvalid: If the element length in the payload is malformed.
|
|
509
|
+
:raises AssertionError: If indices or device IDs are invalid.
|
|
510
|
+
"""
|
|
427
511
|
# .I --- 34:092243 --:------ 34:092243 000C 018 00-0A-7F-FFFFFF 00-0F-7F-FFFFFF 00-10-7F-FFFFFF # noqa: E501
|
|
428
512
|
# RP --- 01:145038 18:013393 --:------ 000C 006 00-00-00-10DAFD
|
|
429
513
|
# RP --- 01:145038 18:013393 --:------ 000C 012 01-00-00-10DAF5 01-00-00-10DAFB
|
|
@@ -515,6 +599,16 @@ def parser_000c(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
515
599
|
|
|
516
600
|
# unknown_000e, from STA
|
|
517
601
|
def parser_000e(payload: str, msg: Message) -> dict[str, Any]:
|
|
602
|
+
"""Parse the 000e packet.
|
|
603
|
+
|
|
604
|
+
:param payload: The raw hex payload
|
|
605
|
+
:type payload: str
|
|
606
|
+
:param msg: The message object
|
|
607
|
+
:type msg: Message
|
|
608
|
+
:return: A dictionary containing the raw payload
|
|
609
|
+
:rtype: dict[str, Any]
|
|
610
|
+
:raises AssertionError: If the payload value is not recognized.
|
|
611
|
+
"""
|
|
518
612
|
assert payload in ("000014", "000028"), _INFORM_DEV_MSG
|
|
519
613
|
|
|
520
614
|
return {
|
|
@@ -524,6 +618,15 @@ def parser_000e(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
524
618
|
|
|
525
619
|
# rf_check
|
|
526
620
|
def parser_0016(payload: str, msg: Message) -> dict[str, Any]:
|
|
621
|
+
"""Parse the 0016 (rf_check) packet.
|
|
622
|
+
|
|
623
|
+
:param payload: The raw hex payload
|
|
624
|
+
:type payload: str
|
|
625
|
+
:param msg: The message object containing context
|
|
626
|
+
:type msg: Message
|
|
627
|
+
:return: A dictionary containing rf_strength and rf_value
|
|
628
|
+
:rtype: dict[str, Any]
|
|
629
|
+
"""
|
|
527
630
|
# TODO: does 0016 include parent_idx?, but RQ|07:|0000?
|
|
528
631
|
# RQ --- 22:060293 01:078710 --:------ 0016 002 0200
|
|
529
632
|
# RP --- 01:078710 22:060293 --:------ 0016 002 021E
|
|
@@ -544,6 +647,15 @@ def parser_0016(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
544
647
|
|
|
545
648
|
# language (of device/system)
|
|
546
649
|
def parser_0100(payload: str, msg: Message) -> PayDictT._0100 | PayDictT.EMPTY:
|
|
650
|
+
"""Parse the 0100 (language) packet.
|
|
651
|
+
|
|
652
|
+
:param payload: The raw hex payload
|
|
653
|
+
:type payload: str
|
|
654
|
+
:param msg: The message object containing context
|
|
655
|
+
:type msg: Message
|
|
656
|
+
:return: A dictionary containing the language string
|
|
657
|
+
:rtype: PayDictT._0100 | PayDictT.EMPTY
|
|
658
|
+
"""
|
|
547
659
|
if msg.verb == RQ and msg.len == 1: # some RQs have a payload
|
|
548
660
|
return {}
|
|
549
661
|
|
|
@@ -555,6 +667,16 @@ def parser_0100(payload: str, msg: Message) -> PayDictT._0100 | PayDictT.EMPTY:
|
|
|
555
667
|
|
|
556
668
|
# unknown_0150, from OTB
|
|
557
669
|
def parser_0150(payload: str, msg: Message) -> dict[str, Any]:
|
|
670
|
+
"""Parse the 0150 packet.
|
|
671
|
+
|
|
672
|
+
:param payload: The raw hex payload
|
|
673
|
+
:type payload: str
|
|
674
|
+
:param msg: The message object
|
|
675
|
+
:type msg: Message
|
|
676
|
+
:return: A dictionary containing the raw payload
|
|
677
|
+
:rtype: dict[str, Any]
|
|
678
|
+
:raises AssertionError: If the payload is not the expected '000000'.
|
|
679
|
+
"""
|
|
558
680
|
assert payload == "000000", _INFORM_DEV_MSG
|
|
559
681
|
|
|
560
682
|
return {
|
|
@@ -564,6 +686,16 @@ def parser_0150(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
564
686
|
|
|
565
687
|
# unknown_01d0, from a HR91 (when its buttons are pushed)
|
|
566
688
|
def parser_01d0(payload: str, msg: Message) -> dict[str, Any]:
|
|
689
|
+
"""Parse the 01d0 packet (HR91 button push).
|
|
690
|
+
|
|
691
|
+
:param payload: The raw hex payload
|
|
692
|
+
:type payload: str
|
|
693
|
+
:param msg: The message object
|
|
694
|
+
:type msg: Message
|
|
695
|
+
:return: A dictionary containing the unknown state value
|
|
696
|
+
:rtype: dict[str, Any]
|
|
697
|
+
:raises AssertionError: If the payload value is not recognized.
|
|
698
|
+
"""
|
|
567
699
|
# 23:57:28.869 045 W --- 04:000722 01:158182 --:------ 01D0 002 0003
|
|
568
700
|
# 23:57:28.931 045 I --- 01:158182 04:000722 --:------ 01D0 002 0003
|
|
569
701
|
# 23:57:31.581 048 W --- 04:000722 01:158182 --:------ 01E9 002 0003
|
|
@@ -579,6 +711,16 @@ def parser_01d0(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
579
711
|
|
|
580
712
|
# unknown_01e9, from a HR91 (when its buttons are pushed)
|
|
581
713
|
def parser_01e9(payload: str, msg: Message) -> dict[str, Any]:
|
|
714
|
+
"""Parse the 01e9 packet (HR91 button push).
|
|
715
|
+
|
|
716
|
+
:param payload: The raw hex payload
|
|
717
|
+
:type payload: str
|
|
718
|
+
:param msg: The message object
|
|
719
|
+
:type msg: Message
|
|
720
|
+
:return: A dictionary containing the unknown state value
|
|
721
|
+
:rtype: dict[str, Any]
|
|
722
|
+
:raises AssertionError: If the payload value is not recognized.
|
|
723
|
+
"""
|
|
582
724
|
# 23:57:31.581348 048 W --- 04:000722 01:158182 --:------ 01E9 002 0003
|
|
583
725
|
# 23:57:31.643188 045 I --- 01:158182 04:000722 --:------ 01E9 002 0000
|
|
584
726
|
|
|
@@ -590,6 +732,16 @@ def parser_01e9(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
590
732
|
|
|
591
733
|
# unknown_01ff, to/from a Itho Spider/Thermostat
|
|
592
734
|
def parser_01ff(payload: str, msg: Message) -> dict[str, Any]:
|
|
735
|
+
"""Parse the 01ff (Itho Spider) packet.
|
|
736
|
+
|
|
737
|
+
:param payload: The raw hex payload
|
|
738
|
+
:type payload: str
|
|
739
|
+
:param msg: The message object containing context
|
|
740
|
+
:type msg: Message
|
|
741
|
+
:return: A dictionary of temperature, setpoint bounds, and flags
|
|
742
|
+
:rtype: dict[str, Any]
|
|
743
|
+
:raises AssertionError: If internal payload constraints are violated.
|
|
744
|
+
"""
|
|
593
745
|
# see: https://github.com/zxdavb/ramses_rf/issues/73 & 101
|
|
594
746
|
|
|
595
747
|
# lots of '80's, and I see temps are `int(payload[6:8], 16) / 2`, so I wonder if 0x80 is N/A?
|
|
@@ -671,6 +823,17 @@ def parser_01ff(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
671
823
|
|
|
672
824
|
# zone_schedule (fragment)
|
|
673
825
|
def parser_0404(payload: str, msg: Message) -> PayDictT._0404:
|
|
826
|
+
"""Parse the 0404 (zone_schedule) fragment.
|
|
827
|
+
|
|
828
|
+
:param payload: The raw hex payload
|
|
829
|
+
:type payload: str
|
|
830
|
+
:param msg: The message object
|
|
831
|
+
:type msg: Message
|
|
832
|
+
:return: A dictionary containing schedule fragment data and total fragments
|
|
833
|
+
:rtype: PayDictT._0404
|
|
834
|
+
:raises PacketPayloadInvalid: If the fragment length does not match the header.
|
|
835
|
+
:raises AssertionError: If internal context bytes are invalid.
|
|
836
|
+
"""
|
|
674
837
|
# Retrieval of Zone schedule (NB: 200008)
|
|
675
838
|
# RQ --- 30:185469 01:037519 --:------ 0404 007 00-200008-00-0100
|
|
676
839
|
# RP --- 01:037519 30:185469 --:------ 0404 048 00-200008-29-0103-6E2...
|
|
@@ -735,6 +898,15 @@ def parser_0404(payload: str, msg: Message) -> PayDictT._0404:
|
|
|
735
898
|
|
|
736
899
|
# system_fault (fault_log_entry) - needs refactoring
|
|
737
900
|
def parser_0418(payload: str, msg: Message) -> PayDictT._0418 | PayDictT._0418_NULL:
|
|
901
|
+
"""Parse the 0418 (system_fault) packet.
|
|
902
|
+
|
|
903
|
+
:param payload: The raw hex payload
|
|
904
|
+
:type payload: str
|
|
905
|
+
:param msg: The message object
|
|
906
|
+
:type msg: Message
|
|
907
|
+
:return: A dictionary containing a fault log entry or null entry
|
|
908
|
+
:rtype: PayDictT._0418 | PayDictT._0418_NULL
|
|
909
|
+
"""
|
|
738
910
|
null_result: PayDictT._0418_NULL
|
|
739
911
|
full_result: PayDictT._0418
|
|
740
912
|
|
|
@@ -813,6 +985,15 @@ def parser_0418(payload: str, msg: Message) -> PayDictT._0418 | PayDictT._0418_N
|
|
|
813
985
|
|
|
814
986
|
# unknown_042f, from STA, VMS
|
|
815
987
|
def parser_042f(payload: str, msg: Message) -> dict[str, Any]:
|
|
988
|
+
"""Parse the 042f packet.
|
|
989
|
+
|
|
990
|
+
:param payload: The raw hex payload
|
|
991
|
+
:type payload: str
|
|
992
|
+
:param msg: The message object
|
|
993
|
+
:type msg: Message
|
|
994
|
+
:return: A dictionary of extracted hex counters
|
|
995
|
+
:rtype: dict[str, Any]
|
|
996
|
+
"""
|
|
816
997
|
return {
|
|
817
998
|
"counter_1": f"0x{payload[2:6]}",
|
|
818
999
|
"counter_3": f"0x{payload[6:10]}",
|
|
@@ -823,6 +1004,15 @@ def parser_042f(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
823
1004
|
|
|
824
1005
|
# TODO: unknown_0b04, from THM (only when its a CTL?)
|
|
825
1006
|
def parser_0b04(payload: str, msg: Message) -> dict[str, Any]:
|
|
1007
|
+
"""Parse the 0b04 packet.
|
|
1008
|
+
|
|
1009
|
+
:param payload: The raw hex payload
|
|
1010
|
+
:type payload: str
|
|
1011
|
+
:param msg: The message object
|
|
1012
|
+
:type msg: Message
|
|
1013
|
+
:return: A dictionary containing the unknown data value
|
|
1014
|
+
:rtype: dict[str, Any]
|
|
1015
|
+
"""
|
|
826
1016
|
# .I --- --:------ --:------ 12:207082 0B04 002 00C8 # batch of 3, every 24h
|
|
827
1017
|
|
|
828
1018
|
return {
|
|
@@ -832,6 +1022,16 @@ def parser_0b04(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
832
1022
|
|
|
833
1023
|
# mixvalve_config (zone), FAN
|
|
834
1024
|
def parser_1030(payload: str, msg: Message) -> PayDictT._1030:
|
|
1025
|
+
"""Parse the 1030 (mixvalve_config) packet.
|
|
1026
|
+
|
|
1027
|
+
:param payload: The raw hex payload
|
|
1028
|
+
:type payload: str
|
|
1029
|
+
:param msg: The message object containing context
|
|
1030
|
+
:type msg: Message
|
|
1031
|
+
:return: A dictionary of mixing valve parameters
|
|
1032
|
+
:rtype: PayDictT._1030
|
|
1033
|
+
:raises AssertionError: If the message length is unexpected or parameters are malformed.
|
|
1034
|
+
"""
|
|
835
1035
|
# .I --- 01:145038 --:------ 01:145038 1030 016 0A-C80137-C9010F-CA0196-CB0100-CC0101
|
|
836
1036
|
# .I --- --:------ --:------ 12:144017 1030 016 01-C80137-C9010F-CA0196-CB010F-CC0101
|
|
837
1037
|
# RP --- 32:155617 18:005904 --:------ 1030 007 00-200100-21011F
|
|
@@ -860,9 +1060,17 @@ def parser_1030(payload: str, msg: Message) -> PayDictT._1030:
|
|
|
860
1060
|
|
|
861
1061
|
# device_battery (battery_state)
|
|
862
1062
|
def parser_1060(payload: str, msg: Message) -> PayDictT._1060:
|
|
863
|
-
"""
|
|
1063
|
+
"""Parse the 1060 (device_battery) packet.
|
|
1064
|
+
Return the battery state.
|
|
864
1065
|
|
|
865
1066
|
Some devices (04:) will also report battery level.
|
|
1067
|
+
:param payload: The raw hex payload
|
|
1068
|
+
:type payload: str
|
|
1069
|
+
:param msg: The message object containing context
|
|
1070
|
+
:type msg: Message
|
|
1071
|
+
:return: A dictionary containing battery low status and level percentage
|
|
1072
|
+
:rtype: PayDictT._1060
|
|
1073
|
+
:raises AssertionError: If the message length is invalid.
|
|
866
1074
|
"""
|
|
867
1075
|
|
|
868
1076
|
assert msg.len == 3, msg.len
|
|
@@ -876,11 +1084,30 @@ def parser_1060(payload: str, msg: Message) -> PayDictT._1060:
|
|
|
876
1084
|
|
|
877
1085
|
# max_ch_setpoint (supply high limit)
|
|
878
1086
|
def parser_1081(payload: str, msg: Message) -> PayDictT._1081:
|
|
1087
|
+
"""Parse the 1081 (max_ch_setpoint) packet.
|
|
1088
|
+
|
|
1089
|
+
:param payload: The raw hex payload
|
|
1090
|
+
:type payload: str
|
|
1091
|
+
:param msg: The message object
|
|
1092
|
+
:type msg: Message
|
|
1093
|
+
:return: A dictionary containing the temperature setpoint
|
|
1094
|
+
:rtype: PayDictT._1081
|
|
1095
|
+
"""
|
|
879
1096
|
return {SZ_SETPOINT: hex_to_temp(payload[2:])}
|
|
880
1097
|
|
|
881
1098
|
|
|
882
1099
|
# unknown_1090 (non-Evohome, e.g. ST9520C)
|
|
883
1100
|
def parser_1090(payload: str, msg: Message) -> PayDictT._1090:
|
|
1101
|
+
"""Parse the 1090 packet.
|
|
1102
|
+
|
|
1103
|
+
:param payload: The raw hex payload
|
|
1104
|
+
:type payload: str
|
|
1105
|
+
:param msg: The message object
|
|
1106
|
+
:type msg: Message
|
|
1107
|
+
:return: A dictionary containing two temperature values
|
|
1108
|
+
:rtype: PayDictT._1090
|
|
1109
|
+
:raises AssertionError: If the message length or payload index is invalid.
|
|
1110
|
+
"""
|
|
884
1111
|
# 14:08:05.176 095 RP --- 23:100224 22:219457 --:------ 1090 005 007FFF01F4
|
|
885
1112
|
# 18:08:05.809 095 RP --- 23:100224 22:219457 --:------ 1090 005 007FFF01F4
|
|
886
1113
|
|
|
@@ -896,6 +1123,16 @@ def parser_1090(payload: str, msg: Message) -> PayDictT._1090:
|
|
|
896
1123
|
|
|
897
1124
|
# unknown_1098, from OTB
|
|
898
1125
|
def parser_1098(payload: str, msg: Message) -> dict[str, Any]:
|
|
1126
|
+
"""Parse the 1098 packet.
|
|
1127
|
+
|
|
1128
|
+
:param payload: The raw hex payload
|
|
1129
|
+
:type payload: str
|
|
1130
|
+
:param msg: The message object
|
|
1131
|
+
:type msg: Message
|
|
1132
|
+
:return: A dictionary containing the raw payload and its interpreted value
|
|
1133
|
+
:rtype: dict[str, Any]
|
|
1134
|
+
:raises AssertionError: If the payload does not match expected constants.
|
|
1135
|
+
"""
|
|
899
1136
|
assert payload == "00C8", _INFORM_DEV_MSG
|
|
900
1137
|
|
|
901
1138
|
return {
|
|
@@ -908,6 +1145,16 @@ def parser_1098(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
908
1145
|
|
|
909
1146
|
# dhw (cylinder) params # FIXME: a bit messy
|
|
910
1147
|
def parser_10a0(payload: str, msg: Message) -> PayDictT._10A0 | PayDictT.EMPTY:
|
|
1148
|
+
"""Parse the 10a0 (dhw_params) packet.
|
|
1149
|
+
|
|
1150
|
+
:param payload: The raw hex payload
|
|
1151
|
+
:type payload: str
|
|
1152
|
+
:param msg: The message object containing context
|
|
1153
|
+
:type msg: Message
|
|
1154
|
+
:return: A dictionary of DHW parameters or an empty dictionary
|
|
1155
|
+
:rtype: PayDictT._10A0 | PayDictT.EMPTY
|
|
1156
|
+
:raises AssertionError: If the message length or valve index is invalid.
|
|
1157
|
+
"""
|
|
911
1158
|
# RQ --- 07:045960 01:145038 --:------ 10A0 006 00-1087-00-03E4 # RQ/RP, every 24h
|
|
912
1159
|
# RP --- 01:145038 07:045960 --:------ 10A0 006 00-109A-00-03E8
|
|
913
1160
|
# RP --- 10:048122 18:006402 --:------ 10A0 003 00-1B58
|
|
@@ -950,6 +1197,16 @@ def parser_10a0(payload: str, msg: Message) -> PayDictT._10A0 | PayDictT.EMPTY:
|
|
|
950
1197
|
|
|
951
1198
|
# unknown_10b0, from OTB
|
|
952
1199
|
def parser_10b0(payload: str, msg: Message) -> dict[str, Any]:
|
|
1200
|
+
"""Parse the 10b0 packet.
|
|
1201
|
+
|
|
1202
|
+
:param payload: The raw hex payload
|
|
1203
|
+
:type payload: str
|
|
1204
|
+
:param msg: The message object
|
|
1205
|
+
:type msg: Message
|
|
1206
|
+
:return: A dictionary containing the raw payload and interpreted value
|
|
1207
|
+
:rtype: dict[str, Any]
|
|
1208
|
+
:raises AssertionError: If the payload is invalid.
|
|
1209
|
+
"""
|
|
953
1210
|
assert payload == "0000", _INFORM_DEV_MSG
|
|
954
1211
|
|
|
955
1212
|
return {
|
|
@@ -962,6 +1219,15 @@ def parser_10b0(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
962
1219
|
|
|
963
1220
|
# filter_change, HVAC
|
|
964
1221
|
def parser_10d0(payload: str, msg: Message) -> dict[str, Any]:
|
|
1222
|
+
"""Parse the 10d0 (filter_change) packet.
|
|
1223
|
+
|
|
1224
|
+
:param payload: The raw hex payload
|
|
1225
|
+
:type payload: str
|
|
1226
|
+
:param msg: The message object containing context
|
|
1227
|
+
:type msg: Message
|
|
1228
|
+
:return: A dictionary of remaining days, lifetime, and percentage
|
|
1229
|
+
:rtype: dict[str, Any]
|
|
1230
|
+
"""
|
|
965
1231
|
# 2022-07-03T22:52:34.571579 045 W --- 37:171871 32:155617 --:------ 10D0 002 00FF
|
|
966
1232
|
# 2022-07-03T22:52:34.596526 066 I --- 32:155617 37:171871 --:------ 10D0 006 0047B44F0000
|
|
967
1233
|
# then...
|
|
@@ -992,6 +1258,16 @@ def parser_10d0(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
992
1258
|
|
|
993
1259
|
# device_info
|
|
994
1260
|
def parser_10e0(payload: str, msg: Message) -> dict[str, Any]:
|
|
1261
|
+
"""Parse the 10e0 (device_info) packet.
|
|
1262
|
+
|
|
1263
|
+
:param payload: The raw hex payload
|
|
1264
|
+
:type payload: str
|
|
1265
|
+
:param msg: The message object
|
|
1266
|
+
:type msg: Message
|
|
1267
|
+
:return: A dictionary of device specifications and manufacturing data
|
|
1268
|
+
:rtype: dict[str, Any]
|
|
1269
|
+
:raises AssertionError: If the message length is invalid for the reported signature.
|
|
1270
|
+
"""
|
|
995
1271
|
if payload == "00": # some HVAC devices will RP|10E0|00
|
|
996
1272
|
return {}
|
|
997
1273
|
|
|
@@ -1031,11 +1307,30 @@ def parser_10e0(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1031
1307
|
|
|
1032
1308
|
# device_id
|
|
1033
1309
|
def parser_10e1(payload: str, msg: Message) -> PayDictT._10E1:
|
|
1310
|
+
"""Parse the 10e1 (device_id) packet.
|
|
1311
|
+
|
|
1312
|
+
:param payload: The raw hex payload
|
|
1313
|
+
:type payload: str
|
|
1314
|
+
:param msg: The message object
|
|
1315
|
+
:type msg: Message
|
|
1316
|
+
:return: A dictionary containing the device ID
|
|
1317
|
+
:rtype: PayDictT._10E1
|
|
1318
|
+
"""
|
|
1034
1319
|
return {SZ_DEVICE_ID: hex_id_to_dev_id(payload[2:])}
|
|
1035
1320
|
|
|
1036
1321
|
|
|
1037
1322
|
# unknown_10e2 - HVAC
|
|
1038
1323
|
def parser_10e2(payload: str, msg: Message) -> dict[str, Any]:
|
|
1324
|
+
"""Parse the 10e2 (HVAC counter) packet.
|
|
1325
|
+
|
|
1326
|
+
:param payload: The raw hex payload
|
|
1327
|
+
:type payload: str
|
|
1328
|
+
:param msg: The message object containing context
|
|
1329
|
+
:type msg: Message
|
|
1330
|
+
:return: A dictionary containing the extracted counter
|
|
1331
|
+
:rtype: dict[str, Any]
|
|
1332
|
+
:raises AssertionError: If the payload length is not 6 or prefix is not '00'.
|
|
1333
|
+
"""
|
|
1039
1334
|
# .I --- --:------ --:------ 20:231151 10E2 003 00AD74 # every 2 minutes
|
|
1040
1335
|
|
|
1041
1336
|
assert payload[:2] == "00", _INFORM_DEV_MSG
|
|
@@ -1050,6 +1345,17 @@ def parser_10e2(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1050
1345
|
def parser_1100(
|
|
1051
1346
|
payload: str, msg: Message
|
|
1052
1347
|
) -> PayDictT._1100 | PayDictT._1100_IDX | PayDictT._JASPER | PayDictT.EMPTY:
|
|
1348
|
+
"""Parse the 1100 (tpi_params) packet.
|
|
1349
|
+
|
|
1350
|
+
:param payload: The raw hex payload
|
|
1351
|
+
:type payload: str
|
|
1352
|
+
:param msg: The message object containing context
|
|
1353
|
+
:type msg: Message
|
|
1354
|
+
:return: A dictionary of TPI parameters or domain index
|
|
1355
|
+
:rtype: PayDictT._1100 | PayDictT._1100_IDX | PayDictT._JASPER | PayDictT.EMPTY
|
|
1356
|
+
:raises AssertionError: If TPI values are outside of recognized ranges.
|
|
1357
|
+
"""
|
|
1358
|
+
|
|
1053
1359
|
def complex_idx(seqx: str) -> PayDictT._1100_IDX | PayDictT.EMPTY:
|
|
1054
1360
|
return {SZ_DOMAIN_ID: seqx} if seqx[:1] == "F" else {} # type: ignore[typeddict-item] # only FC
|
|
1055
1361
|
|
|
@@ -1101,6 +1407,16 @@ def parser_1100(
|
|
|
1101
1407
|
|
|
1102
1408
|
# unknown_11f0, from heatpump relay
|
|
1103
1409
|
def parser_11f0(payload: str, msg: Message) -> dict[str, Any]:
|
|
1410
|
+
"""Parse the 11f0 (heatpump relay) packet.
|
|
1411
|
+
|
|
1412
|
+
:param payload: The raw hex payload
|
|
1413
|
+
:type payload: str
|
|
1414
|
+
:param msg: The message object
|
|
1415
|
+
:type msg: Message
|
|
1416
|
+
:return: A dictionary containing the raw payload
|
|
1417
|
+
:rtype: dict[str, Any]
|
|
1418
|
+
:raises AssertionError: If the payload does not match the expected constant string.
|
|
1419
|
+
"""
|
|
1104
1420
|
assert payload == "000009000000000000", _INFORM_DEV_MSG
|
|
1105
1421
|
|
|
1106
1422
|
return {
|
|
@@ -1110,22 +1426,58 @@ def parser_11f0(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1110
1426
|
|
|
1111
1427
|
# dhw cylinder temperature
|
|
1112
1428
|
def parser_1260(payload: str, msg: Message) -> PayDictT._1260:
|
|
1429
|
+
"""Parse the 1260 (dhw_temp) packet.
|
|
1430
|
+
|
|
1431
|
+
:param payload: The raw hex payload
|
|
1432
|
+
:type payload: str
|
|
1433
|
+
:param msg: The message object
|
|
1434
|
+
:type msg: Message
|
|
1435
|
+
:return: A dictionary containing the DHW temperature
|
|
1436
|
+
:rtype: PayDictT._1260
|
|
1437
|
+
"""
|
|
1113
1438
|
return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
|
|
1114
1439
|
|
|
1115
1440
|
|
|
1116
1441
|
# HVAC: outdoor humidity
|
|
1117
1442
|
def parser_1280(payload: str, msg: Message) -> PayDictT._1280:
|
|
1443
|
+
"""Parse the 1280 (outdoor_humidity) packet.
|
|
1444
|
+
|
|
1445
|
+
:param payload: The raw hex payload
|
|
1446
|
+
:type payload: str
|
|
1447
|
+
:param msg: The message object
|
|
1448
|
+
:type msg: Message
|
|
1449
|
+
:return: A dictionary containing the outdoor humidity percentage
|
|
1450
|
+
:rtype: PayDictT._1280
|
|
1451
|
+
"""
|
|
1118
1452
|
return parse_outdoor_humidity(payload[2:])
|
|
1119
1453
|
|
|
1120
1454
|
|
|
1121
1455
|
# outdoor temperature
|
|
1122
1456
|
def parser_1290(payload: str, msg: Message) -> PayDictT._1290:
|
|
1457
|
+
"""Parse the 1290 (outdoor_temp) packet.
|
|
1458
|
+
|
|
1459
|
+
:param payload: The raw hex payload
|
|
1460
|
+
:type payload: str
|
|
1461
|
+
:param msg: The message object
|
|
1462
|
+
:type msg: Message
|
|
1463
|
+
:return: A dictionary containing the outdoor temperature
|
|
1464
|
+
:rtype: PayDictT._1290
|
|
1465
|
+
"""
|
|
1123
1466
|
# evohome responds to an RQ, also from OTB
|
|
1124
1467
|
return parse_outdoor_temp(payload[2:])
|
|
1125
1468
|
|
|
1126
1469
|
|
|
1127
1470
|
# HVAC: co2_level, see: 31DA[6:10]
|
|
1128
1471
|
def parser_1298(payload: str, msg: Message) -> PayDictT._1298:
|
|
1472
|
+
"""Parse the 1298 (co2_level) packet.
|
|
1473
|
+
|
|
1474
|
+
:param payload: The raw hex payload
|
|
1475
|
+
:type payload: str
|
|
1476
|
+
:param msg: The message object
|
|
1477
|
+
:type msg: Message
|
|
1478
|
+
:return: A dictionary containing the CO2 level in PPM
|
|
1479
|
+
:rtype: PayDictT._1298
|
|
1480
|
+
"""
|
|
1129
1481
|
return parse_co2_level(payload[2:6])
|
|
1130
1482
|
|
|
1131
1483
|
|
|
@@ -1133,6 +1485,15 @@ def parser_1298(payload: str, msg: Message) -> PayDictT._1298:
|
|
|
1133
1485
|
def parser_12a0(
|
|
1134
1486
|
payload: str, msg: Message
|
|
1135
1487
|
) -> PayDictT.INDOOR_HUMIDITY | list[PayDictT._12A0]:
|
|
1488
|
+
"""Parse the 12a0 (indoor_humidity) packet.
|
|
1489
|
+
|
|
1490
|
+
:param payload: The raw hex payload
|
|
1491
|
+
:type payload: str
|
|
1492
|
+
:param msg: The message object containing context
|
|
1493
|
+
:type msg: Message
|
|
1494
|
+
:return: A single humidity dict or a list of sensor element dicts
|
|
1495
|
+
:rtype: PayDictT.INDOOR_HUMIDITY | list[PayDictT._12A0]
|
|
1496
|
+
"""
|
|
1136
1497
|
if len(payload) <= 14:
|
|
1137
1498
|
return parse_indoor_humidity(payload[2:12])
|
|
1138
1499
|
|
|
@@ -1147,6 +1508,16 @@ def parser_12a0(
|
|
|
1147
1508
|
|
|
1148
1509
|
# window_state (of a device/zone)
|
|
1149
1510
|
def parser_12b0(payload: str, msg: Message) -> PayDictT._12B0:
|
|
1511
|
+
"""Parse the 12b0 (window_state) packet.
|
|
1512
|
+
|
|
1513
|
+
:param payload: The raw hex payload
|
|
1514
|
+
:type payload: str
|
|
1515
|
+
:param msg: The message object
|
|
1516
|
+
:type msg: Message
|
|
1517
|
+
:return: A dictionary containing the window open status
|
|
1518
|
+
:rtype: PayDictT._12B0
|
|
1519
|
+
:raises AssertionError: If the payload state bytes are unrecognized.
|
|
1520
|
+
"""
|
|
1150
1521
|
assert payload[2:] in ("0000", "C800", "FFFF"), payload[2:] # "FFFF" means N/A
|
|
1151
1522
|
|
|
1152
1523
|
return {
|
|
@@ -1156,6 +1527,15 @@ def parser_12b0(payload: str, msg: Message) -> PayDictT._12B0:
|
|
|
1156
1527
|
|
|
1157
1528
|
# displayed temperature (on a TR87RF bound to a RFG100)
|
|
1158
1529
|
def parser_12c0(payload: str, msg: Message) -> PayDictT._12C0:
|
|
1530
|
+
"""Parse the 12c0 (displayed_temp) packet.
|
|
1531
|
+
|
|
1532
|
+
:param payload: The raw hex payload
|
|
1533
|
+
:type payload: str
|
|
1534
|
+
:param msg: The message object
|
|
1535
|
+
:type msg: Message
|
|
1536
|
+
:return: A dictionary containing the temperature and its measurement units
|
|
1537
|
+
:rtype: PayDictT._12C0
|
|
1538
|
+
"""
|
|
1159
1539
|
if payload[2:4] == "80":
|
|
1160
1540
|
temp: float | None = None
|
|
1161
1541
|
elif payload[4:6] == "00": # units are 1.0 F
|
|
@@ -1174,22 +1554,59 @@ def parser_12c0(payload: str, msg: Message) -> PayDictT._12C0:
|
|
|
1174
1554
|
|
|
1175
1555
|
# HVAC: air_quality (and air_quality_basis), see: 31DA[2:6]
|
|
1176
1556
|
def parser_12c8(payload: str, msg: Message) -> PayDictT._12C8:
|
|
1557
|
+
"""Parse the 12c8 (air_quality) packet.
|
|
1558
|
+
|
|
1559
|
+
:param payload: The raw hex payload
|
|
1560
|
+
:type payload: str
|
|
1561
|
+
:param msg: The message object
|
|
1562
|
+
:type msg: Message
|
|
1563
|
+
:return: A dictionary containing the air quality percentage and basis
|
|
1564
|
+
:rtype: PayDictT._12C8
|
|
1565
|
+
"""
|
|
1177
1566
|
return parse_air_quality(payload[2:6])
|
|
1178
1567
|
|
|
1179
1568
|
|
|
1180
1569
|
# dhw_flow_rate
|
|
1181
1570
|
def parser_12f0(payload: str, msg: Message) -> PayDictT._12F0:
|
|
1571
|
+
"""Parse the 12f0 (dhw_flow_rate) packet.
|
|
1572
|
+
|
|
1573
|
+
:param payload: The raw hex payload
|
|
1574
|
+
:type payload: str
|
|
1575
|
+
:param msg: The message object
|
|
1576
|
+
:type msg: Message
|
|
1577
|
+
:return: A dictionary containing the DHW flow rate
|
|
1578
|
+
:rtype: PayDictT._12F0
|
|
1579
|
+
"""
|
|
1182
1580
|
return {SZ_DHW_FLOW_RATE: hex_to_temp(payload[2:])}
|
|
1183
1581
|
|
|
1184
1582
|
|
|
1185
1583
|
# ch_pressure
|
|
1186
1584
|
def parser_1300(payload: str, msg: Message) -> PayDictT._1300:
|
|
1585
|
+
"""Parse the 1300 (ch_pressure) packet.
|
|
1586
|
+
|
|
1587
|
+
:param payload: The raw hex payload
|
|
1588
|
+
:type payload: str
|
|
1589
|
+
:param msg: The message object
|
|
1590
|
+
:type msg: Message
|
|
1591
|
+
:return: A dictionary containing the system pressure in bar
|
|
1592
|
+
:rtype: PayDictT._1300
|
|
1593
|
+
"""
|
|
1187
1594
|
# 0x9F6 (2550 dec = 2.55 bar) appears to be a sentinel value
|
|
1188
1595
|
return {SZ_PRESSURE: None if payload[2:] == "09F6" else hex_to_temp(payload[2:])}
|
|
1189
1596
|
|
|
1190
1597
|
|
|
1191
1598
|
# programme_scheme, HVAC
|
|
1192
1599
|
def parser_1470(payload: str, msg: Message) -> dict[str, Any]:
|
|
1600
|
+
"""Parse the 1470 (programme_scheme) packet.
|
|
1601
|
+
|
|
1602
|
+
:param payload: The raw hex payload
|
|
1603
|
+
:type payload: str
|
|
1604
|
+
:param msg: The message object containing context
|
|
1605
|
+
:type msg: Message
|
|
1606
|
+
:return: A dictionary of the schedule scheme and daily setpoint count
|
|
1607
|
+
:rtype: dict[str, Any]
|
|
1608
|
+
:raises AssertionError: If the payload format or constants are unrecognized.
|
|
1609
|
+
"""
|
|
1193
1610
|
# Seen on Orcon: see 1470, 1F70, 22B0
|
|
1194
1611
|
|
|
1195
1612
|
SCHEDULE_SCHEME = {
|
|
@@ -1221,6 +1638,16 @@ def parser_1470(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1221
1638
|
|
|
1222
1639
|
# system_sync
|
|
1223
1640
|
def parser_1f09(payload: str, msg: Message) -> PayDictT._1F09:
|
|
1641
|
+
"""Parse the 1f09 (system_sync) packet.
|
|
1642
|
+
|
|
1643
|
+
:param payload: The raw hex payload
|
|
1644
|
+
:type payload: str
|
|
1645
|
+
:param msg: The message object containing context
|
|
1646
|
+
:type msg: Message
|
|
1647
|
+
:return: A dictionary with remaining seconds and the calculated next sync time
|
|
1648
|
+
:rtype: PayDictT._1F09
|
|
1649
|
+
:raises AssertionError: If the packet length is not 3.
|
|
1650
|
+
"""
|
|
1224
1651
|
# 22:51:19.287 067 I --- --:------ --:------ 12:193204 1F09 003 010A69
|
|
1225
1652
|
# 22:51:19.318 068 I --- --:------ --:------ 12:193204 2309 003 010866
|
|
1226
1653
|
# 22:51:19.321 067 I --- --:------ --:------ 12:193204 30C9 003 0108C3
|
|
@@ -1244,6 +1671,16 @@ def parser_1f09(payload: str, msg: Message) -> PayDictT._1F09:
|
|
|
1244
1671
|
|
|
1245
1672
|
# dhw_mode
|
|
1246
1673
|
def parser_1f41(payload: str, msg: Message) -> PayDictT._1F41:
|
|
1674
|
+
"""Parse the 1f41 (dhw_mode) packet.
|
|
1675
|
+
|
|
1676
|
+
:param payload: The raw hex payload
|
|
1677
|
+
:type payload: str
|
|
1678
|
+
:param msg: The message object
|
|
1679
|
+
:type msg: Message
|
|
1680
|
+
:return: A dictionary containing DHW mode, activity, and duration/until data
|
|
1681
|
+
:rtype: PayDictT._1F41
|
|
1682
|
+
:raises AssertionError: If payload constants or message lengths are invalid.
|
|
1683
|
+
"""
|
|
1247
1684
|
# 053 RP --- 01:145038 18:013393 --:------ 1F41 006 00FF00FFFFFF # no stored DHW
|
|
1248
1685
|
|
|
1249
1686
|
assert payload[4:6] in ZON_MODE_MAP, f"{payload[4:6]} (0xjj)"
|
|
@@ -1270,6 +1707,16 @@ def parser_1f41(payload: str, msg: Message) -> PayDictT._1F41:
|
|
|
1270
1707
|
|
|
1271
1708
|
# programme_config, HVAC
|
|
1272
1709
|
def parser_1f70(payload: str, msg: Message) -> dict[str, Any]:
|
|
1710
|
+
"""Parse the 1f70 (programme_config) packet.
|
|
1711
|
+
|
|
1712
|
+
:param payload: The raw hex payload
|
|
1713
|
+
:type payload: str
|
|
1714
|
+
:param msg: The message object containing context
|
|
1715
|
+
:type msg: Message
|
|
1716
|
+
:return: A dictionary containing schedule indices and start times
|
|
1717
|
+
:rtype: dict[str, Any]
|
|
1718
|
+
:raises AssertionError: If internal payload constraints are violated.
|
|
1719
|
+
"""
|
|
1273
1720
|
# Seen on Orcon: see 1470, 1F70, 22B0
|
|
1274
1721
|
|
|
1275
1722
|
try:
|
|
@@ -1306,6 +1753,18 @@ def parser_1f70(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1306
1753
|
|
|
1307
1754
|
# rf_bind
|
|
1308
1755
|
def parser_1fc9(payload: str, msg: Message) -> PayDictT._1FC9:
|
|
1756
|
+
"""Parse the 1fc9 (rf_bind) packet.
|
|
1757
|
+
|
|
1758
|
+
:param payload: The raw hex payload
|
|
1759
|
+
:type payload: str
|
|
1760
|
+
:param msg: The message object containing context
|
|
1761
|
+
:type msg: Message
|
|
1762
|
+
:return: A dictionary identifying the binding phase (Offer/Accept/Confirm) and bindings
|
|
1763
|
+
:rtype: PayDictT._1FC9
|
|
1764
|
+
:raises PacketPayloadInvalid: If the binding format is unknown.
|
|
1765
|
+
:raises AssertionError: If the payload length or constants are invalid.
|
|
1766
|
+
"""
|
|
1767
|
+
|
|
1309
1768
|
def _parser(seqx: str) -> list[str]:
|
|
1310
1769
|
if seqx[:2] not in ("90",):
|
|
1311
1770
|
assert (
|
|
@@ -1358,6 +1817,15 @@ def parser_1fc9(payload: str, msg: Message) -> PayDictT._1FC9:
|
|
|
1358
1817
|
|
|
1359
1818
|
# unknown_1fca, HVAC?
|
|
1360
1819
|
def parser_1fca(payload: str, msg: Message) -> Mapping[str, str]:
|
|
1820
|
+
"""Parse the 1fca packet.
|
|
1821
|
+
|
|
1822
|
+
:param payload: The raw hex payload
|
|
1823
|
+
:type payload: str
|
|
1824
|
+
:param msg: The message object
|
|
1825
|
+
:type msg: Message
|
|
1826
|
+
:return: A mapping of unknown identifiers and associated device IDs
|
|
1827
|
+
:rtype: Mapping[str, str]
|
|
1828
|
+
"""
|
|
1361
1829
|
# .W --- 30:248208 34:021943 --:------ 1FCA 009 00-01FF-7BC990-FFFFFF # sent x2
|
|
1362
1830
|
|
|
1363
1831
|
return {
|
|
@@ -1370,6 +1838,16 @@ def parser_1fca(payload: str, msg: Message) -> Mapping[str, str]:
|
|
|
1370
1838
|
|
|
1371
1839
|
# unknown_1fd0, from OTB
|
|
1372
1840
|
def parser_1fd0(payload: str, msg: Message) -> dict[str, Any]:
|
|
1841
|
+
"""Parse the 1fd0 (OpenTherm) packet.
|
|
1842
|
+
|
|
1843
|
+
:param payload: The raw hex payload
|
|
1844
|
+
:type payload: str
|
|
1845
|
+
:param msg: The message object
|
|
1846
|
+
:type msg: Message
|
|
1847
|
+
:return: A dictionary containing the raw payload
|
|
1848
|
+
:rtype: dict[str, Any]
|
|
1849
|
+
:raises AssertionError: If the payload does not match the expected null string.
|
|
1850
|
+
"""
|
|
1373
1851
|
assert payload == "0000000000000000", _INFORM_DEV_MSG
|
|
1374
1852
|
|
|
1375
1853
|
return {
|
|
@@ -1379,11 +1857,30 @@ def parser_1fd0(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1379
1857
|
|
|
1380
1858
|
# opentherm_sync, otb_sync
|
|
1381
1859
|
def parser_1fd4(payload: str, msg: Message) -> PayDictT._1FD4:
|
|
1860
|
+
"""Parse the 1fd4 (opentherm_sync) packet.
|
|
1861
|
+
|
|
1862
|
+
:param payload: The raw hex payload
|
|
1863
|
+
:type payload: str
|
|
1864
|
+
:param msg: The message object
|
|
1865
|
+
:type msg: Message
|
|
1866
|
+
:return: A dictionary containing the sync ticker value
|
|
1867
|
+
:rtype: PayDictT._1FD4
|
|
1868
|
+
"""
|
|
1382
1869
|
return {"ticker": int(payload[2:], 16)}
|
|
1383
1870
|
|
|
1384
1871
|
|
|
1385
1872
|
# WIP: HVAC auto requests (confirmed for Orcon, others?)
|
|
1386
1873
|
def parser_2210(payload: str, msg: Message) -> dict[str, Any]:
|
|
1874
|
+
"""Parse the 2210 (HVAC auto request) packet.
|
|
1875
|
+
|
|
1876
|
+
:param payload: The raw hex payload
|
|
1877
|
+
:type payload: str
|
|
1878
|
+
:param msg: The message object
|
|
1879
|
+
:type msg: Message
|
|
1880
|
+
:return: A dictionary of fan speed, request reason, and unknown flags
|
|
1881
|
+
:rtype: dict[str, Any]
|
|
1882
|
+
:raises AssertionError: If payload constants or internal consistency checks fail.
|
|
1883
|
+
"""
|
|
1387
1884
|
try:
|
|
1388
1885
|
assert msg.verb in (RP, I_) or payload == "00"
|
|
1389
1886
|
assert payload[10:12] == payload[38:40], (
|
|
@@ -1429,6 +1926,15 @@ def parser_2210(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1429
1926
|
|
|
1430
1927
|
# now_next_setpoint - Programmer/Hometronics
|
|
1431
1928
|
def parser_2249(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
1929
|
+
"""Parse the 2249 (now_next_setpoint) packet.
|
|
1930
|
+
|
|
1931
|
+
:param payload: The raw hex payload
|
|
1932
|
+
:type payload: str
|
|
1933
|
+
:param msg: The message object
|
|
1934
|
+
:type msg: Message
|
|
1935
|
+
:return: A dictionary or list of current/next setpoints and time remaining
|
|
1936
|
+
:rtype: dict | list[dict]
|
|
1937
|
+
"""
|
|
1432
1938
|
# see: https://github.com/jrosser/honeymon/blob/master/decoder.cpp#L357-L370
|
|
1433
1939
|
# .I --- 23:100224 --:------ 23:100224 2249 007 00-7EFF-7EFF-FFFF
|
|
1434
1940
|
|
|
@@ -1457,6 +1963,15 @@ def parser_2249(payload: str, msg: Message) -> dict | list[dict]: # TODO: only
|
|
|
1457
1963
|
|
|
1458
1964
|
# program_enabled, HVAC
|
|
1459
1965
|
def parser_22b0(payload: str, msg: Message) -> dict[str, Any]:
|
|
1966
|
+
"""Parse the 22b0 (program_enabled) packet.
|
|
1967
|
+
|
|
1968
|
+
:param payload: The raw hex payload
|
|
1969
|
+
:type payload: str
|
|
1970
|
+
:param msg: The message object
|
|
1971
|
+
:type msg: Message
|
|
1972
|
+
:return: A dictionary containing the program enabled status
|
|
1973
|
+
:rtype: dict[str, Any]
|
|
1974
|
+
"""
|
|
1460
1975
|
# Seen on Orcon: see 1470, 1F70, 22B0
|
|
1461
1976
|
|
|
1462
1977
|
# .W --- 37:171871 32:155617 --:------ 22B0 002 0005 # enable, calendar on
|
|
@@ -1472,6 +1987,16 @@ def parser_22b0(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1472
1987
|
|
|
1473
1988
|
# setpoint_bounds, TODO: max length = 24?
|
|
1474
1989
|
def parser_22c9(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
1990
|
+
"""Parse the 22c9 (setpoint_bounds) packet.
|
|
1991
|
+
|
|
1992
|
+
:param payload: The raw hex payload
|
|
1993
|
+
:type payload: str
|
|
1994
|
+
:param msg: The message object
|
|
1995
|
+
:type msg: Message
|
|
1996
|
+
:return: A dictionary or list containing mode and temperature bounds
|
|
1997
|
+
:rtype: dict | list[dict]
|
|
1998
|
+
:raises AssertionError: If the payload length or suffix is unrecognized.
|
|
1999
|
+
"""
|
|
1475
2000
|
# .I --- 02:001107 --:------ 02:001107 22C9 024 00-0834-0A28-01-0108340A2801-0208340A2801-0308340A2801 # noqa: E501
|
|
1476
2001
|
# .I --- 02:001107 --:------ 02:001107 22C9 006 04-0834-0A28-01
|
|
1477
2002
|
|
|
@@ -1505,6 +2030,17 @@ def parser_22c9(payload: str, msg: Message) -> dict | list[dict]: # TODO: only
|
|
|
1505
2030
|
|
|
1506
2031
|
# unknown_22d0, UFH system mode (heat/cool)
|
|
1507
2032
|
def parser_22d0(payload: str, msg: Message) -> dict[str, Any]:
|
|
2033
|
+
"""Parse the 22d0 (UFH system mode) packet.
|
|
2034
|
+
|
|
2035
|
+
:param payload: The raw hex payload
|
|
2036
|
+
:type payload: str
|
|
2037
|
+
:param msg: The message object
|
|
2038
|
+
:type msg: Message
|
|
2039
|
+
:return: A dictionary of UFH index, flags, and active modes
|
|
2040
|
+
:rtype: dict[str, Any]
|
|
2041
|
+
:raises AssertionError: If payload constants or flags are invalid.
|
|
2042
|
+
"""
|
|
2043
|
+
|
|
1508
2044
|
def _parser(seqx: str) -> dict:
|
|
1509
2045
|
# assert seqx[2:4] in ("00", "03", "10", "13", "14"), _INFORM_DEV_MSG
|
|
1510
2046
|
assert seqx[4:6] == "00", _INFORM_DEV_MSG
|
|
@@ -1527,11 +2063,32 @@ def parser_22d0(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1527
2063
|
|
|
1528
2064
|
# desired boiler setpoint
|
|
1529
2065
|
def parser_22d9(payload: str, msg: Message) -> PayDictT._22D9:
|
|
2066
|
+
"""Parse the 22d9 (desired boiler setpoint) packet.
|
|
2067
|
+
|
|
2068
|
+
:param payload: The raw hex payload
|
|
2069
|
+
:type payload: str
|
|
2070
|
+
:param msg: The message object
|
|
2071
|
+
:type msg: Message
|
|
2072
|
+
:return: A dictionary containing the target temperature setpoint
|
|
2073
|
+
:rtype: PayDictT._22D9
|
|
2074
|
+
"""
|
|
1530
2075
|
return {SZ_SETPOINT: hex_to_temp(payload[2:6])}
|
|
1531
2076
|
|
|
1532
2077
|
|
|
1533
2078
|
# WIP: unknown, HVAC
|
|
1534
2079
|
def parser_22e0(payload: str, msg: Message) -> Mapping[str, float | None]:
|
|
2080
|
+
"""Parse the 22e0 packet.
|
|
2081
|
+
|
|
2082
|
+
:param payload: The raw hex payload
|
|
2083
|
+
:type payload: str
|
|
2084
|
+
:param msg: The message object
|
|
2085
|
+
:type msg: Message
|
|
2086
|
+
:return: A mapping of percentage values extracted from the payload
|
|
2087
|
+
:rtype: Mapping[str, float | None]
|
|
2088
|
+
:raises AssertionError: If a value exceeds the expected 200 threshold.
|
|
2089
|
+
:raises ValueError: If the payload cannot be parsed as percentages.
|
|
2090
|
+
"""
|
|
2091
|
+
|
|
1535
2092
|
# RP --- 32:155617 18:005904 --:------ 22E0 004 00-34-A0-1E
|
|
1536
2093
|
# RP --- 32:153258 18:005904 --:------ 22E0 004 00-64-A0-1E
|
|
1537
2094
|
def _parser(seqx: str) -> float:
|
|
@@ -1553,6 +2110,15 @@ def parser_22e0(payload: str, msg: Message) -> Mapping[str, float | None]:
|
|
|
1553
2110
|
|
|
1554
2111
|
# WIP: unknown, HVAC
|
|
1555
2112
|
def parser_22e5(payload: str, msg: Message) -> Mapping[str, float | None]:
|
|
2113
|
+
"""Parse the 22e5 packet.
|
|
2114
|
+
|
|
2115
|
+
:param payload: The raw hex payload
|
|
2116
|
+
:type payload: str
|
|
2117
|
+
:param msg: The message object
|
|
2118
|
+
:type msg: Message
|
|
2119
|
+
:return: A mapping of percentage values extracted from the payload
|
|
2120
|
+
:rtype: Mapping[str, float | None]
|
|
2121
|
+
"""
|
|
1556
2122
|
# RP --- 32:153258 18:005904 --:------ 22E5 004 00-96-C8-14
|
|
1557
2123
|
# RP --- 32:155617 18:005904 --:------ 22E5 004 00-72-C8-14
|
|
1558
2124
|
|
|
@@ -1561,6 +2127,15 @@ def parser_22e5(payload: str, msg: Message) -> Mapping[str, float | None]:
|
|
|
1561
2127
|
|
|
1562
2128
|
# WIP: unknown, HVAC
|
|
1563
2129
|
def parser_22e9(payload: str, msg: Message) -> Mapping[str, float | str | None]:
|
|
2130
|
+
"""Parse the 22e9 packet.
|
|
2131
|
+
|
|
2132
|
+
:param payload: The raw hex payload
|
|
2133
|
+
:type payload: str
|
|
2134
|
+
:param msg: The message object
|
|
2135
|
+
:type msg: Message
|
|
2136
|
+
:return: A mapping of unknown identifiers or percentage values
|
|
2137
|
+
:rtype: Mapping[str, float | str | None]
|
|
2138
|
+
"""
|
|
1564
2139
|
if payload[2:4] == "01":
|
|
1565
2140
|
return {
|
|
1566
2141
|
"unknown_4": payload[4:6],
|
|
@@ -1571,6 +2146,16 @@ def parser_22e9(payload: str, msg: Message) -> Mapping[str, float | str | None]:
|
|
|
1571
2146
|
|
|
1572
2147
|
# fan_speed (switch_mode), HVAC
|
|
1573
2148
|
def parser_22f1(payload: str, msg: Message) -> dict[str, Any]:
|
|
2149
|
+
"""Parse the 22f1 (fan_speed) packet.
|
|
2150
|
+
|
|
2151
|
+
:param payload: The raw hex payload
|
|
2152
|
+
:type payload: str
|
|
2153
|
+
:param msg: The message object containing context
|
|
2154
|
+
:type msg: Message
|
|
2155
|
+
:return: A dictionary containing the fan mode, scheme, and internal indices
|
|
2156
|
+
:rtype: dict[str, Any]
|
|
2157
|
+
:raises AssertionError: If the fan mode or mode set is unrecognized.
|
|
2158
|
+
"""
|
|
1574
2159
|
try:
|
|
1575
2160
|
assert payload[0:2] in ("00", "63")
|
|
1576
2161
|
assert not payload[4:] or int(payload[2:4], 16) <= int(payload[4:], 16), (
|
|
@@ -1632,6 +2217,15 @@ def parser_22f1(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1632
2217
|
|
|
1633
2218
|
# WIP: unknown, HVAC (flow rate?)
|
|
1634
2219
|
def parser_22f2(payload: str, msg: Message) -> list: # TODO: only dict
|
|
2220
|
+
"""Parse the 22f2 (HVAC flow rate) packet.
|
|
2221
|
+
|
|
2222
|
+
:param payload: The raw hex payload
|
|
2223
|
+
:type payload: str
|
|
2224
|
+
:param msg: The message object containing context
|
|
2225
|
+
:type msg: Message
|
|
2226
|
+
:return: A list of dictionaries containing HVAC indices and measurements
|
|
2227
|
+
:rtype: list
|
|
2228
|
+
"""
|
|
1635
2229
|
# ClimeRad minibox uses 22F2 for speed feedback
|
|
1636
2230
|
|
|
1637
2231
|
def _parser(seqx: str) -> dict:
|
|
@@ -1647,6 +2241,16 @@ def parser_22f2(payload: str, msg: Message) -> list: # TODO: only dict
|
|
|
1647
2241
|
|
|
1648
2242
|
# fan_boost, HVAC
|
|
1649
2243
|
def parser_22f3(payload: str, msg: Message) -> dict[str, Any]:
|
|
2244
|
+
"""Parse the 22f3 (fan_boost) packet.
|
|
2245
|
+
|
|
2246
|
+
:param payload: The raw hex payload
|
|
2247
|
+
:type payload: str
|
|
2248
|
+
:param msg: The message object containing context
|
|
2249
|
+
:type msg: Message
|
|
2250
|
+
:return: A dictionary of boost settings, duration, and fan modes
|
|
2251
|
+
:rtype: dict[str, Any]
|
|
2252
|
+
:raises AssertionError: If internal payload structure is malformed.
|
|
2253
|
+
"""
|
|
1650
2254
|
# NOTE: for boost timer for high
|
|
1651
2255
|
try:
|
|
1652
2256
|
assert msg.len <= 7 or payload[14:] == "0000", f"byte 7: {payload[14:]}"
|
|
@@ -1700,6 +2304,16 @@ def parser_22f3(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1700
2304
|
|
|
1701
2305
|
# WIP: unknown, HVAC
|
|
1702
2306
|
def parser_22f4(payload: str, msg: Message) -> dict[str, Any]:
|
|
2307
|
+
"""Parse the 22f4 packet.
|
|
2308
|
+
|
|
2309
|
+
:param payload: The raw hex payload
|
|
2310
|
+
:type payload: str
|
|
2311
|
+
:param msg: The message object containing context
|
|
2312
|
+
:type msg: Message
|
|
2313
|
+
:return: A dictionary containing interpreted fan mode and rate
|
|
2314
|
+
:rtype: dict[str, Any]
|
|
2315
|
+
:raises AssertionError: If the extracted mode or rate is invalid.
|
|
2316
|
+
"""
|
|
1703
2317
|
if msg.len == 13 and payload[14:] == "000000000000":
|
|
1704
2318
|
# ClimaRad Ventura fan & remote
|
|
1705
2319
|
_pl = payload[:4] + payload[12:14] if payload[10:12] == "00" else payload[8:14]
|
|
@@ -1734,6 +2348,15 @@ def parser_22f4(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1734
2348
|
|
|
1735
2349
|
# bypass_mode, HVAC
|
|
1736
2350
|
def parser_22f7(payload: str, msg: Message) -> dict[str, Any]:
|
|
2351
|
+
"""Parse the 22f7 (bypass_mode) packet.
|
|
2352
|
+
|
|
2353
|
+
:param payload: The raw hex payload
|
|
2354
|
+
:type payload: str
|
|
2355
|
+
:param msg: The message object containing context
|
|
2356
|
+
:type msg: Message
|
|
2357
|
+
:return: A dictionary of bypass mode, state, and position
|
|
2358
|
+
:rtype: dict[str, Any]
|
|
2359
|
+
"""
|
|
1737
2360
|
result = {
|
|
1738
2361
|
SZ_BYPASS_MODE: {"00": "off", "C8": "on", "FF": "auto"}.get(payload[2:4]),
|
|
1739
2362
|
}
|
|
@@ -1746,6 +2369,15 @@ def parser_22f7(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1746
2369
|
|
|
1747
2370
|
# WIP: unknown_mode, HVAC
|
|
1748
2371
|
def parser_22f8(payload: str, msg: Message) -> dict[str, Any]:
|
|
2372
|
+
"""Parse the 22f8 packet.
|
|
2373
|
+
|
|
2374
|
+
:param payload: The raw hex payload
|
|
2375
|
+
:type payload: str
|
|
2376
|
+
:param msg: The message object containing context
|
|
2377
|
+
:type msg: Message
|
|
2378
|
+
:return: A dictionary of raw internal values
|
|
2379
|
+
:rtype: dict[str, Any]
|
|
2380
|
+
"""
|
|
1749
2381
|
# from: https://github.com/arjenhiemstra/ithowifi/blob/master/software/NRG_itho_wifi/src/IthoPacket.h
|
|
1750
2382
|
|
|
1751
2383
|
# message command bytes specific for AUTO RFT (536-0150)
|
|
@@ -1766,6 +2398,15 @@ def parser_22f8(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1766
2398
|
def parser_2309(
|
|
1767
2399
|
payload: str, msg: Message
|
|
1768
2400
|
) -> PayDictT._2309 | list[PayDictT._2309] | PayDictT.EMPTY:
|
|
2401
|
+
"""Parse the 2309 (setpoint) packet.
|
|
2402
|
+
|
|
2403
|
+
:param payload: The raw hex payload
|
|
2404
|
+
:type payload: str
|
|
2405
|
+
:param msg: The message object containing context
|
|
2406
|
+
:type msg: Message
|
|
2407
|
+
:return: A setpoint dictionary, list of setpoints, or an empty dictionary
|
|
2408
|
+
:rtype: PayDictT._2309 | list[PayDictT._2309] | PayDictT.EMPTY
|
|
2409
|
+
"""
|
|
1769
2410
|
if msg._has_array:
|
|
1770
2411
|
return [
|
|
1771
2412
|
{
|
|
@@ -1784,6 +2425,16 @@ def parser_2309(
|
|
|
1784
2425
|
|
|
1785
2426
|
# zone_mode # TODO: messy
|
|
1786
2427
|
def parser_2349(payload: str, msg: Message) -> PayDictT._2349 | PayDictT.EMPTY:
|
|
2428
|
+
"""Parse the 2349 (zone_mode) packet.
|
|
2429
|
+
|
|
2430
|
+
:param payload: The raw hex payload
|
|
2431
|
+
:type payload: str
|
|
2432
|
+
:param msg: The message object containing context
|
|
2433
|
+
:type msg: Message
|
|
2434
|
+
:return: A dictionary containing zone mode, setpoint, and override details
|
|
2435
|
+
:rtype: PayDictT._2349 | PayDictT.EMPTY
|
|
2436
|
+
:raises AssertionError: If the message length or mode is invalid.
|
|
2437
|
+
"""
|
|
1787
2438
|
# RQ --- 34:225071 30:258557 --:------ 2349 001 00
|
|
1788
2439
|
# RP --- 30:258557 34:225071 --:------ 2349 013 007FFF00FFFFFFFFFFFFFFFFFF
|
|
1789
2440
|
# RP --- 30:253184 34:010943 --:------ 2349 013 00064000FFFFFF00110E0507E5
|
|
@@ -1823,6 +2474,15 @@ def parser_2349(payload: str, msg: Message) -> PayDictT._2349 | PayDictT.EMPTY:
|
|
|
1823
2474
|
|
|
1824
2475
|
# unknown_2389, from 03:
|
|
1825
2476
|
def parser_2389(payload: str, msg: Message) -> dict[str, Any]:
|
|
2477
|
+
"""Parse the 2389 packet.
|
|
2478
|
+
|
|
2479
|
+
:param payload: The raw hex payload
|
|
2480
|
+
:type payload: str
|
|
2481
|
+
:param msg: The message object containing context
|
|
2482
|
+
:type msg: Message
|
|
2483
|
+
:return: A dictionary containing an unknown temperature measurement
|
|
2484
|
+
:rtype: dict[str, Any]
|
|
2485
|
+
"""
|
|
1826
2486
|
return {
|
|
1827
2487
|
"_unknown": hex_to_temp(payload[2:6]),
|
|
1828
2488
|
}
|
|
@@ -1830,6 +2490,15 @@ def parser_2389(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1830
2490
|
|
|
1831
2491
|
# unknown_2400, from OTB, FAN
|
|
1832
2492
|
def parser_2400(payload: str, msg: Message) -> dict[str, Any]:
|
|
2493
|
+
"""Parse the 2400 packet.
|
|
2494
|
+
|
|
2495
|
+
:param payload: The raw hex payload
|
|
2496
|
+
:type payload: str
|
|
2497
|
+
:param msg: The message object containing context
|
|
2498
|
+
:type msg: Message
|
|
2499
|
+
:return: A dictionary containing the raw payload
|
|
2500
|
+
:rtype: dict[str, Any]
|
|
2501
|
+
"""
|
|
1833
2502
|
# RP --- 32:155617 18:005904 --:------ 2400 045 00001111-1010929292921110101020110010000080100010100000009191111191910011119191111111111100 # Orcon FAN
|
|
1834
2503
|
# RP --- 10:048122 18:006402 --:------ 2400 004 0000000F
|
|
1835
2504
|
# assert payload == "0000000F", _INFORM_DEV_MSG
|
|
@@ -1841,6 +2510,16 @@ def parser_2400(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1841
2510
|
|
|
1842
2511
|
# unknown_2401, from OTB
|
|
1843
2512
|
def parser_2401(payload: str, msg: Message) -> dict[str, Any]:
|
|
2513
|
+
"""Parse the 2401 packet.
|
|
2514
|
+
|
|
2515
|
+
:param payload: The raw hex payload
|
|
2516
|
+
:type payload: str
|
|
2517
|
+
:param msg: The message object containing context
|
|
2518
|
+
:type msg: Message
|
|
2519
|
+
:return: A dictionary of decoded flags and valve demand
|
|
2520
|
+
:rtype: dict[str, Any]
|
|
2521
|
+
:raises AssertionError: If payload constants or bit flags are unrecognized.
|
|
2522
|
+
"""
|
|
1844
2523
|
try:
|
|
1845
2524
|
assert payload[2:4] == "00", f"byte 1: {payload[2:4]}"
|
|
1846
2525
|
assert int(payload[4:6], 16) & 0b11110000 == 0, (
|
|
@@ -1859,6 +2538,16 @@ def parser_2401(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1859
2538
|
|
|
1860
2539
|
# unknown_2410, from OTB, FAN
|
|
1861
2540
|
def parser_2410(payload: str, msg: Message) -> dict[str, Any]:
|
|
2541
|
+
"""Parse the 2410 packet.
|
|
2542
|
+
|
|
2543
|
+
:param payload: The raw hex payload
|
|
2544
|
+
:type payload: str
|
|
2545
|
+
:param msg: The message object containing context
|
|
2546
|
+
:type msg: Message
|
|
2547
|
+
:return: A dictionary of current, min, and max values and metadata
|
|
2548
|
+
:rtype: dict[str, Any]
|
|
2549
|
+
:raises AssertionError: If the payload format does not match expected constants.
|
|
2550
|
+
"""
|
|
1862
2551
|
# RP --- 10:048122 18:006402 --:------ 2410 020 00-00000000-00000000-00000001-00000001-00000C # OTB
|
|
1863
2552
|
# RP --- 32:155617 18:005904 --:------ 2410 020 00-00003EE8-00000000-FFFFFFFF-00000000-1002A6 # Orcon Fan
|
|
1864
2553
|
|
|
@@ -1895,6 +2584,15 @@ def parser_2410(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1895
2584
|
|
|
1896
2585
|
# fan_params, HVAC
|
|
1897
2586
|
def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
|
|
2587
|
+
"""Parse the 2411 (fan_params) packet.
|
|
2588
|
+
|
|
2589
|
+
:param payload: The raw hex payload
|
|
2590
|
+
:type payload: str
|
|
2591
|
+
:param msg: The message object containing context
|
|
2592
|
+
:type msg: Message
|
|
2593
|
+
:return: A dictionary containing the parameter ID, description, and decoded value
|
|
2594
|
+
:rtype: dict[str, Any]
|
|
2595
|
+
"""
|
|
1898
2596
|
# There is a relationship between 0001 and 2411
|
|
1899
2597
|
# RQ --- 37:171871 32:155617 --:------ 0001 005 0020000A04
|
|
1900
2598
|
# RP --- 32:155617 37:171871 --:------ 0001 008 0020000A004E0B00 # 0A -> 2411|4E
|
|
@@ -1997,6 +2695,16 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1997
2695
|
|
|
1998
2696
|
# unknown_2420, from OTB
|
|
1999
2697
|
def parser_2420(payload: str, msg: Message) -> dict[str, Any]:
|
|
2698
|
+
"""Parse the 2420 (OpenTherm) packet.
|
|
2699
|
+
|
|
2700
|
+
:param payload: The raw hex payload
|
|
2701
|
+
:type payload: str
|
|
2702
|
+
:param msg: The message object containing context
|
|
2703
|
+
:type msg: Message
|
|
2704
|
+
:return: A dictionary containing the raw payload
|
|
2705
|
+
:rtype: dict[str, Any]
|
|
2706
|
+
:raises AssertionError: If the payload does not match the expected constant string.
|
|
2707
|
+
"""
|
|
2000
2708
|
assert payload == "00000010" + "00" * 34, _INFORM_DEV_MSG
|
|
2001
2709
|
|
|
2002
2710
|
return {
|
|
@@ -2006,6 +2714,16 @@ def parser_2420(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2006
2714
|
|
|
2007
2715
|
# _state (of cooling?), from BDR91T, hometronics CTL
|
|
2008
2716
|
def parser_2d49(payload: str, msg: Message) -> PayDictT._2D49:
|
|
2717
|
+
"""Parse the 2d49 packet.
|
|
2718
|
+
|
|
2719
|
+
:param payload: The raw hex payload
|
|
2720
|
+
:type payload: str
|
|
2721
|
+
:param msg: The message object containing context
|
|
2722
|
+
:type msg: Message
|
|
2723
|
+
:return: A dictionary containing the boolean state
|
|
2724
|
+
:rtype: PayDictT._2D49
|
|
2725
|
+
:raises AssertionError: If the payload state bytes are unrecognized.
|
|
2726
|
+
"""
|
|
2009
2727
|
assert payload[2:] in ("0000", "00FF", "C800", "C8FF"), _INFORM_DEV_MSG
|
|
2010
2728
|
|
|
2011
2729
|
return {
|
|
@@ -2015,6 +2733,16 @@ def parser_2d49(payload: str, msg: Message) -> PayDictT._2D49:
|
|
|
2015
2733
|
|
|
2016
2734
|
# system_mode
|
|
2017
2735
|
def parser_2e04(payload: str, msg: Message) -> PayDictT._2E04:
|
|
2736
|
+
"""Parse the 2e04 (system_mode) packet.
|
|
2737
|
+
|
|
2738
|
+
:param payload: The raw hex payload
|
|
2739
|
+
:type payload: str
|
|
2740
|
+
:param msg: The message object containing context
|
|
2741
|
+
:type msg: Message
|
|
2742
|
+
:return: A dictionary containing the system mode and optional duration
|
|
2743
|
+
:rtype: PayDictT._2E04
|
|
2744
|
+
:raises AssertionError: If the system mode or packet length is invalid.
|
|
2745
|
+
"""
|
|
2018
2746
|
# if msg.verb == W_:
|
|
2019
2747
|
|
|
2020
2748
|
# .I --— 01:020766 --:------ 01:020766 2E04 016 FFFFFFFFFFFFFF0007FFFFFFFFFFFF04 # Manual # noqa: E501
|
|
@@ -2049,6 +2777,16 @@ def parser_2e04(payload: str, msg: Message) -> PayDictT._2E04:
|
|
|
2049
2777
|
|
|
2050
2778
|
# presence_detect, HVAC sensor, or Timed boost for Vasco D60
|
|
2051
2779
|
def parser_2e10(payload: str, msg: Message) -> dict[str, Any]:
|
|
2780
|
+
"""Parse the 2e10 packet.
|
|
2781
|
+
|
|
2782
|
+
:param payload: The raw hex payload
|
|
2783
|
+
:type payload: str
|
|
2784
|
+
:param msg: The message object containing context
|
|
2785
|
+
:type msg: Message
|
|
2786
|
+
:return: A dictionary defining if presence is detected
|
|
2787
|
+
:rtype: dict[str, Any]
|
|
2788
|
+
:raises AssertionError: If the payload is not in a recognized format.
|
|
2789
|
+
"""
|
|
2052
2790
|
assert payload in ("0001", "000000", "000100"), _INFORM_DEV_MSG
|
|
2053
2791
|
presence: int = int(payload[2:4])
|
|
2054
2792
|
return {
|
|
@@ -2059,6 +2797,15 @@ def parser_2e10(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2059
2797
|
|
|
2060
2798
|
# current temperature (of device, zone/s)
|
|
2061
2799
|
def parser_30c9(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
2800
|
+
"""Parse the 30c9 (temperature) packet.
|
|
2801
|
+
|
|
2802
|
+
:param payload: The raw hex payload
|
|
2803
|
+
:type payload: str
|
|
2804
|
+
:param msg: The message object containing context
|
|
2805
|
+
:type msg: Message
|
|
2806
|
+
:return: A dictionary or list of temperatures by zone index
|
|
2807
|
+
:rtype: dict | list[dict]
|
|
2808
|
+
"""
|
|
2062
2809
|
if msg._has_array:
|
|
2063
2810
|
return [
|
|
2064
2811
|
{
|
|
@@ -2073,6 +2820,16 @@ def parser_30c9(payload: str, msg: Message) -> dict | list[dict]: # TODO: only
|
|
|
2073
2820
|
|
|
2074
2821
|
# ufc_demand, HVAC (Itho autotemp / spider)
|
|
2075
2822
|
def parser_3110(payload: str, msg: Message) -> PayDictT._3110:
|
|
2823
|
+
"""Parse the 3110 (ufc_demand) packet.
|
|
2824
|
+
|
|
2825
|
+
:param payload: The raw hex payload
|
|
2826
|
+
:type payload: str
|
|
2827
|
+
:param msg: The message object containing context
|
|
2828
|
+
:type msg: Message
|
|
2829
|
+
:return: A dictionary containing the operating mode and demand percentage
|
|
2830
|
+
:rtype: PayDictT._3110
|
|
2831
|
+
:raises AssertionError: If payload constants or demand values are invalid.
|
|
2832
|
+
"""
|
|
2076
2833
|
# .I --- 02:250708 --:------ 02:250708 3110 004 0000C820 # cooling, 100%
|
|
2077
2834
|
# .I --- 21:042656 --:------ 21:042656 3110 004 00000010 # heating, 0%
|
|
2078
2835
|
|
|
@@ -2105,6 +2862,16 @@ def parser_3110(payload: str, msg: Message) -> PayDictT._3110:
|
|
|
2105
2862
|
|
|
2106
2863
|
# unknown_3120, from STA, FAN
|
|
2107
2864
|
def parser_3120(payload: str, msg: Message) -> dict[str, Any]:
|
|
2865
|
+
"""Parse the 3120 packet.
|
|
2866
|
+
|
|
2867
|
+
:param payload: The raw hex payload
|
|
2868
|
+
:type payload: str
|
|
2869
|
+
:param msg: The message object containing context
|
|
2870
|
+
:type msg: Message
|
|
2871
|
+
:return: A dictionary of raw internal segments
|
|
2872
|
+
:rtype: dict[str, Any]
|
|
2873
|
+
:raises AssertionError: If individual byte segments fail validation.
|
|
2874
|
+
"""
|
|
2108
2875
|
# .I --- 34:136285 --:------ 34:136285 3120 007 0070B0000000FF # every ~3:45:00!
|
|
2109
2876
|
# RP --- 20:008749 18:142609 --:------ 3120 007 0070B000009CFF
|
|
2110
2877
|
# .I --- 37:258565 --:------ 37:258565 3120 007 0080B0010003FF
|
|
@@ -2129,6 +2896,16 @@ def parser_3120(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2129
2896
|
|
|
2130
2897
|
# WIP: unknown, HVAC
|
|
2131
2898
|
def parser_313e(payload: str, msg: Message) -> dict[str, Any]:
|
|
2899
|
+
"""Parse the 313e packet.
|
|
2900
|
+
|
|
2901
|
+
:param payload: The raw hex payload
|
|
2902
|
+
:type payload: str
|
|
2903
|
+
:param msg: The message object containing context
|
|
2904
|
+
:type msg: Message
|
|
2905
|
+
:return: A dictionary containing calculated Zulu time and raw internal values
|
|
2906
|
+
:rtype: dict[str, Any]
|
|
2907
|
+
:raises AssertionError: If the payload prefix or expected constant suffix is invalid.
|
|
2908
|
+
"""
|
|
2132
2909
|
assert payload[:2] == "00"
|
|
2133
2910
|
assert payload[12:] == "003C800000"
|
|
2134
2911
|
|
|
@@ -2146,6 +2923,16 @@ def parser_313e(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2146
2923
|
|
|
2147
2924
|
# datetime
|
|
2148
2925
|
def parser_313f(payload: str, msg: Message) -> PayDictT._313F: # TODO: look for TZ
|
|
2926
|
+
"""Parse the 313f (datetime) packet.
|
|
2927
|
+
|
|
2928
|
+
:param payload: The raw hex payload
|
|
2929
|
+
:type payload: str
|
|
2930
|
+
:param msg: The message object containing context
|
|
2931
|
+
:type msg: Message
|
|
2932
|
+
:return: A dictionary containing the datetime and DST flag
|
|
2933
|
+
:rtype: PayDictT._313F
|
|
2934
|
+
:raises AssertionError: If the payload context is unexpected for the source device type.
|
|
2935
|
+
"""
|
|
2149
2936
|
# 2020-03-28T03:59:21.315178 045 RP --- 01:158182 04:136513 --:------ 313F 009 00FC3500A41C0307E4
|
|
2150
2937
|
# 2020-03-29T04:58:30.486343 045 RP --- 01:158182 04:136485 --:------ 313F 009 00FC8400C51D0307E4
|
|
2151
2938
|
# 2022-09-20T20:50:32.800676 065 RP --- 01:182924 18:068640 --:------ 313F 009 00F9203234140907E6
|
|
@@ -2178,6 +2965,15 @@ def parser_313f(payload: str, msg: Message) -> PayDictT._313F: # TODO: look for
|
|
|
2178
2965
|
|
|
2179
2966
|
# heat_demand (of device, FC domain) - valve status (%open)
|
|
2180
2967
|
def parser_3150(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
2968
|
+
"""Parse the 3150 (heat_demand) packet.
|
|
2969
|
+
|
|
2970
|
+
:param payload: The raw hex payload
|
|
2971
|
+
:type payload: str
|
|
2972
|
+
:param msg: The message object containing context
|
|
2973
|
+
:type msg: Message
|
|
2974
|
+
:return: A dictionary or list of dictionaries containing zone indices and valve demand
|
|
2975
|
+
:rtype: dict | list[dict]
|
|
2976
|
+
"""
|
|
2181
2977
|
# event-driven, and periodically; FC domain is maximum of all zones
|
|
2182
2978
|
# TODO: all have a valid domain will UFC/CTL respond to an RQ, for FC, for a zone?
|
|
2183
2979
|
|
|
@@ -2202,6 +2998,16 @@ def parser_3150(payload: str, msg: Message) -> dict | list[dict]: # TODO: only
|
|
|
2202
2998
|
|
|
2203
2999
|
# fan state (ventilation status), HVAC
|
|
2204
3000
|
def parser_31d9(payload: str, msg: Message) -> dict[str, Any]:
|
|
3001
|
+
"""Parse the 31d9 (fan state) packet.
|
|
3002
|
+
|
|
3003
|
+
:param payload: The raw hex payload
|
|
3004
|
+
:type payload: str
|
|
3005
|
+
:param msg: The message object containing context
|
|
3006
|
+
:type msg: Message
|
|
3007
|
+
:return: A dictionary containing fan mode, speed, and status flags
|
|
3008
|
+
:rtype: dict[str, Any]
|
|
3009
|
+
:raises AssertionError: If payload constants or byte segments fail validation.
|
|
3010
|
+
"""
|
|
2205
3011
|
# NOTE: Itho and ClimaRad use 0x00-C8 for %, whilst Nuaire uses 0x00-64
|
|
2206
3012
|
try:
|
|
2207
3013
|
assert payload[4:6] == "FF" or int(payload[4:6], 16) <= 200, (
|
|
@@ -2270,6 +3076,15 @@ def parser_31d9(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2270
3076
|
|
|
2271
3077
|
# ventilation state (extended), HVAC
|
|
2272
3078
|
def parser_31da(payload: str, msg: Message) -> PayDictT._31DA:
|
|
3079
|
+
"""Parse the 31da (extended ventilation state) packet.
|
|
3080
|
+
|
|
3081
|
+
:param payload: The raw hex payload
|
|
3082
|
+
:type payload: str
|
|
3083
|
+
:param msg: The message object containing context
|
|
3084
|
+
:type msg: Message
|
|
3085
|
+
:return: A dictionary of all decoded ventilation parameters
|
|
3086
|
+
:rtype: PayDictT._31DA
|
|
3087
|
+
"""
|
|
2273
3088
|
# see: https://github.com/python/typing/issues/1445
|
|
2274
3089
|
result = {
|
|
2275
3090
|
**parse_exhaust_fan_speed(payload[38:40]), # maybe 31D9[4:6] for some?
|
|
@@ -2316,14 +3131,20 @@ def parser_31da(payload: str, msg: Message) -> PayDictT._31DA:
|
|
|
2316
3131
|
|
|
2317
3132
|
# vent_demand, HVAC
|
|
2318
3133
|
def parser_31e0(payload: str, msg: Message) -> dict | list[dict]: # TODO: only dict
|
|
2319
|
-
"""
|
|
2320
|
-
|
|
3134
|
+
"""Parse the 31e0 (vent_demand) packet.
|
|
2321
3135
|
"van" means "of".
|
|
2322
3136
|
- 0 = min. van min. potm would be:
|
|
2323
3137
|
- 0 = minimum of minimum potentiometer
|
|
2324
3138
|
|
|
2325
3139
|
See: https://www.industrialcontrolsonline.com/honeywell-t991a
|
|
2326
3140
|
- modulates air temperatures in ducts
|
|
3141
|
+
:param payload: The raw hex payload
|
|
3142
|
+
:type payload: str
|
|
3143
|
+
:param msg: The message object containing context
|
|
3144
|
+
:type msg: Message
|
|
3145
|
+
:return: A dictionary or list of dictionaries containing flags and demand percentage
|
|
3146
|
+
:rtype: dict | list[dict]
|
|
3147
|
+
:raises AssertionError: If the payload suffix is not a recognized constant.
|
|
2327
3148
|
"""
|
|
2328
3149
|
|
|
2329
3150
|
# coding note:
|
|
@@ -2386,16 +3207,45 @@ def parser_31e0(payload: str, msg: Message) -> dict | list[dict]: # TODO: only
|
|
|
2386
3207
|
|
|
2387
3208
|
# supplied boiler water (flow) temp
|
|
2388
3209
|
def parser_3200(payload: str, msg: Message) -> PayDictT._3200:
|
|
3210
|
+
"""Parse the 3200 (supplied_temp) packet.
|
|
3211
|
+
|
|
3212
|
+
:param payload: The raw hex payload
|
|
3213
|
+
:type payload: str
|
|
3214
|
+
:param msg: The message object containing context
|
|
3215
|
+
:type msg: Message
|
|
3216
|
+
:return: A dictionary containing the water flow temperature
|
|
3217
|
+
:rtype: PayDictT._3200
|
|
3218
|
+
"""
|
|
2389
3219
|
return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
|
|
2390
3220
|
|
|
2391
3221
|
|
|
2392
3222
|
# return (boiler) water temp
|
|
2393
3223
|
def parser_3210(payload: str, msg: Message) -> PayDictT._3210:
|
|
3224
|
+
"""Parse the 3210 (return_temp) packet.
|
|
3225
|
+
|
|
3226
|
+
:param payload: The raw hex payload
|
|
3227
|
+
:type payload: str
|
|
3228
|
+
:param msg: The message object containing context
|
|
3229
|
+
:type msg: Message
|
|
3230
|
+
:return: A dictionary containing the return water temperature
|
|
3231
|
+
:rtype: PayDictT._3210
|
|
3232
|
+
"""
|
|
2394
3233
|
return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
|
|
2395
3234
|
|
|
2396
3235
|
|
|
2397
3236
|
# opentherm_msg, from OTB (and OT_RND)
|
|
2398
3237
|
def parser_3220(payload: str, msg: Message) -> dict[str, Any]:
|
|
3238
|
+
"""Parse an OpenTherm message packet.
|
|
3239
|
+
|
|
3240
|
+
:param payload: The raw hex payload
|
|
3241
|
+
:type payload: str
|
|
3242
|
+
:param msg: The message object containing context
|
|
3243
|
+
:type msg: Message
|
|
3244
|
+
:return: A dictionary of decoded OpenTherm data and descriptions
|
|
3245
|
+
:rtype: dict[str, Any]
|
|
3246
|
+
:raises AssertionError: If internal OpenTherm consistency checks fail.
|
|
3247
|
+
:raises PacketPayloadInvalid: If the OpenTherm frame is malformed or uses unknown IDs.
|
|
3248
|
+
"""
|
|
2399
3249
|
try:
|
|
2400
3250
|
ot_type, ot_id, ot_value, ot_schema = decode_frame(payload[2:10])
|
|
2401
3251
|
except AssertionError as err:
|
|
@@ -2479,6 +3329,16 @@ def parser_3220(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2479
3329
|
|
|
2480
3330
|
# unknown_3221, from OTB, FAN
|
|
2481
3331
|
def parser_3221(payload: str, msg: Message) -> dict[str, Any]:
|
|
3332
|
+
"""Parse the 3221 packet.
|
|
3333
|
+
|
|
3334
|
+
:param payload: The raw hex payload
|
|
3335
|
+
:type payload: str
|
|
3336
|
+
:param msg: The message object containing context
|
|
3337
|
+
:type msg: Message
|
|
3338
|
+
:return: A dictionary containing the extracted numeric value
|
|
3339
|
+
:rtype: dict[str, Any]
|
|
3340
|
+
:raises AssertionError: If the extracted value exceeds the valid 0xC8 threshold.
|
|
3341
|
+
"""
|
|
2482
3342
|
# RP --- 10:052644 18:198151 --:------ 3221 002 000F
|
|
2483
3343
|
# RP --- 10:048122 18:006402 --:------ 3221 002 0000
|
|
2484
3344
|
# RP --- 32:155617 18:005904 --:------ 3221 002 000A
|
|
@@ -2493,6 +3353,16 @@ def parser_3221(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2493
3353
|
|
|
2494
3354
|
# WIP: unknown, HVAC
|
|
2495
3355
|
def parser_3222(payload: str, msg: Message) -> dict[str, Any]:
|
|
3356
|
+
"""Parse the 3222 packet.
|
|
3357
|
+
|
|
3358
|
+
:param payload: The raw hex payload
|
|
3359
|
+
:type payload: str
|
|
3360
|
+
:param msg: The message object containing context
|
|
3361
|
+
:type msg: Message
|
|
3362
|
+
:return: A dictionary containing offset, length, and raw data
|
|
3363
|
+
:rtype: dict[str, Any]
|
|
3364
|
+
:raises AssertionError: If the payload prefix is not '00'.
|
|
3365
|
+
"""
|
|
2496
3366
|
assert payload[:2] == "00"
|
|
2497
3367
|
|
|
2498
3368
|
# e.g. RP|3222|00FE00 (payload = 3 bytes)
|
|
@@ -2513,6 +3383,16 @@ def parser_3222(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2513
3383
|
|
|
2514
3384
|
# unknown_3223, from OTB
|
|
2515
3385
|
def parser_3223(payload: str, msg: Message) -> dict[str, Any]:
|
|
3386
|
+
"""Parse the 3223 (OpenTherm) packet.
|
|
3387
|
+
|
|
3388
|
+
:param payload: The raw hex payload
|
|
3389
|
+
:type payload: str
|
|
3390
|
+
:param msg: The message object containing context
|
|
3391
|
+
:type msg: Message
|
|
3392
|
+
:return: A dictionary containing the extracted value
|
|
3393
|
+
:rtype: dict[str, Any]
|
|
3394
|
+
:raises AssertionError: If the value exceeds the valid 0xC8 threshold.
|
|
3395
|
+
"""
|
|
2516
3396
|
assert int(payload[2:], 16) <= 0xC8, _INFORM_DEV_MSG
|
|
2517
3397
|
|
|
2518
3398
|
return {
|
|
@@ -2523,9 +3403,10 @@ def parser_3223(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2523
3403
|
|
|
2524
3404
|
# actuator_sync (aka sync_tpi: TPI cycle sync)
|
|
2525
3405
|
def parser_3b00(payload: str, msg: Message) -> PayDictT._3B00:
|
|
2526
|
-
# system timing master: the device that sends I/FCC8 pkt controls the heater relay
|
|
2527
3406
|
"""Decode a 3B00 packet (actuator_sync).
|
|
2528
3407
|
|
|
3408
|
+
This signal marks the start or end of a TPI cycle to synchronize relay behavior.
|
|
3409
|
+
|
|
2529
3410
|
The heat relay regularly broadcasts a 3B00 at the end(?) of every TPI cycle, the
|
|
2530
3411
|
frequency of which is determined by the (TPI) cycle rate in 1100.
|
|
2531
3412
|
|
|
@@ -2533,7 +3414,16 @@ def parser_3b00(payload: str, msg: Message) -> PayDictT._3B00:
|
|
|
2533
3414
|
|
|
2534
3415
|
The OTB does not send these packets, but the CTL sends a regular broadcast anyway
|
|
2535
3416
|
for the benefit of any zone actuators (e.g. zone valve zones).
|
|
3417
|
+
|
|
3418
|
+
:param payload: The raw hex payload
|
|
3419
|
+
:type payload: str
|
|
3420
|
+
:param msg: The message object containing context
|
|
3421
|
+
:type msg: Message
|
|
3422
|
+
:return: A dictionary containing the sync state and domain ID
|
|
3423
|
+
:rtype: PayDictT._3B00
|
|
3424
|
+
:raises AssertionError: If the payload length or constants are invalid for the device type.
|
|
2536
3425
|
"""
|
|
3426
|
+
# system timing master: the device that sends I/FCC8 pkt controls the heater relay
|
|
2537
3427
|
|
|
2538
3428
|
# 053 I --- 13:209679 --:------ 13:209679 3B00 002 00C8
|
|
2539
3429
|
# 045 I --- 01:158182 --:------ 01:158182 3B00 002 FCC8
|
|
@@ -2570,6 +3460,16 @@ def parser_3b00(payload: str, msg: Message) -> PayDictT._3B00:
|
|
|
2570
3460
|
|
|
2571
3461
|
# actuator_state
|
|
2572
3462
|
def parser_3ef0(payload: str, msg: Message) -> PayDictT._3EF0 | PayDictT._JASPER:
|
|
3463
|
+
"""Parse the 3ef0 (actuator_state) packet.
|
|
3464
|
+
|
|
3465
|
+
:param payload: The raw hex payload
|
|
3466
|
+
:type payload: str
|
|
3467
|
+
:param msg: The message object containing context
|
|
3468
|
+
:type msg: Message
|
|
3469
|
+
:return: A dictionary of modulation levels, flags, and setpoints
|
|
3470
|
+
:rtype: PayDictT._3EF0 | PayDictT._JASPER
|
|
3471
|
+
:raises AssertionError: If payload constants, flags, or message lengths are unrecognized.
|
|
3472
|
+
"""
|
|
2573
3473
|
result: dict[str, Any]
|
|
2574
3474
|
|
|
2575
3475
|
if msg.src.type == DEV_TYPE_MAP.JIM: # Honeywell Jasper
|
|
@@ -2672,6 +3572,16 @@ def parser_3ef0(payload: str, msg: Message) -> PayDictT._3EF0 | PayDictT._JASPER
|
|
|
2672
3572
|
|
|
2673
3573
|
# actuator_cycle
|
|
2674
3574
|
def parser_3ef1(payload: str, msg: Message) -> PayDictT._3EF1 | PayDictT._JASPER:
|
|
3575
|
+
"""Parse the 3ef1 (actuator_cycle) packet.
|
|
3576
|
+
|
|
3577
|
+
:param payload: The raw hex payload
|
|
3578
|
+
:type payload: str
|
|
3579
|
+
:param msg: The message object containing context
|
|
3580
|
+
:type msg: Message
|
|
3581
|
+
:return: A dictionary of modulation levels and cycle/actuator countdowns
|
|
3582
|
+
:rtype: PayDictT._3EF1 | PayDictT._JASPER
|
|
3583
|
+
:raises AssertionError: If the countdown values exceed recognized thresholds.
|
|
3584
|
+
"""
|
|
2675
3585
|
if msg.src.type == DEV_TYPE_MAP.JIM: # Honeywell Jasper, DEX
|
|
2676
3586
|
assert msg.len == 18, f"expecting len 18, got: {msg.len}"
|
|
2677
3587
|
return {
|
|
@@ -2724,6 +3634,16 @@ def parser_3ef1(payload: str, msg: Message) -> PayDictT._3EF1 | PayDictT._JASPER
|
|
|
2724
3634
|
|
|
2725
3635
|
# timestamp, HVAC
|
|
2726
3636
|
def parser_4401(payload: str, msg: Message) -> dict[str, Any]:
|
|
3637
|
+
"""Parse the 4401 (HVAC timestamp) packet.
|
|
3638
|
+
|
|
3639
|
+
:param payload: The raw hex payload
|
|
3640
|
+
:type payload: str
|
|
3641
|
+
:param msg: The message object containing context
|
|
3642
|
+
:type msg: Message
|
|
3643
|
+
:return: A dictionary of source/destination timestamps and update flags
|
|
3644
|
+
:rtype: dict[str, Any]
|
|
3645
|
+
:raises AssertionError: If the payload format or constants are invalid.
|
|
3646
|
+
"""
|
|
2727
3647
|
if msg.verb == RP:
|
|
2728
3648
|
return {}
|
|
2729
3649
|
|
|
@@ -2786,6 +3706,16 @@ def parser_4401(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2786
3706
|
|
|
2787
3707
|
# temperatures (see: 4e02) - Itho spider/autotemp
|
|
2788
3708
|
def parser_4e01(payload: str, msg: Message) -> dict[str, Any]:
|
|
3709
|
+
"""Parse the 4e01 (Itho temperatures) packet.
|
|
3710
|
+
|
|
3711
|
+
:param payload: The raw hex payload
|
|
3712
|
+
:type payload: str
|
|
3713
|
+
:param msg: The message object containing context
|
|
3714
|
+
:type msg: Message
|
|
3715
|
+
:return: A dictionary containing an array of temperature measurements
|
|
3716
|
+
:rtype: dict[str, Any]
|
|
3717
|
+
:raises AssertionError: If the number of temperature groups does not match the packet length.
|
|
3718
|
+
"""
|
|
2789
3719
|
# .I --- 02:248945 02:250708 --:------ 4E01 018 00-7FFF7FFF7FFF09077FFF7FFF7FFF7FFF-00 # 23.11, 8-group
|
|
2790
3720
|
# .I --- 02:250984 02:250704 --:------ 4E01 018 00-7FFF7FFF7FFF7FFF08387FFF7FFF7FFF-00 # 21.04
|
|
2791
3721
|
|
|
@@ -2808,6 +3738,16 @@ def parser_4e01(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2808
3738
|
def parser_4e02(
|
|
2809
3739
|
payload: str, msg: Message
|
|
2810
3740
|
) -> dict[str, Any]: # sent a triplets, 1 min apart
|
|
3741
|
+
"""Parse the 4e02 (Itho setpoint bounds) packet.
|
|
3742
|
+
|
|
3743
|
+
:param payload: The raw hex payload
|
|
3744
|
+
:type payload: str
|
|
3745
|
+
:param msg: The message object containing context
|
|
3746
|
+
:type msg: Message
|
|
3747
|
+
:return: A dictionary containing the mode and associated setpoint bounds
|
|
3748
|
+
:rtype: dict[str, Any]
|
|
3749
|
+
:raises AssertionError: If the payload constants or mode indicators are invalid.
|
|
3750
|
+
"""
|
|
2811
3751
|
# .I --- 02:248945 02:250708 --:------ 4E02 034 00-7FFF7FFF7FFF07D07FFF7FFF7FFF7FFF-02-7FFF7FFF7FFF08347FFF7FFF7FFF7FFF # 20.00-21.00
|
|
2812
3752
|
# .I --- 02:250984 02:250704 --:------ 4E02 034 00-7FFF7FFF7FFF076C7FFF7FFF7FFF7FFF-02-7FFF7FFF7FFF07D07FFF7FFF7FFF7FFF #
|
|
2813
3753
|
|
|
@@ -2841,6 +3781,16 @@ def parser_4e02(
|
|
|
2841
3781
|
|
|
2842
3782
|
# hvac_4e04
|
|
2843
3783
|
def parser_4e04(payload: str, msg: Message) -> dict[str, Any]:
|
|
3784
|
+
"""Parse the 4e04 (HVAC mode) packet.
|
|
3785
|
+
|
|
3786
|
+
:param payload: The raw hex payload
|
|
3787
|
+
:type payload: str
|
|
3788
|
+
:param msg: The message object containing context
|
|
3789
|
+
:type msg: Message
|
|
3790
|
+
:return: A dictionary containing the system mode
|
|
3791
|
+
:rtype: dict[str, Any]
|
|
3792
|
+
:raises AssertionError: If the mode byte or data value is unrecognized.
|
|
3793
|
+
"""
|
|
2844
3794
|
MODE = {
|
|
2845
3795
|
"00": "off",
|
|
2846
3796
|
"01": "heat",
|
|
@@ -2864,6 +3814,15 @@ def parser_4e04(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2864
3814
|
|
|
2865
3815
|
# WIP: AT outdoor low - Itho spider/autotemp
|
|
2866
3816
|
def parser_4e0d(payload: str, msg: Message) -> dict[str, Any]:
|
|
3817
|
+
"""Parse the 4e0d packet.
|
|
3818
|
+
|
|
3819
|
+
:param payload: The raw hex payload
|
|
3820
|
+
:type payload: str
|
|
3821
|
+
:param msg: The message object containing context
|
|
3822
|
+
:type msg: Message
|
|
3823
|
+
:return: A dictionary containing the raw payload
|
|
3824
|
+
:rtype: dict[str, Any]
|
|
3825
|
+
"""
|
|
2867
3826
|
# .I --- 02:250704 02:250984 --:------ 4E0D 002 0100 # Itho Autotemp: only(?) master -> slave
|
|
2868
3827
|
# .I --- 02:250704 02:250984 --:------ 4E0D 002 0101 # why does it have a context?
|
|
2869
3828
|
|
|
@@ -2874,16 +3833,34 @@ def parser_4e0d(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2874
3833
|
|
|
2875
3834
|
# AT fault circulation - Itho spider/autotemp
|
|
2876
3835
|
def parser_4e14(payload: str, msg: Message) -> dict[str, Any]:
|
|
2877
|
-
"""
|
|
3836
|
+
"""Parse the 4e14 (circulation fault) packet.
|
|
2878
3837
|
result = "AT fault circulation";
|
|
2879
3838
|
result = (((payload[2:] & 0x01) != 0x01) ? " Fault state : no fault " : " Fault state : fault ")
|
|
2880
3839
|
result = (((payload[2:] & 0x02) != 0x02) ? (text4 + "Circulation state : no fault ") : (text4 + " Circulation state : fault "))
|
|
3840
|
+
|
|
3841
|
+
:param payload: The raw hex payload
|
|
3842
|
+
:type payload: str
|
|
3843
|
+
:param msg: The message object containing context
|
|
3844
|
+
:type msg: Message
|
|
3845
|
+
:return: A dictionary indicating fault and circulation states
|
|
3846
|
+
:rtype: dict[str, Any]
|
|
2881
3847
|
"""
|
|
2882
3848
|
return {}
|
|
2883
3849
|
|
|
2884
3850
|
|
|
2885
3851
|
# wpu_state (hvac state) - Itho spider/autotemp
|
|
2886
3852
|
def parser_4e15(payload: str, msg: Message) -> dict[str, Any]:
|
|
3853
|
+
"""Parse the 4e15 (WPU state) packet.
|
|
3854
|
+
|
|
3855
|
+
:param payload: The raw hex payload
|
|
3856
|
+
:type payload: str
|
|
3857
|
+
:param msg: The message object containing context
|
|
3858
|
+
:type msg: Message
|
|
3859
|
+
:return: A dictionary of boolean flags for cooling, heating, and DHW activity
|
|
3860
|
+
:rtype: dict[str, Any]
|
|
3861
|
+
:raises TypeError: If the payload indicates simultaneous heating and cooling.
|
|
3862
|
+
:raises AssertionError: If unknown bit flags are present.
|
|
3863
|
+
"""
|
|
2887
3864
|
# .I --- 21:034158 02:250676 --:------ 4E15 002 0000 # WPU "off" (maybe heating, but compressor off)
|
|
2888
3865
|
# .I --- 21:064743 02:250708 --:------ 4E15 002 0001 # WPU cooling active
|
|
2889
3866
|
# .I --- 21:057565 02:250677 --:------ 4E15 002 0002 # WPU heating, compressor active
|
|
@@ -2917,6 +3894,16 @@ def parser_4e15(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2917
3894
|
|
|
2918
3895
|
# TODO: hvac_4e16 - Itho spider/autotemp
|
|
2919
3896
|
def parser_4e16(payload: str, msg: Message) -> dict[str, Any]:
|
|
3897
|
+
"""Parse the 4e16 packet.
|
|
3898
|
+
|
|
3899
|
+
:param payload: The raw hex payload
|
|
3900
|
+
:type payload: str
|
|
3901
|
+
:param msg: The message object containing context
|
|
3902
|
+
:type msg: Message
|
|
3903
|
+
:return: A dictionary containing the raw payload
|
|
3904
|
+
:rtype: dict[str, Any]
|
|
3905
|
+
:raises AssertionError: If the payload is not the expected null sequence.
|
|
3906
|
+
"""
|
|
2920
3907
|
# .I --- 02:250984 02:250704 --:------ 4E16 007 00000000000000 # Itho Autotemp: slave -> master
|
|
2921
3908
|
|
|
2922
3909
|
assert payload == "00000000000000", _INFORM_DEV_MSG
|
|
@@ -2928,16 +3915,25 @@ def parser_4e16(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2928
3915
|
|
|
2929
3916
|
# TODO: Fan characteristics - Itho
|
|
2930
3917
|
def parser_4e20(payload: str, msg: Message) -> dict[str, Any]:
|
|
2931
|
-
"""
|
|
3918
|
+
"""Parse the 4e20 (fan characteristics) packet.
|
|
3919
|
+
|
|
2932
3920
|
result = "Fan characteristics: "
|
|
2933
3921
|
result += [C[ABC][210] hex_to_sint32[i:i+4] for i in range(2, 34, 4)]
|
|
3922
|
+
|
|
3923
|
+
:param payload: The raw hex payload
|
|
3924
|
+
:type payload: str
|
|
3925
|
+
:param msg: The message object containing context
|
|
3926
|
+
:type msg: Message
|
|
3927
|
+
:return: A dictionary of decoded fan constants
|
|
3928
|
+
:rtype: dict[str, Any]
|
|
2934
3929
|
"""
|
|
2935
3930
|
return {}
|
|
2936
3931
|
|
|
2937
3932
|
|
|
2938
3933
|
# TODO: Potentiometer control - Itho
|
|
2939
3934
|
def parser_4e21(payload: str, msg: Message) -> dict[str, Any]:
|
|
2940
|
-
"""
|
|
3935
|
+
"""Parse the 4e21 (potentiometer control) packet.
|
|
3936
|
+
|
|
2941
3937
|
result = "Potentiometer control: "
|
|
2942
3938
|
result += "Rel min: " + hex_to_sint16(data[2:4]) # 16 bit, 2's complement
|
|
2943
3939
|
result += "Min of rel min: " + hex_to_sint16(data[4:6])
|
|
@@ -2945,12 +3941,27 @@ def parser_4e21(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2945
3941
|
result += "Rel max: " + hex_to_sint16(data[8:10])
|
|
2946
3942
|
result += "Max rel: " + hex_to_sint16(data[10:12])
|
|
2947
3943
|
result += "Abs max: " + hex_to_sint16(data[12:14]))
|
|
3944
|
+
|
|
3945
|
+
:param payload: The raw hex payload
|
|
3946
|
+
:type payload: str
|
|
3947
|
+
:param msg: The message object containing context
|
|
3948
|
+
:type msg: Message
|
|
3949
|
+
:return: A dictionary of absolute and relative power limits
|
|
3950
|
+
:rtype: dict[str, Any]
|
|
2948
3951
|
"""
|
|
2949
3952
|
return {}
|
|
2950
3953
|
|
|
2951
3954
|
|
|
2952
3955
|
# # faked puzzle pkt shouldn't be decorated
|
|
2953
3956
|
def parser_7fff(payload: str, _: Message) -> dict[str, Any]:
|
|
3957
|
+
"""Parse the 7fff (puzzle) packet.
|
|
3958
|
+
|
|
3959
|
+
:param payload: The raw hex payload
|
|
3960
|
+
:type payload: str
|
|
3961
|
+
:param _: The message object (unused)
|
|
3962
|
+
:return: A dictionary containing the message type, timestamp, and metadata
|
|
3963
|
+
:rtype: dict[str, Any]
|
|
3964
|
+
"""
|
|
2954
3965
|
if payload[:2] != "00":
|
|
2955
3966
|
_LOGGER.debug("Invalid/deprecated Puzzle packet")
|
|
2956
3967
|
return {
|
|
@@ -2992,6 +4003,15 @@ def parser_7fff(payload: str, _: Message) -> dict[str, Any]:
|
|
|
2992
4003
|
|
|
2993
4004
|
|
|
2994
4005
|
def parser_unknown(payload: str, msg: Message) -> dict[str, Any]:
|
|
4006
|
+
"""Apply a generic parser for unrecognized packet codes.
|
|
4007
|
+
|
|
4008
|
+
:param payload: The raw hex payload
|
|
4009
|
+
:type payload: str
|
|
4010
|
+
:param msg: The message object containing context
|
|
4011
|
+
:type msg: Message
|
|
4012
|
+
:return: A dictionary containing the raw payload and code information
|
|
4013
|
+
:rtype: dict[str, Any]
|
|
4014
|
+
"""
|
|
2995
4015
|
# TODO: it may be useful to generically search payloads for hex_ids, commands, etc.
|
|
2996
4016
|
|
|
2997
4017
|
# These are generic parsers
|
|
@@ -3022,10 +4042,13 @@ _PAYLOAD_PARSERS = {
|
|
|
3022
4042
|
|
|
3023
4043
|
|
|
3024
4044
|
def parse_payload(msg: Message) -> dict | list[dict]:
|
|
3025
|
-
"""
|
|
3026
|
-
|
|
3027
|
-
:param msg:
|
|
3028
|
-
:
|
|
4045
|
+
"""Apply the appropriate parser defined in this module to the message.
|
|
4046
|
+
|
|
4047
|
+
:param msg: A Message object containing packet data and extra attributes
|
|
4048
|
+
:type msg: Message
|
|
4049
|
+
:return: A dict of key:value pairs or a list of such dicts
|
|
4050
|
+
:rtype: dict | list[dict]
|
|
4051
|
+
:raises AssertionError: If the packet fails an internal consistency check.
|
|
3029
4052
|
"""
|
|
3030
4053
|
result: dict | list[dict]
|
|
3031
4054
|
try:
|