ramses-rf 0.52.2__py3-none-any.whl → 0.52.4__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/binding_fsm.py +18 -4
- ramses_rf/database.py +9 -5
- ramses_rf/device/heat.py +4 -7
- ramses_rf/entity_base.py +150 -55
- ramses_rf/gateway.py +9 -9
- ramses_rf/system/heat.py +1 -1
- ramses_rf/system/zones.py +1 -1
- ramses_rf/version.py +1 -1
- {ramses_rf-0.52.2.dist-info → ramses_rf-0.52.4.dist-info}/METADATA +2 -1
- {ramses_rf-0.52.2.dist-info → ramses_rf-0.52.4.dist-info}/RECORD +22 -22
- ramses_tx/const.py +1 -1
- ramses_tx/gateway.py +3 -0
- ramses_tx/logger.py +8 -0
- ramses_tx/parsers.py +57 -12
- ramses_tx/protocol.py +2 -2
- ramses_tx/ramses.py +9 -5
- ramses_tx/schemas.py +4 -0
- ramses_tx/transport.py +15 -3
- ramses_tx/version.py +1 -1
- {ramses_rf-0.52.2.dist-info → ramses_rf-0.52.4.dist-info}/WHEEL +0 -0
- {ramses_rf-0.52.2.dist-info → ramses_rf-0.52.4.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.52.2.dist-info → ramses_rf-0.52.4.dist-info}/licenses/LICENSE +0 -0
ramses_rf/binding_fsm.py
CHANGED
|
@@ -178,6 +178,8 @@ class BindContextBase:
|
|
|
178
178
|
self, state: type[BindStateBase], result: asyncio.Future[Message] | None = None
|
|
179
179
|
) -> None:
|
|
180
180
|
"""Transition the State of the Context, and process the result, if any."""
|
|
181
|
+
# Ensure prev_state is always available, not only during debugging
|
|
182
|
+
prev_state = self._state
|
|
181
183
|
|
|
182
184
|
# if False and result:
|
|
183
185
|
# try:
|
|
@@ -185,10 +187,6 @@ class BindContextBase:
|
|
|
185
187
|
# except exc.BindingError as err:
|
|
186
188
|
# self._fut.set_result(err)
|
|
187
189
|
|
|
188
|
-
if _DBG_MAINTAIN_STATE_CHAIN: # HACK for debugging
|
|
189
|
-
# if prev_state in (None, )
|
|
190
|
-
prev_state = self._state
|
|
191
|
-
|
|
192
190
|
self._state = state(self)
|
|
193
191
|
if not self.is_binding:
|
|
194
192
|
self._is_respondent = None
|
|
@@ -197,6 +195,14 @@ class BindContextBase:
|
|
|
197
195
|
elif state is SuppSendOfferWaitForAccept:
|
|
198
196
|
self._is_respondent = False
|
|
199
197
|
|
|
198
|
+
# Log binding completion transitions
|
|
199
|
+
if isinstance(
|
|
200
|
+
self._state, (RespHasBoundAsRespondent, SuppHasBoundAsSupplicant)
|
|
201
|
+
):
|
|
202
|
+
_LOGGER.info(
|
|
203
|
+
f"{self._dev.id}: Binding process completed: {type(prev_state).__name__} -> {state.__name__} (role: {self.role})"
|
|
204
|
+
)
|
|
205
|
+
|
|
200
206
|
if _DBG_MAINTAIN_STATE_CHAIN: # HACK for debugging
|
|
201
207
|
setattr(self._state, "_prev_state", prev_state) # noqa: B010
|
|
202
208
|
|
|
@@ -663,6 +669,10 @@ class RespHasBoundAsRespondent(BindStateBase):
|
|
|
663
669
|
|
|
664
670
|
_attr_role = BindRole.IS_DORMANT
|
|
665
671
|
|
|
672
|
+
def __init__(self, context: BindContextBase) -> None:
|
|
673
|
+
super().__init__(context)
|
|
674
|
+
_LOGGER.info(f"{context._dev.id}: Binding completed as respondent")
|
|
675
|
+
|
|
666
676
|
|
|
667
677
|
class RespIsWaitingForAddenda(_DevIsWaitingForMsg, BindStateBase):
|
|
668
678
|
"""Respondent has received a Confirm & is waiting for an Addenda."""
|
|
@@ -715,6 +725,10 @@ class SuppHasBoundAsSupplicant(BindStateBase):
|
|
|
715
725
|
|
|
716
726
|
_attr_role = BindRole.IS_DORMANT
|
|
717
727
|
|
|
728
|
+
def __init__(self, context: BindContextBase) -> None:
|
|
729
|
+
super().__init__(context)
|
|
730
|
+
_LOGGER.info(f"{context._dev.id}: Binding completed as supplicant")
|
|
731
|
+
|
|
718
732
|
|
|
719
733
|
class SuppIsReadyToSendAddenda(
|
|
720
734
|
_DevIsReadyToSendCmd, BindStateBase
|
ramses_rf/database.py
CHANGED
|
@@ -130,11 +130,15 @@ class MessageIndex:
|
|
|
130
130
|
def stop(self) -> None:
|
|
131
131
|
"""Stop the housekeeper loop."""
|
|
132
132
|
|
|
133
|
-
if
|
|
133
|
+
if (
|
|
134
|
+
self.maintain
|
|
135
|
+
and self._housekeeping_task
|
|
136
|
+
and (not self._housekeeping_task.done())
|
|
137
|
+
):
|
|
134
138
|
self._housekeeping_task.cancel() # stop the housekeeper
|
|
135
139
|
|
|
136
140
|
self._cx.commit() # just in case
|
|
137
|
-
|
|
141
|
+
self._cx.close() # may still need to do queries after engine has stopped?
|
|
138
142
|
|
|
139
143
|
@property
|
|
140
144
|
def msgs(self) -> MsgDdT:
|
|
@@ -193,7 +197,7 @@ class MessageIndex:
|
|
|
193
197
|
"""
|
|
194
198
|
dtm = dt_now - _cutoff # .isoformat(timespec="microseconds") < needed?
|
|
195
199
|
|
|
196
|
-
self._cu.execute("SELECT dtm FROM messages WHERE dtm
|
|
200
|
+
self._cu.execute("SELECT dtm FROM messages WHERE dtm >= ?", (dtm,))
|
|
197
201
|
rows = self._cu.fetchall() # fetch dtm of current messages to retain
|
|
198
202
|
|
|
199
203
|
try: # make this operation atomic, i.e. update self._msgs only on success
|
|
@@ -334,7 +338,7 @@ class MessageIndex:
|
|
|
334
338
|
payload_keys(msg.payload),
|
|
335
339
|
),
|
|
336
340
|
)
|
|
337
|
-
_LOGGER.debug(f"Added {msg} to gwy.msg_db")
|
|
341
|
+
# _LOGGER.debug(f"Added {msg} to gwy.msg_db")
|
|
338
342
|
|
|
339
343
|
return _old_msgs[0] if _old_msgs else None
|
|
340
344
|
|
|
@@ -435,7 +439,7 @@ class MessageIndex:
|
|
|
435
439
|
|
|
436
440
|
def qry_dtms(self, **kwargs: bool | dt | str) -> list[Any]:
|
|
437
441
|
"""
|
|
438
|
-
Select from the
|
|
442
|
+
Select from the MessageIndex a list of dtms that match the provided arguments.
|
|
439
443
|
|
|
440
444
|
:param kwargs: data table field names and criteria
|
|
441
445
|
:return: list of unformatted dtms that match, useful for msg lookup, or an empty list if 0 matches
|
ramses_rf/device/heat.py
CHANGED
|
@@ -120,7 +120,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
120
120
|
|
|
121
121
|
class Actuator(DeviceHeat): # 3EF0, 3EF1 (for 10:/13:)
|
|
122
122
|
# .I --- 13:109598 --:------ 13:109598 3EF0 003 00C8FF # event-driven, 00/C8
|
|
123
|
-
# RP --- 13:109598 18:002563 --:------ 0008 002 00C8 # 00/C8, as
|
|
123
|
+
# RP --- 13:109598 18:002563 --:------ 0008 002 00C8 # 00/C8, as above
|
|
124
124
|
# RP --- 13:109598 18:002563 --:------ 3EF1 007 0000BF-00BFC8FF # 00/C8, as above
|
|
125
125
|
|
|
126
126
|
# RP --- 10:048122 18:140805 --:------ 3EF1 007 007FFF-003C2A10 # 10:s only RP, always 7FFF
|
|
@@ -450,7 +450,7 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
|
450
450
|
# )
|
|
451
451
|
# self._send_cmd(cmd)
|
|
452
452
|
|
|
453
|
-
elif msg.code == Code._0008: # relay_demand
|
|
453
|
+
elif msg.code == Code._0008: # relay_demand
|
|
454
454
|
if msg.payload.get(SZ_DOMAIN_ID) == FC:
|
|
455
455
|
self._relay_demand = msg
|
|
456
456
|
else: # FA
|
|
@@ -668,11 +668,8 @@ class OtbGateway(Actuator, HeatDemand): # OTB (10): 3220 (22D9, others)
|
|
|
668
668
|
self._child_id = FC # NOTE: domain_id
|
|
669
669
|
|
|
670
670
|
# TODO(eb): cleanup
|
|
671
|
-
# should fix src/ramses_rf/database.py _add_record try/except when activating next line
|
|
672
671
|
if self._gwy.msg_db:
|
|
673
|
-
self._add_record(
|
|
674
|
-
address=self.addr, code=Code._3220, verb="RP"
|
|
675
|
-
) # << essential?
|
|
672
|
+
self._add_record(address=self.addr, code=Code._3220, verb="RP")
|
|
676
673
|
# adds a "sim" RP opentherm_msg to the SQLite MessageIndex with code _3220
|
|
677
674
|
# causes exc when fetching ALL, when no "real" msg was added to _msgs_. We skip those.
|
|
678
675
|
else:
|
|
@@ -1411,7 +1408,7 @@ class UfhCircuit(Child, Entity): # FIXME
|
|
|
1411
1408
|
def __init__(self, ufc: UfhController, ufh_idx: str) -> None:
|
|
1412
1409
|
super().__init__(ufc._gwy)
|
|
1413
1410
|
|
|
1414
|
-
# FIXME:
|
|
1411
|
+
# FIXME: gwy.msg_db entities must know their parent device ID and their own idx
|
|
1415
1412
|
self._z_id = ufc.id
|
|
1416
1413
|
self._z_idx = ufh_idx
|
|
1417
1414
|
|
ramses_rf/entity_base.py
CHANGED
|
@@ -67,8 +67,7 @@ if TYPE_CHECKING:
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
_QOS_TX_LIMIT = 12 # TODO: needs work
|
|
70
|
-
_ID_SLICE = 9
|
|
71
|
-
_SQL_SLICE = 12 # msg_db dst field query 12
|
|
70
|
+
_ID_SLICE = 9
|
|
72
71
|
_SZ_LAST_PKT: Final = "last_msg"
|
|
73
72
|
_SZ_NEXT_DUE: Final = "next_due"
|
|
74
73
|
_SZ_TIMEOUT: Final = "timeout"
|
|
@@ -225,7 +224,10 @@ class _MessageDB(_Entity):
|
|
|
225
224
|
|
|
226
225
|
# As of 0.52.1 we use SQLite MessageIndex, see ramses_rf/database.py
|
|
227
226
|
# _msgz_ (nested) was only used in this module. Note:
|
|
228
|
-
# _msgz (now rebuilt from _msgs) also used in:
|
|
227
|
+
# _msgz (now rebuilt from _msgs) is also used in:
|
|
228
|
+
# - client.py: for code in device._msgz.values()
|
|
229
|
+
# - base.py: Code._1060 in self._msgz
|
|
230
|
+
# [x] device.heat (no longer used)
|
|
229
231
|
|
|
230
232
|
def _handle_msg(self, msg: Message) -> None:
|
|
231
233
|
"""Store a msg in the DBs.
|
|
@@ -245,13 +247,14 @@ class _MessageDB(_Entity):
|
|
|
245
247
|
|
|
246
248
|
if self._gwy.msg_db: # central SQLite MessageIndex
|
|
247
249
|
_LOGGER.debug(
|
|
248
|
-
"For %s (
|
|
250
|
+
"For %s (_z_id %s) add msg %s, src %s, dst %s to msg_db.",
|
|
249
251
|
self.id,
|
|
250
252
|
self._z_id,
|
|
253
|
+
msg,
|
|
251
254
|
msg.src,
|
|
252
255
|
msg.dst,
|
|
253
256
|
)
|
|
254
|
-
debug_code: Code = Code._3150
|
|
257
|
+
debug_code: Code = Code._3150 # for debugging only log these, pick your own
|
|
255
258
|
if msg.code == debug_code and msg.src.id.startswith("01:"):
|
|
256
259
|
_LOGGER.debug(
|
|
257
260
|
"Added msg from %s with code %s to _gwy.msg_db. hdr=%s",
|
|
@@ -291,8 +294,9 @@ class _MessageDB(_Entity):
|
|
|
291
294
|
|
|
292
295
|
@property
|
|
293
296
|
def _msg_list(self) -> list[Message]:
|
|
294
|
-
"""Return a flattened list of all messages logged on device."""
|
|
297
|
+
"""Return a flattened list of all messages logged on this device."""
|
|
295
298
|
# (only) used in gateway.py#get_state() and in tests/tests/test_eavesdrop_schema.py
|
|
299
|
+
# TODO remove _msg_list Q1 2026
|
|
296
300
|
if self._gwy.msg_db:
|
|
297
301
|
msg_list_qry: list[Message] = []
|
|
298
302
|
code_list = self._msg_dev_qry()
|
|
@@ -302,15 +306,22 @@ class _MessageDB(_Entity):
|
|
|
302
306
|
# safeguard against lookup failures ("sim" packets?)
|
|
303
307
|
msg_list_qry.append(self._msgs[c])
|
|
304
308
|
else:
|
|
309
|
+
# evohome has these errors
|
|
310
|
+
# _msg_list could not fetch self._msgs[7FFF] for 18:072981 (_z_id 18:072981)
|
|
305
311
|
_LOGGER.debug(
|
|
306
|
-
"_msg_list could not fetch self._msgs[%s] for %s (
|
|
312
|
+
"_msg_list could not fetch self._msgs[%s] for %s (_z_id %s)",
|
|
307
313
|
c,
|
|
308
314
|
self.id,
|
|
309
315
|
self._z_id,
|
|
310
316
|
)
|
|
311
317
|
return msg_list_qry
|
|
312
318
|
# else create from legacy nested dict
|
|
313
|
-
return [
|
|
319
|
+
return [
|
|
320
|
+
msg
|
|
321
|
+
for code in self._msgz.values()
|
|
322
|
+
for ctx in code.values()
|
|
323
|
+
for msg in ctx.values()
|
|
324
|
+
]
|
|
314
325
|
|
|
315
326
|
def _add_record(
|
|
316
327
|
self, address: Address, code: Code | None = None, verb: str = " I"
|
|
@@ -422,13 +433,13 @@ class _MessageDB(_Entity):
|
|
|
422
433
|
**kwargs: Any,
|
|
423
434
|
) -> dict | list | None:
|
|
424
435
|
"""
|
|
425
|
-
Query the message dict or the SQLite
|
|
436
|
+
Query the _msgz message dict or the SQLite MessageIndex for the most recent
|
|
426
437
|
key: value pairs(s) for a given code.
|
|
427
438
|
|
|
428
439
|
:param code: filter messages by Code or a tuple of Codes, optional
|
|
429
440
|
:param verb: filter on I, RQ, RP, optional, only with a single Code
|
|
430
441
|
:param key: value keyword to retrieve, not together with verb RQ
|
|
431
|
-
:param kwargs:
|
|
442
|
+
:param kwargs: extra filter, e.g. zone_idx='01'
|
|
432
443
|
:return: a dict containing key: value pairs, or a list of those
|
|
433
444
|
"""
|
|
434
445
|
assert not isinstance(code, tuple) or verb is None, (
|
|
@@ -444,7 +455,9 @@ class _MessageDB(_Entity):
|
|
|
444
455
|
key = None
|
|
445
456
|
try:
|
|
446
457
|
if self._gwy.msg_db: # central SQLite MessageIndex, use verb= kwarg
|
|
447
|
-
code = Code(
|
|
458
|
+
code = Code(
|
|
459
|
+
self._msg_qry_by_code_key(code, key, **kwargs, verb=verb)
|
|
460
|
+
)
|
|
448
461
|
msg = self._msgs.get(code)
|
|
449
462
|
else: # deprecated lookup in nested _msgz
|
|
450
463
|
msgs = self._msgz[code][verb]
|
|
@@ -456,7 +469,9 @@ class _MessageDB(_Entity):
|
|
|
456
469
|
msgs = [m for m in self._msgs.values() if m.code in code]
|
|
457
470
|
msg = max(msgs) if msgs else None
|
|
458
471
|
# return highest = latest? value found in code:value pairs
|
|
459
|
-
else:
|
|
472
|
+
else: # single Code
|
|
473
|
+
# for Zones, this doesn't work, returns first result = often wrong
|
|
474
|
+
# TODO fix in _msg_qry_by_code_key()
|
|
460
475
|
msg = self._msgs.get(code)
|
|
461
476
|
|
|
462
477
|
return self._msg_value_msg(msg, key=key, **kwargs)
|
|
@@ -511,7 +526,7 @@ class _MessageDB(_Entity):
|
|
|
511
526
|
or (idx == SZ_DOMAIN_ID)
|
|
512
527
|
), (
|
|
513
528
|
f"full dict:{msg_dict}, payload:{msg.payload} < Coding error: key='{idx}', val='{val}'"
|
|
514
|
-
) # should not be there
|
|
529
|
+
) # should not be there
|
|
515
530
|
|
|
516
531
|
if (
|
|
517
532
|
key == "*" or not key
|
|
@@ -529,21 +544,53 @@ class _MessageDB(_Entity):
|
|
|
529
544
|
"""
|
|
530
545
|
Retrieve from the MessageIndex a list of Code keys involving this device.
|
|
531
546
|
|
|
532
|
-
:return: list of Codes or empty list when query returned empty
|
|
547
|
+
:return: list of Codes or an empty list when the query returned empty
|
|
533
548
|
"""
|
|
549
|
+
|
|
534
550
|
if self._gwy.msg_db:
|
|
535
551
|
# SQLite query on MessageIndex
|
|
536
|
-
sql = """
|
|
537
|
-
SELECT code from messages WHERE verb in (' I', 'RP')
|
|
538
|
-
AND (src = ? OR dst = ?)
|
|
539
|
-
"""
|
|
540
552
|
res: list[Code] = []
|
|
541
553
|
|
|
554
|
+
if len(self.id) == 9:
|
|
555
|
+
# fetch a ctl's message codes (add all its children?)
|
|
556
|
+
sql = """
|
|
557
|
+
SELECT code from messages WHERE
|
|
558
|
+
verb in (' I', 'RP')
|
|
559
|
+
AND (src = ? OR dst = ?)
|
|
560
|
+
AND ctx LIKE ?
|
|
561
|
+
"""
|
|
562
|
+
_ctx_qry = "%"
|
|
563
|
+
|
|
564
|
+
elif self.id[_ID_SLICE:] == "_HW":
|
|
565
|
+
# fetch a DHW entity's message codes
|
|
566
|
+
sql = """
|
|
567
|
+
SELECT code from messages WHERE
|
|
568
|
+
verb in (' I', 'RP')
|
|
569
|
+
AND (src = ? OR dst = ?)
|
|
570
|
+
AND (ctx IN ('FC', 'FA', 'F9', 'FA') OR plk LIKE ?)
|
|
571
|
+
"""
|
|
572
|
+
_ctx_qry = "%dhw_idx%"
|
|
573
|
+
|
|
574
|
+
else:
|
|
575
|
+
# fetch a zone's message codes
|
|
576
|
+
sql = """
|
|
577
|
+
SELECT code from messages WHERE
|
|
578
|
+
verb in (' I', 'RP')
|
|
579
|
+
AND (src = ? OR dst = ?)
|
|
580
|
+
AND ctx LIKE ?
|
|
581
|
+
"""
|
|
582
|
+
_ctx_qry = f"%{self.id[_ID_SLICE + 1 :]}%"
|
|
583
|
+
|
|
542
584
|
for rec in self._gwy.msg_db.qry_field(
|
|
543
|
-
sql, (self.id[:
|
|
585
|
+
sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE], _ctx_qry)
|
|
544
586
|
):
|
|
545
|
-
_LOGGER.debug(
|
|
546
|
-
|
|
587
|
+
_LOGGER.debug(
|
|
588
|
+
"Fetched from index: %s for %s (_z_id %s)",
|
|
589
|
+
rec[0],
|
|
590
|
+
self.id,
|
|
591
|
+
self._z_id,
|
|
592
|
+
)
|
|
593
|
+
# Example: "Fetched from index: code 1FD4 for 01:123456 (_z_id 01)"
|
|
547
594
|
res.append(Code(str(rec[0])))
|
|
548
595
|
return res
|
|
549
596
|
else:
|
|
@@ -566,32 +613,48 @@ class _MessageDB(_Entity):
|
|
|
566
613
|
:return: Code of most recent query result message or None when query returned empty
|
|
567
614
|
"""
|
|
568
615
|
if self._gwy.msg_db:
|
|
569
|
-
code_qry: str = ""
|
|
616
|
+
code_qry: str = "= "
|
|
570
617
|
if code is None:
|
|
571
|
-
code_qry = "
|
|
618
|
+
code_qry = "LIKE '%'" # wildcard
|
|
572
619
|
elif isinstance(code, tuple):
|
|
573
620
|
for cd in code:
|
|
574
621
|
code_qry += f"'{str(cd)}' OR code = '"
|
|
575
622
|
code_qry = code_qry[:-13] # trim last OR
|
|
576
623
|
else:
|
|
577
|
-
code_qry
|
|
578
|
-
key = "*" if key is None else f"%{key}%"
|
|
624
|
+
code_qry += str(code)
|
|
579
625
|
if kwargs["verb"] and kwargs["verb"] in (" I", "RP"):
|
|
580
626
|
vb = f"('{str(kwargs['verb'])}',)"
|
|
581
627
|
else:
|
|
582
628
|
vb = "(' I', 'RP',)"
|
|
629
|
+
ctx_qry = "%"
|
|
630
|
+
if kwargs["zone_idx"]:
|
|
631
|
+
ctx_qry = f"%{kwargs['zone_idx']}%"
|
|
632
|
+
elif kwargs["dhw_idx"]: # DHW
|
|
633
|
+
ctx_qry = f"%{kwargs['dhw_idx']}%"
|
|
634
|
+
key_qry = "%" if key is None else f"%{key}%"
|
|
635
|
+
|
|
583
636
|
# SQLite query on MessageIndex
|
|
584
637
|
sql = """
|
|
585
|
-
SELECT dtm, code from messages WHERE
|
|
638
|
+
SELECT dtm, code from messages WHERE
|
|
639
|
+
verb in ?
|
|
586
640
|
AND (src = ? OR dst = ?)
|
|
587
|
-
AND (code
|
|
641
|
+
AND (code ?)
|
|
642
|
+
AND (ctx LIKE ?)
|
|
588
643
|
AND (plk LIKE ?)
|
|
589
644
|
"""
|
|
590
645
|
latest: dt = dt(0, 0, 0)
|
|
591
646
|
res = None
|
|
592
647
|
|
|
593
648
|
for rec in self._gwy.msg_db.qry_field(
|
|
594
|
-
sql,
|
|
649
|
+
sql,
|
|
650
|
+
(
|
|
651
|
+
vb,
|
|
652
|
+
self.id[:_ID_SLICE],
|
|
653
|
+
self.id[:_ID_SLICE],
|
|
654
|
+
code_qry,
|
|
655
|
+
ctx_qry,
|
|
656
|
+
key_qry,
|
|
657
|
+
),
|
|
595
658
|
):
|
|
596
659
|
_LOGGER.debug(
|
|
597
660
|
"_msg_qry_by_code_key fetched rec: %s, code: %s", rec, code_qry
|
|
@@ -654,7 +717,7 @@ class _MessageDB(_Entity):
|
|
|
654
717
|
# """SELECT code from messages WHERE verb in (' I', 'RP') AND (src = ? OR dst = ?)
|
|
655
718
|
# AND (code = '31DA' OR ...) AND (plk LIKE '%{SZ_FAN_INFO}%' OR ...)""" = 2 params
|
|
656
719
|
for rec in self._gwy.msg_db.qry_field(
|
|
657
|
-
sql, (self.id[:
|
|
720
|
+
sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE])
|
|
658
721
|
):
|
|
659
722
|
_pl = self._msgs[Code(rec[0])].payload
|
|
660
723
|
# add payload dict to res(ults)
|
|
@@ -694,35 +757,61 @@ class _MessageDB(_Entity):
|
|
|
694
757
|
# _LOGGER.warning("Missing MessageIndex")
|
|
695
758
|
# raise NotImplementedError
|
|
696
759
|
|
|
697
|
-
if self.id[:3] == "18:": # HGI, confirm this is correct, tests suggest so
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
sql = """
|
|
701
|
-
SELECT dtm from messages WHERE verb in (' I', 'RP') AND (src = ? OR dst = ?)
|
|
702
|
-
"""
|
|
760
|
+
# if self.id[:3] == "18:": # HGI, confirm this is correct, tests suggest so
|
|
761
|
+
# return {}
|
|
703
762
|
|
|
704
|
-
#
|
|
763
|
+
# a routine to debug dict creation, see test_systems.py:
|
|
705
764
|
# print(f"Create _msgs for {self.id}:")
|
|
706
765
|
# results = self._gwy.msg_db._cu.execute("SELECT dtm, src, code from messages WHERE verb in (' I', 'RP') and code is '3150'")
|
|
707
766
|
# for r in results:
|
|
708
767
|
# print(r)
|
|
709
768
|
|
|
710
|
-
|
|
769
|
+
if len(self.id) == 9:
|
|
770
|
+
# fetch a ctl's message dtms (add all its children?)
|
|
771
|
+
sql = """
|
|
772
|
+
SELECT dtm from messages WHERE
|
|
773
|
+
verb in (' I', 'RP')
|
|
774
|
+
AND (src = ? OR dst = ?)
|
|
775
|
+
AND ctx LIKE ?
|
|
776
|
+
"""
|
|
777
|
+
_ctx_qry = "%"
|
|
778
|
+
|
|
779
|
+
elif self.id[_ID_SLICE:] == "_HW":
|
|
780
|
+
# fetch a DHW entity's message dtms
|
|
781
|
+
sql = """
|
|
782
|
+
SELECT dtm from messages WHERE
|
|
783
|
+
verb in (' I', 'RP')
|
|
784
|
+
AND (src = ? OR dst = ?)
|
|
785
|
+
AND (ctx IN ('FC', 'FA', 'F9', 'FA') OR plk LIKE ?)
|
|
786
|
+
"""
|
|
787
|
+
_ctx_qry = "%dhw_idx%"
|
|
788
|
+
# TODO add Children messages? self.ctl.dhw
|
|
789
|
+
else:
|
|
790
|
+
# fetch a zone's message dtms
|
|
791
|
+
sql = """
|
|
792
|
+
SELECT dtm from messages WHERE
|
|
793
|
+
verb in (' I', 'RP')
|
|
794
|
+
AND (src = ? OR dst = ?)
|
|
795
|
+
AND ctx LIKE ?
|
|
796
|
+
"""
|
|
797
|
+
_ctx_qry = f"%{self.id[_ID_SLICE + 1 :]}%"
|
|
798
|
+
|
|
799
|
+
_msg_dict = { # since 0.52.3 use ctx (context) instead of just the address
|
|
711
800
|
m.code: m
|
|
712
801
|
for m in self._gwy.msg_db.qry(
|
|
713
|
-
sql, (self.id[:
|
|
714
|
-
) # e.g. 01:123456_HW
|
|
802
|
+
sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE], _ctx_qry)
|
|
803
|
+
) # e.g. 01:123456_HW, 01:123456_02 (Zone)
|
|
715
804
|
}
|
|
716
805
|
# if CTL, remove 3150, 3220 heat_demand, both are only stored on children
|
|
717
806
|
# HACK
|
|
718
|
-
if self.id[:3] == "01:" and self._SLUG == "CTL":
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
807
|
+
# if self.id[:3] == "01:" and self._SLUG == "CTL":
|
|
808
|
+
# with next ON: 2 errors , both 1x UFC, 1x CTR
|
|
809
|
+
# with next OFF: 4 errors, all CTR
|
|
810
|
+
# if Code._3150 in _msg_dict: # Note: CTL can send a 3150 (see heat_ufc_00)
|
|
811
|
+
# _msg_dict.pop(Code._3150) # keep, prefer to have 2 extra instead of missing 1
|
|
812
|
+
# if Code._3220 in _msg_dict:
|
|
813
|
+
# _msg_dict.pop(Code._3220)
|
|
814
|
+
# _LOGGER.debug(f"Removed 3150/3220 from %s._msgs dict", self.id)
|
|
726
815
|
return _msg_dict
|
|
727
816
|
|
|
728
817
|
@property
|
|
@@ -786,7 +875,7 @@ class _Discovery(_MessageDB):
|
|
|
786
875
|
code: CODES_SCHEMA[code][SZ_NAME]
|
|
787
876
|
for code in sorted(
|
|
788
877
|
self._gwy.msg_db.get_rp_codes(
|
|
789
|
-
(self.id[:
|
|
878
|
+
(self.id[:_ID_SLICE], self.id[:_ID_SLICE])
|
|
790
879
|
)
|
|
791
880
|
)
|
|
792
881
|
if self._is_not_deprecated_cmd(code)
|
|
@@ -808,7 +897,6 @@ class _Discovery(_MessageDB):
|
|
|
808
897
|
# return f"{data_id:02X}" # type: ignore[return-value]
|
|
809
898
|
|
|
810
899
|
res: list[str] = []
|
|
811
|
-
# look for the "sim" OT 3220 record initially added in OtbGateway.init
|
|
812
900
|
if self._gwy.msg_db:
|
|
813
901
|
# SQLite query for ctx field on MessageIndex
|
|
814
902
|
sql = """
|
|
@@ -818,7 +906,7 @@ class _Discovery(_MessageDB):
|
|
|
818
906
|
AND (src = ? OR dst = ?)
|
|
819
907
|
"""
|
|
820
908
|
for rec in self._gwy.msg_db.qry_field(
|
|
821
|
-
sql, (self.id[:
|
|
909
|
+
sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE])
|
|
822
910
|
):
|
|
823
911
|
_LOGGER.debug("Fetched OT ctx from index: %s", rec[0])
|
|
824
912
|
res.append(rec[0])
|
|
@@ -941,18 +1029,24 @@ class _Discovery(_MessageDB):
|
|
|
941
1029
|
sql = """
|
|
942
1030
|
SELECT dtm from messages WHERE
|
|
943
1031
|
code = ?
|
|
944
|
-
verb = ' I'
|
|
1032
|
+
AND verb = ' I'
|
|
945
1033
|
AND ctx = 'True'
|
|
946
1034
|
AND (src = ? OR dst = ?)
|
|
947
1035
|
"""
|
|
948
|
-
|
|
1036
|
+
res = self._gwy.msg_db.qry(
|
|
949
1037
|
sql,
|
|
950
1038
|
(
|
|
951
1039
|
task[_SZ_COMMAND].code,
|
|
952
|
-
self.tcs.id[:_ID_SLICE],
|
|
953
|
-
self.tcs.id[:_ID_SLICE],
|
|
1040
|
+
self.tcs.id[:_ID_SLICE],
|
|
1041
|
+
self.tcs.id[:_ID_SLICE],
|
|
954
1042
|
),
|
|
955
|
-
)
|
|
1043
|
+
)
|
|
1044
|
+
if len(res) > 0:
|
|
1045
|
+
msgs += res[0] # expect 1 Message in returned tuple
|
|
1046
|
+
else:
|
|
1047
|
+
_LOGGER.debug(
|
|
1048
|
+
f"No msg found for hdr {hdr}, tesk code {task[_SZ_COMMAND].code}"
|
|
1049
|
+
)
|
|
956
1050
|
else: # TODO(eb) remove next Q1 2026
|
|
957
1051
|
msgs += [self.tcs._msgz[task[_SZ_COMMAND].code][I_][True]]
|
|
958
1052
|
# raise NotImplementedError
|
|
@@ -1091,6 +1185,7 @@ class Parent(Entity): # A System, Zone, DhwZone or a UfhController
|
|
|
1091
1185
|
(incl. the DHW Zone), and also any UFH controllers.
|
|
1092
1186
|
|
|
1093
1187
|
For a heating Zone, children are limited to a sensor, and a number of actuators.
|
|
1188
|
+
|
|
1094
1189
|
For the DHW Zone, the children are limited to a sensor, a DHW valve, and/or a
|
|
1095
1190
|
heating valve.
|
|
1096
1191
|
|
ramses_rf/gateway.py
CHANGED
|
@@ -258,14 +258,6 @@ class Gateway(Engine):
|
|
|
258
258
|
# return True
|
|
259
259
|
return include_expired or not msg._expired
|
|
260
260
|
|
|
261
|
-
msgs = [m for device in self.devices for m in device._msg_list]
|
|
262
|
-
|
|
263
|
-
for system in self.systems:
|
|
264
|
-
msgs.extend(list(system._msgs.values()))
|
|
265
|
-
msgs.extend([m for z in system.zones for m in z._msgs.values()])
|
|
266
|
-
# msgs.extend([m for z in system.dhw for m in z._msgs.values()]) # TODO: DHW
|
|
267
|
-
# Related to/Fixes ramses_cc Issue 249 non-existing via-device _HW ?
|
|
268
|
-
|
|
269
261
|
if self.msg_db:
|
|
270
262
|
pkts = {
|
|
271
263
|
f"{repr(msg._pkt)[:26]}": f"{repr(msg._pkt)[27:]}"
|
|
@@ -273,12 +265,20 @@ class Gateway(Engine):
|
|
|
273
265
|
if wanted_msg(msg, include_expired=include_expired)
|
|
274
266
|
}
|
|
275
267
|
else: # deprecated, to be removed in Q1 2026
|
|
276
|
-
|
|
268
|
+
msgs = [m for device in self.devices for m in device._msg_list]
|
|
269
|
+
# add systems._msgs and zones._msgs
|
|
270
|
+
for system in self.systems:
|
|
271
|
+
msgs.extend(list(system._msgs.values()))
|
|
272
|
+
msgs.extend([m for z in system.zones for m in z._msgs.values()])
|
|
273
|
+
# msgs.extend([m for z in system.dhw for m in z._msgs.values()]) # TODO: DHW
|
|
274
|
+
# Related to/Fixes ramses_cc Issue 249 non-existing via-device _HW ?
|
|
275
|
+
|
|
277
276
|
pkts = { # BUG: assumes pkts have unique dtms: may be untrue for contrived logs
|
|
278
277
|
f"{repr(msg._pkt)[:26]}": f"{repr(msg._pkt)[27:]}"
|
|
279
278
|
for msg in msgs
|
|
280
279
|
if wanted_msg(msg, include_expired=include_expired)
|
|
281
280
|
}
|
|
281
|
+
# _LOGGER.warning("Missing MessageIndex")
|
|
282
282
|
|
|
283
283
|
self._resume()
|
|
284
284
|
|
ramses_rf/system/heat.py
CHANGED
|
@@ -532,7 +532,7 @@ class MultiZone(SystemBase): # 0005 (+/- 000C?)
|
|
|
532
532
|
schema = shrink(SCH_TCS_ZONES_ZON(schema))
|
|
533
533
|
|
|
534
534
|
zon: Zone = self.zone_by_idx.get(zone_idx) # type: ignore[assignment]
|
|
535
|
-
if zon is None:
|
|
535
|
+
if zon is None: # not found in tcs, create it
|
|
536
536
|
zon = zone_factory(self, zone_idx, msg=msg, **schema) # type: ignore[unreachable]
|
|
537
537
|
self.zone_by_idx[zon.idx] = zon
|
|
538
538
|
self.zones.append(zon)
|
ramses_rf/system/zones.py
CHANGED
|
@@ -233,7 +233,7 @@ class DhwZone(ZoneSchedule): # CS92A
|
|
|
233
233
|
# def eavesdrop_dhw_sensor(this: Message, *, prev: Message | None = None) -> None:
|
|
234
234
|
# """Eavesdrop packets, or pairs of packets, to maintain the system state.
|
|
235
235
|
|
|
236
|
-
# There are only 2 ways to
|
|
236
|
+
# There are only 2 ways to find a controller's DHW sensor:
|
|
237
237
|
# 1. The 10A0 RQ/RP *from/to a 07:* (1x/4h) - reliable
|
|
238
238
|
# 2. Use sensor temp matching - non-deterministic
|
|
239
239
|
|
ramses_rf/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ramses_rf
|
|
3
|
-
Version: 0.52.
|
|
3
|
+
Version: 0.52.4
|
|
4
4
|
Summary: A stateful RAMSES-II protocol decoder & analyser.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ramses-rf/ramses_rf
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/ramses-rf/ramses_rf/issues
|
|
@@ -22,6 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|

|
|
23
23
|

|
|
24
24
|

|
|
25
|
+
[](https://github.com/ramses-rf/ramses_rf/actions/workflows/check-cov.yml)
|
|
25
26
|
|
|
26
27
|
## Overview
|
|
27
28
|
|
|
@@ -5,51 +5,51 @@ ramses_cli/discovery.py,sha256=MWoahBnAAVzfK2S7EDLsY2WYqN_ZK9L-lktrj8_4cb0,12978
|
|
|
5
5
|
ramses_cli/utils/cat_slow.py,sha256=AhUpM5gnegCitNKU-JGHn-DrRzSi-49ZR1Qw6lxe_t8,607
|
|
6
6
|
ramses_cli/utils/convert.py,sha256=D_YiCyX5na9pgC-_NhBlW9N1dgRKUK-uLtLBfofjzZM,1804
|
|
7
7
|
ramses_rf/__init__.py,sha256=vp2TyFGqc1fGQHsevhmaw0QEmSSCnZx7fqizKiEwHtw,1245
|
|
8
|
-
ramses_rf/binding_fsm.py,sha256=
|
|
8
|
+
ramses_rf/binding_fsm.py,sha256=fuqvcc9YW-wr8SPH8zadpPqrHAvzl_eeWF-IBtlLppY,26632
|
|
9
9
|
ramses_rf/const.py,sha256=L3z31CZ-xqno6oZp_h-67CB_5tDDqTwSWXsqRtsjMcs,5460
|
|
10
|
-
ramses_rf/database.py,sha256=
|
|
10
|
+
ramses_rf/database.py,sha256=2sHiBhI5121HdX-IMquY3h5qQzDljeGWpQ9x6U4duEo,20408
|
|
11
11
|
ramses_rf/dispatcher.py,sha256=YjEU-QrBLo9IfoEhJo2ikg_FxOaMYoWvzelr9Vi-JZ8,11398
|
|
12
|
-
ramses_rf/entity_base.py,sha256=
|
|
12
|
+
ramses_rf/entity_base.py,sha256=LifFLcxI867j4DmsaNSHsVl2fUEPkXpJdz4b7ZFpupo,58929
|
|
13
13
|
ramses_rf/exceptions.py,sha256=mt_T7irqHSDKir6KLaf6oDglUIdrw0S40JbOrWJk5jc,3657
|
|
14
|
-
ramses_rf/gateway.py,sha256=
|
|
14
|
+
ramses_rf/gateway.py,sha256=c88_HCAEBYPaNETMC0IUK1HU9QuEOnCGnK-5wze08_s,21289
|
|
15
15
|
ramses_rf/helpers.py,sha256=TNk_QkpIOB3alOp1sqnA9LOzi4fuDCeapNlW3zTzNas,4250
|
|
16
16
|
ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
ramses_rf/schemas.py,sha256=jvpx0hZiMhaogyxbxnrbxS7m2GekKK4DFfVTNs7h-WQ,13476
|
|
18
|
-
ramses_rf/version.py,sha256=
|
|
18
|
+
ramses_rf/version.py,sha256=9IsbhdNecTxEJDHwnSjdsa0r2hpABWxeNcD6LfKKNs0,125
|
|
19
19
|
ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
|
|
20
20
|
ramses_rf/device/base.py,sha256=Yx0LZwMEb49naY8FolZ8HEBFb6XCPQBTVN_TWyO2nKg,17777
|
|
21
|
-
ramses_rf/device/heat.py,sha256=
|
|
21
|
+
ramses_rf/device/heat.py,sha256=I9_NlB_cOxCfCb3Zx88hY6wd7yIrknzo6msrWxXeeEs,54545
|
|
22
22
|
ramses_rf/device/hvac.py,sha256=gdpVACUvtq6ERMI0mwRhqIJtKYEmybzJA2NeyN1ELrs,48431
|
|
23
23
|
ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
|
|
24
24
|
ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
|
|
25
|
-
ramses_rf/system/heat.py,sha256=
|
|
25
|
+
ramses_rf/system/heat.py,sha256=qQmzgmyHy2x87gHAstn0ee7ZVVOq-GJIfDxCrC-6gFU,39254
|
|
26
26
|
ramses_rf/system/schedule.py,sha256=Ts6tdZPTQLV5NkgwA73tPa5QUsnZNIIuYoKC-8VsXDk,18808
|
|
27
|
-
ramses_rf/system/zones.py,sha256=
|
|
27
|
+
ramses_rf/system/zones.py,sha256=YN_HAbeaa2YioUOjafEpp-0IHmIwmKNSK_77pPcjtns,36072
|
|
28
28
|
ramses_tx/__init__.py,sha256=sqnjM7pUGJDmec6igTtKViSB8FLX49B5gwhAmcY9ERY,3596
|
|
29
29
|
ramses_tx/address.py,sha256=F5ZE-EbPNNom1fW9XXUILvD7DYSMBxNJvsHVliT5gjw,8452
|
|
30
30
|
ramses_tx/command.py,sha256=tqVECwd_QokEcRv2MSk7TUU4JSBzCZcJh1eQ0jIGgoY,125122
|
|
31
|
-
ramses_tx/const.py,sha256=
|
|
31
|
+
ramses_tx/const.py,sha256=AMwHitDq115rB24f3fzclNGC4ArMW16DbqiFWQc0U5o,30306
|
|
32
32
|
ramses_tx/exceptions.py,sha256=FJSU9YkvpKjs3yeTqUJX1o3TPFSe_B01gRGIh9b3PNc,2632
|
|
33
33
|
ramses_tx/fingerprints.py,sha256=nfftA1E62HQnb-eLt2EqjEi_la0DAoT0wt-PtTMie0s,11974
|
|
34
34
|
ramses_tx/frame.py,sha256=GzNsXr15YLeidJYGtk_xPqsZQh4ehDDlUCtT6rTDhT8,22046
|
|
35
|
-
ramses_tx/gateway.py,sha256=
|
|
35
|
+
ramses_tx/gateway.py,sha256=Fl6EqAUU2DnLOiA2_87sS7VPBEyxA1a_ICCmg55WMMA,11494
|
|
36
36
|
ramses_tx/helpers.py,sha256=J4OCRckp3JshGQTvvqEskFjB1hPS7uA_opVsuIqmZds,32915
|
|
37
|
-
ramses_tx/logger.py,sha256=
|
|
37
|
+
ramses_tx/logger.py,sha256=1iKRHKUaqHqGd76CkE_6mCVR0sYODtxshRRwfY61fTk,10426
|
|
38
38
|
ramses_tx/message.py,sha256=-moQ8v3HVlNSl-x3U0DDfDcj8WQ7vLqclMNxsohbmnw,13449
|
|
39
39
|
ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
|
|
40
40
|
ramses_tx/packet.py,sha256=_qHiPFWpQpKueZOgf1jJ93Y09iZjo3LZWStLglVkXg4,7370
|
|
41
|
-
ramses_tx/parsers.py,sha256=
|
|
42
|
-
ramses_tx/protocol.py,sha256=
|
|
41
|
+
ramses_tx/parsers.py,sha256=xkffUleahvY6uPb34pcBDYnJ7QNQUzRuaED7wOVeEsE,113466
|
|
42
|
+
ramses_tx/protocol.py,sha256=zjOz0TCzMmbUiLouLFXKRL7u9a6p9YsCkLYDVC5vG-Y,28867
|
|
43
43
|
ramses_tx/protocol_fsm.py,sha256=ZKtehCr_4TaDdfdlfidFLJaOVTYtaEq5h4tLqNIhb9s,26827
|
|
44
44
|
ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
-
ramses_tx/ramses.py,sha256=
|
|
46
|
-
ramses_tx/schemas.py,sha256=
|
|
47
|
-
ramses_tx/transport.py,sha256=
|
|
45
|
+
ramses_tx/ramses.py,sha256=vp748Tf_a-56OMM8CWDA2ZktRfTuj0QVyPRcnsOstSM,53983
|
|
46
|
+
ramses_tx/schemas.py,sha256=bqKW_V0bR6VbBD8ZQiBExNtVdXs0fryVKe3GEhupgIo,13424
|
|
47
|
+
ramses_tx/transport.py,sha256=8U6Hskle_0tdI4WViJT0e9Bl6-OeK1uKBvTQZvHtqyY,58923
|
|
48
48
|
ramses_tx/typed_dicts.py,sha256=w-0V5t2Q3GiNUOrRAWiW9GtSwbta_7luME6GfIb1zhI,10869
|
|
49
49
|
ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
|
|
50
|
-
ramses_tx/version.py,sha256=
|
|
51
|
-
ramses_rf-0.52.
|
|
52
|
-
ramses_rf-0.52.
|
|
53
|
-
ramses_rf-0.52.
|
|
54
|
-
ramses_rf-0.52.
|
|
55
|
-
ramses_rf-0.52.
|
|
50
|
+
ramses_tx/version.py,sha256=UqvJQixbILsFY_XbawFKzLgN4gL1DKFAtz6JsZAXk4I,123
|
|
51
|
+
ramses_rf-0.52.4.dist-info/METADATA,sha256=XTTcXOovtegIupIA9b5wQPCfRjUeuqtHRMOKKd_5bPo,4179
|
|
52
|
+
ramses_rf-0.52.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
53
|
+
ramses_rf-0.52.4.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
|
|
54
|
+
ramses_rf-0.52.4.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
|
|
55
|
+
ramses_rf-0.52.4.dist-info/RECORD,,
|
ramses_tx/const.py
CHANGED
|
@@ -440,7 +440,7 @@ DEV_TYPE_MAP = attr_dict_factory(
|
|
|
440
440
|
), # CH/DHW devices instead of HVAC/other
|
|
441
441
|
"HEAT_ZONE_SENSORS": ("00", "01", "03", "04", "12", "22", "34"),
|
|
442
442
|
"HEAT_ZONE_ACTUATORS": ("00", "02", "04", "13"),
|
|
443
|
-
"THM_DEVICES": ("03", "12", "22", "34"),
|
|
443
|
+
"THM_DEVICES": ("03", "12", "21", "22", "34"),
|
|
444
444
|
"TRV_DEVICES": ("00", "04"),
|
|
445
445
|
"CONTROLLERS": ("01", "02", "12", "22", "23", "34"), # potentially controllers
|
|
446
446
|
"PROMOTABLE_SLUGS": (DevType.DEV, DevType.HEA, DevType.HVC),
|
ramses_tx/gateway.py
CHANGED
|
@@ -34,6 +34,7 @@ from .schemas import (
|
|
|
34
34
|
SZ_DISABLE_QOS,
|
|
35
35
|
SZ_DISABLE_SENDING,
|
|
36
36
|
SZ_ENFORCE_KNOWN_LIST,
|
|
37
|
+
SZ_LOG_ALL_MQTT,
|
|
37
38
|
SZ_PACKET_LOG,
|
|
38
39
|
SZ_PORT_CONFIG,
|
|
39
40
|
SZ_PORT_NAME,
|
|
@@ -114,6 +115,7 @@ class Engine:
|
|
|
114
115
|
self._exclude,
|
|
115
116
|
)
|
|
116
117
|
self._sqlite_index = kwargs.pop(SZ_SQLITE_INDEX, False) # default True?
|
|
118
|
+
self._log_all_mqtt = kwargs.pop(SZ_LOG_ALL_MQTT, False)
|
|
117
119
|
self._kwargs: dict[str, Any] = kwargs # HACK
|
|
118
120
|
|
|
119
121
|
self._engine_lock = Lock() # FIXME: threading lock, or asyncio lock?
|
|
@@ -197,6 +199,7 @@ class Engine:
|
|
|
197
199
|
self._protocol,
|
|
198
200
|
disable_sending=self._disable_sending,
|
|
199
201
|
loop=self._loop,
|
|
202
|
+
log_all=self._log_all_mqtt,
|
|
200
203
|
**pkt_source,
|
|
201
204
|
**self._kwargs, # HACK: odd/misc params, e.g. comms_params
|
|
202
205
|
)
|
ramses_tx/logger.py
CHANGED
|
@@ -162,6 +162,14 @@ class StdOutFilter(logging.Filter): # record.levelno < logging.WARNING
|
|
|
162
162
|
return record.levelno < logging.WARNING
|
|
163
163
|
|
|
164
164
|
|
|
165
|
+
class BlockMqttFilter(logging.Filter):
|
|
166
|
+
"""Block mqtt traffic"""
|
|
167
|
+
|
|
168
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
169
|
+
"""Return True if the record is to be processed."""
|
|
170
|
+
return not record.getMessage().startswith("mq Rx: ")
|
|
171
|
+
|
|
172
|
+
|
|
165
173
|
class TimedRotatingFileHandler(_TimedRotatingFileHandler):
|
|
166
174
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
167
175
|
super().__init__(*args, **kwargs)
|
ramses_tx/parsers.py
CHANGED
|
@@ -1912,6 +1912,7 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1912
1912
|
"01": (2, centile), # 52 (0.0-25.0) (%)
|
|
1913
1913
|
"0F": (2, hex_to_percent), # xx (0.0-1.0) (%)
|
|
1914
1914
|
"10": (4, counter), # 31 (0-1800) (days)
|
|
1915
|
+
# "20": (4, counter), # unknown data type, uncomment when we have more info
|
|
1915
1916
|
"92": (4, hex_to_temp), # 75 (0-30) (C)
|
|
1916
1917
|
} # TODO: _2411_TYPES.get(payload[8:10], (8, no_op))
|
|
1917
1918
|
|
|
@@ -1938,11 +1939,35 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1938
1939
|
return result
|
|
1939
1940
|
|
|
1940
1941
|
try:
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1942
|
+
# Handle unknown data types gracefully instead of asserting
|
|
1943
|
+
if payload[8:10] not in _2411_DATA_TYPES:
|
|
1944
|
+
warningmsg = (
|
|
1945
|
+
f"{msg!r} < {_INFORM_DEV_MSG} (param {param_id} has unknown data_type: {payload[8:10]}). "
|
|
1946
|
+
f"This parameter uses an unrecognized data type. "
|
|
1947
|
+
f"Please report this packet and any context about what changed on your system."
|
|
1948
|
+
)
|
|
1949
|
+
# Return partial result with raw hex values for unknown data types
|
|
1950
|
+
if msg.len == 9:
|
|
1951
|
+
result |= {
|
|
1952
|
+
"value": f"0x{payload[10:18]}", # Raw hex value
|
|
1953
|
+
"_value_06": payload[6:10],
|
|
1954
|
+
"_unknown_data_type": payload[8:10],
|
|
1955
|
+
}
|
|
1956
|
+
else:
|
|
1957
|
+
result |= {
|
|
1958
|
+
"value": f"0x{payload[10:18]}", # Raw hex value
|
|
1959
|
+
"_value_06": payload[6:10],
|
|
1960
|
+
"min_value": f"0x{payload[18:26]}", # Raw hex value
|
|
1961
|
+
"max_value": f"0x{payload[26:34]}", # Raw hex value
|
|
1962
|
+
"precision": f"0x{payload[34:42]}", # Raw hex value
|
|
1963
|
+
"_value_42": payload[42:],
|
|
1964
|
+
# Flexible footer - capture everything after precision
|
|
1965
|
+
}
|
|
1966
|
+
_LOGGER.warning(f"{warningmsg}. Found values: {result}")
|
|
1967
|
+
return result
|
|
1945
1968
|
|
|
1969
|
+
# Handle known data types normally
|
|
1970
|
+
length, parser = _2411_DATA_TYPES[payload[8:10]]
|
|
1946
1971
|
result |= {
|
|
1947
1972
|
"value": parser(payload[10:18][-length:]), # type: ignore[operator]
|
|
1948
1973
|
"_value_06": payload[6:10],
|
|
@@ -1962,10 +1987,11 @@ def parser_2411(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1962
1987
|
# eg. older Orcon models may have a footer of 2 bytes
|
|
1963
1988
|
}
|
|
1964
1989
|
)
|
|
1965
|
-
except
|
|
1966
|
-
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1967
|
-
# Return partial result for
|
|
1968
|
-
result["value"] = ""
|
|
1990
|
+
except Exception as err:
|
|
1991
|
+
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} (Error parsing 2411: {err})")
|
|
1992
|
+
# Return partial result for any parsing errors
|
|
1993
|
+
result["value"] = f"0x{payload[10:18]}" # Raw hex value
|
|
1994
|
+
result["_parse_error"] = f"Parser error: {err}"
|
|
1969
1995
|
return result
|
|
1970
1996
|
|
|
1971
1997
|
|
|
@@ -2981,7 +3007,11 @@ def parser_unknown(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
2981
3007
|
"_value": hex_to_temp(payload[2:]),
|
|
2982
3008
|
}
|
|
2983
3009
|
|
|
2984
|
-
|
|
3010
|
+
return {
|
|
3011
|
+
"_payload": payload,
|
|
3012
|
+
"_unknown_code": msg.code,
|
|
3013
|
+
"_parse_error": "No parser available for this packet type",
|
|
3014
|
+
}
|
|
2985
3015
|
|
|
2986
3016
|
|
|
2987
3017
|
_PAYLOAD_PARSERS = {
|
|
@@ -2998,8 +3028,23 @@ def parse_payload(msg: Message) -> dict | list[dict]:
|
|
|
2998
3028
|
:return: a dict of key: value pairs or a list of such dicts, e.g. {'temperature': 21.5}
|
|
2999
3029
|
"""
|
|
3000
3030
|
result: dict | list[dict]
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
result
|
|
3031
|
+
try:
|
|
3032
|
+
result = _PAYLOAD_PARSERS.get(msg.code, parser_unknown)(msg._pkt.payload, msg)
|
|
3033
|
+
if isinstance(result, dict) and msg.seqn.isnumeric(): # e.g. 22F1/3
|
|
3034
|
+
result["seqx_num"] = msg.seqn
|
|
3035
|
+
except AssertionError as err:
|
|
3036
|
+
_LOGGER.warning(
|
|
3037
|
+
f"{msg!r} < {_INFORM_DEV_MSG} ({err}). "
|
|
3038
|
+
f"This packet could not be parsed completely. "
|
|
3039
|
+
f"Please report this message and any context about what changed on your system when this occurred."
|
|
3040
|
+
)
|
|
3041
|
+
# Return partial result with error info
|
|
3042
|
+
result = {
|
|
3043
|
+
"_payload": msg._pkt.payload,
|
|
3044
|
+
"_parse_error": f"AssertionError: {err}",
|
|
3045
|
+
"_unknown_code": msg.code,
|
|
3046
|
+
}
|
|
3047
|
+
if isinstance(result, dict) and msg.seqn.isnumeric():
|
|
3048
|
+
result["seqx_num"] = msg.seqn
|
|
3004
3049
|
|
|
3005
3050
|
return result
|
ramses_tx/protocol.py
CHANGED
|
@@ -583,7 +583,7 @@ class PortProtocol(_DeviceIdFilterMixin, _BaseProtocol):
|
|
|
583
583
|
self._context.pause_writing()
|
|
584
584
|
|
|
585
585
|
def resume_writing(self) -> None:
|
|
586
|
-
"""Inform the FSM that the Protocol has been
|
|
586
|
+
"""Inform the FSM that the Protocol has been resumed."""
|
|
587
587
|
|
|
588
588
|
super().resume_writing()
|
|
589
589
|
if self._context:
|
|
@@ -597,7 +597,7 @@ class PortProtocol(_DeviceIdFilterMixin, _BaseProtocol):
|
|
|
597
597
|
self._context.pkt_received(pkt)
|
|
598
598
|
|
|
599
599
|
async def _send_impersonation_alert(self, cmd: Command) -> None:
|
|
600
|
-
"""Send
|
|
600
|
+
"""Send a puzzle packet warning that impersonation is occurring."""
|
|
601
601
|
|
|
602
602
|
if _DBG_DISABLE_IMPERSONATION_ALERTS:
|
|
603
603
|
return
|
ramses_tx/ramses.py
CHANGED
|
@@ -377,10 +377,10 @@ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
|
|
|
377
377
|
I_: r"^(0[0-9A-F][0-9A-F]{8}0[12]){1,4}(0[12]03)?$", # (0[12]03)? only if len(array) == 1
|
|
378
378
|
W_: r"^(0[0-9A-F][0-9A-F]{8}0[12])$", # never an array
|
|
379
379
|
},
|
|
380
|
-
Code._22D0: { # unknown_22d0, HVAC system switch?
|
|
380
|
+
Code._22D0: { # unknown_22d0, Spider thermostat, HVAC system switch?
|
|
381
381
|
SZ_NAME: "message_22d0",
|
|
382
|
-
I_: r"^(00|03)",
|
|
383
|
-
W_: r"^03",
|
|
382
|
+
I_: r"^(00|03)[0-9]{6}$",
|
|
383
|
+
W_: r"^03[0-9]{4}1E14030020$",
|
|
384
384
|
},
|
|
385
385
|
Code._22D9: { # boiler_setpoint
|
|
386
386
|
SZ_NAME: "boiler_setpoint",
|
|
@@ -843,6 +843,7 @@ _DEV_KLASSES_HEAT: dict[str, dict[Code, dict[VerbT, Any]]] = {
|
|
|
843
843
|
Code._000C: {I_: {}},
|
|
844
844
|
Code._000E: {I_: {}},
|
|
845
845
|
Code._0016: {RQ: {}},
|
|
846
|
+
Code._01FF: {I_: {}, RQ: {}},
|
|
846
847
|
Code._042F: {I_: {}},
|
|
847
848
|
Code._1030: {I_: {}},
|
|
848
849
|
Code._1060: {I_: {}},
|
|
@@ -853,9 +854,11 @@ _DEV_KLASSES_HEAT: dict[str, dict[Code, dict[VerbT, Any]]] = {
|
|
|
853
854
|
Code._1F09: {I_: {}},
|
|
854
855
|
Code._1FC9: {I_: {}},
|
|
855
856
|
Code._22C9: {W_: {}}, # DT4R
|
|
857
|
+
Code._22D0: {W_: {}}, # Spider master THM
|
|
856
858
|
Code._2309: {I_: {}, RQ: {}, W_: {}},
|
|
857
859
|
Code._2349: {RQ: {}, W_: {}},
|
|
858
860
|
Code._30C9: {I_: {}},
|
|
861
|
+
Code._3110: {I_: {}}, # Spider THM
|
|
859
862
|
Code._3120: {I_: {}},
|
|
860
863
|
Code._313F: {
|
|
861
864
|
I_: {}
|
|
@@ -880,6 +883,7 @@ _DEV_KLASSES_HEAT: dict[str, dict[Code, dict[VerbT, Any]]] = {
|
|
|
880
883
|
Code._3110: {I_: {}}, # Spider Autotemp
|
|
881
884
|
Code._3150: {I_: {}},
|
|
882
885
|
Code._4E01: {I_: {}}, # Spider Autotemp Zone controller
|
|
886
|
+
Code._4E04: {I_: {}}, # idem
|
|
883
887
|
},
|
|
884
888
|
DevType.TRV: { # e.g. HR92/HR91: Radiator Controller
|
|
885
889
|
Code._0001: {W_: {r"^0[0-9A-F]"}},
|
|
@@ -1178,9 +1182,9 @@ _CODE_ONLY_FROM_CTL: tuple[Code, ...] = tuple(
|
|
|
1178
1182
|
CODES_ONLY_FROM_CTL: tuple[Code, ...] = (
|
|
1179
1183
|
Code._1030,
|
|
1180
1184
|
Code._1F09,
|
|
1181
|
-
Code._22D0,
|
|
1185
|
+
# Code._22D0, # also _W from the Spider master THM! issue #340
|
|
1182
1186
|
Code._313F,
|
|
1183
|
-
) # I packets, TODO: 31Dx too?
|
|
1187
|
+
) # I packets, TODO: 31Dx too? not 31D9/31DA!
|
|
1184
1188
|
|
|
1185
1189
|
#
|
|
1186
1190
|
########################################################################################
|
ramses_tx/schemas.py
CHANGED
|
@@ -413,6 +413,7 @@ SZ_EVOFW_FLAG: Final = "evofw_flag"
|
|
|
413
413
|
SZ_SQLITE_INDEX: Final = (
|
|
414
414
|
"sqlite_index" # temporary 0.52.x SQLite dev config option in ramses_cc
|
|
415
415
|
)
|
|
416
|
+
SZ_LOG_ALL_MQTT: Final = "log_all_mqtt"
|
|
416
417
|
SZ_USE_REGEX: Final = "use_regex"
|
|
417
418
|
|
|
418
419
|
SCH_ENGINE_DICT = {
|
|
@@ -427,6 +428,9 @@ SCH_ENGINE_DICT = {
|
|
|
427
428
|
vol.Optional(
|
|
428
429
|
SZ_SQLITE_INDEX, default=False
|
|
429
430
|
): bool, # temporary 0.52.x dev config option
|
|
431
|
+
vol.Optional(
|
|
432
|
+
SZ_LOG_ALL_MQTT, default=False
|
|
433
|
+
): bool, # log all incoming MQTT traffic config option
|
|
430
434
|
vol.Optional(SZ_USE_REGEX): dict, # vol.All(ConvertNullToDict(), dict),
|
|
431
435
|
vol.Optional(SZ_COMMS_PARAMS): SCH_COMMS_PARAMS,
|
|
432
436
|
}
|
ramses_tx/transport.py
CHANGED
|
@@ -1024,6 +1024,7 @@ class PortTransport(_RegHackMixin, _FullTransport, _PortTransportAbstractor): #
|
|
|
1024
1024
|
|
|
1025
1025
|
class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
1026
1026
|
"""Send/receive packets to/from ramses_esp via MQTT.
|
|
1027
|
+
For full RX logging, turn on debug logging.
|
|
1027
1028
|
|
|
1028
1029
|
See: https://github.com/IndaloTech/ramses_esp
|
|
1029
1030
|
"""
|
|
@@ -1066,6 +1067,9 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1066
1067
|
self._max_tokens: float = self._MAX_TOKENS * 2 # allow for the initial burst
|
|
1067
1068
|
self._num_tokens: float = self._MAX_TOKENS * 2
|
|
1068
1069
|
|
|
1070
|
+
# set log MQTT flag
|
|
1071
|
+
self._log_all = kwargs.pop("log_all", False)
|
|
1072
|
+
|
|
1069
1073
|
# instantiate a paho mqtt client
|
|
1070
1074
|
self.client = mqtt.Client(
|
|
1071
1075
|
protocol=mqtt.MQTTv5, callback_api_version=CallbackAPIVersion.VERSION2
|
|
@@ -1283,8 +1287,9 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1283
1287
|
|
|
1284
1288
|
if _DBG_FORCE_FRAME_LOGGING:
|
|
1285
1289
|
_LOGGER.warning("Rx: %s", msg.payload)
|
|
1286
|
-
elif _LOGGER.getEffectiveLevel() == logging.INFO:
|
|
1287
|
-
|
|
1290
|
+
elif self._log_all and _LOGGER.getEffectiveLevel() == logging.INFO:
|
|
1291
|
+
# log for INFO not DEBUG
|
|
1292
|
+
_LOGGER.info("mq Rx: %s", msg.payload) # TODO remove mq marker?
|
|
1288
1293
|
|
|
1289
1294
|
if msg.topic[-3:] != "/rx": # then, e.g. 'RAMSES/GATEWAY/18:017804'
|
|
1290
1295
|
if msg.payload == b"offline":
|
|
@@ -1507,6 +1512,7 @@ async def transport_factory(
|
|
|
1507
1512
|
disable_sending: bool | None = False,
|
|
1508
1513
|
extra: dict[str, Any] | None = None,
|
|
1509
1514
|
loop: asyncio.AbstractEventLoop | None = None,
|
|
1515
|
+
log_all: bool = False,
|
|
1510
1516
|
**kwargs: Any, # HACK: odd/misc params
|
|
1511
1517
|
) -> RamsesTransportT:
|
|
1512
1518
|
"""Create and return a Ramses-specific async packet Transport."""
|
|
@@ -1558,19 +1564,24 @@ async def transport_factory(
|
|
|
1558
1564
|
"Packet source must be exactly one of: packet_dict, packet_log, port_name"
|
|
1559
1565
|
)
|
|
1560
1566
|
|
|
1567
|
+
# File
|
|
1561
1568
|
if (pkt_source := packet_log or packet_dict) is not None:
|
|
1562
1569
|
return FileTransport(pkt_source, protocol, extra=extra, loop=loop, **kwargs)
|
|
1563
1570
|
|
|
1564
1571
|
assert port_name is not None # mypy check
|
|
1565
1572
|
assert port_config is not None # mypy check
|
|
1566
1573
|
|
|
1574
|
+
# MQTT
|
|
1567
1575
|
if port_name[:4] == "mqtt": # TODO: handle disable_sending
|
|
1568
|
-
transport = MqttTransport(
|
|
1576
|
+
transport = MqttTransport(
|
|
1577
|
+
port_name, protocol, extra=extra, loop=loop, log_all=log_all, **kwargs
|
|
1578
|
+
)
|
|
1569
1579
|
|
|
1570
1580
|
# TODO: remove this? better to invoke timeout after factory returns?
|
|
1571
1581
|
await protocol.wait_for_connection_made(timeout=_DEFAULT_TIMEOUT_MQTT)
|
|
1572
1582
|
return transport
|
|
1573
1583
|
|
|
1584
|
+
# Serial
|
|
1574
1585
|
ser_instance = get_serial_instance(port_name, port_config)
|
|
1575
1586
|
|
|
1576
1587
|
if os.name == "nt" or ser_instance.portstr[:7] in ("rfc2217", "socket:"):
|
|
@@ -1587,4 +1598,5 @@ async def transport_factory(
|
|
|
1587
1598
|
|
|
1588
1599
|
# TODO: remove this? better to invoke timeout after factory returns?
|
|
1589
1600
|
await protocol.wait_for_connection_made(timeout=_DEFAULT_TIMEOUT_PORT)
|
|
1601
|
+
# pytest-cov times out in virtual_rf.py when set below 30.0 on GitHub Actions
|
|
1590
1602
|
return transport
|
ramses_tx/version.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|