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/const.py
CHANGED
|
@@ -788,7 +788,7 @@ class MsgId(StrEnum):
|
|
|
788
788
|
_7F = "7F"
|
|
789
789
|
|
|
790
790
|
|
|
791
|
-
# StrEnum is intended include all known codes, see: test suite, code schema in ramses.py
|
|
791
|
+
# StrEnum is intended to include all known codes, see: test suite, code schema in ramses.py
|
|
792
792
|
@verify(EnumCheck.UNIQUE)
|
|
793
793
|
class Code(StrEnum):
|
|
794
794
|
_0001 = "0001"
|
ramses_tx/frame.py
CHANGED
|
@@ -65,7 +65,7 @@ class Frame:
|
|
|
65
65
|
def __init__(self, frame: str) -> None:
|
|
66
66
|
"""Create a frame from a string.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
:raises InvalidPacketError: if provided string is invalid.
|
|
69
69
|
"""
|
|
70
70
|
|
|
71
71
|
self._frame: str = frame
|
|
@@ -123,7 +123,7 @@ class Frame:
|
|
|
123
123
|
if not strict_checking:
|
|
124
124
|
return
|
|
125
125
|
|
|
126
|
-
try: # Strict checking: helps users avoid
|
|
126
|
+
try: # Strict checking: helps users avoid constructing bad commands
|
|
127
127
|
if addrs[0] == NON_DEV_ADDR:
|
|
128
128
|
assert self.verb == I_, "wrong verb or dst addr should be present"
|
|
129
129
|
elif addrs[2] == NON_DEV_ADDR:
|
|
@@ -138,7 +138,7 @@ class Frame:
|
|
|
138
138
|
raise exc.PacketInvalid(f"Bad frame: Invalid address set: {err}") from err
|
|
139
139
|
|
|
140
140
|
def __repr__(self) -> str:
|
|
141
|
-
"""Return
|
|
141
|
+
"""Return an unambiguous string representation of this object."""
|
|
142
142
|
|
|
143
143
|
if self._repr is None:
|
|
144
144
|
self._repr = " ".join( # type: ignore[unreachable]
|
|
@@ -387,7 +387,7 @@ class Frame:
|
|
|
387
387
|
|
|
388
388
|
@property
|
|
389
389
|
def _hdr(self) -> HeaderT: # incl. self._ctx
|
|
390
|
-
"""Return the QoS header (fingerprint) of this packet (i.e. device_id
|
|
390
|
+
"""Return the QoS header (fingerprint) of this packet (i.e. device_id|code|verb).
|
|
391
391
|
|
|
392
392
|
Used for QoS (timeouts, retries), callbacks, etc.
|
|
393
393
|
"""
|
ramses_tx/gateway.py
CHANGED
|
@@ -91,7 +91,7 @@ class Engine:
|
|
|
91
91
|
if input_file:
|
|
92
92
|
self._disable_sending = True
|
|
93
93
|
elif not port_name:
|
|
94
|
-
raise TypeError("Either a port_name or
|
|
94
|
+
raise TypeError("Either a port_name or an input_file must be specified")
|
|
95
95
|
|
|
96
96
|
self.ser_name = port_name
|
|
97
97
|
self._input_file = input_file
|
ramses_tx/helpers.py
CHANGED
|
@@ -787,9 +787,9 @@ def fan_info_to_byte(info: str) -> int:
|
|
|
787
787
|
|
|
788
788
|
def fan_info_flags(flags_list: list[int]) -> int:
|
|
789
789
|
flag_res: int = 0
|
|
790
|
-
for index,
|
|
790
|
+
for index, shift in enumerate(range(7, 4, -1)): # index = 7, 6 and 5
|
|
791
791
|
if flags_list[index] == 1:
|
|
792
|
-
flag_res |= 1 <<
|
|
792
|
+
flag_res |= 1 << shift # set bits
|
|
793
793
|
return flag_res
|
|
794
794
|
|
|
795
795
|
|
ramses_tx/message.py
CHANGED
|
@@ -54,7 +54,7 @@ class MessageBase:
|
|
|
54
54
|
def __init__(self, pkt: Packet) -> None:
|
|
55
55
|
"""Create a message from a valid packet.
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
:raises InvalidPacketError if message payload is invalid.
|
|
58
58
|
"""
|
|
59
59
|
|
|
60
60
|
self._pkt = pkt
|
|
@@ -66,11 +66,15 @@ class MessageBase:
|
|
|
66
66
|
self.dtm: dt = pkt.dtm
|
|
67
67
|
|
|
68
68
|
self.verb: VerbT = pkt.verb
|
|
69
|
-
self.seqn: str =
|
|
69
|
+
self.seqn: str = (
|
|
70
|
+
pkt.seqn
|
|
71
|
+
) # the msg is part of a set for 1 Code, received in order
|
|
70
72
|
self.code: Code = pkt.code
|
|
71
73
|
self.len: int = pkt._len
|
|
72
74
|
|
|
73
|
-
self._payload = self._validate(
|
|
75
|
+
self._payload = self._validate(
|
|
76
|
+
self._pkt.payload
|
|
77
|
+
) # ? may raise InvalidPacketError
|
|
74
78
|
|
|
75
79
|
self._str: str = None # type: ignore[assignment]
|
|
76
80
|
|
|
@@ -92,7 +96,7 @@ class MessageBase:
|
|
|
92
96
|
|
|
93
97
|
if self.src.id == self._addrs[0].id: # type: ignore[unreachable]
|
|
94
98
|
name_0 = self._name(self.src)
|
|
95
|
-
name_1 = "" if self.dst
|
|
99
|
+
name_1 = "" if self.dst == self.src else self._name(self.dst)
|
|
96
100
|
else:
|
|
97
101
|
name_0 = ""
|
|
98
102
|
name_1 = self._name(self.src)
|
|
@@ -139,16 +143,18 @@ class MessageBase:
|
|
|
139
143
|
|
|
140
144
|
@property
|
|
141
145
|
def _has_array(self) -> bool:
|
|
142
|
-
"""
|
|
146
|
+
"""
|
|
147
|
+
:return: True if the message's raw payload is an array.
|
|
148
|
+
"""
|
|
143
149
|
|
|
144
150
|
return bool(self._pkt._has_array)
|
|
145
151
|
|
|
146
152
|
@property
|
|
147
153
|
def _idx(self) -> dict[str, str]:
|
|
148
|
-
"""
|
|
154
|
+
"""Get the domain_id/zone_idx/other_idx of a message payload, if any.
|
|
155
|
+
Used to identify the zone/domain that a message applies to.
|
|
149
156
|
|
|
150
|
-
|
|
151
|
-
dict if there is none such, or None if undetermined.
|
|
157
|
+
:return: an empty dict if there is none such, or None if undetermined.
|
|
152
158
|
"""
|
|
153
159
|
|
|
154
160
|
# .I --- 01:145038 --:------ 01:145038 3B00 002 FCC8
|
|
@@ -229,7 +235,7 @@ class MessageBase:
|
|
|
229
235
|
assert isinstance(self._pkt._idx, str) # mypy hint
|
|
230
236
|
return {IDX_NAMES[Code._22C9]: self._pkt._idx}
|
|
231
237
|
|
|
232
|
-
assert isinstance(self._pkt._idx, str) # mypy
|
|
238
|
+
assert isinstance(self._pkt._idx, str) # mypy hint
|
|
233
239
|
idx_name = SZ_DOMAIN_ID if self._pkt._idx[:1] == "F" else SZ_ZONE_IDX
|
|
234
240
|
index_name = IDX_NAMES.get(self.code, idx_name)
|
|
235
241
|
|
|
@@ -237,9 +243,10 @@ class MessageBase:
|
|
|
237
243
|
|
|
238
244
|
# TODO: needs work...
|
|
239
245
|
def _validate(self, raw_payload: str) -> dict | list[dict]: # type: ignore[type-arg]
|
|
240
|
-
"""Validate
|
|
246
|
+
"""Validate a message packet payload, and parse it if valid.
|
|
241
247
|
|
|
242
|
-
|
|
248
|
+
:return: a dict containing key: value pairs, or a list of those created from the payload
|
|
249
|
+
:raises an InvalidPacketError exception if it is not valid.
|
|
243
250
|
"""
|
|
244
251
|
|
|
245
252
|
try: # parse the payload
|
|
@@ -249,10 +256,9 @@ class MessageBase:
|
|
|
249
256
|
if not self._has_payload and (
|
|
250
257
|
self.verb == RQ and self.code not in RQ_IDX_COMPLEX
|
|
251
258
|
):
|
|
252
|
-
# _LOGGER.error("%s", msg)
|
|
253
259
|
return {}
|
|
254
260
|
|
|
255
|
-
result = parse_payload(self)
|
|
261
|
+
result = parse_payload(self) # invoke the code parsers
|
|
256
262
|
|
|
257
263
|
if isinstance(result, list):
|
|
258
264
|
return result
|
|
@@ -353,7 +359,7 @@ def re_compile_re_match(regex: str, string: str) -> bool: # Optional[Match[Any]
|
|
|
353
359
|
def _check_msg_payload(msg: MessageBase, payload: str) -> None:
|
|
354
360
|
"""Validate the packet's payload against its verb/code pair.
|
|
355
361
|
|
|
356
|
-
|
|
362
|
+
:raises InvalidPayloadError if the payload is seen as invalid. Such payloads may
|
|
357
363
|
actually be valid, in which case the rules (likely the regex) will need updating.
|
|
358
364
|
"""
|
|
359
365
|
|
ramses_tx/packet.py
CHANGED
|
@@ -104,7 +104,7 @@ class Packet(Frame):
|
|
|
104
104
|
return f"{dtm} ... {self}{hdr}"
|
|
105
105
|
|
|
106
106
|
def __str__(self) -> str:
|
|
107
|
-
"""Return a brief readable string representation of this object."""
|
|
107
|
+
"""Return a brief readable string representation of this object aka 'header'."""
|
|
108
108
|
# e.g.: 000A|RQ|01:145038|08
|
|
109
109
|
return super().__repr__() # TODO: self._hdr
|
|
110
110
|
|
ramses_tx/parsers.py
CHANGED
|
@@ -1391,15 +1391,18 @@ def parser_2210(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1391
1391
|
"02", # requested by CO2 level/sensor
|
|
1392
1392
|
"03", # requested by humidity level/sensor
|
|
1393
1393
|
), f"expected req_reason (00|02|03), not {payload[20:22]}"
|
|
1394
|
-
assert payload[78:80] in (
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1394
|
+
assert payload[78:80] in (
|
|
1395
|
+
"00",
|
|
1396
|
+
"02",
|
|
1397
|
+
), f"expected byte 39 (00|02), not {payload[78:80]}"
|
|
1398
|
+
assert payload[80:82] in (
|
|
1399
|
+
"01",
|
|
1400
|
+
"08",
|
|
1401
|
+
), f"expected byte 40 (01|08), not {payload[80:82]}"
|
|
1402
|
+
assert payload[82:] in (
|
|
1403
|
+
"00",
|
|
1404
|
+
"40",
|
|
1405
|
+
), f"expected byte 41- (00|40), not {payload[82:]}"
|
|
1403
1406
|
|
|
1404
1407
|
except AssertionError as err:
|
|
1405
1408
|
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
@@ -1909,42 +1912,57 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1909
1912
|
"92": (4, hex_to_temp), # 75 (0-30) (C)
|
|
1910
1913
|
} # TODO: _2411_TYPES.get(payload[8:10], (8, no_op))
|
|
1911
1914
|
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1915
|
+
# Handle unknown parameters gracefully instead of asserting
|
|
1916
|
+
param_id = payload[4:6]
|
|
1917
|
+
try:
|
|
1918
|
+
description = _2411_TABLE.get(param_id, "Unknown")
|
|
1919
|
+
if param_id not in _2411_TABLE:
|
|
1920
|
+
_LOGGER.warning(
|
|
1921
|
+
f"2411 message received with unknown parameter ID: {param_id}. "
|
|
1922
|
+
f"This parameter is not in the known parameter schema. "
|
|
1923
|
+
f"Message: {msg!r}"
|
|
1924
|
+
)
|
|
1925
|
+
except Exception as err:
|
|
1926
|
+
_LOGGER.warning(f"Error looking up 2411 parameter {param_id}: {err}")
|
|
1927
|
+
description = "Unknown"
|
|
1916
1928
|
|
|
1917
1929
|
result = {
|
|
1918
|
-
"parameter":
|
|
1930
|
+
"parameter": param_id,
|
|
1919
1931
|
"description": description,
|
|
1920
1932
|
}
|
|
1921
1933
|
|
|
1922
1934
|
if msg.verb == RQ:
|
|
1923
1935
|
return result
|
|
1924
1936
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1937
|
+
try:
|
|
1938
|
+
assert payload[8:10] in _2411_DATA_TYPES, (
|
|
1939
|
+
f"param {param_id} has unknown data_type: {payload[8:10]}"
|
|
1940
|
+
) # _INFORM_DEV_MSG
|
|
1941
|
+
length, parser = _2411_DATA_TYPES.get(payload[8:10], (8, lambda x: x))
|
|
1942
|
+
|
|
1943
|
+
result |= {
|
|
1944
|
+
"value": parser(payload[10:18][-length:]), # type: ignore[operator]
|
|
1945
|
+
"_value_06": payload[6:10],
|
|
1946
|
+
}
|
|
1929
1947
|
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
"_value_06": payload[6:10],
|
|
1933
|
-
}
|
|
1948
|
+
if msg.len == 9:
|
|
1949
|
+
return result
|
|
1934
1950
|
|
|
1935
|
-
|
|
1951
|
+
return (
|
|
1952
|
+
result
|
|
1953
|
+
| {
|
|
1954
|
+
"min_value": parser(payload[18:26][-length:]), # type: ignore[operator]
|
|
1955
|
+
"max_value": parser(payload[26:34][-length:]), # type: ignore[operator]
|
|
1956
|
+
"precision": parser(payload[34:42][-length:]), # type: ignore[operator]
|
|
1957
|
+
"_value_42": payload[42:],
|
|
1958
|
+
}
|
|
1959
|
+
)
|
|
1960
|
+
except AssertionError as err:
|
|
1961
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1962
|
+
# Return partial result for unknown parameters
|
|
1963
|
+
result["value"] = ""
|
|
1936
1964
|
return result
|
|
1937
1965
|
|
|
1938
|
-
return (
|
|
1939
|
-
result
|
|
1940
|
-
| {
|
|
1941
|
-
"min_value": parser(payload[18:26][-length:]), # type: ignore[operator]
|
|
1942
|
-
"max_value": parser(payload[26:34][-length:]), # type: ignore[operator]
|
|
1943
|
-
"precision": parser(payload[34:42][-length:]), # type: ignore[operator]
|
|
1944
|
-
"_value_42": payload[42:],
|
|
1945
|
-
}
|
|
1946
|
-
)
|
|
1947
|
-
|
|
1948
1966
|
|
|
1949
1967
|
# unknown_2420, from OTB
|
|
1950
1968
|
def parser_2420(payload: str, msg: Message) -> dict[str, Any]:
|
|
@@ -2344,7 +2362,7 @@ def parser_3210(payload: str, msg: Message) -> PayDictT._3210:
|
|
|
2344
2362
|
return {SZ_TEMPERATURE: hex_to_temp(payload[2:])}
|
|
2345
2363
|
|
|
2346
2364
|
|
|
2347
|
-
# opentherm_msg, from OTB (and
|
|
2365
|
+
# opentherm_msg, from OTB (and OT_RND)
|
|
2348
2366
|
def parser_3220(payload: str, msg: Message) -> dict[str, Any]:
|
|
2349
2367
|
try:
|
|
2350
2368
|
ot_type, ot_id, ot_value, ot_schema = decode_frame(payload[2:10])
|
|
@@ -2968,8 +2986,12 @@ _PAYLOAD_PARSERS = {
|
|
|
2968
2986
|
|
|
2969
2987
|
|
|
2970
2988
|
def parse_payload(msg: Message) -> dict | list[dict]:
|
|
2989
|
+
"""
|
|
2990
|
+
Apply the appropriate parser defined in this module to the message.
|
|
2991
|
+
:param msg: a Message object containing packet data and extra attributes
|
|
2992
|
+
:return: a dict of key: value pairs or a list of such dicts, e.g. {'temperature': 21.5}
|
|
2993
|
+
"""
|
|
2971
2994
|
result: dict | list[dict]
|
|
2972
|
-
|
|
2973
2995
|
result = _PAYLOAD_PARSERS.get(msg.code, parser_unknown)(msg._pkt.payload, msg)
|
|
2974
2996
|
if isinstance(result, dict) and msg.seqn.isnumeric(): # e.g. 22F1/3
|
|
2975
2997
|
result["seqx_num"] = msg.seqn
|
ramses_tx/protocol.py
CHANGED
|
@@ -332,7 +332,7 @@ class _DeviceIdFilterMixin(_BaseProtocol):
|
|
|
332
332
|
/,
|
|
333
333
|
*,
|
|
334
334
|
disable_warnings: bool = False,
|
|
335
|
-
|
|
335
|
+
strict_checking: bool = False,
|
|
336
336
|
) -> DeviceIdT | None:
|
|
337
337
|
"""Return the device_id of the gateway specified in the include_list, if any.
|
|
338
338
|
|
|
@@ -384,7 +384,7 @@ class _DeviceIdFilterMixin(_BaseProtocol):
|
|
|
384
384
|
f"The {SZ_KNOWN_LIST} includes exactly one gateway (HGI): {known_hgi}"
|
|
385
385
|
)
|
|
386
386
|
|
|
387
|
-
if
|
|
387
|
+
if strict_checking:
|
|
388
388
|
return known_hgi if [known_hgi] == explicit_hgis else None
|
|
389
389
|
return known_hgi
|
|
390
390
|
|
ramses_tx/protocol_fsm.py
CHANGED
|
@@ -291,7 +291,7 @@ class ProtocolContext:
|
|
|
291
291
|
# may want to set some instance variables, according to type of transport
|
|
292
292
|
self._state.connection_made()
|
|
293
293
|
|
|
294
|
-
# TODO: Should we clear the buffer if connection is lost (and
|
|
294
|
+
# TODO: Should we clear the buffer if connection is lost (and apologise to senders?
|
|
295
295
|
def connection_lost(self, err: ExceptionT | None) -> None:
|
|
296
296
|
self._state.connection_lost()
|
|
297
297
|
|
ramses_tx/ramses.py
CHANGED
|
@@ -223,7 +223,7 @@ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
|
|
|
223
223
|
SZ_NAME: "device_info",
|
|
224
224
|
I_: r"^(00|FF)([0-9A-F]{30,})?$", # r"^[0-9A-F]{32,}$" might be OK
|
|
225
225
|
RQ: r"^00$", # NOTE: 63 seen (no RP), some devices will accept [0-9A-F]{2}
|
|
226
|
-
# RP: r"^[0-9A-F]{2}([0-9A-F]){30,}$", # NOTE:
|
|
226
|
+
# RP: r"^[0-9A-F]{2}([0-9A-F]){30,}$", # NOTE: index same as RQ
|
|
227
227
|
SZ_LIFESPAN: False,
|
|
228
228
|
},
|
|
229
229
|
Code._10E1: { # device_id
|
|
@@ -480,6 +480,7 @@ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
|
|
|
480
480
|
SZ_NAME: "fan_params",
|
|
481
481
|
I_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}([0-9A-F]{8}){4}[0-9A-F]{4}$",
|
|
482
482
|
RQ: r"^(00|01|15|16|17|21)00[0-9A-F]{2}((00){19})?$",
|
|
483
|
+
RP: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}(([0-9A-F]{8}){3}[0-9A-F]{4})?$",
|
|
483
484
|
W_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}(([0-9A-F]{8}){3}[0-9A-F]{4})?$",
|
|
484
485
|
},
|
|
485
486
|
Code._2420: { # unknown_2420, from OTB
|
|
@@ -1220,6 +1221,7 @@ SZ_MIN_VALUE: Final = "min_value"
|
|
|
1220
1221
|
SZ_MAX_VALUE: Final = "max_value"
|
|
1221
1222
|
SZ_PRECISION: Final = "precision"
|
|
1222
1223
|
SZ_DATA_TYPE: Final = "data_type"
|
|
1224
|
+
SZ_DATA_UNIT: Final = "data_unit"
|
|
1223
1225
|
|
|
1224
1226
|
_22F1_MODE_ITHO: dict[str, str] = {
|
|
1225
1227
|
"00": "off", # not seen
|
|
@@ -1265,12 +1267,13 @@ _22F1_SCHEMES: dict[str, dict[str, str]] = {
|
|
|
1265
1267
|
|
|
1266
1268
|
# unclear if true for only Orcon/*all* models
|
|
1267
1269
|
_2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
1268
|
-
"31": { # slot 09
|
|
1270
|
+
"31": { # slot 09 (FANs produced after 2021)
|
|
1269
1271
|
SZ_DESCRIPTION: "Time to change filter (days)",
|
|
1270
1272
|
SZ_MIN_VALUE: 0,
|
|
1271
1273
|
SZ_MAX_VALUE: 1800,
|
|
1272
1274
|
SZ_PRECISION: 30,
|
|
1273
1275
|
SZ_DATA_TYPE: "10",
|
|
1276
|
+
SZ_DATA_UNIT: "days",
|
|
1274
1277
|
},
|
|
1275
1278
|
"3D": { # slot 00
|
|
1276
1279
|
SZ_DESCRIPTION: "Away mode Supply fan rate (%)",
|
|
@@ -1278,6 +1281,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1278
1281
|
SZ_MAX_VALUE: 0.4,
|
|
1279
1282
|
SZ_PRECISION: 0.005,
|
|
1280
1283
|
SZ_DATA_TYPE: "0F",
|
|
1284
|
+
SZ_DATA_UNIT: "%",
|
|
1281
1285
|
},
|
|
1282
1286
|
"3E": { # slot 01
|
|
1283
1287
|
SZ_DESCRIPTION: "Away mode Exhaust fan rate (%)",
|
|
@@ -1285,6 +1289,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1285
1289
|
SZ_MAX_VALUE: 0.4,
|
|
1286
1290
|
SZ_PRECISION: 0.005,
|
|
1287
1291
|
SZ_DATA_TYPE: "0F",
|
|
1292
|
+
SZ_DATA_UNIT: "%",
|
|
1288
1293
|
},
|
|
1289
1294
|
"3F": { # slot 02
|
|
1290
1295
|
SZ_DESCRIPTION: "Low mode Supply fan rate (%)",
|
|
@@ -1292,6 +1297,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1292
1297
|
SZ_MAX_VALUE: 0.8,
|
|
1293
1298
|
SZ_PRECISION: 0.005,
|
|
1294
1299
|
SZ_DATA_TYPE: "0F",
|
|
1300
|
+
SZ_DATA_UNIT: "%",
|
|
1295
1301
|
},
|
|
1296
1302
|
"40": { # slot 03
|
|
1297
1303
|
SZ_DESCRIPTION: "Low mode Exhaust fan rate (%)",
|
|
@@ -1299,13 +1305,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1299
1305
|
SZ_MAX_VALUE: 0.8,
|
|
1300
1306
|
SZ_PRECISION: 0.005,
|
|
1301
1307
|
SZ_DATA_TYPE: "0F",
|
|
1308
|
+
SZ_DATA_UNIT: "%",
|
|
1302
1309
|
},
|
|
1303
1310
|
"41": { # slot 04
|
|
1304
1311
|
SZ_DESCRIPTION: "Medium mode Supply fan rate (%)",
|
|
1305
|
-
SZ_MIN_VALUE: 0.0,
|
|
1312
|
+
SZ_MIN_VALUE: 0.1, # Orcon FAN responds with 0.0, but I guess this should be the same as for "42"
|
|
1306
1313
|
SZ_MAX_VALUE: 1.0,
|
|
1307
1314
|
SZ_PRECISION: 0.005,
|
|
1308
1315
|
SZ_DATA_TYPE: "0F",
|
|
1316
|
+
SZ_DATA_UNIT: "%",
|
|
1309
1317
|
},
|
|
1310
1318
|
"42": { # slot 05
|
|
1311
1319
|
SZ_DESCRIPTION: "Medium mode Exhaust fan rate (%)",
|
|
@@ -1313,13 +1321,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1313
1321
|
SZ_MAX_VALUE: 1.0,
|
|
1314
1322
|
SZ_PRECISION: 0.005,
|
|
1315
1323
|
SZ_DATA_TYPE: "0F",
|
|
1324
|
+
SZ_DATA_UNIT: "%",
|
|
1316
1325
|
},
|
|
1317
1326
|
"43": { # slot 06
|
|
1318
1327
|
SZ_DESCRIPTION: "High mode Supply fan rate (%)",
|
|
1319
|
-
SZ_MIN_VALUE: 0.
|
|
1328
|
+
SZ_MIN_VALUE: 0.1,
|
|
1320
1329
|
SZ_MAX_VALUE: 1.0,
|
|
1321
1330
|
SZ_PRECISION: 0.005,
|
|
1322
1331
|
SZ_DATA_TYPE: "0F",
|
|
1332
|
+
SZ_DATA_UNIT: "%",
|
|
1323
1333
|
},
|
|
1324
1334
|
"44": { # slot 07
|
|
1325
1335
|
SZ_DESCRIPTION: "High mode Exhaust fan rate (%)",
|
|
@@ -1327,6 +1337,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1327
1337
|
SZ_MAX_VALUE: 1.0,
|
|
1328
1338
|
SZ_PRECISION: 0.005,
|
|
1329
1339
|
SZ_DATA_TYPE: "0F",
|
|
1340
|
+
SZ_DATA_UNIT: "%",
|
|
1341
|
+
},
|
|
1342
|
+
"4B": { # slot 09 (FANs produced before 2021) Also check code 22F7
|
|
1343
|
+
SZ_DESCRIPTION: "(Test) Bypass Valve (0=auto, 1=open, 2=closed)",
|
|
1344
|
+
SZ_MIN_VALUE: 0,
|
|
1345
|
+
SZ_MAX_VALUE: 2,
|
|
1346
|
+
SZ_PRECISION: 1,
|
|
1347
|
+
SZ_DATA_TYPE: "00",
|
|
1348
|
+
SZ_DATA_UNIT: "",
|
|
1330
1349
|
},
|
|
1331
1350
|
"4E": { # slot 0A
|
|
1332
1351
|
SZ_DESCRIPTION: "Moisture scenario position (0=medium, 1=high)",
|
|
@@ -1334,13 +1353,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1334
1353
|
SZ_MAX_VALUE: 1,
|
|
1335
1354
|
SZ_PRECISION: 1,
|
|
1336
1355
|
SZ_DATA_TYPE: "00",
|
|
1356
|
+
SZ_DATA_UNIT: "",
|
|
1337
1357
|
},
|
|
1338
1358
|
"52": { # slot 0B
|
|
1339
1359
|
SZ_DESCRIPTION: "Sensor sensitivity (%)",
|
|
1340
1360
|
SZ_MIN_VALUE: 0,
|
|
1341
1361
|
SZ_MAX_VALUE: 25.0,
|
|
1342
1362
|
SZ_PRECISION: 0.1,
|
|
1343
|
-
SZ_DATA_TYPE: "
|
|
1363
|
+
SZ_DATA_TYPE: "01",
|
|
1364
|
+
SZ_DATA_UNIT: "%",
|
|
1344
1365
|
},
|
|
1345
1366
|
"54": { # slot 0C
|
|
1346
1367
|
SZ_DESCRIPTION: "Moisture sensor overrun time (mins)",
|
|
@@ -1348,13 +1369,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1348
1369
|
SZ_MAX_VALUE: 60,
|
|
1349
1370
|
SZ_PRECISION: 1,
|
|
1350
1371
|
SZ_DATA_TYPE: "00",
|
|
1372
|
+
SZ_DATA_UNIT: "min",
|
|
1351
1373
|
},
|
|
1352
1374
|
"75": { # slot 0D
|
|
1353
1375
|
SZ_DESCRIPTION: "Comfort temperature (°C)",
|
|
1354
1376
|
SZ_MIN_VALUE: 0.0,
|
|
1355
1377
|
SZ_MAX_VALUE: 30.0,
|
|
1356
1378
|
SZ_PRECISION: 0.01,
|
|
1357
|
-
SZ_DATA_TYPE: 92,
|
|
1379
|
+
SZ_DATA_TYPE: "92",
|
|
1380
|
+
SZ_DATA_UNIT: "°C",
|
|
1358
1381
|
},
|
|
1359
1382
|
"95": { # slot 08
|
|
1360
1383
|
SZ_DESCRIPTION: "Boost mode Supply/exhaust fan rate (%)",
|
|
@@ -1362,6 +1385,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1362
1385
|
SZ_MAX_VALUE: 1.0,
|
|
1363
1386
|
SZ_PRECISION: 0.005,
|
|
1364
1387
|
SZ_DATA_TYPE: "0F",
|
|
1388
|
+
SZ_DATA_UNIT: "%",
|
|
1365
1389
|
},
|
|
1366
1390
|
}
|
|
1367
1391
|
|
|
@@ -1456,3 +1480,19 @@ _31DA_FAN_INFO: dict[int, str] = {
|
|
|
1456
1480
|
# RAMSES_ZONES_ALL = RAMSES_ZONES.pop("ALL")
|
|
1457
1481
|
# RAMSES_ZONES_DHW = RAMSES_ZONES[ZON_ROLE.DHW]
|
|
1458
1482
|
# [RAMSES_ZONES[k].update(RAMSES_ZONES_ALL) for k in RAMSES_ZONES if k != ZON_ROLE.DHW]
|
|
1483
|
+
|
|
1484
|
+
__all__ = [
|
|
1485
|
+
"CODES_BY_DEV_SLUG",
|
|
1486
|
+
"CODES_SCHEMA",
|
|
1487
|
+
"CODE_NAME_LOOKUP",
|
|
1488
|
+
"CODES_BY_DEV_SLUG",
|
|
1489
|
+
"CODES_SCHEMA",
|
|
1490
|
+
"HVAC_KLASS_BY_VC_PAIR",
|
|
1491
|
+
"_2411_PARAMS_SCHEMA",
|
|
1492
|
+
"SZ_DESCRIPTION",
|
|
1493
|
+
"SZ_MIN_VALUE",
|
|
1494
|
+
"SZ_MAX_VALUE",
|
|
1495
|
+
"SZ_PRECISION",
|
|
1496
|
+
"SZ_DATA_TYPE",
|
|
1497
|
+
"SZ_DATA_UNIT",
|
|
1498
|
+
]
|
ramses_tx/schemas.py
CHANGED
|
@@ -215,6 +215,7 @@ def ConvertNullToDict() -> Callable[[_T | None], _T | dict[Never, Never]]:
|
|
|
215
215
|
|
|
216
216
|
|
|
217
217
|
SZ_ALIAS: Final = "alias"
|
|
218
|
+
SZ_BOUND_TO: Final = "bound"
|
|
218
219
|
SZ_CLASS: Final = "class"
|
|
219
220
|
SZ_FAKED: Final = "faked"
|
|
220
221
|
SZ_SCHEME: Final = "scheme"
|
|
@@ -296,6 +297,8 @@ def sch_global_traits_dict_factory(
|
|
|
296
297
|
vol.Optional(SZ_CLASS, default="HVC"): vol.Any(
|
|
297
298
|
None, *hvac_slugs, *(str(DEV_TYPE_MAP[s]) for s in hvac_slugs)
|
|
298
299
|
), # TODO: consider removing None
|
|
300
|
+
# Add 'bound' trait for FAN devices
|
|
301
|
+
vol.Optional(SZ_BOUND_TO): vol.Any(None, vol.Match(DEVICE_ID_REGEX.ANY)),
|
|
299
302
|
}
|
|
300
303
|
)
|
|
301
304
|
SCH_TRAITS_HVAC = SCH_TRAITS_HVAC.extend(
|
ramses_tx/transport.py
CHANGED
|
@@ -44,7 +44,7 @@ import sys
|
|
|
44
44
|
from collections import deque
|
|
45
45
|
from collections.abc import Awaitable, Callable, Iterable
|
|
46
46
|
from datetime import datetime as dt, timedelta as td
|
|
47
|
-
from functools import wraps
|
|
47
|
+
from functools import partial, wraps
|
|
48
48
|
from io import TextIOWrapper
|
|
49
49
|
from string import printable
|
|
50
50
|
from time import perf_counter
|
|
@@ -142,8 +142,7 @@ else: # is linux
|
|
|
142
142
|
|
|
143
143
|
def list_links(devices: set[str]) -> list[str]:
|
|
144
144
|
"""Search for symlinks to ports already listed in devices."""
|
|
145
|
-
|
|
146
|
-
links = []
|
|
145
|
+
links: list[str] = []
|
|
147
146
|
for device in glob.glob("/dev/*") + glob.glob("/dev/serial/by-id/*"):
|
|
148
147
|
if os.path.islink(device) and os.path.realpath(device) in devices:
|
|
149
148
|
links.append(device)
|
|
@@ -174,7 +173,7 @@ else: # is linux
|
|
|
174
173
|
return result
|
|
175
174
|
|
|
176
175
|
|
|
177
|
-
def is_hgi80(serial_port: SerPortNameT) -> bool | None:
|
|
176
|
+
async def is_hgi80(serial_port: SerPortNameT) -> bool | None:
|
|
178
177
|
"""Return True/False if the device attached to the port has the attrs of an HGI80.
|
|
179
178
|
|
|
180
179
|
Return None if it's not possible to tell (falsy should assume is evofw3).
|
|
@@ -209,7 +208,10 @@ def is_hgi80(serial_port: SerPortNameT) -> bool | None:
|
|
|
209
208
|
|
|
210
209
|
# otherwise, we can look at device attrs via comports()...
|
|
211
210
|
try:
|
|
212
|
-
|
|
211
|
+
loop = asyncio.get_running_loop()
|
|
212
|
+
komports = await loop.run_in_executor(
|
|
213
|
+
None, partial(comports, include_links=True)
|
|
214
|
+
)
|
|
213
215
|
except ImportError as err:
|
|
214
216
|
raise exc.TransportSerialError(f"Unable to find {serial_port}: {err}") from err
|
|
215
217
|
|
|
@@ -841,8 +843,6 @@ class PortTransport(_RegHackMixin, _FullTransport, _PortTransportAbstractor): #
|
|
|
841
843
|
self._leak_sem(), name="PortTransport._leak_sem()"
|
|
842
844
|
)
|
|
843
845
|
|
|
844
|
-
self._is_hgi80 = is_hgi80(self.serial.name)
|
|
845
|
-
|
|
846
846
|
self._loop.create_task(
|
|
847
847
|
self._create_connection(), name="PortTransport._create_connection()"
|
|
848
848
|
)
|
|
@@ -855,6 +855,8 @@ class PortTransport(_RegHackMixin, _FullTransport, _PortTransportAbstractor): #
|
|
|
855
855
|
|
|
856
856
|
# signature also serves to discover the HGI's device_id (& for pkt log, if any)
|
|
857
857
|
|
|
858
|
+
self._is_hgi80 = await is_hgi80(self.serial.name)
|
|
859
|
+
|
|
858
860
|
async def connect_sans_signature() -> None:
|
|
859
861
|
"""Call connection_made() without sending/waiting for a signature."""
|
|
860
862
|
|
ramses_tx/version.py
CHANGED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
ramses_cli/__init__.py,sha256=uvGzWqOf4avvgzxJNSLFWEelIWqSZ-AeLAZzg5x58bc,397
|
|
2
|
-
ramses_cli/client.py,sha256=vbKS3KVPiGsDWLp5cR3SVBtXrs-TinzlxSbTgcb4G2k,19724
|
|
3
|
-
ramses_cli/debug.py,sha256=vgR0lOHoYjWarN948dI617WZZGNuqHbeq6Tc16Da7b4,608
|
|
4
|
-
ramses_cli/discovery.py,sha256=81XbmpNiCpUHVZBwo2g1eRwyJG-wZhpSsc44G3hHlFA,12972
|
|
5
|
-
ramses_cli/utils/cat_slow.py,sha256=AhUpM5gnegCitNKU-JGHn-DrRzSi-49ZR1Qw6lxe_t8,607
|
|
6
|
-
ramses_cli/utils/convert.py,sha256=D_YiCyX5na9pgC-_NhBlW9N1dgRKUK-uLtLBfofjzZM,1804
|
|
7
|
-
ramses_rf/__init__.py,sha256=zONFBiRdf07cPTSxzr2V3t-b3CGokZjT9SGit4JUKRA,1055
|
|
8
|
-
ramses_rf/binding_fsm.py,sha256=uZAOl3i19KCXqqlaLJWkEqMMP7NJBhVPW3xTikQD1fY,25996
|
|
9
|
-
ramses_rf/const.py,sha256=L3z31CZ-xqno6oZp_h-67CB_5tDDqTwSWXsqRtsjMcs,5460
|
|
10
|
-
ramses_rf/database.py,sha256=ZZZgucyuU1IHsSewGukZfDg2gu8KeNaEFriWKM0TUHs,10287
|
|
11
|
-
ramses_rf/dispatcher.py,sha256=JGkqSi1o-YhQ2rj8tNkXwYLLeJIC7F061xpHoH8sSsM,11201
|
|
12
|
-
ramses_rf/entity_base.py,sha256=V9m_Q5SOLP5ko3sok0NDvyz3YdYch1QsxM6tHCIE7cA,39212
|
|
13
|
-
ramses_rf/exceptions.py,sha256=rzVZDcYxFH7BjUAQ3U1fHWtgBpwk3BgjX1TO1L1iM8c,2538
|
|
14
|
-
ramses_rf/gateway.py,sha256=WdIIGgs87CYfXwSCSVb2YzqOgLC7W4bkpulWQb7PFNw,20564
|
|
15
|
-
ramses_rf/helpers.py,sha256=TNk_QkpIOB3alOp1sqnA9LOzi4fuDCeapNlW3zTzNas,4250
|
|
16
|
-
ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
ramses_rf/schemas.py,sha256=mYOUZOH5OIDNBxRM2vd8POzDWEEmLhxh5UtqjTpFNek,13287
|
|
18
|
-
ramses_rf/version.py,sha256=4EfbWfYC4nSL7JCP_xnhFTpV6Yt5Vc4kNosAUW-CKNs,125
|
|
19
|
-
ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
|
|
20
|
-
ramses_rf/device/base.py,sha256=WGkBTUNjRUEe-phxdtdiXVCZnTi6-i1i_YT6g689UTM,17450
|
|
21
|
-
ramses_rf/device/heat.py,sha256=2sCsggySVcuTzyXDmgWy76QbhlU5MQWSejy3zgI5BDE,54242
|
|
22
|
-
ramses_rf/device/hvac.py,sha256=H_PUfG_jrrvJgtnu6Bco6PLxHn7CHALwebZzZI1ygFo,23917
|
|
23
|
-
ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
|
|
24
|
-
ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
|
|
25
|
-
ramses_rf/system/heat.py,sha256=3jaFEChU-HlWCRMY1y7u09s7AH4hT0pC63hnqwdmZOc,39223
|
|
26
|
-
ramses_rf/system/schedule.py,sha256=Ts6tdZPTQLV5NkgwA73tPa5QUsnZNIIuYoKC-8VsXDk,18808
|
|
27
|
-
ramses_rf/system/zones.py,sha256=9AH7ooN5QfiqvWuor2P1Dn8aILjQb2RWL9rWqDH1IjA,36075
|
|
28
|
-
ramses_tx/__init__.py,sha256=4FsVOzICJ4H80LJ0MknZCN0_V-g0k1nMkHUQ0IdrJW8,3161
|
|
29
|
-
ramses_tx/address.py,sha256=5swDr_SvOs1CxBmT-iJpldf8R00mOb7gKPMiEnxLz84,8452
|
|
30
|
-
ramses_tx/command.py,sha256=y69y9NYgQHuPbm7h6xC0osf3e1YIKY9jwmsfPiJ8N6U,58348
|
|
31
|
-
ramses_tx/const.py,sha256=QmwSS4BIN3ZFrLUiiFScP1RCUHuJ782V3ycRPQTtB_c,30297
|
|
32
|
-
ramses_tx/exceptions.py,sha256=FJSU9YkvpKjs3yeTqUJX1o3TPFSe_B01gRGIh9b3PNc,2632
|
|
33
|
-
ramses_tx/fingerprints.py,sha256=nfftA1E62HQnb-eLt2EqjEi_la0DAoT0wt-PtTMie0s,11974
|
|
34
|
-
ramses_tx/frame.py,sha256=9lUVh8gAMXNRAolfFw2WuWANjn24AWkmscuM9Tm5imE,22036
|
|
35
|
-
ramses_tx/gateway.py,sha256=TXLYwT6tFpmSokD29Qyj1ze7UGCxKidooeyP557Jfoo,11266
|
|
36
|
-
ramses_tx/helpers.py,sha256=0VAJ505kpq4K9b9ZeskWI1o2sWwyCbdnKOKZviKFdgY,32913
|
|
37
|
-
ramses_tx/logger.py,sha256=qYbUoNPnPaFWKVsYvLG6uTVuPTdZ8HsMzBbGx0DpBqc,10177
|
|
38
|
-
ramses_tx/message.py,sha256=hl_gLfwrF79ftUNnsgNt3XGsIhM2Pts0MtZZuGjfaxk,13169
|
|
39
|
-
ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
|
|
40
|
-
ramses_tx/packet.py,sha256=NGunaGCkEjhTp9t4mARK5e7kbqT-Z_JKCH7ibMYMJXU,7357
|
|
41
|
-
ramses_tx/parsers.py,sha256=PVTbPqcYPUko3BKDaOQoFDwIo4LWAUx5kfRb2KURAMI,109917
|
|
42
|
-
ramses_tx/protocol.py,sha256=ifj3qwcQivjQDaQUwM94xp-U8Pmef6zwSH7mav8DLWA,28867
|
|
43
|
-
ramses_tx/protocol_fsm.py,sha256=YhHkTqbl8w-myimsOjV50uIFgg9HiApwPU7xA_jg5nU,26827
|
|
44
|
-
ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
-
ramses_tx/ramses.py,sha256=9R-JrInORWUNMPklrAPQWwtr_2aaruQmFqQPw5mFkrE,52223
|
|
46
|
-
ramses_tx/schemas.py,sha256=h2AcArVROy1_C4n6F0Crj4e-2BxXxH74xogFlc6nKHI,12769
|
|
47
|
-
ramses_tx/transport.py,sha256=MwPnkQ0L-2qJt4mIJy3-C9XmHwBDjT7Kg-1LthPByVw,58331
|
|
48
|
-
ramses_tx/typed_dicts.py,sha256=w-0V5t2Q3GiNUOrRAWiW9GtSwbta_7luME6GfIb1zhI,10869
|
|
49
|
-
ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
|
|
50
|
-
ramses_tx/version.py,sha256=hWp2I2S7p1_tlItYBqDNAFpsM7kL_J8xYU-6mC2e_Ws,123
|
|
51
|
-
ramses_rf-0.51.7.dist-info/METADATA,sha256=w3QiOIRJncgPc3R3h1MaxjMDOjzm0NYBHFEoeAr6o6A,3909
|
|
52
|
-
ramses_rf-0.51.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
53
|
-
ramses_rf-0.51.7.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
|
|
54
|
-
ramses_rf-0.51.7.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
|
|
55
|
-
ramses_rf-0.51.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|