ramses-rf 0.52.5__py3-none-any.whl → 0.53.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ramses_cli/client.py +167 -54
- ramses_cli/py.typed +0 -0
- ramses_rf/__init__.py +2 -0
- ramses_rf/entity_base.py +3 -1
- ramses_rf/gateway.py +203 -27
- ramses_rf/schemas.py +1 -0
- ramses_rf/version.py +1 -1
- {ramses_rf-0.52.5.dist-info → ramses_rf-0.53.0.dist-info}/METADATA +1 -1
- {ramses_rf-0.52.5.dist-info → ramses_rf-0.53.0.dist-info}/RECORD +19 -18
- {ramses_rf-0.52.5.dist-info → ramses_rf-0.53.0.dist-info}/licenses/LICENSE +1 -1
- ramses_tx/command.py +1 -1
- ramses_tx/const.py +110 -23
- ramses_tx/protocol.py +22 -8
- ramses_tx/protocol_fsm.py +28 -10
- ramses_tx/schemas.py +2 -2
- ramses_tx/transport.py +499 -41
- ramses_tx/version.py +1 -1
- {ramses_rf-0.52.5.dist-info → ramses_rf-0.53.0.dist-info}/WHEEL +0 -0
- {ramses_rf-0.52.5.dist-info → ramses_rf-0.53.0.dist-info}/entry_points.txt +0 -0
ramses_tx/const.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""RAMSES RF - a RAMSES-II protocol decoder & analyser.
|
|
2
|
+
"""RAMSES RF - a RAMSES-II protocol decoder & analyser.
|
|
3
|
+
|
|
4
|
+
This module contains constants, enums, and helper classes used throughout the
|
|
5
|
+
library to decode and encode RAMSES-II protocol packets.
|
|
6
|
+
"""
|
|
3
7
|
|
|
4
8
|
from __future__ import annotations
|
|
5
9
|
|
|
@@ -15,16 +19,23 @@ DEV_MODE = __dev_mode__
|
|
|
15
19
|
DEFAULT_DISABLE_QOS: Final[bool | None] = None
|
|
16
20
|
DEFAULT_WAIT_FOR_REPLY: Final[bool | None] = None
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
#: Waiting for echo pkt after cmd sent (seconds)
|
|
23
|
+
DEFAULT_ECHO_TIMEOUT: Final[float] = 0.50
|
|
24
|
+
|
|
25
|
+
#: Waiting for reply pkt after echo pkt rcvd (seconds)
|
|
26
|
+
DEFAULT_RPLY_TIMEOUT: Final[float] = 0.50
|
|
20
27
|
DEFAULT_BUFFER_SIZE: Final[int] = 32
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
#: Total waiting for successful send (seconds)
|
|
30
|
+
DEFAULT_SEND_TIMEOUT: Final[float] = 20.0
|
|
31
|
+
#: For a command to be sent, incl. queuing time (seconds)
|
|
32
|
+
MAX_SEND_TIMEOUT: Final[float] = 20.0
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
#: For a command to be re-sent (not incl. 1st send)
|
|
35
|
+
MAX_RETRY_LIMIT: Final[int] = 3
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
#: Minimum gap between writes (seconds)
|
|
38
|
+
MIN_INTER_WRITE_GAP: Final[float] = 0.05
|
|
28
39
|
DEFAULT_GAP_DURATION: Final[float] = MIN_INTER_WRITE_GAP
|
|
29
40
|
DEFAULT_MAX_RETRIES: Final[int] = 3
|
|
30
41
|
DEFAULT_NUM_REPEATS: Final[int] = 0
|
|
@@ -139,6 +150,8 @@ SZ_OTC_ACTIVE: Final = "otc_active"
|
|
|
139
150
|
|
|
140
151
|
@verify(EnumCheck.UNIQUE)
|
|
141
152
|
class Priority(IntEnum):
|
|
153
|
+
"""Priority levels for protocol messages."""
|
|
154
|
+
|
|
142
155
|
LOWEST = 4
|
|
143
156
|
LOW = 2
|
|
144
157
|
DEFAULT = 0
|
|
@@ -147,31 +160,60 @@ class Priority(IntEnum):
|
|
|
147
160
|
|
|
148
161
|
|
|
149
162
|
def slug(string: str) -> str:
|
|
150
|
-
"""Convert a string to snake_case.
|
|
163
|
+
"""Convert a string to snake_case.
|
|
164
|
+
|
|
165
|
+
:param string: The input string to convert.
|
|
166
|
+
:return: The string converted to snake_case (lowercase, with non-alphanumerics replaced by underscores).
|
|
167
|
+
"""
|
|
151
168
|
return re.sub(r"[\W_]+", "_", string.lower())
|
|
152
169
|
|
|
153
170
|
|
|
154
171
|
# TODO: FIXME: This is a mess - needs converting to StrEnum
|
|
155
172
|
class AttrDict(dict): # type: ignore[type-arg]
|
|
173
|
+
"""A read-only dictionary that supports dot-access and two-way lookup.
|
|
174
|
+
|
|
175
|
+
This class is typically used to map hex codes (keys) to human-readable slugs (values),
|
|
176
|
+
while also allowing reverse lookup via dot notation (e.g., ``map.SLUG``).
|
|
177
|
+
|
|
178
|
+
.. warning::
|
|
179
|
+
This class is immutable. Attempting to modify it will raise a :exc:`TypeError`.
|
|
180
|
+
"""
|
|
181
|
+
|
|
156
182
|
_SZ_AKA_SLUG: Final = "_root_slug"
|
|
157
183
|
_SZ_DEFAULT: Final = "_default"
|
|
158
184
|
_SZ_SLUGS: Final = "SLUGS"
|
|
159
185
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
raise TypeError(f"'{
|
|
186
|
+
def _readonly(self, *args: Any, **kwargs: Any) -> NoReturn:
|
|
187
|
+
"""Raise TypeError for read-only operations."""
|
|
188
|
+
raise TypeError(f"'{self.__class__.__name__}' object is read only")
|
|
189
|
+
|
|
190
|
+
def __setitem__(self, key: Any, value: Any) -> NoReturn:
|
|
191
|
+
self._readonly()
|
|
163
192
|
|
|
164
|
-
__delitem__
|
|
165
|
-
|
|
166
|
-
clear = __readonly
|
|
167
|
-
pop = __readonly
|
|
168
|
-
popitem = __readonly
|
|
169
|
-
setdefault = __readonly
|
|
170
|
-
update = __readonly
|
|
193
|
+
def __delitem__(self, key: Any) -> NoReturn:
|
|
194
|
+
self._readonly()
|
|
171
195
|
|
|
172
|
-
|
|
196
|
+
def clear(self) -> NoReturn:
|
|
197
|
+
self._readonly()
|
|
198
|
+
|
|
199
|
+
def pop(self, *args: Any, **kwargs: Any) -> NoReturn:
|
|
200
|
+
self._readonly()
|
|
201
|
+
|
|
202
|
+
def popitem(self) -> NoReturn:
|
|
203
|
+
self._readonly()
|
|
204
|
+
|
|
205
|
+
def setdefault(self, *args: Any, **kwargs: Any) -> NoReturn:
|
|
206
|
+
self._readonly()
|
|
207
|
+
|
|
208
|
+
def update(self, *args: Any, **kwargs: Any) -> NoReturn:
|
|
209
|
+
self._readonly()
|
|
173
210
|
|
|
174
211
|
def __init__(self, main_table: dict[str, dict], attr_table: dict[str, Any]) -> None: # type: ignore[type-arg]
|
|
212
|
+
"""Initialize the AttrDict.
|
|
213
|
+
|
|
214
|
+
:param main_table: A dictionary mapping keys (usually hex codes) to property dictionaries.
|
|
215
|
+
:param attr_table: A dictionary of additional attributes to expose on the object.
|
|
216
|
+
"""
|
|
175
217
|
self._main_table = main_table
|
|
176
218
|
self._attr_table = attr_table
|
|
177
219
|
self._attr_table[self._SZ_SLUGS] = tuple(sorted(main_table.keys()))
|
|
@@ -237,7 +279,12 @@ class AttrDict(dict): # type: ignore[type-arg]
|
|
|
237
279
|
return self.__getattribute__(name)
|
|
238
280
|
|
|
239
281
|
def _hex(self, key: str) -> str:
|
|
240
|
-
"""Return the key/ID (2-byte hex string) of the two-way dict (e.g. '04').
|
|
282
|
+
"""Return the key/ID (2-byte hex string) of the two-way dict (e.g. '04').
|
|
283
|
+
|
|
284
|
+
:param key: The lookup key (can be slug or code).
|
|
285
|
+
:raises KeyError: If the key is not found.
|
|
286
|
+
:return: The 2-byte hex string identifier.
|
|
287
|
+
"""
|
|
241
288
|
if key in self._main_table:
|
|
242
289
|
return list(self._main_table[key].keys())[0] # type: ignore[no-any-return]
|
|
243
290
|
if key in self._reverse:
|
|
@@ -245,7 +292,12 @@ class AttrDict(dict): # type: ignore[type-arg]
|
|
|
245
292
|
raise KeyError(key)
|
|
246
293
|
|
|
247
294
|
def _str(self, key: str) -> str:
|
|
248
|
-
"""Return the value (string) of the two-way dict (e.g. 'radiator_valve').
|
|
295
|
+
"""Return the value (string) of the two-way dict (e.g. 'radiator_valve').
|
|
296
|
+
|
|
297
|
+
:param key: The lookup key.
|
|
298
|
+
:raises KeyError: If the key is not found.
|
|
299
|
+
:return: The human-readable slug string.
|
|
300
|
+
"""
|
|
249
301
|
if key in self._main_table:
|
|
250
302
|
return list(self._main_table[key].values())[0] # type: ignore[no-any-return]
|
|
251
303
|
if key in self:
|
|
@@ -256,14 +308,23 @@ class AttrDict(dict): # type: ignore[type-arg]
|
|
|
256
308
|
# return {k: k for k in super().values()}.values()
|
|
257
309
|
|
|
258
310
|
def slug(self, key: str) -> str:
|
|
259
|
-
"""
|
|
311
|
+
"""Return master slug for a hex key/ID.
|
|
312
|
+
|
|
313
|
+
Example: 00 -> 'TRV' (master), not 'TR0'.
|
|
314
|
+
|
|
315
|
+
:param key: The hex key to look up.
|
|
316
|
+
:return: The master slug.
|
|
317
|
+
"""
|
|
260
318
|
slug_ = self._slug_lookup[key]
|
|
261
319
|
# if slug_ in self._attr_table["_TRANSFORMS"]:
|
|
262
320
|
# return self._attr_table["_TRANSFORMS"][slug_]
|
|
263
321
|
return slug_ # type: ignore[no-any-return]
|
|
264
322
|
|
|
265
323
|
def slugs(self) -> tuple[str]:
|
|
266
|
-
"""Return the slugs from the main table.
|
|
324
|
+
"""Return the slugs from the main table.
|
|
325
|
+
|
|
326
|
+
:return: A tuple of all available slugs.
|
|
327
|
+
"""
|
|
267
328
|
return self._attr_table[self._SZ_SLUGS] # type: ignore[no-any-return]
|
|
268
329
|
|
|
269
330
|
|
|
@@ -271,6 +332,12 @@ def attr_dict_factory(
|
|
|
271
332
|
main_table: dict[str, dict], # type: ignore[type-arg]
|
|
272
333
|
attr_table: dict | None = None, # type: ignore[type-arg]
|
|
273
334
|
) -> AttrDict: # is: SlottedAttrDict
|
|
335
|
+
"""Create a new AttrDict instance with a slotted subclass.
|
|
336
|
+
|
|
337
|
+
:param main_table: The primary mapping of codes to slugs.
|
|
338
|
+
:param attr_table: Optional additional attributes to attach to the instance.
|
|
339
|
+
:return: An instance of a dynamic AttrDict subclass.
|
|
340
|
+
"""
|
|
274
341
|
if attr_table is None:
|
|
275
342
|
attr_table = {}
|
|
276
343
|
|
|
@@ -295,6 +362,8 @@ def attr_dict_factory(
|
|
|
295
362
|
# slugs for device/zone entity klasses, used by 0005/000C
|
|
296
363
|
@verify(EnumCheck.UNIQUE)
|
|
297
364
|
class DevRole(StrEnum):
|
|
365
|
+
"""Slugs for device/zone entity classes, used by commands 0005/000C."""
|
|
366
|
+
|
|
298
367
|
#
|
|
299
368
|
# Generic device/zone classes
|
|
300
369
|
ACT = "ACT" # Generic heating zone actuator group
|
|
@@ -345,6 +414,8 @@ DEV_ROLE_MAP = attr_dict_factory(
|
|
|
345
414
|
# slugs for device entity types, used in device_ids
|
|
346
415
|
@verify(EnumCheck.UNIQUE)
|
|
347
416
|
class DevType(StrEnum):
|
|
417
|
+
"""Slugs for device entity types, used in device_ids."""
|
|
418
|
+
|
|
348
419
|
#
|
|
349
420
|
# Promotable/Generic devices
|
|
350
421
|
DEV = "DEV" # xx: Promotable device
|
|
@@ -457,6 +528,8 @@ DEV_TYPE_MAP = attr_dict_factory(
|
|
|
457
528
|
|
|
458
529
|
# slugs for zone entity klasses, used by 0005/000C
|
|
459
530
|
class ZoneRole(StrEnum):
|
|
531
|
+
"""Slugs for zone entity classes, used by commands 0005/000C."""
|
|
532
|
+
|
|
460
533
|
#
|
|
461
534
|
# Generic device/zone classes
|
|
462
535
|
ACT = "ACT" # Generic heating zone actuator group
|
|
@@ -645,6 +718,8 @@ MESSAGE_REGEX = re.compile(f"^{r} {v} {r} {d} {d} {d} {c} {l} {p}$")
|
|
|
645
718
|
|
|
646
719
|
# Used by 0418/system_fault parser
|
|
647
720
|
class FaultDeviceClass(StrEnum):
|
|
721
|
+
"""Device classes for system faults."""
|
|
722
|
+
|
|
648
723
|
CONTROLLER = "controller"
|
|
649
724
|
SENSOR = "sensor"
|
|
650
725
|
SETPOINT = "setpoint"
|
|
@@ -666,6 +741,8 @@ FAULT_DEVICE_CLASS: Final[dict[str, FaultDeviceClass]] = {
|
|
|
666
741
|
|
|
667
742
|
|
|
668
743
|
class FaultState(StrEnum):
|
|
744
|
+
"""States for system faults."""
|
|
745
|
+
|
|
669
746
|
FAULT = "fault"
|
|
670
747
|
RESTORE = "restore"
|
|
671
748
|
UNKNOWN_C0 = "unknown_c0"
|
|
@@ -680,6 +757,8 @@ FAULT_STATE: Final[dict[str, FaultState]] = { # a bitmap?
|
|
|
680
757
|
|
|
681
758
|
|
|
682
759
|
class FaultType(StrEnum):
|
|
760
|
+
"""Types of system faults."""
|
|
761
|
+
|
|
683
762
|
SYSTEM_FAULT = "system_fault"
|
|
684
763
|
MAINS_LOW = "mains_low"
|
|
685
764
|
BATTERY_LOW = "battery_low"
|
|
@@ -703,6 +782,8 @@ FAULT_TYPE: Final[dict[str, FaultType]] = {
|
|
|
703
782
|
|
|
704
783
|
|
|
705
784
|
class SystemType(StrEnum):
|
|
785
|
+
"""System types (e.g. Evohome, Hometronics)."""
|
|
786
|
+
|
|
706
787
|
CHRONOTHERM = "chronotherm"
|
|
707
788
|
EVOHOME = "evohome"
|
|
708
789
|
HOMETRONICS = "hometronics"
|
|
@@ -742,6 +823,8 @@ FAN_RATE: Final = "fan_rate" # percentage, 0.0 - 1.0 # deprecated, use SZ_FAN_
|
|
|
742
823
|
# Below, verbs & codes - can use Verb/Code/Index for mypy type checking
|
|
743
824
|
@verify(EnumCheck.UNIQUE)
|
|
744
825
|
class VerbT(StrEnum):
|
|
826
|
+
"""Protocol verbs (message types)."""
|
|
827
|
+
|
|
745
828
|
I_ = " I"
|
|
746
829
|
RQ = "RQ"
|
|
747
830
|
RP = "RP"
|
|
@@ -756,6 +839,8 @@ W_: Final = VerbT.W_
|
|
|
756
839
|
|
|
757
840
|
@verify(EnumCheck.UNIQUE)
|
|
758
841
|
class MsgId(StrEnum):
|
|
842
|
+
"""Message identifiers."""
|
|
843
|
+
|
|
759
844
|
_00 = "00"
|
|
760
845
|
_03 = "03"
|
|
761
846
|
_06 = "06"
|
|
@@ -791,6 +876,8 @@ class MsgId(StrEnum):
|
|
|
791
876
|
# StrEnum is intended to include all known codes, see: test suite, code schema in ramses.py
|
|
792
877
|
@verify(EnumCheck.UNIQUE)
|
|
793
878
|
class Code(StrEnum):
|
|
879
|
+
"""Protocol command codes."""
|
|
880
|
+
|
|
794
881
|
_0001 = "0001"
|
|
795
882
|
_0002 = "0002"
|
|
796
883
|
_0004 = "0004"
|
ramses_tx/protocol.py
CHANGED
|
@@ -251,21 +251,30 @@ class _BaseProtocol(asyncio.Protocol):
|
|
|
251
251
|
|
|
252
252
|
# FIX: Patch command with actual HGI ID if it uses the default placeholder
|
|
253
253
|
# NOTE: HGI80s (TI 3410) require the default ID (18:000730), or they will silent-fail
|
|
254
|
+
|
|
254
255
|
if (
|
|
255
|
-
self.
|
|
256
|
+
self.hgi_id
|
|
256
257
|
and self._is_evofw3 # Only patch if using evofw3 (not HGI80)
|
|
257
258
|
and cmd._addrs[0].id == HGI_DEV_ADDR.id
|
|
258
|
-
and self.
|
|
259
|
+
and self.hgi_id != HGI_DEV_ADDR.id
|
|
259
260
|
):
|
|
260
261
|
# The command uses the default 18:000730, but we know the real ID.
|
|
261
262
|
# Reconstruct the command string with the correct address.
|
|
262
263
|
|
|
264
|
+
_LOGGER.debug(
|
|
265
|
+
f"Patching command with active HGI ID: swapped {HGI_DEV_ADDR.id} -> {self.hgi_id} for {cmd._hdr}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Get current addresses as strings
|
|
269
|
+
# The command uses the default 18:000730, but we know the real ID.
|
|
270
|
+
# Reconstruct the command string with the correct address.
|
|
271
|
+
|
|
263
272
|
# Get current addresses as strings
|
|
264
273
|
new_addrs = [a.id for a in cmd._addrs]
|
|
265
274
|
|
|
266
275
|
# ONLY patch the Source Address (Index 0).
|
|
267
276
|
# Leave Dest (Index 1/2) alone to avoid breaking tests that expect 18:000730 there.
|
|
268
|
-
new_addrs[0] = self.
|
|
277
|
+
new_addrs[0] = self.hgi_id
|
|
269
278
|
|
|
270
279
|
new_frame = f"{cmd.verb} {cmd.seqn} {new_addrs[0]} {new_addrs[1]} {new_addrs[2]} {cmd.code} {int(cmd.len_):03d} {cmd.payload}"
|
|
271
280
|
cmd = Command(new_frame)
|
|
@@ -349,6 +358,7 @@ class _BaseProtocol(asyncio.Protocol):
|
|
|
349
358
|
"""
|
|
350
359
|
|
|
351
360
|
if self._msg_handler: # type: ignore[truthy-function]
|
|
361
|
+
_LOGGER.debug(f"Dispatching valid message to handler: {msg}")
|
|
352
362
|
self._loop.call_soon_threadsafe(self._msg_handler, msg)
|
|
353
363
|
for callback in self._msg_handlers:
|
|
354
364
|
# TODO: if handler's filter returns True:
|
|
@@ -389,9 +399,10 @@ class _DeviceIdFilterMixin(_BaseProtocol):
|
|
|
389
399
|
def hgi_id(self) -> DeviceIdT:
|
|
390
400
|
if not self._transport:
|
|
391
401
|
return self._known_hgi or HGI_DEV_ADDR.id
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
)
|
|
402
|
+
# CRITICAL FIX: get_extra_info returns None if key exists but val is None
|
|
403
|
+
# We must ensure we fallback to the known HGI or default if it returns None
|
|
404
|
+
hgi = self._transport.get_extra_info(SZ_ACTIVE_HGI)
|
|
405
|
+
return hgi or self._known_hgi or HGI_DEV_ADDR.id
|
|
395
406
|
|
|
396
407
|
@staticmethod
|
|
397
408
|
def _extract_known_hgi_id(
|
|
@@ -434,7 +445,10 @@ class _DeviceIdFilterMixin(_BaseProtocol):
|
|
|
434
445
|
|
|
435
446
|
known_hgi = (explicit_hgis if explicit_hgis else implicit_hgis)[0]
|
|
436
447
|
|
|
437
|
-
if include_list[known_hgi].get(SZ_CLASS)
|
|
448
|
+
if include_list[known_hgi].get(SZ_CLASS) not in (
|
|
449
|
+
DevType.HGI,
|
|
450
|
+
DEV_TYPE_MAP[DevType.HGI],
|
|
451
|
+
):
|
|
438
452
|
logger(
|
|
439
453
|
f"The {SZ_KNOWN_LIST} SHOULD include exactly one gateway (HGI): "
|
|
440
454
|
f"{known_hgi} should specify 'class: HGI', as 18: is also used for HVAC"
|
|
@@ -866,7 +880,7 @@ async def create_stack(
|
|
|
866
880
|
)
|
|
867
881
|
|
|
868
882
|
transport: RamsesTransportT = await (transport_factory_ or transport_factory)( # type: ignore[operator]
|
|
869
|
-
protocol, disable_sending=disable_sending, **kwargs
|
|
883
|
+
protocol, disable_sending=bool(disable_sending), **kwargs
|
|
870
884
|
)
|
|
871
885
|
|
|
872
886
|
if not kwargs.get(SZ_PORT_NAME):
|
ramses_tx/protocol_fsm.py
CHANGED
|
@@ -146,9 +146,13 @@ class ProtocolContext:
|
|
|
146
146
|
self._multiplier = min(3, old_val + 1)
|
|
147
147
|
|
|
148
148
|
if isinstance(self._state, WantEcho):
|
|
149
|
-
_LOGGER.warning(
|
|
149
|
+
_LOGGER.warning(
|
|
150
|
+
f"Timeout expired waiting for echo: {self} (delay={delay})"
|
|
151
|
+
)
|
|
150
152
|
else: # isinstance(self._state, WantRply):
|
|
151
|
-
_LOGGER.warning(
|
|
153
|
+
_LOGGER.warning(
|
|
154
|
+
f"Timeout expired waiting for reply: {self} (delay={delay})"
|
|
155
|
+
)
|
|
152
156
|
|
|
153
157
|
assert isinstance(self.is_sending, bool), (
|
|
154
158
|
f"{self}: Coding error"
|
|
@@ -197,8 +201,16 @@ class ProtocolContext:
|
|
|
197
201
|
# _fut.cancel() (incl. via a send_cmd(qos.timeout) -> wait_for(timeout))
|
|
198
202
|
|
|
199
203
|
# Changing the order of the following is fraught with danger
|
|
204
|
+
# Formatitng: [TRACE_FSM] [State: Old->New] [Reason] | ...
|
|
205
|
+
|
|
206
|
+
current_state_name = self._state.__class__.__name__
|
|
207
|
+
new_state_name = state_class.__name__
|
|
208
|
+
transition = f"{current_state_name}->{new_state_name}"
|
|
209
|
+
|
|
200
210
|
if self._fut is None: # logging only - IsInIdle, Inactive
|
|
201
|
-
_LOGGER.debug(
|
|
211
|
+
_LOGGER.debug(
|
|
212
|
+
f"FSM state changed {transition}: no active future (ctx={self})"
|
|
213
|
+
)
|
|
202
214
|
assert self._cmd is None, f"{self}: Coding error" # mypy hint
|
|
203
215
|
assert isinstance(self._state, IsInIdle | Inactive | None), (
|
|
204
216
|
f"{self}: Coding error"
|
|
@@ -207,14 +219,18 @@ class ProtocolContext:
|
|
|
207
219
|
elif self._fut.cancelled() and not isinstance(self._state, IsInIdle):
|
|
208
220
|
# cancelled by wait_for(timeout), cancel("buffer overflow"), or other?
|
|
209
221
|
# was for previous send_cmd if currently IsInIdle (+/- Inactive?)
|
|
210
|
-
_LOGGER.debug(
|
|
222
|
+
_LOGGER.debug(
|
|
223
|
+
f"FSM state changed {transition}: future cancelled (expired={expired}, ctx={self})"
|
|
224
|
+
)
|
|
211
225
|
assert self._cmd is not None, f"{self}: Coding error" # mypy hint
|
|
212
226
|
assert isinstance(self._state, WantEcho | WantRply), (
|
|
213
227
|
f"{self}: Coding error"
|
|
214
228
|
) # mypy hint
|
|
215
229
|
|
|
216
230
|
elif exception:
|
|
217
|
-
_LOGGER.debug(
|
|
231
|
+
_LOGGER.debug(
|
|
232
|
+
f"FSM state changed {transition}: exception occurred (error={exception}, ctx={self})"
|
|
233
|
+
)
|
|
218
234
|
assert not self._fut.done(), (
|
|
219
235
|
f"{self}: Coding error ({self._fut})"
|
|
220
236
|
) # mypy hint
|
|
@@ -224,7 +240,9 @@ class ProtocolContext:
|
|
|
224
240
|
self._fut.set_exception(exception) # apologise to the sender
|
|
225
241
|
|
|
226
242
|
elif result:
|
|
227
|
-
_LOGGER.debug(
|
|
243
|
+
_LOGGER.debug(
|
|
244
|
+
f"FSM state changed {transition}: result received (result={result._hdr}, ctx={self})"
|
|
245
|
+
)
|
|
228
246
|
assert not self._fut.done(), (
|
|
229
247
|
f"{self}: Coding error ({self._fut})"
|
|
230
248
|
) # mypy hint
|
|
@@ -234,7 +252,7 @@ class ProtocolContext:
|
|
|
234
252
|
self._fut.set_result(result)
|
|
235
253
|
|
|
236
254
|
elif expired: # by expire_state_on_timeout(echo_timeout/reply_timeout)
|
|
237
|
-
_LOGGER.debug("
|
|
255
|
+
_LOGGER.debug(f"FSM state changed {transition}: timer expired (ctx={self})")
|
|
238
256
|
assert not self._fut.done(), (
|
|
239
257
|
f"{self}: Coding error ({self._fut})"
|
|
240
258
|
) # mypy hint
|
|
@@ -246,7 +264,7 @@ class ProtocolContext:
|
|
|
246
264
|
)
|
|
247
265
|
|
|
248
266
|
else: # logging only - WantEcho, WantRply
|
|
249
|
-
_LOGGER.debug("
|
|
267
|
+
_LOGGER.debug(f"FSM state changed {transition}: successful (ctx={self})")
|
|
250
268
|
assert self._fut is None or self._fut.cancelled() or not self._fut.done(), (
|
|
251
269
|
f"{self}: Coding error ({self._fut})"
|
|
252
270
|
) # mypy hint
|
|
@@ -281,11 +299,11 @@ class ProtocolContext:
|
|
|
281
299
|
self._loop.call_soon_threadsafe(effect_state, timed_out) # calls expire_state
|
|
282
300
|
|
|
283
301
|
if not isinstance(self._state, WantRply):
|
|
284
|
-
_LOGGER.debug("AFTER. = %s", self)
|
|
302
|
+
# _LOGGER.debug("AFTER. = %s", self)
|
|
285
303
|
return
|
|
286
304
|
|
|
287
305
|
assert self._qos is not None, f"{self}: Coding error" # mypy hint
|
|
288
|
-
_LOGGER.debug("AFTER. = %s: wait_for_reply=%s", self, self._qos.wait_for_reply)
|
|
306
|
+
# _LOGGER.debug("AFTER. = %s: wait_for_reply=%s", self, self._qos.wait_for_reply)
|
|
289
307
|
|
|
290
308
|
def connection_made(self, transport: RamsesTransportT) -> None:
|
|
291
309
|
# may want to set some instance variables, according to type of transport
|
ramses_tx/schemas.py
CHANGED
|
@@ -294,7 +294,7 @@ def sch_global_traits_dict_factory(
|
|
|
294
294
|
)
|
|
295
295
|
SCH_TRAITS_HEAT = SCH_TRAITS_HEAT.extend(
|
|
296
296
|
heat_traits,
|
|
297
|
-
extra=vol.PREVENT_EXTRA
|
|
297
|
+
extra=vol.PREVENT_EXTRA, # Always prevent extra keys
|
|
298
298
|
)
|
|
299
299
|
|
|
300
300
|
# NOTE: voluptuous doesn't like StrEnums, hence str(s)
|
|
@@ -314,7 +314,7 @@ def sch_global_traits_dict_factory(
|
|
|
314
314
|
)
|
|
315
315
|
SCH_TRAITS_HVAC = SCH_TRAITS_HVAC.extend(
|
|
316
316
|
hvac_traits,
|
|
317
|
-
extra=vol.PREVENT_EXTRA
|
|
317
|
+
extra=vol.PREVENT_EXTRA, # Always prevent extra keys
|
|
318
318
|
)
|
|
319
319
|
|
|
320
320
|
SCH_TRAITS = vol.Any(
|