ramses-rf 0.52.3__py3-none-any.whl → 0.52.5__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 +1 -0
- ramses_cli/debug.py +1 -1
- ramses_cli/utils/convert.py +2 -2
- ramses_rf/database.py +47 -20
- ramses_rf/device/base.py +14 -3
- ramses_rf/device/heat.py +2 -2
- ramses_rf/device/hvac.py +24 -21
- ramses_rf/entity_base.py +70 -29
- ramses_rf/gateway.py +30 -19
- ramses_rf/schemas.py +1 -1
- ramses_rf/system/heat.py +1 -1
- ramses_rf/system/zones.py +22 -2
- ramses_rf/version.py +1 -1
- {ramses_rf-0.52.3.dist-info → ramses_rf-0.52.5.dist-info}/METADATA +2 -1
- ramses_rf-0.52.5.dist-info/RECORD +55 -0
- {ramses_rf-0.52.3.dist-info → ramses_rf-0.52.5.dist-info}/WHEEL +1 -1
- ramses_tx/address.py +21 -6
- ramses_tx/command.py +18 -2
- ramses_tx/const.py +1 -1
- ramses_tx/helpers.py +30 -10
- ramses_tx/message.py +11 -5
- ramses_tx/packet.py +13 -5
- ramses_tx/parsers.py +1096 -28
- ramses_tx/protocol.py +95 -20
- ramses_tx/ramses.py +9 -5
- ramses_tx/transport.py +31 -6
- ramses_tx/version.py +1 -1
- ramses_rf-0.52.3.dist-info/RECORD +0 -55
- {ramses_rf-0.52.3.dist-info → ramses_rf-0.52.5.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.52.3.dist-info → ramses_rf-0.52.5.dist-info}/licenses/LICENSE +0 -0
ramses_cli/client.py
CHANGED
|
@@ -588,6 +588,7 @@ def main() -> None:
|
|
|
588
588
|
print(" - event_loop_policy set for win32") # do before asyncio.run()
|
|
589
589
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
590
590
|
|
|
591
|
+
profile = None
|
|
591
592
|
try:
|
|
592
593
|
if _PROFILE_LIBRARY:
|
|
593
594
|
profile = cProfile.Profile()
|
ramses_cli/debug.py
CHANGED
|
@@ -9,7 +9,7 @@ DEBUG_PORT = 5678
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def start_debugging(wait_for_client: bool) -> None:
|
|
12
|
-
import debugpy
|
|
12
|
+
import debugpy
|
|
13
13
|
|
|
14
14
|
debugpy.listen(address=(DEBUG_ADDR, DEBUG_PORT))
|
|
15
15
|
print(f" - Debugging is enabled, listening on: {DEBUG_ADDR}:{DEBUG_PORT}")
|
ramses_cli/utils/convert.py
CHANGED
|
@@ -17,7 +17,7 @@ parser.add_argument("-i", "--input-file", type=argparse.FileType("r"), default="
|
|
|
17
17
|
args = parser.parse_args()
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def convert_json_to_yaml(data: dict) ->
|
|
20
|
+
def convert_json_to_yaml(data: dict) -> None:
|
|
21
21
|
"""Convert from json (client.py -C config.json) to yaml (HA configuration.yaml)."""
|
|
22
22
|
(config, schema, include, exclude) = load_config("/dev/ttyMOCK", None, **data)
|
|
23
23
|
|
|
@@ -37,7 +37,7 @@ def convert_json_to_yaml(data: dict) -> str:
|
|
|
37
37
|
print(yaml.dump({"ramses_cc": result}, sort_keys=False))
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def convert_yaml_to_json(data: dict) ->
|
|
40
|
+
def convert_yaml_to_json(data: dict) -> None:
|
|
41
41
|
"""Convert from yaml (HA configuration.yaml) to json (client.py -C config.json)."""
|
|
42
42
|
|
|
43
43
|
result = data["ramses_cc"]
|
ramses_rf/database.py
CHANGED
|
@@ -30,7 +30,7 @@ from collections import OrderedDict
|
|
|
30
30
|
from datetime import datetime as dt, timedelta as td
|
|
31
31
|
from typing import TYPE_CHECKING, Any, NewType
|
|
32
32
|
|
|
33
|
-
from ramses_tx import CODES_SCHEMA, Code, Message
|
|
33
|
+
from ramses_tx import CODES_SCHEMA, RQ, Code, Message, Packet
|
|
34
34
|
|
|
35
35
|
if TYPE_CHECKING:
|
|
36
36
|
DtmStrT = NewType("DtmStrT", str)
|
|
@@ -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:
|
|
@@ -151,7 +155,7 @@ class MessageIndex:
|
|
|
151
155
|
- verb " I", "RQ" etc.
|
|
152
156
|
- src message origin address
|
|
153
157
|
- dst message destination address
|
|
154
|
-
- code packet code aka command class e.g.
|
|
158
|
+
- code packet code aka command class e.g. 0005, 31DA
|
|
155
159
|
- ctx message context, created from payload as index + extra markers (Heat)
|
|
156
160
|
- hdr packet header e.g. 000C|RP|01:223036|0208 (see: src/ramses_tx/frame.py)
|
|
157
161
|
- plk the keys stored in the parsed payload, separated by the | char
|
|
@@ -183,7 +187,7 @@ class MessageIndex:
|
|
|
183
187
|
|
|
184
188
|
async def _housekeeping_loop(self) -> None:
|
|
185
189
|
"""Periodically remove stale messages from the index,
|
|
186
|
-
unless `self.maintain` is False."""
|
|
190
|
+
unless `self.maintain` is False - as in (most) tests."""
|
|
187
191
|
|
|
188
192
|
async def housekeeping(dt_now: dt, _cutoff: td = td(days=1)) -> None:
|
|
189
193
|
"""
|
|
@@ -191,9 +195,10 @@ class MessageIndex:
|
|
|
191
195
|
:param dt_now: current timestamp
|
|
192
196
|
:param _cutoff: the oldest timestamp to retain, default is 24 hours ago
|
|
193
197
|
"""
|
|
194
|
-
|
|
198
|
+
msgs = None
|
|
199
|
+
dtm = dt_now - _cutoff
|
|
195
200
|
|
|
196
|
-
self._cu.execute("SELECT dtm FROM messages WHERE dtm
|
|
201
|
+
self._cu.execute("SELECT dtm FROM messages WHERE dtm >= ?", (dtm,))
|
|
197
202
|
rows = self._cu.fetchall() # fetch dtm of current messages to retain
|
|
198
203
|
|
|
199
204
|
try: # make this operation atomic, i.e. update self._msgs only on success
|
|
@@ -208,6 +213,10 @@ class MessageIndex:
|
|
|
208
213
|
self._msgs = msgs
|
|
209
214
|
finally:
|
|
210
215
|
self._lock.release()
|
|
216
|
+
if msgs:
|
|
217
|
+
_LOGGER.debug(
|
|
218
|
+
"MessageIndex size was: %d, now: %d", len(rows), len(msgs)
|
|
219
|
+
)
|
|
211
220
|
|
|
212
221
|
while True:
|
|
213
222
|
self._last_housekeeping = dt.now()
|
|
@@ -242,13 +251,17 @@ class MessageIndex:
|
|
|
242
251
|
else:
|
|
243
252
|
# _msgs dict requires a timestamp reformat
|
|
244
253
|
dtm: DtmStrT = msg.dtm.isoformat(timespec="microseconds") # type: ignore[assignment]
|
|
254
|
+
# add msg to self._msgs dict
|
|
245
255
|
self._msgs[dtm] = msg
|
|
246
256
|
|
|
247
257
|
finally:
|
|
248
258
|
pass # self._lock.release()
|
|
249
259
|
|
|
250
260
|
if (
|
|
251
|
-
dup
|
|
261
|
+
dup
|
|
262
|
+
and (msg.src is not msg.dst)
|
|
263
|
+
and not msg.dst.id.startswith("18:") # HGI
|
|
264
|
+
and msg.verb != RQ # these may come very quickly
|
|
252
265
|
): # when src==dst, expect to add duplicate, don't warn
|
|
253
266
|
_LOGGER.debug(
|
|
254
267
|
"Overwrote dtm (%s) for %s: %s (contrived log?)",
|
|
@@ -256,8 +269,6 @@ class MessageIndex:
|
|
|
256
269
|
msg._pkt._hdr,
|
|
257
270
|
dup[0]._pkt,
|
|
258
271
|
)
|
|
259
|
-
if old is not None:
|
|
260
|
-
_LOGGER.debug("Old msg replaced: %s", old)
|
|
261
272
|
|
|
262
273
|
return old
|
|
263
274
|
|
|
@@ -270,7 +281,8 @@ class MessageIndex:
|
|
|
270
281
|
:param verb: two letter verb str to use
|
|
271
282
|
"""
|
|
272
283
|
# Used by OtbGateway init, via entity_base.py
|
|
273
|
-
|
|
284
|
+
_now: dt = dt.now()
|
|
285
|
+
dtm: DtmStrT = _now.isoformat(timespec="microseconds") # type: ignore[assignment]
|
|
274
286
|
hdr = f"{code}|{verb}|{src}|00" # dummy record has no contents
|
|
275
287
|
|
|
276
288
|
dup = self._delete_from(hdr=hdr)
|
|
@@ -283,7 +295,7 @@ class MessageIndex:
|
|
|
283
295
|
self._cu.execute(
|
|
284
296
|
sql,
|
|
285
297
|
(
|
|
286
|
-
|
|
298
|
+
_now,
|
|
287
299
|
verb,
|
|
288
300
|
src,
|
|
289
301
|
src,
|
|
@@ -295,6 +307,14 @@ class MessageIndex:
|
|
|
295
307
|
)
|
|
296
308
|
except sqlite3.Error:
|
|
297
309
|
self._cx.rollback()
|
|
310
|
+
else:
|
|
311
|
+
# also add dummy 3220 msg to self._msgs dict to allow maintenance loop
|
|
312
|
+
msg: Message = Message._from_pkt(
|
|
313
|
+
Packet(
|
|
314
|
+
_now, f"... {verb} --- {src} --:------ {src} {code} 005 0000000000"
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
self._msgs[dtm] = msg
|
|
298
318
|
|
|
299
319
|
if dup: # expected when more than one heat system in schema
|
|
300
320
|
_LOGGER.debug("Replaced record with same hdr: %s", hdr)
|
|
@@ -355,7 +375,7 @@ class MessageIndex:
|
|
|
355
375
|
if not bool(msg) ^ bool(kwargs):
|
|
356
376
|
raise ValueError("Either a Message or kwargs should be provided, not both")
|
|
357
377
|
if msg:
|
|
358
|
-
kwargs["dtm"] = msg.dtm
|
|
378
|
+
kwargs["dtm"] = msg.dtm
|
|
359
379
|
|
|
360
380
|
msgs = None
|
|
361
381
|
try: # make this operation atomic, i.e. update self._msgs only on success
|
|
@@ -406,7 +426,7 @@ class MessageIndex:
|
|
|
406
426
|
raise ValueError("Either a Message or kwargs should be provided, not both")
|
|
407
427
|
|
|
408
428
|
if msg:
|
|
409
|
-
kwargs["dtm"] = msg.dtm
|
|
429
|
+
kwargs["dtm"] = msg.dtm
|
|
410
430
|
|
|
411
431
|
return self._select_from(**kwargs)
|
|
412
432
|
|
|
@@ -428,10 +448,15 @@ class MessageIndex:
|
|
|
428
448
|
:returns: a tuple of qualifying messages
|
|
429
449
|
"""
|
|
430
450
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
451
|
+
# CHANGE: Use a list comprehension with a check to avoid KeyError
|
|
452
|
+
res: list[Message] = []
|
|
453
|
+
for row in self.qry_dtms(**kwargs):
|
|
454
|
+
ts: DtmStrT = row[0].isoformat(timespec="microseconds")
|
|
455
|
+
if ts in self._msgs:
|
|
456
|
+
res.append(self._msgs[ts])
|
|
457
|
+
else:
|
|
458
|
+
_LOGGER.debug("MessageIndex timestamp %s not in device messages", ts)
|
|
459
|
+
return tuple(res)
|
|
435
460
|
|
|
436
461
|
def qry_dtms(self, **kwargs: bool | dt | str) -> list[Any]:
|
|
437
462
|
"""
|
|
@@ -483,6 +508,7 @@ class MessageIndex:
|
|
|
483
508
|
# _msgs stamp format: 2022-09-08T13:40:52.447364
|
|
484
509
|
if ts in self._msgs:
|
|
485
510
|
lst.append(self._msgs[ts])
|
|
511
|
+
# _LOGGER.debug("MessageIndex ts %s added to qry.lst", ts) # too frequent
|
|
486
512
|
else: # happens in tests with artificial msg from heat
|
|
487
513
|
_LOGGER.info("MessageIndex timestamp %s not in device messages", ts)
|
|
488
514
|
return tuple(lst)
|
|
@@ -546,8 +572,9 @@ class MessageIndex:
|
|
|
546
572
|
if ts in self._msgs:
|
|
547
573
|
# if include_expired or not self._msgs[ts].HAS_EXPIRED: # not working
|
|
548
574
|
lst.append(self._msgs[ts])
|
|
549
|
-
|
|
550
|
-
|
|
575
|
+
_LOGGER.debug("MessageIndex ts %s added to all.lst", ts)
|
|
576
|
+
else: # happens in tests and real evohome setups with dummy msg from heat init
|
|
577
|
+
_LOGGER.debug("MessageIndex ts %s not in device messages", ts)
|
|
551
578
|
return tuple(lst)
|
|
552
579
|
|
|
553
580
|
def clr(self) -> None:
|
ramses_rf/device/base.py
CHANGED
|
@@ -87,9 +87,10 @@ class DeviceBase(Entity):
|
|
|
87
87
|
return self.id < other.id # type: ignore[no-any-return]
|
|
88
88
|
|
|
89
89
|
def _update_traits(self, **traits: Any) -> None:
|
|
90
|
-
"""Update a device with new schema
|
|
90
|
+
"""Update a device with new schema attributes.
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
:param traits: The traits to apply (e.g., alias, class, faked)
|
|
93
|
+
:raises TypeError: If the device is not fakeable but 'faked' is set.
|
|
93
94
|
"""
|
|
94
95
|
|
|
95
96
|
traits = shrink(SCH_TRAITS(traits))
|
|
@@ -342,7 +343,17 @@ class Fakeable(DeviceBase):
|
|
|
342
343
|
idx: IndexT = "00",
|
|
343
344
|
require_ratify: bool = False,
|
|
344
345
|
) -> tuple[Packet, Packet, Packet, Packet | None]:
|
|
345
|
-
"""Listen for a binding and return the Offer
|
|
346
|
+
"""Listen for a binding and return the Offer packets.
|
|
347
|
+
|
|
348
|
+
:param accept_codes: The codes allowed for this binding
|
|
349
|
+
:type accept_codes: Iterable[Code]
|
|
350
|
+
:param idx: The index to bind to, defaults to "00"
|
|
351
|
+
:type idx: IndexT
|
|
352
|
+
:param require_ratify: Whether a ratification step is required, defaults to False
|
|
353
|
+
:type require_ratify: bool
|
|
354
|
+
:return: A tuple of the four binding transaction packets
|
|
355
|
+
:rtype: tuple[Packet, Packet, Packet, Packet | None]
|
|
356
|
+
"""
|
|
346
357
|
|
|
347
358
|
if not self._bind_context:
|
|
348
359
|
raise TypeError(f"{self}: Faking not enabled")
|
ramses_rf/device/heat.py
CHANGED
|
@@ -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
|
|
@@ -669,7 +669,7 @@ class OtbGateway(Actuator, HeatDemand): # OTB (10): 3220 (22D9, others)
|
|
|
669
669
|
|
|
670
670
|
# TODO(eb): cleanup
|
|
671
671
|
if self._gwy.msg_db:
|
|
672
|
-
self._add_record(
|
|
672
|
+
self._add_record(id=self.id, code=Code._3220, verb="RP")
|
|
673
673
|
# adds a "sim" RP opentherm_msg to the SQLite MessageIndex with code _3220
|
|
674
674
|
# causes exc when fetching ALL, when no "real" msg was added to _msgs_. We skip those.
|
|
675
675
|
else:
|
ramses_rf/device/hvac.py
CHANGED
|
@@ -921,27 +921,30 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
|
|
|
921
921
|
|
|
922
922
|
:return: string describing fan mode, speed
|
|
923
923
|
"""
|
|
924
|
-
if self._gwy.msg_db:
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
924
|
+
# if self._gwy.msg_db:
|
|
925
|
+
# Use SQLite query on MessageIndex. res_rate/res_mode not exposed yet
|
|
926
|
+
# working fine in 0.52.4, no need to specify code, only payload key
|
|
927
|
+
# sql = f"""
|
|
928
|
+
# SELECT code from messages WHERE verb in (' I', 'RP')
|
|
929
|
+
# AND (src = ? OR dst = ?)
|
|
930
|
+
# AND (plk LIKE '%{SZ_FAN_MODE}%')
|
|
931
|
+
# """
|
|
932
|
+
# res_mode: list = self._msg_qry(sql)
|
|
933
|
+
# # SQLite query on MessageIndex
|
|
934
|
+
# _LOGGER.debug(
|
|
935
|
+
# f"# Fetched FAN_MODE for {self.id} from MessageIndex: {res_mode}"
|
|
936
|
+
# )
|
|
937
|
+
|
|
938
|
+
# sql = f"""
|
|
939
|
+
# SELECT code from messages WHERE verb in (' I', 'RP')
|
|
940
|
+
# AND (src = ? OR dst = ?)
|
|
941
|
+
# AND (plk LIKE '%{SZ_FAN_RATE}%')
|
|
942
|
+
# """
|
|
943
|
+
# res_rate: list = self._msg_qry(sql)
|
|
944
|
+
# # SQLite query on MessageIndex
|
|
945
|
+
# _LOGGER.debug(
|
|
946
|
+
# f"# Fetched FAN_RATE for {self.id} from MessageIndex: {res_rate}"
|
|
947
|
+
# )
|
|
945
948
|
|
|
946
949
|
if Code._31D9 in self._msgs:
|
|
947
950
|
# was a dict by Code
|
ramses_rf/entity_base.py
CHANGED
|
@@ -15,7 +15,7 @@ from types import ModuleType
|
|
|
15
15
|
from typing import TYPE_CHECKING, Any, Final
|
|
16
16
|
|
|
17
17
|
from ramses_rf.helpers import schedule_task
|
|
18
|
-
from ramses_tx import
|
|
18
|
+
from ramses_tx import Priority, QosParams
|
|
19
19
|
from ramses_tx.address import ALL_DEVICE_ID
|
|
20
20
|
from ramses_tx.const import MsgId
|
|
21
21
|
from ramses_tx.opentherm import OPENTHERM_MESSAGES
|
|
@@ -224,7 +224,10 @@ class _MessageDB(_Entity):
|
|
|
224
224
|
|
|
225
225
|
# As of 0.52.1 we use SQLite MessageIndex, see ramses_rf/database.py
|
|
226
226
|
# _msgz_ (nested) was only used in this module. Note:
|
|
227
|
-
# _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)
|
|
228
231
|
|
|
229
232
|
def _handle_msg(self, msg: Message) -> None:
|
|
230
233
|
"""Store a msg in the DBs.
|
|
@@ -244,14 +247,14 @@ class _MessageDB(_Entity):
|
|
|
244
247
|
|
|
245
248
|
if self._gwy.msg_db: # central SQLite MessageIndex
|
|
246
249
|
_LOGGER.debug(
|
|
247
|
-
"For %s (
|
|
250
|
+
"For %s (_z_id %s) add to msg_db: %s, src %s, dst %s",
|
|
248
251
|
self.id,
|
|
249
252
|
self._z_id,
|
|
250
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",
|
|
@@ -293,6 +296,7 @@ class _MessageDB(_Entity):
|
|
|
293
296
|
def _msg_list(self) -> list[Message]:
|
|
294
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()
|
|
@@ -303,24 +307,29 @@ class _MessageDB(_Entity):
|
|
|
303
307
|
msg_list_qry.append(self._msgs[c])
|
|
304
308
|
else:
|
|
305
309
|
# evohome has these errors
|
|
306
|
-
# _msg_list could not fetch self._msgs[7FFF] for 18:072981 (
|
|
310
|
+
# _msg_list could not fetch self._msgs[7FFF] for 18:072981 (_z_id 18:072981)
|
|
307
311
|
_LOGGER.debug(
|
|
308
|
-
"_msg_list could not fetch self._msgs[%s] for %s (
|
|
312
|
+
"_msg_list could not fetch self._msgs[%s] for %s (_z_id %s)",
|
|
309
313
|
c,
|
|
310
314
|
self.id,
|
|
311
315
|
self._z_id,
|
|
312
316
|
)
|
|
313
317
|
return msg_list_qry
|
|
314
318
|
# else create from legacy nested dict
|
|
315
|
-
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
|
+
]
|
|
316
325
|
|
|
317
326
|
def _add_record(
|
|
318
|
-
self,
|
|
327
|
+
self, id: DeviceIdT, code: Code | None = None, verb: str = " I"
|
|
319
328
|
) -> None:
|
|
320
329
|
"""Add a (dummy) record to the central SQLite MessageIndex."""
|
|
321
330
|
# used by heat.py init
|
|
322
331
|
if self._gwy.msg_db:
|
|
323
|
-
self._gwy.msg_db.add_record(
|
|
332
|
+
self._gwy.msg_db.add_record(id, code=str(code), verb=verb)
|
|
324
333
|
# else:
|
|
325
334
|
# _LOGGER.warning("Missing MessageIndex")
|
|
326
335
|
# raise NotImplementedError
|
|
@@ -424,7 +433,7 @@ class _MessageDB(_Entity):
|
|
|
424
433
|
**kwargs: Any,
|
|
425
434
|
) -> dict | list | None:
|
|
426
435
|
"""
|
|
427
|
-
Query the message dict or the SQLite
|
|
436
|
+
Query the _msgz message dict or the SQLite MessageIndex for the most recent
|
|
428
437
|
key: value pairs(s) for a given code.
|
|
429
438
|
|
|
430
439
|
:param code: filter messages by Code or a tuple of Codes, optional
|
|
@@ -517,7 +526,7 @@ class _MessageDB(_Entity):
|
|
|
517
526
|
or (idx == SZ_DOMAIN_ID)
|
|
518
527
|
), (
|
|
519
528
|
f"full dict:{msg_dict}, payload:{msg.payload} < Coding error: key='{idx}', val='{val}'"
|
|
520
|
-
) # should not be there
|
|
529
|
+
) # should not be there
|
|
521
530
|
|
|
522
531
|
if (
|
|
523
532
|
key == "*" or not key
|
|
@@ -535,21 +544,35 @@ class _MessageDB(_Entity):
|
|
|
535
544
|
"""
|
|
536
545
|
Retrieve from the MessageIndex a list of Code keys involving this device.
|
|
537
546
|
|
|
538
|
-
: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
|
|
539
548
|
"""
|
|
540
549
|
|
|
541
550
|
if self._gwy.msg_db:
|
|
542
551
|
# SQLite query on MessageIndex
|
|
543
552
|
res: list[Code] = []
|
|
544
|
-
|
|
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
|
|
545
566
|
sql = """
|
|
546
567
|
SELECT code from messages WHERE
|
|
547
568
|
verb in (' I', 'RP')
|
|
548
569
|
AND (src = ? OR dst = ?)
|
|
549
570
|
AND (ctx IN ('FC', 'FA', 'F9', 'FA') OR plk LIKE ?)
|
|
550
571
|
"""
|
|
551
|
-
_ctx_qry = "%dhw_idx%"
|
|
572
|
+
_ctx_qry = "%dhw_idx%"
|
|
573
|
+
|
|
552
574
|
else:
|
|
575
|
+
# fetch a zone's message codes
|
|
553
576
|
sql = """
|
|
554
577
|
SELECT code from messages WHERE
|
|
555
578
|
verb in (' I', 'RP')
|
|
@@ -562,12 +585,12 @@ class _MessageDB(_Entity):
|
|
|
562
585
|
sql, (self.id[:_ID_SLICE], self.id[:_ID_SLICE], _ctx_qry)
|
|
563
586
|
):
|
|
564
587
|
_LOGGER.debug(
|
|
565
|
-
"Fetched from index: %s for %s (
|
|
588
|
+
"Fetched from index: %s for %s (_z_id %s)",
|
|
566
589
|
rec[0],
|
|
567
590
|
self.id,
|
|
568
591
|
self._z_id,
|
|
569
592
|
)
|
|
570
|
-
# Example: "Fetched from index: code 1FD4 for 01:123456 (
|
|
593
|
+
# Example: "Fetched from index: code 1FD4 for 01:123456 (_z_id 01)"
|
|
571
594
|
res.append(Code(str(rec[0])))
|
|
572
595
|
return res
|
|
573
596
|
else:
|
|
@@ -743,7 +766,18 @@ class _MessageDB(_Entity):
|
|
|
743
766
|
# for r in results:
|
|
744
767
|
# print(r)
|
|
745
768
|
|
|
746
|
-
if self.id
|
|
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
|
|
747
781
|
sql = """
|
|
748
782
|
SELECT dtm from messages WHERE
|
|
749
783
|
verb in (' I', 'RP')
|
|
@@ -753,6 +787,7 @@ class _MessageDB(_Entity):
|
|
|
753
787
|
_ctx_qry = "%dhw_idx%"
|
|
754
788
|
# TODO add Children messages? self.ctl.dhw
|
|
755
789
|
else:
|
|
790
|
+
# fetch a zone's message dtms
|
|
756
791
|
sql = """
|
|
757
792
|
SELECT dtm from messages WHERE
|
|
758
793
|
verb in (' I', 'RP')
|
|
@@ -769,14 +804,14 @@ class _MessageDB(_Entity):
|
|
|
769
804
|
}
|
|
770
805
|
# if CTL, remove 3150, 3220 heat_demand, both are only stored on children
|
|
771
806
|
# HACK
|
|
772
|
-
if self.id[:3] == "01:" and self._SLUG == "CTL":
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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)
|
|
780
815
|
return _msg_dict
|
|
781
816
|
|
|
782
817
|
@property
|
|
@@ -862,7 +897,6 @@ class _Discovery(_MessageDB):
|
|
|
862
897
|
# return f"{data_id:02X}" # type: ignore[return-value]
|
|
863
898
|
|
|
864
899
|
res: list[str] = []
|
|
865
|
-
# look for the "sim" OT 3220 record initially added in OtbGateway.init
|
|
866
900
|
if self._gwy.msg_db:
|
|
867
901
|
# SQLite query for ctx field on MessageIndex
|
|
868
902
|
sql = """
|
|
@@ -995,18 +1029,24 @@ class _Discovery(_MessageDB):
|
|
|
995
1029
|
sql = """
|
|
996
1030
|
SELECT dtm from messages WHERE
|
|
997
1031
|
code = ?
|
|
998
|
-
verb
|
|
1032
|
+
AND verb in (' I', 'RP')
|
|
999
1033
|
AND ctx = 'True'
|
|
1000
1034
|
AND (src = ? OR dst = ?)
|
|
1001
1035
|
"""
|
|
1002
|
-
|
|
1036
|
+
res = self._gwy.msg_db.qry(
|
|
1003
1037
|
sql,
|
|
1004
1038
|
(
|
|
1005
1039
|
task[_SZ_COMMAND].code,
|
|
1006
1040
|
self.tcs.id[:_ID_SLICE],
|
|
1007
1041
|
self.tcs.id[:_ID_SLICE],
|
|
1008
1042
|
),
|
|
1009
|
-
)
|
|
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}, task code {task[_SZ_COMMAND].code}"
|
|
1049
|
+
)
|
|
1010
1050
|
else: # TODO(eb) remove next Q1 2026
|
|
1011
1051
|
msgs += [self.tcs._msgz[task[_SZ_COMMAND].code][I_][True]]
|
|
1012
1052
|
# raise NotImplementedError
|
|
@@ -1145,6 +1185,7 @@ class Parent(Entity): # A System, Zone, DhwZone or a UfhController
|
|
|
1145
1185
|
(incl. the DHW Zone), and also any UFH controllers.
|
|
1146
1186
|
|
|
1147
1187
|
For a heating Zone, children are limited to a sensor, and a number of actuators.
|
|
1188
|
+
|
|
1148
1189
|
For the DHW Zone, the children are limited to a sensor, a DHW valve, and/or a
|
|
1149
1190
|
heating valve.
|
|
1150
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
|
|
|
@@ -360,13 +360,25 @@ class Gateway(Engine):
|
|
|
360
360
|
child_id: str | None = None,
|
|
361
361
|
is_sensor: bool | None = None,
|
|
362
362
|
) -> Device: # TODO: **schema/traits) -> Device: # may: LookupError
|
|
363
|
-
"""Return a device,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
All devices have traits, but only
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
363
|
+
"""Return a device, creating it if it does not already exist.
|
|
364
|
+
|
|
365
|
+
This method uses provided traits to create or update a device and optionally
|
|
366
|
+
passes a message for it to handle. All devices have traits, but only
|
|
367
|
+
controllers (CTL, UFC) have a schema.
|
|
368
|
+
|
|
369
|
+
:param device_id: The unique identifier for the device (e.g., '01:123456')
|
|
370
|
+
:type device_id: DeviceIdT
|
|
371
|
+
:param msg: An optional initial message for the device to process, defaults to None
|
|
372
|
+
:type msg: Message | None
|
|
373
|
+
:param parent: The parent entity of this device, if any, defaults to None
|
|
374
|
+
:type parent: Parent | None
|
|
375
|
+
:param child_id: The specific ID of the child component if applicable, defaults to None
|
|
376
|
+
:type child_id: str | None
|
|
377
|
+
:param is_sensor: Indicates if this device should be treated as a sensor, defaults to None
|
|
378
|
+
:type is_sensor: bool | None
|
|
379
|
+
:return: The existing or newly created device instance
|
|
380
|
+
:rtype: Device
|
|
381
|
+
:raises LookupError: If the device ID is blocked or not in the allowed known_list.
|
|
370
382
|
"""
|
|
371
383
|
|
|
372
384
|
def check_filter_lists(dev_id: DeviceIdT) -> None: # may: LookupError
|
|
@@ -620,9 +632,8 @@ class Gateway(Engine):
|
|
|
620
632
|
If wait_for_reply is True (*and* the Command has a rx_header), return the
|
|
621
633
|
reply Packet. Otherwise, simply return the echo Packet.
|
|
622
634
|
|
|
623
|
-
If the
|
|
624
|
-
|
|
625
|
-
ProtocolError: didn't attempt to Tx Command for some reason
|
|
635
|
+
:raises ProtocolSendFailed: If the command was sent but no reply/echo was received.
|
|
636
|
+
:raises ProtocolError: If the system failed to attempt the transmission.
|
|
626
637
|
"""
|
|
627
638
|
|
|
628
639
|
return await super().async_send_cmd(
|
ramses_rf/schemas.py
CHANGED
|
@@ -285,7 +285,7 @@ SCH_GLOBAL_CONFIG = (
|
|
|
285
285
|
#
|
|
286
286
|
# 6/7: External Schemas, to be used by clients of this library
|
|
287
287
|
def NormaliseRestoreCache() -> Callable[[bool | dict[str, bool]], dict[str, bool]]:
|
|
288
|
-
"""Convert a
|
|
288
|
+
"""Convert a shorthand restore_cache bool to a dict.
|
|
289
289
|
|
|
290
290
|
restore_cache: bool -> restore_cache:
|
|
291
291
|
restore_schema: bool
|