ramses-rf 0.51.7__py3-none-any.whl → 0.51.9__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_rf/__init__.py +5 -0
- ramses_rf/database.py +247 -69
- ramses_rf/device/hvac.py +561 -32
- ramses_rf/dispatcher.py +7 -5
- ramses_rf/entity_base.py +1 -1
- ramses_rf/exceptions.py +37 -3
- ramses_rf/gateway.py +1 -1
- ramses_rf/schemas.py +5 -2
- ramses_rf/version.py +1 -1
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.9.dist-info}/METADATA +6 -6
- ramses_rf-0.51.9.dist-info/RECORD +55 -0
- ramses_tx/__init__.py +25 -4
- ramses_tx/address.py +1 -1
- ramses_tx/command.py +1449 -138
- ramses_tx/const.py +1 -1
- ramses_tx/frame.py +4 -4
- ramses_tx/gateway.py +1 -1
- ramses_tx/helpers.py +2 -2
- ramses_tx/message.py +20 -14
- ramses_tx/packet.py +1 -1
- ramses_tx/parsers.py +57 -35
- ramses_tx/protocol.py +2 -2
- ramses_tx/protocol_fsm.py +1 -1
- ramses_tx/ramses.py +46 -6
- ramses_tx/schemas.py +3 -0
- ramses_tx/transport.py +9 -7
- ramses_tx/version.py +1 -1
- ramses_rf-0.51.7.dist-info/RECORD +0 -55
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.9.dist-info}/WHEEL +0 -0
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.9.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.9.dist-info}/licenses/LICENSE +0 -0
ramses_tx/command.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""RAMSES RF - a RAMSES-II protocol decoder & analyser.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
This module provides the `Command` class for constructing and managing RAMSES-II protocol
|
|
5
|
+
commands (packets) that are to be sent to HVAC devices. It includes methods for creating
|
|
6
|
+
commands to control various aspects of the heating system including zones, DHW, and fan controls.
|
|
7
|
+
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
from __future__ import annotations
|
|
8
11
|
|
|
9
12
|
import logging
|
|
13
|
+
import math
|
|
10
14
|
from collections.abc import Iterable
|
|
11
15
|
from datetime import datetime as dt, timedelta as td
|
|
12
16
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
@@ -54,7 +58,13 @@ from .helpers import (
|
|
|
54
58
|
)
|
|
55
59
|
from .opentherm import parity
|
|
56
60
|
from .parsers import LOOKUP_PUZZ
|
|
57
|
-
from .ramses import
|
|
61
|
+
from .ramses import (
|
|
62
|
+
_2411_PARAMS_SCHEMA,
|
|
63
|
+
SZ_DATA_TYPE,
|
|
64
|
+
SZ_MAX_VALUE,
|
|
65
|
+
SZ_MIN_VALUE,
|
|
66
|
+
SZ_PRECISION,
|
|
67
|
+
)
|
|
58
68
|
from .version import VERSION
|
|
59
69
|
|
|
60
70
|
from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
@@ -94,6 +104,8 @@ _ZoneIdxT = TypeVar("_ZoneIdxT", int, str)
|
|
|
94
104
|
class Qos:
|
|
95
105
|
"""The QoS class - this is a mess - it is the first step in cleaning up QoS."""
|
|
96
106
|
|
|
107
|
+
# TODO: this needs work
|
|
108
|
+
|
|
97
109
|
POLL_INTERVAL = 0.002
|
|
98
110
|
|
|
99
111
|
TX_PRIORITY_DEFAULT = Priority.DEFAULT
|
|
@@ -150,6 +162,23 @@ class Qos:
|
|
|
150
162
|
|
|
151
163
|
|
|
152
164
|
def _check_idx(zone_idx: int | str) -> str:
|
|
165
|
+
"""Validate and normalize a zone index or DHW index.
|
|
166
|
+
|
|
167
|
+
This helper function validates that a zone index is within the valid range
|
|
168
|
+
and converts it to a consistent string format.
|
|
169
|
+
|
|
170
|
+
:param zone_idx: The zone index to validate. Can be:
|
|
171
|
+
- int: 0-15 for zones, 0xFA for DHW
|
|
172
|
+
- str: String representation of the index (hex or 'HW' for DHW)
|
|
173
|
+
:type zone_idx: int | str
|
|
174
|
+
:return: The normalized zone index as a 2-character hex string
|
|
175
|
+
:rtype: str
|
|
176
|
+
:raises CommandInvalid: If the zone index is invalid
|
|
177
|
+
|
|
178
|
+
.. note::
|
|
179
|
+
- For DHW (Domestic Hot Water), use 0xFA or 'HW'
|
|
180
|
+
- For zones, use 0-15 (or '00'-'0F' as hex strings)
|
|
181
|
+
"""
|
|
153
182
|
# if zone_idx is None:
|
|
154
183
|
# return "00"
|
|
155
184
|
if not isinstance(zone_idx, int | str):
|
|
@@ -168,9 +197,33 @@ def _normalise_mode(
|
|
|
168
197
|
until: dt | str | None,
|
|
169
198
|
duration: int | None,
|
|
170
199
|
) -> str:
|
|
171
|
-
"""Validate
|
|
172
|
-
|
|
173
|
-
|
|
200
|
+
"""Validate and normalize a heating mode for zone or DHW control.
|
|
201
|
+
|
|
202
|
+
This helper function ensures the operating mode is valid and consistent
|
|
203
|
+
with the provided target and timing parameters.
|
|
204
|
+
|
|
205
|
+
:param mode: The operating mode. Can be:
|
|
206
|
+
- None: Auto-determined from other parameters
|
|
207
|
+
- int/str: Mode code (see ZON_MODE_MAP for valid values)
|
|
208
|
+
:type mode: int | str | None
|
|
209
|
+
:param target: The target value for the mode:
|
|
210
|
+
- For zone modes: The temperature setpoint
|
|
211
|
+
- For DHW modes: Active state (True/False)
|
|
212
|
+
:type target: bool | float | None
|
|
213
|
+
:param until: The end time for temporary modes
|
|
214
|
+
:type until: datetime | str | None
|
|
215
|
+
:param duration: The duration in minutes for countdown modes
|
|
216
|
+
:type duration: int | None
|
|
217
|
+
:return: Normalized 2-character hex mode string
|
|
218
|
+
:rtype: str
|
|
219
|
+
:raises CommandInvalid: If the parameters are inconsistent or invalid
|
|
220
|
+
|
|
221
|
+
.. note::
|
|
222
|
+
- If mode is None, it will be determined based on other parameters:
|
|
223
|
+
- If until is set: TEMPORARY mode
|
|
224
|
+
- If duration is set: COUNTDOWN mode
|
|
225
|
+
- Otherwise: PERMANENT mode
|
|
226
|
+
- The target parameter must be provided for all modes except FOLLOW
|
|
174
227
|
"""
|
|
175
228
|
|
|
176
229
|
if mode is None and target is None:
|
|
@@ -210,9 +263,28 @@ def _normalise_until(
|
|
|
210
263
|
until: dt | str | None,
|
|
211
264
|
duration: int | None,
|
|
212
265
|
) -> tuple[Any, Any]:
|
|
213
|
-
"""Validate
|
|
214
|
-
|
|
215
|
-
|
|
266
|
+
"""Validate and normalize timing parameters for zone/DHW mode changes.
|
|
267
|
+
|
|
268
|
+
This helper function ensures that the timing parameters (until/duration)
|
|
269
|
+
are consistent with the specified mode.
|
|
270
|
+
|
|
271
|
+
:param mode: The operating mode (from ZON_MODE_MAP)
|
|
272
|
+
:type mode: int | str | None
|
|
273
|
+
:param _: Unused parameter (kept for compatibility with call signatures)
|
|
274
|
+
:type _: Any
|
|
275
|
+
:param until: The end time for temporary modes
|
|
276
|
+
:type until: datetime | str | None
|
|
277
|
+
:param duration: The duration in minutes for countdown modes
|
|
278
|
+
:type duration: int | None
|
|
279
|
+
:return: A tuple of (until, duration) with validated values
|
|
280
|
+
:rtype: tuple[Any, Any]
|
|
281
|
+
:raises CommandInvalid: If the timing parameters are inconsistent with the mode
|
|
282
|
+
|
|
283
|
+
.. note::
|
|
284
|
+
- For TEMPORARY mode: 'until' must be provided, 'duration' must be None
|
|
285
|
+
- For COUNTDOWN mode: 'duration' must be provided, 'until' must be None
|
|
286
|
+
- For other modes: Both 'until' and 'duration' must be None
|
|
287
|
+
- If mode is TEMPORARY and until is None, it will be changed to ADVANCED mode
|
|
216
288
|
"""
|
|
217
289
|
if mode == ZON_MODE_MAP.TEMPORARY:
|
|
218
290
|
if duration is not None:
|
|
@@ -432,45 +504,120 @@ class Command(Frame):
|
|
|
432
504
|
|
|
433
505
|
@classmethod # constructor for RQ|0004
|
|
434
506
|
def get_zone_name(cls, ctl_id: DeviceIdT | str, zone_idx: _ZoneIdxT) -> Command:
|
|
435
|
-
"""
|
|
507
|
+
"""Get the name of a zone. (c.f. parser_0004)
|
|
508
|
+
|
|
509
|
+
This method constructs a command to request the name of a specific zone
|
|
510
|
+
from the controller.
|
|
436
511
|
|
|
512
|
+
:param ctl_id: The device ID of the controller
|
|
513
|
+
:type ctl_id: DeviceIdT | str
|
|
514
|
+
:param zone_idx: The index of the zone (00-31)
|
|
515
|
+
:type zone_idx: _ZoneIdxT
|
|
516
|
+
:return: A Command object for the RQ|0004 message
|
|
517
|
+
:rtype: Command
|
|
518
|
+
|
|
519
|
+
.. note::
|
|
520
|
+
The zone name is typically a user-assigned identifier for the zone,
|
|
521
|
+
such as "Living Room" or "Bedroom 1".
|
|
522
|
+
"""
|
|
437
523
|
return cls.from_attrs(RQ, ctl_id, Code._0004, f"{_check_idx(zone_idx)}00")
|
|
438
524
|
|
|
439
525
|
@classmethod # constructor for W|0004
|
|
440
526
|
def set_zone_name(
|
|
441
527
|
cls, ctl_id: DeviceIdT | str, zone_idx: _ZoneIdxT, name: str
|
|
442
528
|
) -> Command:
|
|
443
|
-
"""
|
|
444
|
-
|
|
529
|
+
"""Set the name of a zone. (c.f. parser_0004)
|
|
530
|
+
|
|
531
|
+
This method constructs a command to set the name of a specific zone
|
|
532
|
+
on the controller. The name will be truncated to 20 characters (40 hex digits).
|
|
533
|
+
|
|
534
|
+
:param ctl_id: The device ID of the controller
|
|
535
|
+
:type ctl_id: DeviceIdT | str
|
|
536
|
+
:param zone_idx: The index of the zone (00-31)
|
|
537
|
+
:type zone_idx: _ZoneIdxT
|
|
538
|
+
:param name: The new name for the zone (max 20 characters)
|
|
539
|
+
:type name: str
|
|
540
|
+
:return: A Command object for the W|0004 message
|
|
541
|
+
:rtype: Command
|
|
542
|
+
|
|
543
|
+
.. note::
|
|
544
|
+
The name will be converted to uppercase and non-ASCII characters
|
|
545
|
+
will be replaced with '?'. The name is limited to 20 characters.
|
|
546
|
+
"""
|
|
445
547
|
payload = f"{_check_idx(zone_idx)}00{hex_from_str(name)[:40]:0<40}"
|
|
446
548
|
return cls.from_attrs(W_, ctl_id, Code._0004, payload)
|
|
447
549
|
|
|
448
550
|
@classmethod # constructor for RQ|0006
|
|
449
551
|
def get_schedule_version(cls, ctl_id: DeviceIdT | str) -> Command:
|
|
450
|
-
"""
|
|
451
|
-
|
|
452
|
-
This number is
|
|
453
|
-
|
|
454
|
-
|
|
552
|
+
"""Get the current version (change counter) of the schedules.
|
|
553
|
+
|
|
554
|
+
This method retrieves a version number that is incremented whenever any zone's
|
|
555
|
+
schedule (including the DHW zone) is modified. This allows clients to efficiently
|
|
556
|
+
check if schedules have changed before downloading them.
|
|
557
|
+
|
|
558
|
+
:param ctl_id: The device ID of the controller
|
|
559
|
+
:type ctl_id: DeviceIdT | str
|
|
560
|
+
:return: A Command object for the RQ|0006 message
|
|
561
|
+
:rtype: Command
|
|
562
|
+
|
|
563
|
+
.. note::
|
|
564
|
+
The version number is a simple counter that increments with each schedule
|
|
565
|
+
change. It has no inherent meaning beyond indicating that a change has
|
|
566
|
+
occurred. The actual value should be compared with a previously stored
|
|
567
|
+
version to detect changes.
|
|
455
568
|
"""
|
|
456
|
-
|
|
457
569
|
return cls.from_attrs(RQ, ctl_id, Code._0006, "00")
|
|
458
570
|
|
|
459
571
|
@classmethod # constructor for RQ|0008
|
|
460
572
|
def get_relay_demand(
|
|
461
573
|
cls, dev_id: DeviceIdT | str, zone_idx: _ZoneIdxT | None = None
|
|
462
574
|
) -> Command:
|
|
463
|
-
"""
|
|
464
|
-
|
|
575
|
+
"""Get the current demand value for a relay or zone. (c.f. parser_0008)
|
|
576
|
+
|
|
577
|
+
This method constructs a command to request the current demand value for a
|
|
578
|
+
specific relay or zone. The demand value typically represents the requested
|
|
579
|
+
output level (0-100%) for the relay or zone.
|
|
580
|
+
|
|
581
|
+
:param dev_id: The device ID of the relay or controller
|
|
582
|
+
:type dev_id: DeviceIdT | str
|
|
583
|
+
:param zone_idx: The index of the zone (00-31), or None for the relay itself
|
|
584
|
+
:type zone_idx: _ZoneIdxT | None
|
|
585
|
+
:return: A Command object for the RQ|0008 message
|
|
586
|
+
:rtype: Command
|
|
587
|
+
|
|
588
|
+
.. note::
|
|
589
|
+
- If zone_idx is None, the command requests the relay's overall demand.
|
|
590
|
+
- If zone_idx is specified, the command requests the demand for that specific zone.
|
|
591
|
+
- The response will contain the current demand value as a percentage (0-100%).
|
|
592
|
+
"""
|
|
465
593
|
payload = "00" if zone_idx is None else _check_idx(zone_idx)
|
|
466
594
|
return cls.from_attrs(RQ, dev_id, Code._0008, payload)
|
|
467
595
|
|
|
468
596
|
@classmethod # constructor for RQ|000A
|
|
469
597
|
def get_zone_config(cls, ctl_id: DeviceIdT | str, zone_idx: _ZoneIdxT) -> Command:
|
|
470
|
-
"""
|
|
471
|
-
|
|
598
|
+
"""Get the configuration of a specific zone. (c.f. parser_000a)
|
|
599
|
+
|
|
600
|
+
This method constructs a command to request the configuration parameters
|
|
601
|
+
for a specific zone from the controller. The configuration includes
|
|
602
|
+
settings related to the zone's operation, such as temperature setpoints,
|
|
603
|
+
mode, and other zone-specific parameters.
|
|
604
|
+
|
|
605
|
+
:param ctl_id: The device ID of the controller
|
|
606
|
+
:type ctl_id: DeviceIdT | str
|
|
607
|
+
:param zone_idx: The index of the zone (00-31)
|
|
608
|
+
:type zone_idx: _ZoneIdxT
|
|
609
|
+
:return: A Command object for the RQ|000A message
|
|
610
|
+
:rtype: Command
|
|
611
|
+
|
|
612
|
+
.. note::
|
|
613
|
+
The response to this command will include various configuration parameters
|
|
614
|
+
for the specified zone, such as:
|
|
615
|
+
- Zone type (radiator, underfloor heating, etc.)
|
|
616
|
+
- Temperature setpoints
|
|
617
|
+
- Mode (heating/cooling)
|
|
618
|
+
- Other zone-specific settings
|
|
619
|
+
"""
|
|
472
620
|
zon_idx = _check_idx(zone_idx)
|
|
473
|
-
|
|
474
621
|
return cls.from_attrs(RQ, ctl_id, Code._000A, zon_idx)
|
|
475
622
|
|
|
476
623
|
@classmethod # constructor for W|000A
|
|
@@ -485,8 +632,35 @@ class Command(Frame):
|
|
|
485
632
|
openwindow_function: bool = False,
|
|
486
633
|
multiroom_mode: bool = False,
|
|
487
634
|
) -> Command:
|
|
488
|
-
"""
|
|
489
|
-
|
|
635
|
+
"""Set the configuration parameters for a specific zone. (c.f. parser_000a)
|
|
636
|
+
|
|
637
|
+
This method constructs a command to configure various parameters for a zone,
|
|
638
|
+
including temperature limits and operational modes.
|
|
639
|
+
|
|
640
|
+
:param ctl_id: The device ID of the controller
|
|
641
|
+
:type ctl_id: DeviceIdT | str
|
|
642
|
+
:param zone_idx: The index of the zone (00-31)
|
|
643
|
+
:type zone_idx: _ZoneIdxT
|
|
644
|
+
:param min_temp: Minimum allowed temperature for the zone (5-21°C)
|
|
645
|
+
:type min_temp: float
|
|
646
|
+
:param max_temp: Maximum allowed temperature for the zone (21-35°C)
|
|
647
|
+
:type max_temp: float
|
|
648
|
+
:param local_override: If True, allows local temperature override at the device
|
|
649
|
+
:type local_override: bool
|
|
650
|
+
:param openwindow_function: If True, enables open window detection function
|
|
651
|
+
:type openwindow_function: bool
|
|
652
|
+
:param multiroom_mode: If True, enables multi-room mode for this zone
|
|
653
|
+
:type multiroom_mode: bool
|
|
654
|
+
:return: A Command object for the W|000A message
|
|
655
|
+
:rtype: Command
|
|
656
|
+
:raises CommandInvalid: If any parameter is out of range or of incorrect type
|
|
657
|
+
|
|
658
|
+
.. note::
|
|
659
|
+
- The minimum temperature must be between 5°C and 21°C
|
|
660
|
+
- The maximum temperature must be between 21°C and 35°C
|
|
661
|
+
- The minimum temperature cannot be higher than the maximum temperature
|
|
662
|
+
- These settings affect how the zone behaves in different operating modes
|
|
663
|
+
"""
|
|
490
664
|
zon_idx = _check_idx(zone_idx)
|
|
491
665
|
|
|
492
666
|
if not (5 <= min_temp <= 21):
|
|
@@ -514,8 +688,21 @@ class Command(Frame):
|
|
|
514
688
|
|
|
515
689
|
@classmethod # constructor for RQ|0100
|
|
516
690
|
def get_system_language(cls, ctl_id: DeviceIdT | str, **kwargs: Any) -> Command:
|
|
517
|
-
"""
|
|
691
|
+
"""Get the configured language of the system. (c.f. parser_0100)
|
|
692
|
+
|
|
693
|
+
This method constructs a command to request the current language setting
|
|
694
|
+
from the system controller.
|
|
695
|
+
|
|
696
|
+
:param ctl_id: The device ID of the controller
|
|
697
|
+
:type ctl_id: DeviceIdT | str
|
|
698
|
+
:param kwargs: Additional keyword arguments (not used, for compatibility only)
|
|
699
|
+
:return: A Command object for the RQ|0100 message
|
|
700
|
+
:rtype: Command
|
|
518
701
|
|
|
702
|
+
.. note::
|
|
703
|
+
The response will contain a language code that corresponds to the
|
|
704
|
+
system's configured language setting.
|
|
705
|
+
"""
|
|
519
706
|
assert not kwargs, kwargs
|
|
520
707
|
return cls.from_attrs(RQ, ctl_id, Code._0100, "00", **kwargs)
|
|
521
708
|
|
|
@@ -528,9 +715,29 @@ class Command(Frame):
|
|
|
528
715
|
total_frags: int | None,
|
|
529
716
|
**kwargs: Any,
|
|
530
717
|
) -> Command:
|
|
531
|
-
"""
|
|
532
|
-
|
|
533
|
-
|
|
718
|
+
"""Get a specific fragment of a schedule. (c.f. parser_0404)
|
|
719
|
+
|
|
720
|
+
This method constructs a command to request a specific fragment of a schedule
|
|
721
|
+
from the controller. Schedules are typically broken into multiple fragments
|
|
722
|
+
for efficient transmission.
|
|
723
|
+
|
|
724
|
+
:param ctl_id: The device ID of the controller
|
|
725
|
+
:type ctl_id: DeviceIdT | str
|
|
726
|
+
:param zone_idx: The index of the zone (00-31), or 0xFA/'FA'/'HW' for DHW schedule
|
|
727
|
+
:type zone_idx: _ZoneIdxT
|
|
728
|
+
:param frag_number: The fragment number to retrieve (0-based)
|
|
729
|
+
:type frag_number: int
|
|
730
|
+
:param total_frags: Total number of fragments (optional)
|
|
731
|
+
:type total_frags: int | None
|
|
732
|
+
:param kwargs: Additional keyword arguments
|
|
733
|
+
:return: A Command object for the RQ|0404 message
|
|
734
|
+
:rtype: Command
|
|
735
|
+
|
|
736
|
+
.. note::
|
|
737
|
+
- For zone schedules, use a zone index between 00-31
|
|
738
|
+
- For DHW (Domestic Hot Water) schedule, use 0xFA, 'FA', or 'HW' as zone_idx
|
|
739
|
+
- The schedule is typically retrieved in multiple fragments to handle
|
|
740
|
+
the potentially large amount of data
|
|
534
741
|
"""
|
|
535
742
|
|
|
536
743
|
assert not kwargs, kwargs
|
|
@@ -568,9 +775,31 @@ class Command(Frame):
|
|
|
568
775
|
frag_cnt: int,
|
|
569
776
|
fragment: str,
|
|
570
777
|
) -> Command:
|
|
571
|
-
"""
|
|
572
|
-
|
|
573
|
-
|
|
778
|
+
"""Set a specific fragment of a schedule. (c.f. parser_0404)
|
|
779
|
+
|
|
780
|
+
This method constructs a command to set a specific fragment of a schedule
|
|
781
|
+
on the controller. Schedules are typically set in multiple fragments
|
|
782
|
+
due to their potentially large size.
|
|
783
|
+
|
|
784
|
+
:param ctl_id: The device ID of the controller
|
|
785
|
+
:type ctl_id: DeviceIdT | str
|
|
786
|
+
:param zone_idx: The index of the zone (00-31), or 0xFA/'FA'/'HW' for DHW schedule
|
|
787
|
+
:type zone_idx: _ZoneIdxT
|
|
788
|
+
:param frag_num: The fragment number being set (1-based index)
|
|
789
|
+
:type frag_num: int
|
|
790
|
+
:param frag_cnt: Total number of fragments in the schedule
|
|
791
|
+
:type frag_cnt: int
|
|
792
|
+
:param fragment: The schedule fragment data as a hex string
|
|
793
|
+
:type fragment: str
|
|
794
|
+
:return: A Command object for the W|0404 message
|
|
795
|
+
:rtype: Command
|
|
796
|
+
:raises CommandInvalid: If fragment number is invalid or out of range
|
|
797
|
+
|
|
798
|
+
.. note::
|
|
799
|
+
- For zone schedules, use a zone index between 00-31
|
|
800
|
+
- For DHW (Domestic Hot Water) schedule, use 0xFA, 'FA', or 'HW' as zone_idx
|
|
801
|
+
- The first fragment (frag_num=1) typically contains schedule metadata
|
|
802
|
+
- Fragment numbers are 1-based (1 to frag_cnt)
|
|
574
803
|
"""
|
|
575
804
|
|
|
576
805
|
zon_idx = _check_idx(zone_idx)
|
|
@@ -593,8 +822,23 @@ class Command(Frame):
|
|
|
593
822
|
def get_system_log_entry(
|
|
594
823
|
cls, ctl_id: DeviceIdT | str, log_idx: int | str
|
|
595
824
|
) -> Command:
|
|
596
|
-
"""
|
|
597
|
-
|
|
825
|
+
"""Retrieve a specific log entry from the system log. (c.f. parser_0418)
|
|
826
|
+
|
|
827
|
+
This method constructs a command to request a specific log entry from the
|
|
828
|
+
system's event log. The log contains historical events and fault records.
|
|
829
|
+
|
|
830
|
+
:param ctl_id: The device ID of the controller
|
|
831
|
+
:type ctl_id: DeviceIdT | str
|
|
832
|
+
:param log_idx: The index of the log entry to retrieve (0-based)
|
|
833
|
+
:type log_idx: int | str (hex string)
|
|
834
|
+
:return: A Command object for the RQ|0418 message
|
|
835
|
+
:rtype: Command
|
|
836
|
+
|
|
837
|
+
.. note::
|
|
838
|
+
- The log index is 0-based, where 0 is the most recent entry
|
|
839
|
+
- The log typically contains system events, faults, and warnings
|
|
840
|
+
- The response will include details about the log entry
|
|
841
|
+
"""
|
|
598
842
|
log_idx = log_idx if isinstance(log_idx, int) else int(log_idx, 16)
|
|
599
843
|
return cls.from_attrs(RQ, ctl_id, Code._0418, f"{log_idx:06X}")
|
|
600
844
|
|
|
@@ -611,8 +855,38 @@ class Command(Frame):
|
|
|
611
855
|
timestamp: dt | str | None = None,
|
|
612
856
|
**kwargs: Any,
|
|
613
857
|
) -> Command:
|
|
614
|
-
"""
|
|
615
|
-
|
|
858
|
+
"""Create a log entry in the system log. (c.f. parser_0418)
|
|
859
|
+
|
|
860
|
+
This internal method constructs a command to create a log entry in the system's
|
|
861
|
+
event log. It's primarily used for testing purposes to simulate log entries.
|
|
862
|
+
|
|
863
|
+
:param ctl_id: The device ID of the controller
|
|
864
|
+
:type ctl_id: DeviceIdT | str
|
|
865
|
+
:param fault_state: The state of the fault (e.g., 'on', 'off', 'unknown')
|
|
866
|
+
:type fault_state: FaultState | str
|
|
867
|
+
:param fault_type: The type of fault being logged
|
|
868
|
+
:type fault_type: FaultType | str
|
|
869
|
+
:param device_class: The class of device associated with the fault
|
|
870
|
+
:type device_class: FaultDeviceClass | str
|
|
871
|
+
:param device_id: The ID of the device associated with the fault (optional)
|
|
872
|
+
:type device_id: DeviceIdT | str | None
|
|
873
|
+
:param domain_idx: The domain index (default: '00')
|
|
874
|
+
:type domain_idx: int | str
|
|
875
|
+
:param _log_idx: The log index (for internal use, optional)
|
|
876
|
+
:type _log_idx: int | str | None
|
|
877
|
+
:param timestamp: The timestamp of the log entry (default: current time)
|
|
878
|
+
:type timestamp: dt | str | None
|
|
879
|
+
:param kwargs: Additional keyword arguments
|
|
880
|
+
:return: A Command object for the I|0418 message
|
|
881
|
+
:rtype: Command
|
|
882
|
+
:raises AssertionError: If device_class is invalid
|
|
883
|
+
|
|
884
|
+
.. note::
|
|
885
|
+
- This is an internal method primarily used for testing
|
|
886
|
+
- The log entry will appear in the system's event log
|
|
887
|
+
- The fault_state and fault_type should match the expected enums
|
|
888
|
+
- If timestamp is not provided, the current time will be used
|
|
889
|
+
"""
|
|
616
890
|
if isinstance(device_class, FaultDeviceClass):
|
|
617
891
|
device_class = {v: k for k, v in FAULT_DEVICE_CLASS.items()}[device_class]
|
|
618
892
|
assert device_class in FAULT_DEVICE_CLASS
|
|
@@ -661,8 +935,25 @@ class Command(Frame):
|
|
|
661
935
|
def get_mix_valve_params(
|
|
662
936
|
cls, ctl_id: DeviceIdT | str, zone_idx: _ZoneIdxT
|
|
663
937
|
) -> Command:
|
|
664
|
-
"""
|
|
665
|
-
|
|
938
|
+
"""Retrieve the mixing valve parameters for a specific zone. (c.f. parser_1030)
|
|
939
|
+
|
|
940
|
+
This method constructs a command to request the current mixing valve parameters
|
|
941
|
+
for a specific zone from the controller. These parameters control how the
|
|
942
|
+
mixing valve operates for the specified zone.
|
|
943
|
+
|
|
944
|
+
:param ctl_id: The device ID of the controller
|
|
945
|
+
:type ctl_id: DeviceIdT | str
|
|
946
|
+
:param zone_idx: The index of the zone (00-31)
|
|
947
|
+
:type zone_idx: _ZoneIdxT
|
|
948
|
+
:return: A Command object for the RQ|1030 message
|
|
949
|
+
:rtype: Command
|
|
950
|
+
|
|
951
|
+
.. note::
|
|
952
|
+
- The mixing valve controls the temperature of the water in the heating circuit
|
|
953
|
+
by mixing hot water from the boiler with cooler return water
|
|
954
|
+
- The parameters include settings like the minimum and maximum flow temperatures
|
|
955
|
+
and the proportional band for the valve control
|
|
956
|
+
"""
|
|
666
957
|
zon_idx = _check_idx(zone_idx)
|
|
667
958
|
|
|
668
959
|
return cls.from_attrs(RQ, ctl_id, Code._1030, zon_idx)
|
|
@@ -679,8 +970,37 @@ class Command(Frame):
|
|
|
679
970
|
pump_run_time: int = 15,
|
|
680
971
|
**kwargs: Any,
|
|
681
972
|
) -> Command:
|
|
682
|
-
"""
|
|
683
|
-
|
|
973
|
+
"""Set the mixing valve parameters for a specific zone. (c.f. parser_1030)
|
|
974
|
+
|
|
975
|
+
This method constructs a command to configure the mixing valve parameters
|
|
976
|
+
for a specific zone. These parameters control how the mixing valve operates
|
|
977
|
+
to regulate the temperature of the water in the heating circuit.
|
|
978
|
+
|
|
979
|
+
:param ctl_id: The device ID of the controller
|
|
980
|
+
:type ctl_id: DeviceIdT | str
|
|
981
|
+
:param zone_idx: The index of the zone (00-31)
|
|
982
|
+
:type zone_idx: _ZoneIdxT
|
|
983
|
+
:param max_flow_setpoint: Maximum flow temperature setpoint in °C (0-99)
|
|
984
|
+
:type max_flow_setpoint: int
|
|
985
|
+
:param min_flow_setpoint: Minimum flow temperature setpoint in °C (0-50)
|
|
986
|
+
:type min_flow_setpoint: int
|
|
987
|
+
:param valve_run_time: Valve run time in seconds (0-240)
|
|
988
|
+
:type valve_run_time: int
|
|
989
|
+
:param pump_run_time: Pump overrun time in seconds after valve closes (0-99)
|
|
990
|
+
:type pump_run_time: int
|
|
991
|
+
:param kwargs: Additional keyword arguments (e.g., boolean_cc)
|
|
992
|
+
:return: A Command object for the W|1030 message
|
|
993
|
+
:rtype: Command
|
|
994
|
+
:raises CommandInvalid: If any parameter is out of valid range
|
|
995
|
+
|
|
996
|
+
.. note::
|
|
997
|
+
- The mixing valve controls the temperature by mixing hot water from the boiler
|
|
998
|
+
with cooler return water
|
|
999
|
+
- The pump overrun time allows the pump to continue running after the valve
|
|
1000
|
+
closes to dissipate residual heat
|
|
1001
|
+
- The valve run time determines how long the valve takes to move between
|
|
1002
|
+
fully open and fully closed positions
|
|
1003
|
+
"""
|
|
684
1004
|
boolean_cc = kwargs.pop("boolean_cc", 1)
|
|
685
1005
|
assert not kwargs, kwargs
|
|
686
1006
|
|
|
@@ -714,10 +1034,27 @@ class Command(Frame):
|
|
|
714
1034
|
|
|
715
1035
|
@classmethod # constructor for RQ|10A0
|
|
716
1036
|
def get_dhw_params(cls, ctl_id: DeviceIdT | str, **kwargs: Any) -> Command:
|
|
717
|
-
"""
|
|
718
|
-
|
|
1037
|
+
"""Get the parameters of the Domestic Hot Water (DHW) system. (c.f. parser_10a0)
|
|
1038
|
+
|
|
1039
|
+
This method constructs a command to retrieve the current parameters
|
|
1040
|
+
of the DHW system, including setpoint, overrun, and differential settings.
|
|
1041
|
+
|
|
1042
|
+
:param ctl_id: The device ID of the controller
|
|
1043
|
+
:type ctl_id: DeviceIdT | str
|
|
1044
|
+
:param kwargs: Additional keyword arguments
|
|
1045
|
+
- dhw_idx: Index of the DHW circuit (0 or 1), defaults to 0
|
|
1046
|
+
- Other arguments will raise an exception
|
|
1047
|
+
:return: A Command object for the RQ|10A0 message
|
|
1048
|
+
:rtype: Command
|
|
1049
|
+
:raises AssertionError: If unexpected keyword arguments are provided
|
|
1050
|
+
|
|
1051
|
+
.. note::
|
|
1052
|
+
- Most systems only have one DHW circuit (index 0)
|
|
1053
|
+
- The response includes current setpoint, overrun, and differential values
|
|
1054
|
+
- The actual values are parsed by parser_10a0
|
|
1055
|
+
"""
|
|
719
1056
|
dhw_idx = _check_idx(kwargs.pop(SZ_DHW_IDX, 0)) # 00 or 01 (rare)
|
|
720
|
-
assert not kwargs, kwargs
|
|
1057
|
+
assert not kwargs, f"Unexpected arguments: {kwargs}"
|
|
721
1058
|
|
|
722
1059
|
return cls.from_attrs(RQ, ctl_id, Code._10A0, dhw_idx)
|
|
723
1060
|
|
|
@@ -731,7 +1068,33 @@ class Command(Frame):
|
|
|
731
1068
|
differential: float | None = 1,
|
|
732
1069
|
**kwargs: Any, # only expect "dhw_idx"
|
|
733
1070
|
) -> Command:
|
|
734
|
-
"""
|
|
1071
|
+
"""Set the parameters of the Domestic Hot Water (DHW) system. (c.f. parser_10a0)
|
|
1072
|
+
|
|
1073
|
+
This method constructs a command to configure the parameters of the DHW system,
|
|
1074
|
+
including temperature setpoint, pump overrun time, and temperature differential.
|
|
1075
|
+
|
|
1076
|
+
:param ctl_id: The device ID of the controller
|
|
1077
|
+
:type ctl_id: DeviceIdT | str
|
|
1078
|
+
:param setpoint: Target temperature for DHW in °C (30.0-85.0), defaults to 50.0
|
|
1079
|
+
:type setpoint: float | None
|
|
1080
|
+
:param overrun: Pump overrun time in minutes (0-10), defaults to 5
|
|
1081
|
+
:type overrun: int | None
|
|
1082
|
+
:param differential: Temperature differential in °C (1.0-10.0), defaults to 1.0
|
|
1083
|
+
:type differential: float | None
|
|
1084
|
+
:param kwargs: Additional keyword arguments
|
|
1085
|
+
- dhw_idx: Index of the DHW circuit (0 or 1), defaults to 0
|
|
1086
|
+
:return: A Command object for the W|10A0 message
|
|
1087
|
+
:rtype: Command
|
|
1088
|
+
:raises CommandInvalid: If any parameter is out of valid range
|
|
1089
|
+
:raises AssertionError: If unexpected keyword arguments are provided
|
|
1090
|
+
|
|
1091
|
+
.. note::
|
|
1092
|
+
- The setpoint is the target temperature for the hot water
|
|
1093
|
+
- Overrun keeps the pump running after heating stops to dissipate residual heat
|
|
1094
|
+
- Differential prevents rapid cycling by requiring this much temperature drop
|
|
1095
|
+
before reheating
|
|
1096
|
+
- Most systems only have one DHW circuit (index 0)
|
|
1097
|
+
"""
|
|
735
1098
|
# Defaults for newer evohome colour:
|
|
736
1099
|
# Defaults for older evohome colour: ?? (30-85) C, ? (0-10) min, ? (1-10) C
|
|
737
1100
|
# Defaults for evohome monochrome:
|
|
@@ -741,7 +1104,7 @@ class Command(Frame):
|
|
|
741
1104
|
# 14:34:26.764 074 I --- 01:145038 18:013393 --:------ 10A0 006 000F6E0003E8
|
|
742
1105
|
|
|
743
1106
|
dhw_idx = _check_idx(kwargs.pop(SZ_DHW_IDX, 0)) # 00 or 01 (rare)
|
|
744
|
-
assert not kwargs, kwargs
|
|
1107
|
+
assert not kwargs, f"Unexpected arguments: {kwargs}"
|
|
745
1108
|
|
|
746
1109
|
setpoint = 50.0 if setpoint is None else setpoint
|
|
747
1110
|
overrun = 5 if overrun is None else overrun
|
|
@@ -762,8 +1125,24 @@ class Command(Frame):
|
|
|
762
1125
|
def get_tpi_params(
|
|
763
1126
|
cls, dev_id: DeviceIdT | str, *, domain_id: int | str | None = None
|
|
764
1127
|
) -> Command:
|
|
765
|
-
"""
|
|
766
|
-
|
|
1128
|
+
"""Get the Time Proportional and Integral (TPI) parameters of a system. (c.f. parser_1100)
|
|
1129
|
+
|
|
1130
|
+
This method constructs a command to retrieve the TPI parameters for a specific domain.
|
|
1131
|
+
TPI is a control algorithm used to maintain temperature by cycling the boiler on/off.
|
|
1132
|
+
|
|
1133
|
+
:param dev_id: The device ID of the controller or BDR91 relay
|
|
1134
|
+
:type dev_id: DeviceIdT | str
|
|
1135
|
+
:param domain_id: The domain ID to get parameters for, or None for default
|
|
1136
|
+
(00 for BDR devices, FC for controllers)
|
|
1137
|
+
:type domain_id: int | str | None
|
|
1138
|
+
:return: A Command object for the RQ|1100 message
|
|
1139
|
+
:rtype: Command
|
|
1140
|
+
|
|
1141
|
+
.. note::
|
|
1142
|
+
- TPI parameters control how the system maintains temperature by cycling the boiler
|
|
1143
|
+
- Different domains can have different TPI settings
|
|
1144
|
+
- The response will include cycle rate, minimum on/off times, and other parameters
|
|
1145
|
+
"""
|
|
767
1146
|
if domain_id is None:
|
|
768
1147
|
domain_id = "00" if dev_id[:2] == DEV_TYPE_MAP.BDR else FC
|
|
769
1148
|
|
|
@@ -780,11 +1159,39 @@ class Command(Frame):
|
|
|
780
1159
|
min_off_time: int = 5, # TODO: check
|
|
781
1160
|
proportional_band_width: float | None = None, # TODO: check
|
|
782
1161
|
) -> Command:
|
|
783
|
-
"""
|
|
784
|
-
|
|
1162
|
+
"""Set the Time Proportional and Integral (TPI) parameters of a system. (c.f. parser_1100)
|
|
1163
|
+
|
|
1164
|
+
This method constructs a command to configure the TPI parameters for a specific domain.
|
|
1165
|
+
TPI is a control algorithm that maintains temperature by cycling the boiler on/off.
|
|
1166
|
+
|
|
1167
|
+
:param ctl_id: The device ID of the controller
|
|
1168
|
+
:type ctl_id: DeviceIdT | str
|
|
1169
|
+
:param domain_id: The domain ID to configure, or None for default domain (00)
|
|
1170
|
+
:type domain_id: int | str | None
|
|
1171
|
+
:param cycle_rate: Number of on/off cycles per hour (TODO: validate range, typically 3,6,9,12)
|
|
1172
|
+
:type cycle_rate: int
|
|
1173
|
+
:param min_on_time: Minimum time in minutes the boiler stays on (TODO: validate range, typically 1-5)
|
|
1174
|
+
:type min_on_time: int
|
|
1175
|
+
:param min_off_time: Minimum time in minutes the boiler stays off (TODO: validate range, typically 1-5)
|
|
1176
|
+
:type min_off_time: int
|
|
1177
|
+
:param proportional_band_width: Width of the proportional band in °C (TODO: validate range, typically 1.5-3.0)
|
|
1178
|
+
:type proportional_band_width: float | None
|
|
1179
|
+
:return: A Command object for the W|1100 message
|
|
1180
|
+
:rtype: Command
|
|
1181
|
+
:raises AssertionError: If any parameter is out of valid range
|
|
1182
|
+
|
|
1183
|
+
.. note::
|
|
1184
|
+
- TPI parameters control how the system maintains temperature by cycling the boiler
|
|
1185
|
+
- Different domains can have different TPI settings
|
|
1186
|
+
- The proportional band determines how much the temperature can vary before the
|
|
1187
|
+
boiler cycles on/off
|
|
1188
|
+
- The cycle rate affects how frequently the boiler cycles when maintaining temperature
|
|
1189
|
+
- Parameters are converted to appropriate hex values in the payload (e.g., minutes * 4)
|
|
1190
|
+
"""
|
|
785
1191
|
if domain_id is None:
|
|
786
1192
|
domain_id = "00"
|
|
787
1193
|
|
|
1194
|
+
# TODO: Uncomment and fix these validations once ranges are confirmed
|
|
788
1195
|
# assert cycle_rate is None or cycle_rate in (3, 6, 9, 12), cycle_rate
|
|
789
1196
|
# assert min_on_time is None or 1 <= min_on_time <= 5, min_on_time
|
|
790
1197
|
# assert min_off_time is None or 1 <= min_off_time <= 5, min_off_time
|
|
@@ -795,10 +1202,10 @@ class Command(Frame):
|
|
|
795
1202
|
payload = "".join(
|
|
796
1203
|
(
|
|
797
1204
|
_check_idx(domain_id),
|
|
798
|
-
f"{cycle_rate * 4:02X}",
|
|
799
|
-
f"{int(min_on_time * 4):02X}",
|
|
800
|
-
f"{int(min_off_time * 4):02X}00", # or: ...FF
|
|
801
|
-
f"{hex_from_temp(proportional_band_width)}01",
|
|
1205
|
+
f"{cycle_rate * 4:02X}", # Convert cycles/hour to internal format
|
|
1206
|
+
f"{int(min_on_time * 4):02X}", # Convert minutes to internal format
|
|
1207
|
+
f"{int(min_off_time * 4):02X}00", # Convert minutes to internal format (or: ...FF)
|
|
1208
|
+
f"{hex_from_temp(proportional_band_width)}01", # Convert temperature to hex
|
|
802
1209
|
)
|
|
803
1210
|
)
|
|
804
1211
|
|
|
@@ -806,10 +1213,27 @@ class Command(Frame):
|
|
|
806
1213
|
|
|
807
1214
|
@classmethod # constructor for RQ|1260
|
|
808
1215
|
def get_dhw_temp(cls, ctl_id: DeviceIdT | str, **kwargs: Any) -> Command:
|
|
809
|
-
"""
|
|
810
|
-
|
|
1216
|
+
"""Get the current temperature from a Domestic Hot Water (DHW) sensor. (c.f. parser_10a0)
|
|
1217
|
+
|
|
1218
|
+
This method constructs a command to request the current temperature reading from
|
|
1219
|
+
a DHW temperature sensor. The sensor is typically located in the hot water tank.
|
|
1220
|
+
|
|
1221
|
+
:param ctl_id: The device ID of the controller
|
|
1222
|
+
:type ctl_id: DeviceIdT | str
|
|
1223
|
+
:param kwargs: Additional keyword arguments
|
|
1224
|
+
- dhw_idx: Index of the DHW sensor (0 or 1), defaults to 0
|
|
1225
|
+
- Other arguments will raise an exception
|
|
1226
|
+
:return: A Command object for the RQ|1260 message
|
|
1227
|
+
:rtype: Command
|
|
1228
|
+
:raises AssertionError: If unexpected keyword arguments are provided
|
|
1229
|
+
|
|
1230
|
+
.. note::
|
|
1231
|
+
- Most systems only have one DHW sensor (index 0)
|
|
1232
|
+
- The response will include the current temperature in degrees Celsius
|
|
1233
|
+
- The actual temperature is parsed by parser_10a0
|
|
1234
|
+
"""
|
|
811
1235
|
dhw_idx = _check_idx(kwargs.pop(SZ_DHW_IDX, 0)) # 00 or 01 (rare)
|
|
812
|
-
assert not kwargs, kwargs
|
|
1236
|
+
assert not kwargs, f"Unexpected arguments: {kwargs}"
|
|
813
1237
|
|
|
814
1238
|
return cls.from_attrs(RQ, ctl_id, Code._1260, dhw_idx)
|
|
815
1239
|
|
|
@@ -817,13 +1241,33 @@ class Command(Frame):
|
|
|
817
1241
|
def put_dhw_temp(
|
|
818
1242
|
cls, dev_id: DeviceIdT | str, temperature: float | None, **kwargs: Any
|
|
819
1243
|
) -> Command:
|
|
820
|
-
"""
|
|
821
|
-
|
|
822
|
-
This
|
|
1244
|
+
"""Announce the current temperature of a Domestic Hot Water (DHW) sensor. (1260)
|
|
1245
|
+
|
|
1246
|
+
This method constructs a command to announce/simulate a temperature reading from
|
|
1247
|
+
a DHW temperature sensor. This is primarily intended for use with simulated or
|
|
1248
|
+
emulated devices like a faked CS92A sensor.
|
|
1249
|
+
|
|
1250
|
+
:param dev_id: The device ID of the DHW sensor (must start with DHW type code)
|
|
1251
|
+
:type dev_id: DeviceIdT | str
|
|
1252
|
+
:param temperature: The temperature to report in °C, or None for no reading
|
|
1253
|
+
:type temperature: float | None
|
|
1254
|
+
:param kwargs: Additional keyword arguments
|
|
1255
|
+
- dhw_idx: Index of the DHW sensor (0 or 1), defaults to 0
|
|
1256
|
+
- Other arguments will raise an exception
|
|
1257
|
+
:return: A Command object for the I|1260 message
|
|
1258
|
+
:rtype: Command
|
|
1259
|
+
:raises CommandInvalid: If the device type is not a DHW sensor
|
|
1260
|
+
:raises AssertionError: If unexpected keyword arguments are provided
|
|
1261
|
+
|
|
1262
|
+
.. note::
|
|
1263
|
+
- This is typically used for testing or simulation purposes
|
|
1264
|
+
- The temperature is converted to the appropriate hex format
|
|
1265
|
+
- The device ID must be a valid DHW sensor type (starts with DHW code)
|
|
1266
|
+
- Most systems only have one DHW sensor (index 0)
|
|
1267
|
+
- The message is sent as an I-type (unsolicited) message
|
|
823
1268
|
"""
|
|
824
|
-
|
|
825
1269
|
dhw_idx = _check_idx(kwargs.pop(SZ_DHW_IDX, 0)) # 00 or 01 (rare)
|
|
826
|
-
assert not kwargs, kwargs
|
|
1270
|
+
assert not kwargs, f"Unexpected arguments: {kwargs}"
|
|
827
1271
|
|
|
828
1272
|
if dev_id[:2] != DEV_TYPE_MAP.DHW:
|
|
829
1273
|
raise exc.CommandInvalid(
|
|
@@ -838,19 +1282,52 @@ class Command(Frame):
|
|
|
838
1282
|
def put_outdoor_temp(
|
|
839
1283
|
cls, dev_id: DeviceIdT | str, temperature: float | None
|
|
840
1284
|
) -> Command:
|
|
841
|
-
"""
|
|
842
|
-
|
|
843
|
-
This
|
|
1285
|
+
"""Announce the current outdoor temperature from a sensor. (1290)
|
|
1286
|
+
|
|
1287
|
+
This method constructs a command to announce/simulate an outdoor temperature reading.
|
|
1288
|
+
This is for use by a faked HVAC sensor, or similar.
|
|
1289
|
+
|
|
1290
|
+
:param dev_id: The device ID of the outdoor temperature sensor
|
|
1291
|
+
:type dev_id: DeviceIdT | str
|
|
1292
|
+
:param temperature: The temperature to report in °C, or None for no reading
|
|
1293
|
+
:type temperature: float | None
|
|
1294
|
+
:return: A Command object for the I|1290 message
|
|
1295
|
+
:rtype: Command
|
|
1296
|
+
|
|
1297
|
+
.. note::
|
|
1298
|
+
- This is typically used for testing or simulation purposes
|
|
1299
|
+
- The temperature is converted to the appropriate hex format
|
|
1300
|
+
- The message is sent as an I-type (unsolicited) message
|
|
1301
|
+
- The sensor index is hardcoded to 00 (most systems have only one outdoor sensor)
|
|
1302
|
+
- The device ID should match the expected format for an outdoor temperature sensor
|
|
844
1303
|
"""
|
|
845
|
-
|
|
846
1304
|
payload = f"00{hex_from_temp(temperature)}"
|
|
847
1305
|
return cls._from_attrs(I_, Code._1290, payload, addr0=dev_id, addr2=dev_id)
|
|
848
1306
|
|
|
849
1307
|
@classmethod # constructor for I|1298
|
|
850
1308
|
def put_co2_level(cls, dev_id: DeviceIdT | str, co2_level: float | None) -> Command:
|
|
851
|
-
"""
|
|
852
|
-
|
|
853
|
-
|
|
1309
|
+
"""Announce the current CO₂ level from a sensor. (1298)
|
|
1310
|
+
.I --- 37:039266 --:------ 37:039266 1298 003 000316
|
|
1311
|
+
|
|
1312
|
+
This method constructs a command to announce/simulate a CO₂ level reading from
|
|
1313
|
+
an indoor air quality sensor. The message is typically sent by devices that
|
|
1314
|
+
monitor indoor air quality.
|
|
1315
|
+
|
|
1316
|
+
:param dev_id: The device ID of the CO₂ sensor
|
|
1317
|
+
:type dev_id: DeviceIdT | str
|
|
1318
|
+
:param co2_level: The CO₂ level to report in ppm (parts per million), or None for no reading
|
|
1319
|
+
:type co2_level: float | None
|
|
1320
|
+
:return: A Command object for the I|1298 message
|
|
1321
|
+
:rtype: Command
|
|
1322
|
+
|
|
1323
|
+
.. note::
|
|
1324
|
+
- This is typically used for testing or simulation purposes
|
|
1325
|
+
- The CO₂ level is converted to the appropriate hex format using double precision
|
|
1326
|
+
- The message is sent as an I-type (unsolicited) message
|
|
1327
|
+
- The sensor index is hardcoded to 00 (most systems have only one CO₂ sensor)
|
|
1328
|
+
- The device ID should match the expected format for a CO₂ sensor
|
|
1329
|
+
- Example message format: ``.I --- 37:039266 --:------ 37:039266 1298 003 000316``
|
|
1330
|
+
"""
|
|
854
1331
|
payload = f"00{hex_from_double(co2_level)}"
|
|
855
1332
|
return cls._from_attrs(I_, Code._1298, payload, addr0=dev_id, addr2=dev_id)
|
|
856
1333
|
|
|
@@ -858,9 +1335,29 @@ class Command(Frame):
|
|
|
858
1335
|
def put_indoor_humidity(
|
|
859
1336
|
cls, dev_id: DeviceIdT | str, indoor_humidity: float | None
|
|
860
1337
|
) -> Command:
|
|
861
|
-
"""
|
|
862
|
-
|
|
863
|
-
|
|
1338
|
+
"""Announce the current indoor humidity from a sensor or fan. (12A0)
|
|
1339
|
+
.I --- 37:039266 --:------ 37:039266 1298 003 000316
|
|
1340
|
+
|
|
1341
|
+
This method constructs a command to announce/simulate an indoor humidity reading.
|
|
1342
|
+
The message is typically sent by devices that monitor indoor air quality,
|
|
1343
|
+
such as humidity sensors or ventilation systems with humidity sensing capabilities.
|
|
1344
|
+
|
|
1345
|
+
:param dev_id: The device ID of the humidity sensor or fan
|
|
1346
|
+
:type dev_id: DeviceIdT | str
|
|
1347
|
+
:param indoor_humidity: The relative humidity to report (0-100%), or None for no reading
|
|
1348
|
+
:type indoor_humidity: float | None
|
|
1349
|
+
:return: A Command object for the I|12A0 message
|
|
1350
|
+
:rtype: Command
|
|
1351
|
+
|
|
1352
|
+
.. note::
|
|
1353
|
+
- This is typically used for testing or simulation purposes
|
|
1354
|
+
- The humidity is converted to the appropriate hex format using standard precision
|
|
1355
|
+
- The message is sent as an I-type (unsolicited) message
|
|
1356
|
+
- The sensor index is hardcoded to 00 (most systems have only one humidity sensor)
|
|
1357
|
+
- The device ID should match the expected format for a humidity sensor or fan
|
|
1358
|
+
- The humidity value is expected to be in the range 0-100%
|
|
1359
|
+
- Example message format: ``.I --- 37:039266 --:------ 37:039266 12A0 003 0032`` (for 50%)
|
|
1360
|
+
"""
|
|
864
1361
|
payload = "00" + hex_from_percent(indoor_humidity, high_res=False)
|
|
865
1362
|
return cls._from_attrs(I_, Code._12A0, payload, addr0=dev_id, addr2=dev_id)
|
|
866
1363
|
|
|
@@ -868,16 +1365,50 @@ class Command(Frame):
|
|
|
868
1365
|
def get_zone_window_state(
|
|
869
1366
|
cls, ctl_id: DeviceIdT | str, zone_idx: _ZoneIdxT
|
|
870
1367
|
) -> Command:
|
|
871
|
-
"""
|
|
872
|
-
|
|
1368
|
+
"""Request the open window state of a zone (c.f. parser 12B0).
|
|
1369
|
+
|
|
1370
|
+
This method constructs a command to query whether a particular zone has an open window.
|
|
1371
|
+
The response will indicate if the window in the specified zone is open or closed.
|
|
1372
|
+
|
|
1373
|
+
:param ctl_id: The device ID of the controller managing the zone
|
|
1374
|
+
:type ctl_id: DeviceIdT | str
|
|
1375
|
+
:param zone_idx: The zone index (0-based) to query
|
|
1376
|
+
:type zone_idx: _ZoneIdxT
|
|
1377
|
+
:return: A Command object for the RQ|12B0 message
|
|
1378
|
+
:rtype: Command
|
|
1379
|
+
|
|
1380
|
+
.. note::
|
|
1381
|
+
- The zone index is 0-based (0 = Zone 1, 1 = Zone 2, etc.)
|
|
1382
|
+
- The controller will respond with a message indicating the window state
|
|
1383
|
+
- This is typically used by thermostats to enable/disable heating when windows are open
|
|
1384
|
+
- The actual window state detection is usually done by a separate sensor
|
|
1385
|
+
"""
|
|
873
1386
|
return cls.from_attrs(RQ, ctl_id, Code._12B0, _check_idx(zone_idx))
|
|
874
1387
|
|
|
875
1388
|
@classmethod # constructor for RQ|1F41
|
|
876
1389
|
def get_dhw_mode(cls, ctl_id: DeviceIdT | str, **kwargs: Any) -> Command:
|
|
877
|
-
"""
|
|
878
|
-
|
|
1390
|
+
"""Request the current mode of the Domestic Hot Water (DHW) system. (c.f. parser 1F41)
|
|
1391
|
+
|
|
1392
|
+
This method constructs a command to query the operating mode of the DHW system.
|
|
1393
|
+
The response will indicate whether the DHW is in automatic, manual, or other modes.
|
|
1394
|
+
|
|
1395
|
+
:param ctl_id: The device ID of the DHW controller
|
|
1396
|
+
:type ctl_id: DeviceIdT | str
|
|
1397
|
+
:param **kwargs: Additional parameters (currently only 'dhw_idx' is supported)
|
|
1398
|
+
:key dhw_idx: The DHW circuit index (0 or 1, defaults to 0 for single-DHW systems)
|
|
1399
|
+
:type dhw_idx: int, optional
|
|
1400
|
+
:return: A Command object for the RQ|1F41 message
|
|
1401
|
+
:rtype: Command
|
|
1402
|
+
:raises AssertionError: If unexpected keyword arguments are provided
|
|
1403
|
+
|
|
1404
|
+
.. note::
|
|
1405
|
+
- Most systems have a single DHW circuit (index 0)
|
|
1406
|
+
- The response will indicate the current DHW mode (e.g., auto, manual, off)
|
|
1407
|
+
- This is typically used by heating controllers to monitor DHW state
|
|
1408
|
+
- The actual mode values are defined in the response parser (parser_1f41)
|
|
1409
|
+
"""
|
|
879
1410
|
dhw_idx = _check_idx(kwargs.pop(SZ_DHW_IDX, 0)) # 00 or 01 (rare)
|
|
880
|
-
assert not kwargs, kwargs
|
|
1411
|
+
assert not kwargs, f"Unexpected arguments: {kwargs}"
|
|
881
1412
|
|
|
882
1413
|
return cls.from_attrs(RQ, ctl_id, Code._1F41, dhw_idx)
|
|
883
1414
|
|
|
@@ -892,10 +1423,38 @@ class Command(Frame):
|
|
|
892
1423
|
duration: int | None = None, # never supplied by DhwZone.set_mode()
|
|
893
1424
|
**kwargs: Any,
|
|
894
1425
|
) -> Command:
|
|
895
|
-
"""
|
|
896
|
-
|
|
1426
|
+
"""Set or reset the mode of the Domestic Hot Water (DHW) system. (c.f. parser 1F41)
|
|
1427
|
+
|
|
1428
|
+
This method constructs a command to change the operating mode of the DHW system.
|
|
1429
|
+
It can set the DHW to automatic, manual on/off, or scheduled modes with specific durations.
|
|
1430
|
+
|
|
1431
|
+
:param ctl_id: The device ID of the DHW controller
|
|
1432
|
+
:type ctl_id: DeviceIdT | str
|
|
1433
|
+
:param mode: The desired DHW mode (None, "auto", "heat", "off", or numeric values)
|
|
1434
|
+
:type mode: int | str | None
|
|
1435
|
+
:param active: If specified, sets the DHW on/off state (alternative to mode)
|
|
1436
|
+
:type active: bool | None
|
|
1437
|
+
:param until: End time for temporary mode (datetime or "YYYY-MM-DD HH:MM" string)
|
|
1438
|
+
:type until: datetime | str | None
|
|
1439
|
+
:param duration: Duration in seconds for temporary mode (alternative to 'until')
|
|
1440
|
+
:type duration: int | None
|
|
1441
|
+
:param **kwargs: Additional parameters (currently only 'dhw_idx' is supported)
|
|
1442
|
+
:key dhw_idx: The DHW circuit index (0 or 1, defaults to 0 for single-DHW systems)
|
|
1443
|
+
:type dhw_idx: int, optional
|
|
1444
|
+
:return: A Command object for the W|1F41 message
|
|
1445
|
+
:rtype: Command
|
|
1446
|
+
:raises AssertionError: If unexpected keyword arguments are provided
|
|
1447
|
+
:raises CommandInvalid: If invalid parameters are provided
|
|
1448
|
+
|
|
1449
|
+
.. note::
|
|
1450
|
+
- Mode takes precedence over 'active' if both are specified
|
|
1451
|
+
- When using 'active' with 'until' or 'duration', the mode will be temporary
|
|
1452
|
+
- Supported mode values are defined in ZON_MODE_MAP
|
|
1453
|
+
- Most systems have a single DHW circuit (index 0)
|
|
1454
|
+
- The actual mode values are defined in the response parser (parser_1f41)
|
|
1455
|
+
"""
|
|
897
1456
|
dhw_idx = _check_idx(kwargs.pop(SZ_DHW_IDX, 0)) # 00 or 01 (rare)
|
|
898
|
-
assert not kwargs, kwargs
|
|
1457
|
+
assert not kwargs, f"Unexpected arguments: {kwargs}"
|
|
899
1458
|
|
|
900
1459
|
mode = _normalise_mode(mode, active, until, duration)
|
|
901
1460
|
|
|
@@ -929,16 +1488,34 @@ class Command(Frame):
|
|
|
929
1488
|
dst_id: DeviceIdT | str | None = None,
|
|
930
1489
|
**kwargs: Any,
|
|
931
1490
|
) -> Command:
|
|
932
|
-
"""
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1491
|
+
"""Create an RF bind command (1FC9) for device binding operations.
|
|
1492
|
+
|
|
1493
|
+
This method constructs commands used in the 3-way handshake process for binding
|
|
1494
|
+
devices in the Ramses RF protocol. It's primarily used by faked/test devices.
|
|
1495
|
+
|
|
1496
|
+
:param verb: The verb for the command (I, RQ, RP, W, etc.)
|
|
1497
|
+
:type verb: VerbT
|
|
1498
|
+
:param src_id: Source device ID initiating the bind
|
|
1499
|
+
:type src_id: DeviceIdT | str
|
|
1500
|
+
:param codes: Single code or list of codes to bind
|
|
1501
|
+
:type codes: Code | Iterable[Code] | None
|
|
1502
|
+
:param dst_id: Optional destination device ID (defaults to broadcast)
|
|
1503
|
+
:type dst_id: DeviceIdT | str | None
|
|
1504
|
+
:param **kwargs: Additional parameters
|
|
1505
|
+
:key oem_code: OEM code for bind offers (only used with I-type messages)
|
|
1506
|
+
:type oem_code: str, optional
|
|
1507
|
+
:return: A Command object for the bind operation
|
|
1508
|
+
:rtype: Command
|
|
1509
|
+
:raises CommandInvalid: If invalid codes are provided for binding
|
|
1510
|
+
|
|
1511
|
+
.. note::
|
|
1512
|
+
- Common use cases include:
|
|
1513
|
+
- FAN binding to CO2 (1298), HUM (12A0), PER (2E10), or SWI (22F1, 22F3)
|
|
1514
|
+
- CTL binding to DHW (1260), RND/THM (30C9)
|
|
1515
|
+
- More complex bindings (e.g., TRV to CTL) may require custom constructors
|
|
1516
|
+
- The binding process typically involves a 3-way handshake
|
|
1517
|
+
- For I-type messages with no specific destination, this creates a bind offer
|
|
940
1518
|
"""
|
|
941
|
-
|
|
942
1519
|
kodes: list[Code]
|
|
943
1520
|
|
|
944
1521
|
if not codes: # None, "", or []
|
|
@@ -952,7 +1529,7 @@ class Command(Frame):
|
|
|
952
1529
|
|
|
953
1530
|
if verb == I_ and dst_id in (None, src_id, ALL_DEV_ADDR.id):
|
|
954
1531
|
oem_code = kwargs.pop("oem_code", None)
|
|
955
|
-
assert not kwargs, kwargs
|
|
1532
|
+
assert not kwargs, f"Unexpected arguments: {kwargs}"
|
|
956
1533
|
return cls._put_bind_offer(src_id, dst_id, kodes, oem_code=oem_code)
|
|
957
1534
|
|
|
958
1535
|
elif verb == W_ and dst_id not in (None, src_id):
|
|
@@ -978,7 +1555,33 @@ class Command(Frame):
|
|
|
978
1555
|
*,
|
|
979
1556
|
oem_code: str | None = None,
|
|
980
1557
|
) -> Command:
|
|
1558
|
+
"""Create a bind offer message (I-type) for device binding.
|
|
1559
|
+
|
|
981
1560
|
# TODO: should preserve order of codes, else tests may fail
|
|
1561
|
+
|
|
1562
|
+
This internal method constructs the initial bind offer message in the 3-way
|
|
1563
|
+
binding handshake. It's typically called by `put_bind()` and not used directly.
|
|
1564
|
+
|
|
1565
|
+
:param src_id: Source device ID making the offer
|
|
1566
|
+
:type src_id: DeviceIdT | str
|
|
1567
|
+
:param dst_id: Optional destination device ID (broadcast if None)
|
|
1568
|
+
:type dst_id: DeviceIdT | str | None
|
|
1569
|
+
:param codes: List of codes to include in the bind offer
|
|
1570
|
+
:type codes: list[Code]
|
|
1571
|
+
:param oem_code: Optional OEM-specific code for the binding
|
|
1572
|
+
:type oem_code: str | None
|
|
1573
|
+
:return: A Command object for the bind offer message
|
|
1574
|
+
:rtype: Command
|
|
1575
|
+
:raises CommandInvalid: If no valid codes are provided for the offer
|
|
1576
|
+
|
|
1577
|
+
.. note::
|
|
1578
|
+
- This creates an I-type (unsolicited) bind offer message
|
|
1579
|
+
- The message includes the source device's ID and the requested bind codes
|
|
1580
|
+
- OEM-specific bindings can include an additional OEM code
|
|
1581
|
+
- The actual binding codes are filtered to exclude 1FC9 and 10E0
|
|
1582
|
+
- The order of codes is preserved in the output message
|
|
1583
|
+
"""
|
|
1584
|
+
# Filter out 1FC9 and 10E0 from the codes list
|
|
982
1585
|
kodes = [c for c in codes if c not in (Code._1FC9, Code._10E0)]
|
|
983
1586
|
if not kodes: # might be []
|
|
984
1587
|
raise exc.CommandInvalid(f"Invalid codes for a bind offer: {codes}")
|
|
@@ -1003,7 +1606,31 @@ class Command(Frame):
|
|
|
1003
1606
|
*,
|
|
1004
1607
|
idx: str | None = "00",
|
|
1005
1608
|
) -> Command:
|
|
1006
|
-
|
|
1609
|
+
"""Create a bind accept message (W-type) for device binding.
|
|
1610
|
+
|
|
1611
|
+
This internal method constructs the bind accept message in the 3-way binding
|
|
1612
|
+
handshake. It's typically called by `put_bind()` and is mainly used for testing.
|
|
1613
|
+
|
|
1614
|
+
:param src_id: Source device ID accepting the bind
|
|
1615
|
+
:type src_id: DeviceIdT | str
|
|
1616
|
+
:param dst_id: Destination device ID that sent the bind offer
|
|
1617
|
+
:type dst_id: DeviceIdT | str
|
|
1618
|
+
:param codes: List of codes to include in the bind accept
|
|
1619
|
+
:type codes: list[Code]
|
|
1620
|
+
:param idx: Optional index for the binding (defaults to "00")
|
|
1621
|
+
:type idx: str | None
|
|
1622
|
+
:return: A Command object for the bind accept message
|
|
1623
|
+
:rtype: Command
|
|
1624
|
+
:raises CommandInvalid: If no valid codes are provided for the accept
|
|
1625
|
+
|
|
1626
|
+
.. note::
|
|
1627
|
+
- This creates a W-type (write) bind accept message
|
|
1628
|
+
- The message includes the source device's ID and the accepted bind codes
|
|
1629
|
+
- The index parameter allows for multiple bindings between the same devices
|
|
1630
|
+
- Primarily used in test suites to simulate device binding
|
|
1631
|
+
- The actual binding codes should match those in the original offer
|
|
1632
|
+
"""
|
|
1633
|
+
if not codes: # might be empty list
|
|
1007
1634
|
raise exc.CommandInvalid(f"Invalid codes for a bind accept: {codes}")
|
|
1008
1635
|
|
|
1009
1636
|
hex_id = Address.convert_to_hex(src_id) # type: ignore[arg-type]
|
|
@@ -1020,6 +1647,31 @@ class Command(Frame):
|
|
|
1020
1647
|
*,
|
|
1021
1648
|
idx: str | None = "00",
|
|
1022
1649
|
) -> Command:
|
|
1650
|
+
"""Create a bind confirmation message (I-type) to complete device binding.
|
|
1651
|
+
|
|
1652
|
+
This internal method constructs the final confirmation message in the 3-way
|
|
1653
|
+
binding handshake. It's typically called by `put_bind()` to confirm that
|
|
1654
|
+
the binding process has been completed successfully.
|
|
1655
|
+
|
|
1656
|
+
:param src_id: Source device ID confirming the bind
|
|
1657
|
+
:type src_id: DeviceIdT | str
|
|
1658
|
+
:param dst_id: Destination device ID that needs confirmation
|
|
1659
|
+
:type dst_id: DeviceIdT | str
|
|
1660
|
+
:param codes: List of codes that were bound (only first code is used)
|
|
1661
|
+
:type codes: list[Code]
|
|
1662
|
+
:param idx: Optional index for the binding (defaults to "00")
|
|
1663
|
+
:type idx: str | None
|
|
1664
|
+
:return: A Command object for the bind confirmation message
|
|
1665
|
+
:rtype: Command
|
|
1666
|
+
|
|
1667
|
+
.. note::
|
|
1668
|
+
- This creates an I-type (unsolicited) bind confirmation message
|
|
1669
|
+
- The message includes the source device's ID and the first bound code
|
|
1670
|
+
- If no codes are provided, only the index is used as payload
|
|
1671
|
+
- The index is important (e.g., Nuaire 4-way switch uses "21")
|
|
1672
|
+
- This is the final step in the 3-way binding handshake
|
|
1673
|
+
- The binding is considered complete after this message is received
|
|
1674
|
+
"""
|
|
1023
1675
|
if not codes: # if not payload
|
|
1024
1676
|
payload = idx or "00" # e.g. Nuaire 4-way switch uses 21!
|
|
1025
1677
|
else:
|
|
@@ -1038,11 +1690,47 @@ class Command(Frame):
|
|
|
1038
1690
|
src_id: DeviceIdT | str | None = None,
|
|
1039
1691
|
idx: str = "00", # could be e.g. "63"
|
|
1040
1692
|
) -> Command:
|
|
1041
|
-
"""
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1693
|
+
"""Set the operating mode of a ventilation fan.
|
|
1694
|
+
|
|
1695
|
+
This method constructs a command to control the speed and operating mode of a
|
|
1696
|
+
ventilation fan. The command can be sent with either a sequence number or a
|
|
1697
|
+
source device ID, depending on the system configuration.
|
|
1698
|
+
|
|
1699
|
+
There are two types of this packet observed:
|
|
1700
|
+
- With sequence number: ``I 018 --:------ --:------ 39:159057 22F1 003 000x04``
|
|
1701
|
+
- With source ID: ``I --- 21:039407 28:126495 --:------ 22F1 003 000x07``
|
|
1702
|
+
|
|
1703
|
+
:param fan_id: The device ID of the target fan (e.g., '39:159057')
|
|
1704
|
+
:type fan_id: DeviceIdT | str
|
|
1705
|
+
:param fan_mode: The desired fan mode, which can be specified as:
|
|
1706
|
+
- Integer: 0-9 for different speed levels
|
|
1707
|
+
- String: Descriptive mode like 'auto', 'low', 'medium', 'high'
|
|
1708
|
+
- None: Default mode (typically auto)
|
|
1709
|
+
:type fan_mode: int | str | None
|
|
1710
|
+
:param seqn: Optional sequence number (0-255), mutually exclusive with src_id
|
|
1711
|
+
:type seqn: int | str | None
|
|
1712
|
+
:param src_id: Optional source device ID, mutually exclusive with seqn
|
|
1713
|
+
:type src_id: DeviceIdT | str | None
|
|
1714
|
+
:param idx: Index identifier, typically '00' but can be other values like '63'
|
|
1715
|
+
:type idx: str
|
|
1716
|
+
:return: A configured Command object ready to be sent to the device.
|
|
1717
|
+
:rtype: Command
|
|
1718
|
+
:raises CommandInvalid: If both seqn and src_id are provided, or if fan_mode is invalid.
|
|
1719
|
+
|
|
1720
|
+
.. note::
|
|
1721
|
+
This command is typically sent as part of a triplet with 0.1s intervals
|
|
1722
|
+
when using sequence numbers. The sequence number should increase
|
|
1723
|
+
monotonically modulo 256 after each triplet.
|
|
1724
|
+
|
|
1725
|
+
**Scheme 1 (with sequence number):**
|
|
1726
|
+
- Sent as a triplet, 0.1s apart
|
|
1727
|
+
- Uses a sequence number (000-255)
|
|
1728
|
+
- Example: ``I 218 --:------ --:------ 39:159057 22F1 003 000204`` (low speed)
|
|
1729
|
+
|
|
1730
|
+
**Scheme 2 (with source ID):**
|
|
1731
|
+
- Sent as a triplet, 0.085s apart
|
|
1732
|
+
- Uses source device ID instead of sequence number
|
|
1733
|
+
- Example: ``I --- 21:039407 28:126495 --:------ 22F1 003 000507``
|
|
1046
1734
|
"""
|
|
1047
1735
|
# NOTE: WIP: rate can be int or str
|
|
1048
1736
|
|
|
@@ -1096,12 +1784,36 @@ class Command(Frame):
|
|
|
1096
1784
|
src_id: DeviceIdT | str | None = None,
|
|
1097
1785
|
**kwargs: Any,
|
|
1098
1786
|
) -> Command:
|
|
1099
|
-
"""
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1787
|
+
"""Set the position or mode of a bypass valve in a ventilation system.
|
|
1788
|
+
|
|
1789
|
+
This method constructs a command to control the bypass valve position or mode
|
|
1790
|
+
for a ventilation system. The bypass valve regulates the flow of air between
|
|
1791
|
+
the supply and exhaust air streams, typically for heat recovery.
|
|
1792
|
+
|
|
1793
|
+
The method supports two ways to control the bypass:
|
|
1794
|
+
- Direct position control using `bypass_position` (0.0 to 1.0)
|
|
1795
|
+
- Predefined modes using `bypass_mode` ('auto', 'on', 'off')
|
|
1796
|
+
|
|
1797
|
+
:param fan_id: The device ID of the target fan/ventilation unit (e.g., '01:123456')
|
|
1798
|
+
:type fan_id: DeviceIdT | str
|
|
1799
|
+
:param bypass_position: The desired position as a float between 0.0 (fully closed)
|
|
1800
|
+
and 1.0 (fully open). If None, the system will use auto mode.
|
|
1801
|
+
:type bypass_position: float | None
|
|
1802
|
+
:param src_id: The source device ID sending the command. If None, defaults to fan_id.
|
|
1803
|
+
:type src_id: DeviceIdT | str | None
|
|
1804
|
+
:keyword bypass_mode: Alternative to bypass_position, accepts:
|
|
1805
|
+
- 'auto': Let the system control the bypass automatically
|
|
1806
|
+
- 'on': Force bypass fully open
|
|
1807
|
+
- 'off': Force bypass fully closed
|
|
1808
|
+
:type bypass_mode: str | None
|
|
1809
|
+
:return: A configured Command object ready to be sent to the device.
|
|
1810
|
+
:rtype: Command
|
|
1811
|
+
:raises CommandInvalid: If both bypass_position and bypass_mode are provided,
|
|
1812
|
+
or if an invalid bypass_mode is specified.
|
|
1813
|
+
|
|
1814
|
+
.. note::
|
|
1815
|
+
The bypass valve position affects heat recovery efficiency and indoor air quality.
|
|
1816
|
+
Use with caution as incorrect settings may impact system performance.
|
|
1105
1817
|
"""
|
|
1106
1818
|
|
|
1107
1819
|
# RQ --- 37:155617 32:155617 --:------ 22F7 002 0064 # officially: 00C8EF
|
|
@@ -1130,23 +1842,86 @@ class Command(Frame):
|
|
|
1130
1842
|
|
|
1131
1843
|
@classmethod # constructor for RQ|2309
|
|
1132
1844
|
def get_zone_setpoint(cls, ctl_id: DeviceIdT | str, zone_idx: _ZoneIdxT) -> Command:
|
|
1133
|
-
"""
|
|
1134
|
-
|
|
1845
|
+
"""Get the current temperature setpoint for a specific zone.
|
|
1846
|
+
|
|
1847
|
+
This method constructs a command to request the current temperature setpoint
|
|
1848
|
+
for a specified zone from the controller. The response will contain the current
|
|
1849
|
+
target temperature for the zone.
|
|
1850
|
+
|
|
1851
|
+
:param ctl_id: The device ID of the controller (e.g., '01:123456')
|
|
1852
|
+
:type ctl_id: DeviceIdT | str
|
|
1853
|
+
:param zone_idx: The index of the zone (0-31 or '00'-'1F')
|
|
1854
|
+
:type zone_idx: _ZoneIdxT
|
|
1855
|
+
:return: A configured Command object that can be sent to the device.
|
|
1856
|
+
:rtype: Command
|
|
1857
|
+
:raises ValueError: If the zone index is out of valid range (0-31)
|
|
1858
|
+
|
|
1859
|
+
.. note::
|
|
1860
|
+
The zone index is 0-based, where:
|
|
1861
|
+
- 0 = Zone 1 (typically main living area)
|
|
1862
|
+
- 1 = Zone 2 (e.g., bedrooms)
|
|
1863
|
+
- And so on up to zone 32
|
|
1864
|
+
|
|
1865
|
+
The actual number of available zones depends on the controller configuration.
|
|
1866
|
+
Requesting a non-existent zone will typically result in no response.
|
|
1867
|
+
"""
|
|
1135
1868
|
return cls.from_attrs(W_, ctl_id, Code._2309, _check_idx(zone_idx))
|
|
1136
1869
|
|
|
1137
|
-
@classmethod # constructor for W|2309
|
|
1870
|
+
@classmethod # constructor for W|2309
|
|
1138
1871
|
def set_zone_setpoint(
|
|
1139
1872
|
cls, ctl_id: DeviceIdT | str, zone_idx: _ZoneIdxT, setpoint: float
|
|
1140
1873
|
) -> Command:
|
|
1141
|
-
"""
|
|
1142
|
-
|
|
1143
|
-
|
|
1874
|
+
"""Set the temperature setpoint for a specific zone.
|
|
1875
|
+
|
|
1876
|
+
This method constructs a command to set the target temperature for a specified
|
|
1877
|
+
zone. The setpoint is specified in degrees Celsius with a resolution of 0.1°C.
|
|
1878
|
+
|
|
1879
|
+
:param ctl_id: The device ID of the controller (e.g., '01:123456')
|
|
1880
|
+
:type ctl_id: DeviceIdT | str
|
|
1881
|
+
:param zone_idx: The index of the zone (0-31 or '00'-'1F')
|
|
1882
|
+
:type zone_idx: _ZoneIdxT
|
|
1883
|
+
:param setpoint: The desired temperature in °C (typically 5.0-35.0)
|
|
1884
|
+
:type setpoint: float
|
|
1885
|
+
:return: A configured Command object ready to be sent to the device.
|
|
1886
|
+
:rtype: Command
|
|
1887
|
+
:raises ValueError: If the setpoint is outside the valid range or if the
|
|
1888
|
+
zone index is invalid.
|
|
1889
|
+
|
|
1890
|
+
.. note::
|
|
1891
|
+
The controller will typically round the setpoint to the nearest 0.5°C.
|
|
1892
|
+
The actual temperature range may be further limited by:
|
|
1893
|
+
- System-wide minimum/maximum limits
|
|
1894
|
+
- Zone-specific overrides
|
|
1895
|
+
- Current operating mode (heating/cooling)
|
|
1896
|
+
|
|
1897
|
+
When setting a new setpoint, the system may take some time to acknowledge
|
|
1898
|
+
the change. Use `get_zone_setpoint` to verify the new setting.
|
|
1899
|
+
|
|
1900
|
+
Some systems may have additional restrictions on when setpoints can be
|
|
1901
|
+
modified, such as during specific operating modes or schedules.
|
|
1902
|
+
"""
|
|
1903
|
+
# Example: .W --- 34:092243 01:145038 --:------ 2309 003 0107D0
|
|
1144
1904
|
payload = f"{_check_idx(zone_idx)}{hex_from_temp(setpoint)}"
|
|
1145
1905
|
return cls.from_attrs(W_, ctl_id, Code._2309, payload)
|
|
1146
1906
|
|
|
1147
1907
|
@classmethod # constructor for RQ|2349
|
|
1148
1908
|
def get_zone_mode(cls, ctl_id: DeviceIdT | str, zone_idx: _ZoneIdxT) -> Command:
|
|
1149
|
-
"""
|
|
1909
|
+
"""Get the current operating mode of a zone.
|
|
1910
|
+
|
|
1911
|
+
This method constructs a command to request the current operating mode
|
|
1912
|
+
and setpoint information for a specific zone from the controller.
|
|
1913
|
+
|
|
1914
|
+
:param ctl_id: The device ID of the controller (e.g., '01:123456')
|
|
1915
|
+
:type ctl_id: DeviceIdT | str
|
|
1916
|
+
:param zone_idx: The index of the zone (0-31 or '00'-'1F')
|
|
1917
|
+
:type zone_idx: _ZoneIdxT
|
|
1918
|
+
:return: A configured Command object that can be sent to the device.
|
|
1919
|
+
:rtype: Command
|
|
1920
|
+
|
|
1921
|
+
:Example:
|
|
1922
|
+
>>> # Get mode for zone 0
|
|
1923
|
+
>>> cmd = Command.get_zone_mode('01:123456', '00')
|
|
1924
|
+
"""
|
|
1150
1925
|
|
|
1151
1926
|
return cls.from_attrs(RQ, ctl_id, Code._2349, _check_idx(zone_idx))
|
|
1152
1927
|
|
|
@@ -1161,17 +1936,36 @@ class Command(Frame):
|
|
|
1161
1936
|
until: dt | str | None = None,
|
|
1162
1937
|
duration: int | None = None, # never supplied by Zone.set_mode()
|
|
1163
1938
|
) -> Command:
|
|
1164
|
-
"""
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1939
|
+
"""Set or reset the operating mode of a zone.
|
|
1940
|
+
|
|
1941
|
+
This method constructs a command to configure the operating mode and setpoint
|
|
1942
|
+
for a specific zone. The command can set the zone to various modes including
|
|
1943
|
+
follow schedule, temporary override, or permanent override.
|
|
1944
|
+
|
|
1945
|
+
:param ctl_id: The device ID of the controller (e.g., '01:123456')
|
|
1946
|
+
:type ctl_id: DeviceIdT | str
|
|
1947
|
+
:param zone_idx: The index of the zone (0-31 or '00'-'1F')
|
|
1948
|
+
:type zone_idx: _ZoneIdxT
|
|
1949
|
+
:keyword mode: The desired operating mode. Can be an integer, string, or None.
|
|
1950
|
+
Common values include 'follow_schedule', 'temporary', 'permanent_override'.
|
|
1951
|
+
:type mode: int | str | None
|
|
1952
|
+
:keyword setpoint: The target temperature in °C (resolution 0.1°C). Required for
|
|
1953
|
+
some modes. If None, the system will use the maximum possible value.
|
|
1954
|
+
:type setpoint: float | None
|
|
1955
|
+
:keyword until: The end time for a temporary override. Required for 'temporary' mode.
|
|
1956
|
+
Can be a datetime object or ISO 8601 formatted string.
|
|
1957
|
+
:type until: datetime | str | None
|
|
1958
|
+
:keyword duration: Duration in minutes for the override. Mutually exclusive with 'until'.
|
|
1959
|
+
:type duration: int | None
|
|
1960
|
+
:return: A configured Command object ready to be sent to the device.
|
|
1961
|
+
:rtype: Command
|
|
1962
|
+
:raises CommandInvalid: If invalid arguments are provided.
|
|
1963
|
+
|
|
1964
|
+
.. note::
|
|
1965
|
+
Incompatible combinations:
|
|
1966
|
+
- mode == 'follow_schedule' & setpoint is not None (setpoint will be ignored)
|
|
1967
|
+
- mode == 'temporary' & until is None (until is required)
|
|
1968
|
+
- until and duration are mutually exclusive (use only one)
|
|
1175
1969
|
"""
|
|
1176
1970
|
|
|
1177
1971
|
# .W --- 18:013393 01:145038 --:------ 2349 013 0004E201FFFFFF330B1A0607E4
|
|
@@ -1203,20 +1997,259 @@ class Command(Frame):
|
|
|
1203
1997
|
cls,
|
|
1204
1998
|
fan_id: DeviceIdT | str,
|
|
1205
1999
|
param_id: str,
|
|
1206
|
-
value: str,
|
|
2000
|
+
value: str | int | float | bool,
|
|
1207
2001
|
*,
|
|
1208
2002
|
src_id: DeviceIdT | str | None = None,
|
|
1209
2003
|
) -> Command:
|
|
1210
|
-
"""
|
|
2004
|
+
"""Set a configuration parameter for a fan/ventilation device.
|
|
2005
|
+
|
|
2006
|
+
This method constructs a command to configure various parameters of a
|
|
2007
|
+
fan or ventilation device using the RAMSES-II protocol.
|
|
2008
|
+
|
|
2009
|
+
:param fan_id: The device ID of the fan/ventilation unit
|
|
2010
|
+
:type fan_id: DeviceIdT | str
|
|
2011
|
+
:param param_id: The parameter ID to set (e.g., 'bypass_position' or hex code '00')
|
|
2012
|
+
:type param_id: str
|
|
2013
|
+
:param value: The value to set for the parameter. Type depends on the parameter.
|
|
2014
|
+
:type value: str | int | float | bool
|
|
2015
|
+
:param src_id: Optional source device ID. If not provided, fan_id will be used.
|
|
2016
|
+
:type src_id: DeviceIdT | str | None
|
|
2017
|
+
:return: A configured Command object ready to be sent to the device.
|
|
2018
|
+
:rtype: Command
|
|
2019
|
+
:raises CommandInvalid: If the parameter ID is unknown or value is invalid.
|
|
2020
|
+
|
|
2021
|
+
.. note::
|
|
2022
|
+
The parameter ID must be a valid 2-character hexadecimal string (00-FF) that
|
|
2023
|
+
exists in the _2411_PARAMS_SCHEMA. The payload format follows the pattern:
|
|
2024
|
+
^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}(([0-9A-F]{8}){3}[0-9A-F]{4})?$
|
|
2025
|
+
--- Ramses-II 2411 payload: 23 bytes, 46 hex digits ---
|
|
2026
|
+
|
|
2027
|
+
Raises:
|
|
2028
|
+
CommandInvalid: For invalid parameters or values
|
|
2029
|
+
"""
|
|
2030
|
+
# Validate and normalize parameter ID
|
|
2031
|
+
try:
|
|
2032
|
+
param_id = param_id.strip().upper()
|
|
2033
|
+
if len(param_id) != 2:
|
|
2034
|
+
raise ValueError(
|
|
2035
|
+
"Parameter ID must be exactly 2 hexadecimal characters"
|
|
2036
|
+
)
|
|
2037
|
+
int(param_id, 16) # Validate hex
|
|
2038
|
+
except ValueError as err:
|
|
2039
|
+
raise exc.CommandInvalid(
|
|
2040
|
+
f"Invalid parameter ID: '{param_id}'. Must be a 2-digit hexadecimal value (00-FF)"
|
|
2041
|
+
) from err
|
|
1211
2042
|
|
|
1212
|
-
|
|
2043
|
+
# Get parameter schema
|
|
2044
|
+
if (param_schema := _2411_PARAMS_SCHEMA.get(param_id)) is None:
|
|
2045
|
+
raise exc.CommandInvalid(
|
|
2046
|
+
f"Unknown parameter ID: '{param_id}'. This parameter is not defined in the device schema"
|
|
2047
|
+
)
|
|
2048
|
+
|
|
2049
|
+
# Get value constraints with defaults
|
|
2050
|
+
min_val = param_schema[SZ_MIN_VALUE]
|
|
2051
|
+
max_val = param_schema[SZ_MAX_VALUE]
|
|
2052
|
+
precision = param_schema.get(SZ_PRECISION, 1.0)
|
|
2053
|
+
data_type = param_schema.get(SZ_DATA_TYPE, "00")
|
|
2054
|
+
|
|
2055
|
+
try:
|
|
2056
|
+
# Check for special float values first
|
|
2057
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
2058
|
+
raise exc.CommandInvalid(
|
|
2059
|
+
f"Parameter {param_id}: Invalid value '{value}'. Must be a finite number"
|
|
2060
|
+
)
|
|
2061
|
+
|
|
2062
|
+
# Scaling
|
|
2063
|
+
if str(data_type) == "01": # %
|
|
2064
|
+
# Special handling for parameter 52 (Sensor sensitivity)
|
|
2065
|
+
value_scaled = int(round(float(value) / precision))
|
|
2066
|
+
min_val_scaled = int(round(float(min_val) / precision))
|
|
2067
|
+
max_val_scaled = int(round(float(max_val) / precision))
|
|
2068
|
+
precision_scaled = int(round(float(precision) * 10))
|
|
2069
|
+
trailer = "0032" # Trailer for percentage parameters
|
|
2070
|
+
|
|
2071
|
+
# For percentage values, validate input is in range
|
|
2072
|
+
if not min_val_scaled <= value_scaled <= max_val_scaled:
|
|
2073
|
+
raise exc.CommandInvalid(
|
|
2074
|
+
f"Parameter {param_id}: Value {value_scaled / 10}% is out of allowed range ({min_val_scaled / 10}% to {max_val_scaled / 10}%)"
|
|
2075
|
+
)
|
|
2076
|
+
elif str(data_type) == "0F": # %
|
|
2077
|
+
# For other percentage parameters, use the standard scaling
|
|
2078
|
+
value_scaled = int(round((float(value) / 100.0) / float(precision)))
|
|
2079
|
+
min_val_scaled = int(round(float(min_val) / float(precision)))
|
|
2080
|
+
max_val_scaled = int(round(float(max_val) / float(precision)))
|
|
2081
|
+
precision_scaled = int(round(float(precision) * 200))
|
|
2082
|
+
trailer = "0032" # Trailer for percentage parameters
|
|
2083
|
+
|
|
2084
|
+
# For percentage values, validate input is in range
|
|
2085
|
+
if not min_val_scaled <= value_scaled <= max_val_scaled:
|
|
2086
|
+
raise exc.CommandInvalid(
|
|
2087
|
+
f"Parameter {param_id}: Value {value_scaled / 2}% is out of allowed range ({min_val_scaled / 2}% to {max_val_scaled / 2}%)"
|
|
2088
|
+
)
|
|
2089
|
+
elif str(data_type) == "92": # °C
|
|
2090
|
+
# Scale temperature values by 100 (21.5°C -> 2150 = 0x0866)
|
|
2091
|
+
# Round to 0.1°C precision first, then scale
|
|
2092
|
+
value_rounded = (
|
|
2093
|
+
round(float(value) * 10) / 10
|
|
2094
|
+
) # Round to 1 decimal place
|
|
2095
|
+
value_scaled = int(
|
|
2096
|
+
value_rounded * 100
|
|
2097
|
+
) # Convert to integer (e.g., 21.5 -> 2150)
|
|
2098
|
+
min_val_scaled = int(float(min_val) * 100)
|
|
2099
|
+
max_val_scaled = int(float(max_val) * 100)
|
|
2100
|
+
precision_scaled = int(float(precision) * 100)
|
|
2101
|
+
trailer = (
|
|
2102
|
+
"0001" # always 4 hex not sure about the value, but seems to work.
|
|
2103
|
+
)
|
|
2104
|
+
# For temperature values, validate input is within allowed range
|
|
2105
|
+
if not min_val_scaled <= value_scaled <= max_val_scaled:
|
|
2106
|
+
raise exc.CommandInvalid(
|
|
2107
|
+
f"Parameter {param_id}: Temperature {value_scaled / 100:.1f}°C is out of allowed range ({min_val_scaled / 100:.1f}°C to {max_val_scaled / 100:.1f}°C)"
|
|
2108
|
+
)
|
|
2109
|
+
elif (str(data_type) == "00") or (
|
|
2110
|
+
str(data_type) == "10"
|
|
2111
|
+
): # numeric (minutes, medium(0)/high(1) or days)
|
|
2112
|
+
value_scaled = int(float(value))
|
|
2113
|
+
min_val_scaled = int(float(min_val))
|
|
2114
|
+
max_val_scaled = int(float(max_val))
|
|
2115
|
+
precision = 1
|
|
2116
|
+
precision_scaled = int(precision)
|
|
2117
|
+
trailer = (
|
|
2118
|
+
"0001" # always 4 hex not sure about the value, but seems to work.
|
|
2119
|
+
)
|
|
2120
|
+
# For numeric values, validate input is between min and max
|
|
2121
|
+
if not min_val_scaled <= value_scaled <= max_val_scaled:
|
|
2122
|
+
unit = "minutes" if data_type == "00" else ""
|
|
2123
|
+
raise exc.CommandInvalid(
|
|
2124
|
+
f"Parameter {param_id}: Value {value_scaled}{' ' + unit if unit else ''} is out of allowed range ({min_val_scaled} to {max_val_scaled}{' ' + unit if unit else ''})"
|
|
2125
|
+
)
|
|
2126
|
+
else:
|
|
2127
|
+
# Validate value against min/max
|
|
2128
|
+
raise exc.CommandInvalid(
|
|
2129
|
+
f"Parameter {param_id}: Invalid data type '{data_type}'. Must be one of '00', '01', '0F', '10', or '92'"
|
|
2130
|
+
f"Invalid Data_type {data_type} for parameter {param_id}"
|
|
2131
|
+
)
|
|
2132
|
+
|
|
2133
|
+
# Assemble payload fields
|
|
2134
|
+
leading = "00" # always 2 hex
|
|
2135
|
+
param_id_hex = f"{int(param_id, 16):04X}" # 4 hex, upper, zero-padded
|
|
2136
|
+
|
|
2137
|
+
# data_type (6 hex): always from schema, zero-padded to 6 hex
|
|
2138
|
+
data_type_hex = f"00{data_type}"
|
|
2139
|
+
value_hex = f"{value_scaled:08X}"
|
|
2140
|
+
min_hex = f"{min_val_scaled:08X}"
|
|
2141
|
+
max_hex = f"{max_val_scaled:08X}"
|
|
2142
|
+
precision_hex = f"{precision_scaled:08X}"
|
|
2143
|
+
|
|
2144
|
+
_LOGGER.debug(
|
|
2145
|
+
f"set_fan_param: value={value}, min={min_val}, max={max_val}, precision={precision}"
|
|
2146
|
+
f"\n Scaled: value={value_scaled} (0x{value_hex}), min={min_val_scaled} (0x{min_hex}), "
|
|
2147
|
+
f"max={max_val_scaled} (0x{max_hex}), precision={precision_scaled} (0x{precision_hex})"
|
|
2148
|
+
)
|
|
2149
|
+
|
|
2150
|
+
# Final field order: 2+4+4+8+8+8+8+4 = 46 hex -> 23 bytes
|
|
2151
|
+
payload = (
|
|
2152
|
+
f"{leading}"
|
|
2153
|
+
f"{param_id_hex}"
|
|
2154
|
+
f"{data_type_hex}"
|
|
2155
|
+
f"{value_hex}"
|
|
2156
|
+
f"{min_hex}"
|
|
2157
|
+
f"{max_hex}"
|
|
2158
|
+
f"{precision_hex}"
|
|
2159
|
+
f"{trailer}"
|
|
2160
|
+
)
|
|
2161
|
+
payload = "".join(payload)
|
|
2162
|
+
_LOGGER.debug(
|
|
2163
|
+
f"set_fan_param: Final frame: {W_} --- {src_id} {fan_id} --:------ 2411 {len(payload):03d} {payload}"
|
|
2164
|
+
)
|
|
2165
|
+
|
|
2166
|
+
# Create the command with exactly 2 addresses: from_id and fan_id
|
|
2167
|
+
return cls._from_attrs(
|
|
2168
|
+
W_,
|
|
2169
|
+
Code._2411,
|
|
2170
|
+
payload,
|
|
2171
|
+
addr0=src_id,
|
|
2172
|
+
addr1=fan_id,
|
|
2173
|
+
addr2=NON_DEV_ADDR.id,
|
|
2174
|
+
)
|
|
2175
|
+
|
|
2176
|
+
except (ValueError, TypeError) as err:
|
|
2177
|
+
raise exc.CommandInvalid(f"Invalid value: {value}") from err
|
|
1213
2178
|
|
|
1214
|
-
|
|
1215
|
-
|
|
2179
|
+
@classmethod # constructor for RQ|2411
|
|
2180
|
+
def get_fan_param(
|
|
2181
|
+
cls,
|
|
2182
|
+
fan_id: DeviceIdT | str,
|
|
2183
|
+
param_id: str,
|
|
2184
|
+
*,
|
|
2185
|
+
src_id: DeviceIdT | str,
|
|
2186
|
+
) -> Command:
|
|
2187
|
+
"""Create a command to get a fan parameter value.
|
|
2188
|
+
|
|
2189
|
+
This method constructs a command to read a specific parameter from a fan device
|
|
2190
|
+
using the RAMSES-II 2411 command. The parameter ID must be a valid 2-character
|
|
2191
|
+
hexadecimal string (00-FF).
|
|
2192
|
+
|
|
2193
|
+
:param fan_id: The device ID of the target fan (e.g., '01:123456')
|
|
2194
|
+
:type fan_id: DeviceIdT | str
|
|
2195
|
+
:param param_id: The parameter ID to read (2-character hex string, e.g., '4E')
|
|
2196
|
+
:type param_id: str
|
|
2197
|
+
:param src_id: The source device ID that will send the command
|
|
2198
|
+
:type src_id: DeviceIdT | str
|
|
2199
|
+
:return: A Command object for the RQ|2411 message
|
|
2200
|
+
:rtype: Command
|
|
2201
|
+
:raises CommandInvalid: If the parameter ID is invalid (None, wrong type, wrong format)
|
|
2202
|
+
|
|
2203
|
+
.. note::
|
|
2204
|
+
For a complete working example, see the `test_get_fan_param.py` test file
|
|
2205
|
+
which demonstrates:
|
|
2206
|
+
- Setting up the gateway
|
|
2207
|
+
- Sending the command
|
|
2208
|
+
- Handling the response
|
|
2209
|
+
- Proper error handling
|
|
2210
|
+
|
|
2211
|
+
.. warning::
|
|
2212
|
+
The parameter ID must be a valid 2-character hexadecimal string (00-FF).
|
|
2213
|
+
The following will raise CommandInvalid:
|
|
2214
|
+
- None value
|
|
2215
|
+
- Non-string types
|
|
2216
|
+
- Leading/trailing whitespace
|
|
2217
|
+
- Incorrect length (not 2 characters)
|
|
2218
|
+
- Non-hexadecimal characters
|
|
2219
|
+
"""
|
|
2220
|
+
if param_id is None:
|
|
2221
|
+
raise exc.CommandInvalid("Parameter ID cannot be None")
|
|
1216
2222
|
|
|
1217
|
-
|
|
2223
|
+
if not isinstance(param_id, str):
|
|
2224
|
+
raise exc.CommandInvalid(
|
|
2225
|
+
f"Parameter ID must be a string, got {type(param_id).__name__}"
|
|
2226
|
+
)
|
|
1218
2227
|
|
|
1219
|
-
|
|
2228
|
+
param_id_stripped = param_id.strip()
|
|
2229
|
+
if param_id != param_id_stripped:
|
|
2230
|
+
raise exc.CommandInvalid(
|
|
2231
|
+
f"Parameter ID cannot have leading or trailing whitespace: '{param_id}'"
|
|
2232
|
+
)
|
|
2233
|
+
|
|
2234
|
+
# validate the string format
|
|
2235
|
+
try:
|
|
2236
|
+
if len(param_id) != 2:
|
|
2237
|
+
raise ValueError("Invalid length")
|
|
2238
|
+
int(param_id, 16) # Will raise ValueError if not valid hex
|
|
2239
|
+
except ValueError as err:
|
|
2240
|
+
raise exc.CommandInvalid(
|
|
2241
|
+
f"Invalid parameter ID: '{param_id}'. Must be a 2-character hex string (00-FF)."
|
|
2242
|
+
) from err
|
|
2243
|
+
|
|
2244
|
+
payload = f"0000{param_id.upper()}" # Convert to uppercase for consistency
|
|
2245
|
+
_LOGGER.debug(
|
|
2246
|
+
"Created get_fan_param command for %s from %s to %s",
|
|
2247
|
+
param_id,
|
|
2248
|
+
src_id,
|
|
2249
|
+
fan_id,
|
|
2250
|
+
)
|
|
2251
|
+
|
|
2252
|
+
return cls._from_attrs(RQ, Code._2411, payload, addr0=src_id, addr1=fan_id)
|
|
1220
2253
|
|
|
1221
2254
|
@classmethod # constructor for RQ|2E04
|
|
1222
2255
|
def get_system_mode(cls, ctl_id: DeviceIdT | str) -> Command:
|
|
@@ -1232,7 +2265,41 @@ class Command(Frame):
|
|
|
1232
2265
|
*,
|
|
1233
2266
|
until: dt | str | None = None,
|
|
1234
2267
|
) -> Command:
|
|
1235
|
-
"""
|
|
2268
|
+
"""Set or reset the operating mode of the HVAC system. (c.f. parser_2e04)
|
|
2269
|
+
|
|
2270
|
+
This method constructs a command to change the system-wide operating mode,
|
|
2271
|
+
such as switching between heating modes or setting a temporary override.
|
|
2272
|
+
|
|
2273
|
+
:param ctl_id: The device ID of the controller (e.g., '01:123456')
|
|
2274
|
+
:type ctl_id: DeviceIdT | str
|
|
2275
|
+
:param system_mode: The desired system mode. Can be specified as:
|
|
2276
|
+
- Integer: Numeric mode code (0-5)
|
|
2277
|
+
- String: Mode name (e.g., 'auto', 'heat_eco')
|
|
2278
|
+
- Hex string: Two-character hex code (e.g., '00' for auto)
|
|
2279
|
+
If None, defaults to 'auto' mode.
|
|
2280
|
+
:type system_mode: int | str | None
|
|
2281
|
+
:param until: Optional timestamp when the mode should revert.
|
|
2282
|
+
Required for temporary modes like 'eco' or 'advanced'.
|
|
2283
|
+
Not allowed for 'auto' or 'heat_off' modes.
|
|
2284
|
+
:type until: datetime | str | None
|
|
2285
|
+
:return: A configured Command object ready to be sent to the device.
|
|
2286
|
+
:rtype: Command
|
|
2287
|
+
:raises CommandInvalid: If the combination of mode and until is invalid.
|
|
2288
|
+
:raises KeyError: If an invalid mode is specified.
|
|
2289
|
+
|
|
2290
|
+
.. note::
|
|
2291
|
+
Available modes are defined in SYS_MODE_MAP and typically include:
|
|
2292
|
+
- 'auto': System follows the schedule (code '00')
|
|
2293
|
+
- 'heat_off': Heating disabled (code '04')
|
|
2294
|
+
- 'eco': Reduced temperature mode (code '01')
|
|
2295
|
+
- 'advanced': Custom temperature mode (code '02')
|
|
2296
|
+
- 'holiday': Away mode (code '03')
|
|
2297
|
+
- 'custom': Custom mode (code '05')
|
|
2298
|
+
|
|
2299
|
+
When using temporary modes (eco/advanced), the 'until' parameter
|
|
2300
|
+
must be provided. The system will automatically revert to the
|
|
2301
|
+
schedule when the time elapses.
|
|
2302
|
+
"""
|
|
1236
2303
|
|
|
1237
2304
|
if system_mode is None:
|
|
1238
2305
|
system_mode = SYS_MODE_MAP.AUTO
|
|
@@ -1267,25 +2334,92 @@ class Command(Frame):
|
|
|
1267
2334
|
def put_presence_detected(
|
|
1268
2335
|
cls, dev_id: DeviceIdT | str, presence_detected: bool | None
|
|
1269
2336
|
) -> Command:
|
|
1270
|
-
"""
|
|
2337
|
+
"""Announce the current presence detection state from a sensor. (c.f. parser_2e10)
|
|
1271
2338
|
# .I --- ...
|
|
1272
2339
|
|
|
2340
|
+
This method constructs an I-type (unsolicited) command to report the
|
|
2341
|
+
presence detection state from a presence sensor to the system.
|
|
2342
|
+
|
|
2343
|
+
:param dev_id: The device ID of the presence sensor (e.g., '01:123456')
|
|
2344
|
+
:type dev_id: DeviceIdT | str
|
|
2345
|
+
:param presence_detected: The current presence state:
|
|
2346
|
+
- True: Presence detected
|
|
2347
|
+
- False: No presence detected
|
|
2348
|
+
- None: Sensor state unknown/error
|
|
2349
|
+
:type presence_detected: bool | None
|
|
2350
|
+
:return: A configured Command object ready to be sent to the system.
|
|
2351
|
+
:rtype: Command
|
|
2352
|
+
|
|
2353
|
+
.. note::
|
|
2354
|
+
This is typically used by presence sensors to report their state
|
|
2355
|
+
to the HVAC system. The system may use this information for
|
|
2356
|
+
occupancy-based control strategies.
|
|
2357
|
+
|
|
2358
|
+
The command uses the 2E10 code, which is specifically designed
|
|
2359
|
+
for presence/occupancy reporting in the RAMSES-II protocol.
|
|
2360
|
+
"""
|
|
1273
2361
|
payload = f"00{hex_from_bool(presence_detected)}"
|
|
1274
2362
|
return cls._from_attrs(I_, Code._2E10, payload, addr0=dev_id, addr2=dev_id)
|
|
1275
2363
|
|
|
1276
2364
|
@classmethod # constructor for RQ|30C9
|
|
1277
2365
|
def get_zone_temp(cls, ctl_id: DeviceIdT | str, zone_idx: _ZoneIdxT) -> Command:
|
|
1278
|
-
"""
|
|
1279
|
-
|
|
2366
|
+
"""Request the current temperature reading for a specific zone. (c.f. parser_30c9)
|
|
2367
|
+
|
|
2368
|
+
This method constructs a command to request the current temperature
|
|
2369
|
+
from a zone's temperature sensor. The response will include the current
|
|
2370
|
+
temperature in degrees Celsius with 0.1°C resolution.
|
|
2371
|
+
|
|
2372
|
+
:param ctl_id: The device ID of the controller managing the zone (e.g., '01:123456')
|
|
2373
|
+
:type ctl_id: DeviceIdT | str
|
|
2374
|
+
:param zone_idx: The index of the zone to query. Can be specified as:
|
|
2375
|
+
- Integer (0-31)
|
|
2376
|
+
- Hex string ('00'-'1F')
|
|
2377
|
+
- String representation of integer ('0'-'31')
|
|
2378
|
+
:type zone_idx: _ZoneIdxT
|
|
2379
|
+
:return: A configured Command object that can be sent to the device.
|
|
2380
|
+
:rtype: Command
|
|
2381
|
+
:raises ValueError: If the zone index is out of valid range (0-31)
|
|
2382
|
+
|
|
2383
|
+
.. note::
|
|
2384
|
+
The zone index is 0-based. For example:
|
|
2385
|
+
- 0 = Zone 1 (typically main living area)
|
|
2386
|
+
- 1 = Zone 2 (e.g., bedrooms)
|
|
2387
|
+
- And so on up to zone 32
|
|
2388
|
+
|
|
2389
|
+
The actual number of available zones depends on the controller configuration.
|
|
2390
|
+
Requesting a non-existent zone will typically result in no response.
|
|
2391
|
+
"""
|
|
1280
2392
|
return cls.from_attrs(RQ, ctl_id, Code._30C9, _check_idx(zone_idx))
|
|
1281
2393
|
|
|
1282
2394
|
@classmethod # constructor for I|30C9 # TODO: trap corrupt temps?
|
|
1283
2395
|
def put_sensor_temp(
|
|
1284
2396
|
cls, dev_id: DeviceIdT | str, temperature: float | None
|
|
1285
2397
|
) -> Command:
|
|
1286
|
-
"""
|
|
1287
|
-
|
|
2398
|
+
"""Announce the current temperature reading from a thermostat. (c.f. parser_30c9)
|
|
1288
2399
|
This is for use by a faked DTS92(E) or similar.
|
|
2400
|
+
|
|
2401
|
+
This method constructs an I-type (unsolicited) command to report the current
|
|
2402
|
+
temperature from a thermostat or temperature sensor to the system. This is
|
|
2403
|
+
typically used to simulate a physical thermostat's temperature reporting.
|
|
2404
|
+
|
|
2405
|
+
:param dev_id: The device ID of the thermostat or sensor (e.g., '01:123456')
|
|
2406
|
+
:type dev_id: DeviceIdT | str
|
|
2407
|
+
:param temperature: The current temperature in degrees Celsius.
|
|
2408
|
+
Use None to indicate a sensor error or invalid reading.
|
|
2409
|
+
The valid range is typically 0-40°C, but this may vary by device.
|
|
2410
|
+
:type temperature: float | None
|
|
2411
|
+
:return: A configured Command object ready to be sent to the system.
|
|
2412
|
+
:rtype: Command
|
|
2413
|
+
|
|
2414
|
+
.. note::
|
|
2415
|
+
This is primarily used for testing or simulating thermostats like the DTS92(E).
|
|
2416
|
+
The temperature is transmitted with 0.1°C resolution.
|
|
2417
|
+
|
|
2418
|
+
The command uses the 30C9 code, which is used by thermostats to report
|
|
2419
|
+
their current temperature reading to the controller.
|
|
2420
|
+
|
|
2421
|
+
When temperature is None, it typically indicates a sensor fault or
|
|
2422
|
+
invalid reading, which the system may interpret as a maintenance alert.
|
|
1289
2423
|
"""
|
|
1290
2424
|
# .I --- 34:021943 --:------ 34:021943 30C9 003 000C0D
|
|
1291
2425
|
|
|
@@ -1350,7 +2484,62 @@ class Command(Frame):
|
|
|
1350
2484
|
exhaust_flow: float | None,
|
|
1351
2485
|
**kwargs: Any, # option: air_quality_basis: str | None,
|
|
1352
2486
|
) -> Command:
|
|
1353
|
-
"""
|
|
2487
|
+
"""Construct an I|31DA command for HVAC fan status updates.
|
|
2488
|
+
|
|
2489
|
+
This method creates an unsolicited status update command for HVAC fan systems,
|
|
2490
|
+
reporting various sensor readings and system states.
|
|
2491
|
+
|
|
2492
|
+
:param dev_id: The device ID of the HVAC controller
|
|
2493
|
+
:type dev_id: DeviceIdT | str
|
|
2494
|
+
:param hvac_id: The ID of the HVAC unit
|
|
2495
|
+
:type hvac_id: str
|
|
2496
|
+
:param bypass_position: Current bypass damper position (0.0-1.0)
|
|
2497
|
+
:type bypass_position: float | None
|
|
2498
|
+
:param air_quality: Current air quality reading
|
|
2499
|
+
:type air_quality: int | None
|
|
2500
|
+
:param co2_level: Current CO₂ level in ppm
|
|
2501
|
+
:type co2_level: int | None
|
|
2502
|
+
:param indoor_humidity: Current indoor relative humidity (0.0-1.0)
|
|
2503
|
+
:type indoor_humidity: float | None
|
|
2504
|
+
:param outdoor_humidity: Current outdoor relative humidity (0.0-1.0)
|
|
2505
|
+
:type outdoor_humidity: float | None
|
|
2506
|
+
:param exhaust_temp: Current exhaust air temperature in °C
|
|
2507
|
+
:type exhaust_temp: float | None
|
|
2508
|
+
:param supply_temp: Current supply air temperature in °C
|
|
2509
|
+
:type supply_temp: float | None
|
|
2510
|
+
:param indoor_temp: Current indoor temperature in °C
|
|
2511
|
+
:type indoor_temp: float | None
|
|
2512
|
+
:param outdoor_temp: Current outdoor temperature in °C
|
|
2513
|
+
:type outdoor_temp: float | None
|
|
2514
|
+
:param speed_capabilities: List of supported fan speed settings
|
|
2515
|
+
:type speed_capabilities: list[str]
|
|
2516
|
+
:param fan_info: Current fan mode/status information
|
|
2517
|
+
:type fan_info: str
|
|
2518
|
+
:param _unknown_fan_info_flags: Internal flags (reserved for future use)
|
|
2519
|
+
:type _unknown_fan_info_flags: list[int]
|
|
2520
|
+
:param exhaust_fan_speed: Current exhaust fan speed (0.0-1.0)
|
|
2521
|
+
:type exhaust_fan_speed: float | None
|
|
2522
|
+
:param supply_fan_speed: Current supply fan speed (0.0-1.0)
|
|
2523
|
+
:type supply_fan_speed: float | None
|
|
2524
|
+
:param remaining_mins: Remaining time in current mode (minutes)
|
|
2525
|
+
:type remaining_mins: int | None
|
|
2526
|
+
:param post_heat: Post-heat status/level
|
|
2527
|
+
:type post_heat: int | None
|
|
2528
|
+
:param pre_heat: Pre-heat status/level
|
|
2529
|
+
:type pre_heat: int | None
|
|
2530
|
+
:param supply_flow: Current supply air flow rate (if available)
|
|
2531
|
+
:type supply_flow: float | None
|
|
2532
|
+
:param exhaust_flow: Current exhaust air flow rate (if available)
|
|
2533
|
+
:type exhaust_flow: float | None
|
|
2534
|
+
:param **kwargs: Additional parameters (reserved for future use)
|
|
2535
|
+
:return: A configured Command object for the HVAC fan status update
|
|
2536
|
+
:rtype: Command
|
|
2537
|
+
|
|
2538
|
+
.. note::
|
|
2539
|
+
This command is typically sent periodically by the HVAC controller to report
|
|
2540
|
+
current system status. All parameters are optional, but providing complete
|
|
2541
|
+
information will result in more accurate system monitoring and control.
|
|
2542
|
+
"""
|
|
1354
2543
|
# 00 EF00 7FFF 34 33 0898 0898 088A 0882 F800 00 15 14 14 0000 EF EF 05F5 0613:
|
|
1355
2544
|
# {"hvac_id": '00', 'bypass_position': 0.000, 'air_quality': None,
|
|
1356
2545
|
# 'co2_level': None, 'indoor_humidity': 0.52, 'outdoor_humidity': 0.51,
|
|
@@ -1429,8 +2618,39 @@ class Command(Frame):
|
|
|
1429
2618
|
|
|
1430
2619
|
@classmethod # constructor for RQ|3220
|
|
1431
2620
|
def get_opentherm_data(cls, otb_id: DeviceIdT | str, msg_id: int | str) -> Command:
|
|
1432
|
-
"""
|
|
1433
|
-
|
|
2621
|
+
"""Request OpenTherm protocol data from a device. (c.f. parser_3220)
|
|
2622
|
+
|
|
2623
|
+
This method constructs a command to request data from an OpenTherm compatible
|
|
2624
|
+
device using the OpenTherm protocol. It sends a Read-Data request for a
|
|
2625
|
+
specific data ID to the target device.
|
|
2626
|
+
|
|
2627
|
+
:param otb_id: The device ID of the OpenTherm bridge/controller
|
|
2628
|
+
:type otb_id: DeviceIdT | str
|
|
2629
|
+
:param msg_id: The OpenTherm message ID to request. Can be specified as:
|
|
2630
|
+
- Integer (e.g., 0 for Status)
|
|
2631
|
+
- Hex string (e.g., '00' for Status)
|
|
2632
|
+
See OpenTherm specification for valid message IDs.
|
|
2633
|
+
:type msg_id: int | str
|
|
2634
|
+
:return: A configured Command object ready to be sent to the device.
|
|
2635
|
+
:rtype: Command
|
|
2636
|
+
|
|
2637
|
+
.. note::
|
|
2638
|
+
The OpenTherm protocol is used for communication between heating systems
|
|
2639
|
+
and thermostats. Common message IDs include:
|
|
2640
|
+
- 0x00: Status (0x00)
|
|
2641
|
+
- 0x01: Control setpoint (0x01)
|
|
2642
|
+
- 0x11: Relative modulation level (0x11)
|
|
2643
|
+
- 0x12: CH water pressure (0x12)
|
|
2644
|
+
- 0x19: Boiler water temperature (0x19)
|
|
2645
|
+
- 0x1A: DHW temperature (0x1A)
|
|
2646
|
+
- 0x71: DHW setpoint (0x71)
|
|
2647
|
+
|
|
2648
|
+
The response will contain the requested data in the OpenTherm format,
|
|
2649
|
+
which includes status flags and the data value.
|
|
2650
|
+
|
|
2651
|
+
The command automatically handles the parity bit required by the
|
|
2652
|
+
OpenTherm protocol.
|
|
2653
|
+
"""
|
|
1434
2654
|
msg_id = msg_id if isinstance(msg_id, int) else int(msg_id, 16)
|
|
1435
2655
|
payload = f"0080{msg_id:02X}0000" if parity(msg_id) else f"0000{msg_id:02X}0000"
|
|
1436
2656
|
return cls.from_attrs(RQ, otb_id, Code._3220, payload)
|
|
@@ -1439,9 +2659,32 @@ class Command(Frame):
|
|
|
1439
2659
|
def put_actuator_state(
|
|
1440
2660
|
cls, dev_id: DeviceIdT | str, modulation_level: float
|
|
1441
2661
|
) -> Command:
|
|
1442
|
-
"""
|
|
1443
|
-
|
|
2662
|
+
"""Announce the current modulation level of a heating actuator. (c.f. parser_3ef0)
|
|
1444
2663
|
This is for use by a faked BDR91A or similar.
|
|
2664
|
+
|
|
2665
|
+
This method constructs an I-type (unsolicited) command to report the current
|
|
2666
|
+
modulation level of a heating actuator, such as a BDR91A relay. The modulation
|
|
2667
|
+
level represents the current output state of the actuator as a percentage.
|
|
2668
|
+
|
|
2669
|
+
:param dev_id: The device ID of the actuator (e.g., '13:123456').
|
|
2670
|
+
Must be a device type compatible with BDR91A.
|
|
2671
|
+
:type dev_id: DeviceIdT | str
|
|
2672
|
+
:param modulation_level: The current modulation level as a float between 0.0 and 1.0.
|
|
2673
|
+
- 0.0: Actuator is fully off
|
|
2674
|
+
- 1.0: Actuator is fully on
|
|
2675
|
+
- Values in between represent partial modulation (if supported)
|
|
2676
|
+
- None: Indicates an error or unknown state
|
|
2677
|
+
:type modulation_level: float | None
|
|
2678
|
+
:return: A configured Command object ready to be sent to the system.
|
|
2679
|
+
:rtype: Command
|
|
2680
|
+
:raises CommandInvalid: If the device ID is not a valid BDR-type device.
|
|
2681
|
+
|
|
2682
|
+
.. note::
|
|
2683
|
+
This is primarily used for testing or simulating BDR91A relay modules.
|
|
2684
|
+
The modulation level is converted to a percentage (0-100%) with 0.5% resolution.
|
|
2685
|
+
|
|
2686
|
+
The command uses the 3EF0 code, which is specifically designed for
|
|
2687
|
+
reporting actuator states in the RAMSES-II protocol.
|
|
1445
2688
|
"""
|
|
1446
2689
|
# .I --- 13:049798 --:------ 13:049798 3EF0 003 00C8FF
|
|
1447
2690
|
# .I --- 13:106039 --:------ 13:106039 3EF0 003 0000FF
|
|
@@ -1469,9 +2712,40 @@ class Command(Frame):
|
|
|
1469
2712
|
*,
|
|
1470
2713
|
cycle_countdown: int | None = None,
|
|
1471
2714
|
) -> Command:
|
|
1472
|
-
"""
|
|
1473
|
-
|
|
2715
|
+
"""Announce the internal cycling state of a heating actuator. (c.f. parser_3ef1)
|
|
1474
2716
|
This is for use by a faked BDR91A or similar.
|
|
2717
|
+
|
|
2718
|
+
This method constructs an RP-type (request/response) command to report the
|
|
2719
|
+
internal cycling state of a heating actuator, such as a BDR91A relay. It provides
|
|
2720
|
+
detailed timing information about the actuator's modulation cycle.
|
|
2721
|
+
|
|
2722
|
+
:param src_id: The device ID of the actuator sending the report (e.g., '13:123456').
|
|
2723
|
+
Must be a device type compatible with BDR91A.
|
|
2724
|
+
:type src_id: DeviceIdT | str
|
|
2725
|
+
:param dst_id: The device ID of the intended recipient of this report.
|
|
2726
|
+
:type dst_id: DeviceIdT | str
|
|
2727
|
+
:param modulation_level: The current modulation level as a float between 0.0 and 1.0.
|
|
2728
|
+
- 0.0: Actuator is fully off
|
|
2729
|
+
- 1.0: Actuator is fully on
|
|
2730
|
+
- Values in between represent partial modulation (if supported)
|
|
2731
|
+
:type modulation_level: float
|
|
2732
|
+
:param actuator_countdown: Time in seconds until the next actuator cycle state change.
|
|
2733
|
+
This is used for PWM (Pulse Width Modulation) control.
|
|
2734
|
+
:type actuator_countdown: int
|
|
2735
|
+
:param cycle_countdown: Optional time in seconds until the next complete cycle.
|
|
2736
|
+
If None, indicates the cycle is not currently active.
|
|
2737
|
+
:type cycle_countdown: int | None
|
|
2738
|
+
:return: A configured Command object ready to be sent to the system.
|
|
2739
|
+
:rtype: Command
|
|
2740
|
+
:raises CommandInvalid: If the source device ID is not a valid BDR-type device.
|
|
2741
|
+
|
|
2742
|
+
.. note::
|
|
2743
|
+
This is primarily used for testing or simulating BDR91A relay modules.
|
|
2744
|
+
The method automatically handles the conversion of timing values to the
|
|
2745
|
+
appropriate hexadecimal format required by the RAMSES-II protocol.
|
|
2746
|
+
|
|
2747
|
+
The command uses the 3EF1 code, which is specifically designed for
|
|
2748
|
+
reporting detailed actuator cycling information.
|
|
1475
2749
|
"""
|
|
1476
2750
|
# RP --- 13:049798 18:006402 --:------ 3EF1 007 00-0126-0126-00-FF
|
|
1477
2751
|
|
|
@@ -1490,6 +2764,43 @@ class Command(Frame):
|
|
|
1490
2764
|
|
|
1491
2765
|
@classmethod # constructor for internal use only
|
|
1492
2766
|
def _puzzle(cls, msg_type: str | None = None, message: str = "") -> Command:
|
|
2767
|
+
"""Construct a puzzle command used for device discovery and version reporting.
|
|
2768
|
+
|
|
2769
|
+
This internal method creates a special 'puzzle' command used during device
|
|
2770
|
+
discovery and version reporting. The command format varies based on the
|
|
2771
|
+
message type and content.
|
|
2772
|
+
|
|
2773
|
+
:param msg_type: The type of puzzle message to create. If None, it will be
|
|
2774
|
+
automatically determined based on the presence of a message:
|
|
2775
|
+
- '10': Version request (empty message)
|
|
2776
|
+
- '12': Version response (with message)
|
|
2777
|
+
Other valid types include '11' and '13' for specific message formats,
|
|
2778
|
+
and '20' and above for timestamp-based messages.
|
|
2779
|
+
:type msg_type: str | None
|
|
2780
|
+
:param message: The message content to include in the puzzle.
|
|
2781
|
+
Format depends on msg_type:
|
|
2782
|
+
- For type '10': Should be empty (version request)
|
|
2783
|
+
- For type '11': Should be a 10-character string (MAC address)
|
|
2784
|
+
- For type '12': Version string (e.g., 'v0.20.0')
|
|
2785
|
+
- For other types: Arbitrary message content
|
|
2786
|
+
:type message: str
|
|
2787
|
+
:return: A configured Command object with the puzzle message.
|
|
2788
|
+
:rtype: Command
|
|
2789
|
+
:raises AssertionError: If msg_type is not in LOOKUP_PUZZ.
|
|
2790
|
+
|
|
2791
|
+
.. note::
|
|
2792
|
+
This is an internal method used by the RAMSES-II protocol for device
|
|
2793
|
+
discovery and version reporting. The message format varies:
|
|
2794
|
+
|
|
2795
|
+
- Type '10': Version request (empty message)
|
|
2796
|
+
- Type '11': MAC address report (special format)
|
|
2797
|
+
- Type '12': Version response (includes version string)
|
|
2798
|
+
- Type '13': Basic message (no timestamp)
|
|
2799
|
+
- Type '20+': Timestamped message (high precision)
|
|
2800
|
+
|
|
2801
|
+
The method automatically handles timestamp generation and message
|
|
2802
|
+
formatting based on the message type.
|
|
2803
|
+
"""
|
|
1493
2804
|
if msg_type is None:
|
|
1494
2805
|
msg_type = "12" if message else "10"
|
|
1495
2806
|
|