ramses-rf 0.52.4__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 +40 -17
- ramses_rf/device/base.py +14 -3
- ramses_rf/device/heat.py +1 -1
- ramses_rf/device/hvac.py +24 -21
- ramses_rf/entity_base.py +6 -6
- ramses_rf/gateway.py +21 -10
- ramses_rf/schemas.py +1 -1
- ramses_rf/system/zones.py +22 -2
- ramses_rf/version.py +1 -1
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.52.5.dist-info}/METADATA +1 -1
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.52.5.dist-info}/RECORD +26 -26
- {ramses_rf-0.52.4.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/helpers.py +30 -10
- ramses_tx/message.py +11 -5
- ramses_tx/packet.py +13 -5
- ramses_tx/parsers.py +1039 -16
- ramses_tx/protocol.py +93 -18
- ramses_tx/transport.py +30 -6
- ramses_tx/version.py +1 -1
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.52.5.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.52.4.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)
|
|
@@ -155,7 +155,7 @@ class MessageIndex:
|
|
|
155
155
|
- verb " I", "RQ" etc.
|
|
156
156
|
- src message origin address
|
|
157
157
|
- dst message destination address
|
|
158
|
-
- code packet code aka command class e.g.
|
|
158
|
+
- code packet code aka command class e.g. 0005, 31DA
|
|
159
159
|
- ctx message context, created from payload as index + extra markers (Heat)
|
|
160
160
|
- hdr packet header e.g. 000C|RP|01:223036|0208 (see: src/ramses_tx/frame.py)
|
|
161
161
|
- plk the keys stored in the parsed payload, separated by the | char
|
|
@@ -187,7 +187,7 @@ class MessageIndex:
|
|
|
187
187
|
|
|
188
188
|
async def _housekeeping_loop(self) -> None:
|
|
189
189
|
"""Periodically remove stale messages from the index,
|
|
190
|
-
unless `self.maintain` is False."""
|
|
190
|
+
unless `self.maintain` is False - as in (most) tests."""
|
|
191
191
|
|
|
192
192
|
async def housekeeping(dt_now: dt, _cutoff: td = td(days=1)) -> None:
|
|
193
193
|
"""
|
|
@@ -195,7 +195,8 @@ class MessageIndex:
|
|
|
195
195
|
:param dt_now: current timestamp
|
|
196
196
|
:param _cutoff: the oldest timestamp to retain, default is 24 hours ago
|
|
197
197
|
"""
|
|
198
|
-
|
|
198
|
+
msgs = None
|
|
199
|
+
dtm = dt_now - _cutoff
|
|
199
200
|
|
|
200
201
|
self._cu.execute("SELECT dtm FROM messages WHERE dtm >= ?", (dtm,))
|
|
201
202
|
rows = self._cu.fetchall() # fetch dtm of current messages to retain
|
|
@@ -212,6 +213,10 @@ class MessageIndex:
|
|
|
212
213
|
self._msgs = msgs
|
|
213
214
|
finally:
|
|
214
215
|
self._lock.release()
|
|
216
|
+
if msgs:
|
|
217
|
+
_LOGGER.debug(
|
|
218
|
+
"MessageIndex size was: %d, now: %d", len(rows), len(msgs)
|
|
219
|
+
)
|
|
215
220
|
|
|
216
221
|
while True:
|
|
217
222
|
self._last_housekeeping = dt.now()
|
|
@@ -246,13 +251,17 @@ class MessageIndex:
|
|
|
246
251
|
else:
|
|
247
252
|
# _msgs dict requires a timestamp reformat
|
|
248
253
|
dtm: DtmStrT = msg.dtm.isoformat(timespec="microseconds") # type: ignore[assignment]
|
|
254
|
+
# add msg to self._msgs dict
|
|
249
255
|
self._msgs[dtm] = msg
|
|
250
256
|
|
|
251
257
|
finally:
|
|
252
258
|
pass # self._lock.release()
|
|
253
259
|
|
|
254
260
|
if (
|
|
255
|
-
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
|
|
256
265
|
): # when src==dst, expect to add duplicate, don't warn
|
|
257
266
|
_LOGGER.debug(
|
|
258
267
|
"Overwrote dtm (%s) for %s: %s (contrived log?)",
|
|
@@ -260,8 +269,6 @@ class MessageIndex:
|
|
|
260
269
|
msg._pkt._hdr,
|
|
261
270
|
dup[0]._pkt,
|
|
262
271
|
)
|
|
263
|
-
if old is not None:
|
|
264
|
-
_LOGGER.debug("Old msg replaced: %s", old)
|
|
265
272
|
|
|
266
273
|
return old
|
|
267
274
|
|
|
@@ -274,7 +281,8 @@ class MessageIndex:
|
|
|
274
281
|
:param verb: two letter verb str to use
|
|
275
282
|
"""
|
|
276
283
|
# Used by OtbGateway init, via entity_base.py
|
|
277
|
-
|
|
284
|
+
_now: dt = dt.now()
|
|
285
|
+
dtm: DtmStrT = _now.isoformat(timespec="microseconds") # type: ignore[assignment]
|
|
278
286
|
hdr = f"{code}|{verb}|{src}|00" # dummy record has no contents
|
|
279
287
|
|
|
280
288
|
dup = self._delete_from(hdr=hdr)
|
|
@@ -287,7 +295,7 @@ class MessageIndex:
|
|
|
287
295
|
self._cu.execute(
|
|
288
296
|
sql,
|
|
289
297
|
(
|
|
290
|
-
|
|
298
|
+
_now,
|
|
291
299
|
verb,
|
|
292
300
|
src,
|
|
293
301
|
src,
|
|
@@ -299,6 +307,14 @@ class MessageIndex:
|
|
|
299
307
|
)
|
|
300
308
|
except sqlite3.Error:
|
|
301
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
|
|
302
318
|
|
|
303
319
|
if dup: # expected when more than one heat system in schema
|
|
304
320
|
_LOGGER.debug("Replaced record with same hdr: %s", hdr)
|
|
@@ -359,7 +375,7 @@ class MessageIndex:
|
|
|
359
375
|
if not bool(msg) ^ bool(kwargs):
|
|
360
376
|
raise ValueError("Either a Message or kwargs should be provided, not both")
|
|
361
377
|
if msg:
|
|
362
|
-
kwargs["dtm"] = msg.dtm
|
|
378
|
+
kwargs["dtm"] = msg.dtm
|
|
363
379
|
|
|
364
380
|
msgs = None
|
|
365
381
|
try: # make this operation atomic, i.e. update self._msgs only on success
|
|
@@ -410,7 +426,7 @@ class MessageIndex:
|
|
|
410
426
|
raise ValueError("Either a Message or kwargs should be provided, not both")
|
|
411
427
|
|
|
412
428
|
if msg:
|
|
413
|
-
kwargs["dtm"] = msg.dtm
|
|
429
|
+
kwargs["dtm"] = msg.dtm
|
|
414
430
|
|
|
415
431
|
return self._select_from(**kwargs)
|
|
416
432
|
|
|
@@ -432,10 +448,15 @@ class MessageIndex:
|
|
|
432
448
|
:returns: a tuple of qualifying messages
|
|
433
449
|
"""
|
|
434
450
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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)
|
|
439
460
|
|
|
440
461
|
def qry_dtms(self, **kwargs: bool | dt | str) -> list[Any]:
|
|
441
462
|
"""
|
|
@@ -487,6 +508,7 @@ class MessageIndex:
|
|
|
487
508
|
# _msgs stamp format: 2022-09-08T13:40:52.447364
|
|
488
509
|
if ts in self._msgs:
|
|
489
510
|
lst.append(self._msgs[ts])
|
|
511
|
+
# _LOGGER.debug("MessageIndex ts %s added to qry.lst", ts) # too frequent
|
|
490
512
|
else: # happens in tests with artificial msg from heat
|
|
491
513
|
_LOGGER.info("MessageIndex timestamp %s not in device messages", ts)
|
|
492
514
|
return tuple(lst)
|
|
@@ -550,8 +572,9 @@ class MessageIndex:
|
|
|
550
572
|
if ts in self._msgs:
|
|
551
573
|
# if include_expired or not self._msgs[ts].HAS_EXPIRED: # not working
|
|
552
574
|
lst.append(self._msgs[ts])
|
|
553
|
-
|
|
554
|
-
|
|
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)
|
|
555
578
|
return tuple(lst)
|
|
556
579
|
|
|
557
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
|
@@ -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
|
|
@@ -247,7 +247,7 @@ class _MessageDB(_Entity):
|
|
|
247
247
|
|
|
248
248
|
if self._gwy.msg_db: # central SQLite MessageIndex
|
|
249
249
|
_LOGGER.debug(
|
|
250
|
-
"For %s (_z_id %s) add
|
|
250
|
+
"For %s (_z_id %s) add to msg_db: %s, src %s, dst %s",
|
|
251
251
|
self.id,
|
|
252
252
|
self._z_id,
|
|
253
253
|
msg,
|
|
@@ -324,12 +324,12 @@ class _MessageDB(_Entity):
|
|
|
324
324
|
]
|
|
325
325
|
|
|
326
326
|
def _add_record(
|
|
327
|
-
self,
|
|
327
|
+
self, id: DeviceIdT, code: Code | None = None, verb: str = " I"
|
|
328
328
|
) -> None:
|
|
329
329
|
"""Add a (dummy) record to the central SQLite MessageIndex."""
|
|
330
330
|
# used by heat.py init
|
|
331
331
|
if self._gwy.msg_db:
|
|
332
|
-
self._gwy.msg_db.add_record(
|
|
332
|
+
self._gwy.msg_db.add_record(id, code=str(code), verb=verb)
|
|
333
333
|
# else:
|
|
334
334
|
# _LOGGER.warning("Missing MessageIndex")
|
|
335
335
|
# raise NotImplementedError
|
|
@@ -1029,7 +1029,7 @@ class _Discovery(_MessageDB):
|
|
|
1029
1029
|
sql = """
|
|
1030
1030
|
SELECT dtm from messages WHERE
|
|
1031
1031
|
code = ?
|
|
1032
|
-
AND verb
|
|
1032
|
+
AND verb in (' I', 'RP')
|
|
1033
1033
|
AND ctx = 'True'
|
|
1034
1034
|
AND (src = ? OR dst = ?)
|
|
1035
1035
|
"""
|
|
@@ -1045,7 +1045,7 @@ class _Discovery(_MessageDB):
|
|
|
1045
1045
|
msgs += res[0] # expect 1 Message in returned tuple
|
|
1046
1046
|
else:
|
|
1047
1047
|
_LOGGER.debug(
|
|
1048
|
-
f"No msg found for hdr {hdr},
|
|
1048
|
+
f"No msg found for hdr {hdr}, task code {task[_SZ_COMMAND].code}"
|
|
1049
1049
|
)
|
|
1050
1050
|
else: # TODO(eb) remove next Q1 2026
|
|
1051
1051
|
msgs += [self.tcs._msgz[task[_SZ_COMMAND].code][I_][True]]
|
ramses_rf/gateway.py
CHANGED
|
@@ -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
|
ramses_rf/system/zones.py
CHANGED
|
@@ -36,7 +36,7 @@ from ramses_rf.device import (
|
|
|
36
36
|
TrvActuator,
|
|
37
37
|
UfhController,
|
|
38
38
|
)
|
|
39
|
-
from ramses_rf.entity_base import Child, Entity, Parent, class_by_attr
|
|
39
|
+
from ramses_rf.entity_base import _ID_SLICE, Child, Entity, Parent, class_by_attr
|
|
40
40
|
from ramses_rf.helpers import shrink
|
|
41
41
|
from ramses_rf.schemas import (
|
|
42
42
|
SCH_TCS_DHW,
|
|
@@ -728,13 +728,33 @@ class Zone(ZoneSchedule):
|
|
|
728
728
|
"""Set the target temperature, until the next scheduled setpoint."""
|
|
729
729
|
|
|
730
730
|
if value is None:
|
|
731
|
-
|
|
731
|
+
self.reset_mode()
|
|
732
732
|
|
|
733
733
|
cmd = Command.set_zone_setpoint(self.ctl.id, self.idx, value)
|
|
734
734
|
self._gwy.send_cmd(cmd, priority=Priority.HIGH)
|
|
735
735
|
|
|
736
736
|
@property
|
|
737
737
|
def temperature(self) -> float | None: # 30C9
|
|
738
|
+
if self._gwy.msg_db:
|
|
739
|
+
# evohome zones only get initial temp from src + idx, so use zone sensor if newer
|
|
740
|
+
sql = f"""
|
|
741
|
+
SELECT dtm from messages WHERE verb in (' I', 'RP')
|
|
742
|
+
AND code = '30C9'
|
|
743
|
+
AND (plk LIKE '%{SZ_TEMPERATURE}%')
|
|
744
|
+
AND ((src = ? AND ctx = ?) OR src = ?)
|
|
745
|
+
"""
|
|
746
|
+
sensor_id = "aa:aaaaaa" # should not match any device_id
|
|
747
|
+
if self._sensor:
|
|
748
|
+
sensor_id = self._sensor.id
|
|
749
|
+
# custom SQLite query on MessageIndex
|
|
750
|
+
msgs = self._gwy.msg_db.qry(
|
|
751
|
+
sql, (self.id[:_ID_SLICE], self.idx, sensor_id[:_ID_SLICE])
|
|
752
|
+
)
|
|
753
|
+
if msgs and len(msgs) > 0:
|
|
754
|
+
msgs_sorted = sorted(msgs, reverse=True)
|
|
755
|
+
return msgs_sorted[0].payload.get(SZ_TEMPERATURE) # type: ignore[no-any-return]
|
|
756
|
+
return None
|
|
757
|
+
# else: TODO Q1 2026 remove remainder
|
|
738
758
|
return self._msg_value(Code._30C9, key=SZ_TEMPERATURE) # type: ignore[no-any-return]
|
|
739
759
|
|
|
740
760
|
@property
|
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.5
|
|
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
|
|
@@ -1,55 +1,55 @@
|
|
|
1
1
|
ramses_cli/__init__.py,sha256=uvGzWqOf4avvgzxJNSLFWEelIWqSZ-AeLAZzg5x58bc,397
|
|
2
|
-
ramses_cli/client.py,sha256=
|
|
3
|
-
ramses_cli/debug.py,sha256=
|
|
2
|
+
ramses_cli/client.py,sha256=h6liNKFXGsR2q9Unn81ushK53zqKgmkT0oAWNB5Jn0I,20369
|
|
3
|
+
ramses_cli/debug.py,sha256=PLcz-3PjUiMVqtD_p6VqTA92eHUM58lOBFXh_qgQ_wA,576
|
|
4
4
|
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
|
-
ramses_cli/utils/convert.py,sha256=
|
|
6
|
+
ramses_cli/utils/convert.py,sha256=N3LxGe3_0pclijtmYW-ChqCuPTzbkoJA4XNAnoSnBk0,1806
|
|
7
7
|
ramses_rf/__init__.py,sha256=vp2TyFGqc1fGQHsevhmaw0QEmSSCnZx7fqizKiEwHtw,1245
|
|
8
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=9nY1Wt2hjkBoDvDqopzPP3mPSH5-Q5XHPQuJnM2GHOw,21362
|
|
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=Psa75zsZf6GOvXIv4c37vJDbg5iVQ1gQCADkBNOz1m8,58912
|
|
13
13
|
ramses_rf/exceptions.py,sha256=mt_T7irqHSDKir6KLaf6oDglUIdrw0S40JbOrWJk5jc,3657
|
|
14
|
-
ramses_rf/gateway.py,sha256=
|
|
14
|
+
ramses_rf/gateway.py,sha256=B7BlrNHx26lIHzjgy6-4NMtohb_GATaBuucgsUUxRj4,22005
|
|
15
15
|
ramses_rf/helpers.py,sha256=TNk_QkpIOB3alOp1sqnA9LOzi4fuDCeapNlW3zTzNas,4250
|
|
16
16
|
ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
ramses_rf/schemas.py,sha256=
|
|
18
|
-
ramses_rf/version.py,sha256=
|
|
17
|
+
ramses_rf/schemas.py,sha256=0_qQZSGj2ixq4UCtMxoAeWklOJiM4bbBUzMsPw8gUPw,13475
|
|
18
|
+
ramses_rf/version.py,sha256=wV7QvKiXDL06V_DHo0XYcc-6CIYMlhztVGzCh1m45D8,125
|
|
19
19
|
ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
|
|
20
|
-
ramses_rf/device/base.py,sha256=
|
|
21
|
-
ramses_rf/device/heat.py,sha256=
|
|
22
|
-
ramses_rf/device/hvac.py,sha256=
|
|
20
|
+
ramses_rf/device/base.py,sha256=Tu5I8Lj7KplfRsIBQAYjilS6YPgTyjpU8qgKugMR2Jk,18281
|
|
21
|
+
ramses_rf/device/heat.py,sha256=T-ENFzH1AEOd0m4-TgV-Qk0TnLNir_pYmyKlJtxy6NI,54538
|
|
22
|
+
ramses_rf/device/hvac.py,sha256=vdgiPiLtCAGr7CVsGhQl6XuAFkyYdQSE_2AEdCmRl2I,48502
|
|
23
23
|
ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
|
|
24
24
|
ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
|
|
25
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=qMv7CuvZUKBNLpPRXgFA70NFRStgVJcw062h5mc3Y8Q,37034
|
|
28
28
|
ramses_tx/__init__.py,sha256=sqnjM7pUGJDmec6igTtKViSB8FLX49B5gwhAmcY9ERY,3596
|
|
29
|
-
ramses_tx/address.py,sha256=
|
|
30
|
-
ramses_tx/command.py,sha256=
|
|
29
|
+
ramses_tx/address.py,sha256=IuwUwZxykn3fP1UCRcv4D-zbTICBe2FJjDAFX5X6VoI,9108
|
|
30
|
+
ramses_tx/command.py,sha256=NeqWbdaWrXjP5KEZgXFicy_1UYOoUznRHN2ZfhVyDH0,125652
|
|
31
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
35
|
ramses_tx/gateway.py,sha256=Fl6EqAUU2DnLOiA2_87sS7VPBEyxA1a_ICCmg55WMMA,11494
|
|
36
|
-
ramses_tx/helpers.py,sha256=
|
|
36
|
+
ramses_tx/helpers.py,sha256=VIPSw0lrDrE3S92YchzeRo7G2AlQ_vE8OuYUF-rlpQw,33616
|
|
37
37
|
ramses_tx/logger.py,sha256=1iKRHKUaqHqGd76CkE_6mCVR0sYODtxshRRwfY61fTk,10426
|
|
38
|
-
ramses_tx/message.py,sha256
|
|
38
|
+
ramses_tx/message.py,sha256=HUSNiBgGlnWEBeBqb4K28GcU5oB7RbS9a0JC2p9SGe4,13676
|
|
39
39
|
ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
|
|
40
|
-
ramses_tx/packet.py,sha256=
|
|
41
|
-
ramses_tx/parsers.py,sha256=
|
|
42
|
-
ramses_tx/protocol.py,sha256=
|
|
40
|
+
ramses_tx/packet.py,sha256=_nzuInS_WhdOI26SYvgsdDqIaDvVNguc2YDwdPOVCbU,7661
|
|
41
|
+
ramses_tx/parsers.py,sha256=qQz8VOKaH26MUvo9AfUwhrnbH7VwXLxFc2gKJYvjXvY,148571
|
|
42
|
+
ramses_tx/protocol.py,sha256=ApA__ubfxM63c1OAxRcH6fhjMQTdC07SnSH9cGfWO2U,32427
|
|
43
43
|
ramses_tx/protocol_fsm.py,sha256=ZKtehCr_4TaDdfdlfidFLJaOVTYtaEq5h4tLqNIhb9s,26827
|
|
44
44
|
ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
45
|
ramses_tx/ramses.py,sha256=vp748Tf_a-56OMM8CWDA2ZktRfTuj0QVyPRcnsOstSM,53983
|
|
46
46
|
ramses_tx/schemas.py,sha256=bqKW_V0bR6VbBD8ZQiBExNtVdXs0fryVKe3GEhupgIo,13424
|
|
47
|
-
ramses_tx/transport.py,sha256
|
|
47
|
+
ramses_tx/transport.py,sha256=-Ikwj_Hy6JEnQPS9gBalV3H5AKQd3ATxKskR7Dy7_sE,59774
|
|
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=j7HXmU97PXKJClRv1saySQ_JZYHAxBznxUTdWH64uHE,123
|
|
51
|
+
ramses_rf-0.52.5.dist-info/METADATA,sha256=gGSPoi37fljrfFxDIVHXFUU5RuzGu6wwwD0JbvdwiVQ,4179
|
|
52
|
+
ramses_rf-0.52.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
53
|
+
ramses_rf-0.52.5.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
|
|
54
|
+
ramses_rf-0.52.5.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
|
|
55
|
+
ramses_rf-0.52.5.dist-info/RECORD,,
|
ramses_tx/address.py
CHANGED
|
@@ -39,7 +39,12 @@ class Address:
|
|
|
39
39
|
_SLUG = None
|
|
40
40
|
|
|
41
41
|
def __init__(self, device_id: DeviceIdT) -> None:
|
|
42
|
-
"""Create an address from a valid device
|
|
42
|
+
"""Create an address from a valid device ID.
|
|
43
|
+
|
|
44
|
+
:param device_id: The RAMSES II device ID (e.g., '01:123456')
|
|
45
|
+
:type device_id: DeviceIdT
|
|
46
|
+
:raises ValueError: If the device_id is not a valid format.
|
|
47
|
+
"""
|
|
43
48
|
|
|
44
49
|
# if device_id is None:
|
|
45
50
|
# device_id = NON_DEVICE_ID
|
|
@@ -91,7 +96,15 @@ class Address:
|
|
|
91
96
|
|
|
92
97
|
@classmethod
|
|
93
98
|
def convert_from_hex(cls, device_hex: str, friendly_id: bool = False) -> str:
|
|
94
|
-
"""Convert
|
|
99
|
+
"""Convert a 6-character hex string to a device ID.
|
|
100
|
+
|
|
101
|
+
:param device_hex: The hex string to convert (e.g., '06368E')
|
|
102
|
+
:type device_hex: str
|
|
103
|
+
:param friendly_id: If True, returns a named ID (e.g., 'CTL:145038'), defaults to False
|
|
104
|
+
:type friendly_id: bool
|
|
105
|
+
:return: The formatted device ID string
|
|
106
|
+
:rtype: str
|
|
107
|
+
"""
|
|
95
108
|
|
|
96
109
|
if device_hex == "FFFFFE": # aka '63:262142'
|
|
97
110
|
return ">null dev<" if friendly_id else ALL_DEVICE_ID
|
|
@@ -191,11 +204,13 @@ def is_valid_dev_id(value: str, dev_class: None | str = None) -> bool:
|
|
|
191
204
|
|
|
192
205
|
@lru_cache(maxsize=256) # there is definite benefit in caching this
|
|
193
206
|
def pkt_addrs(addr_fragment: str) -> tuple[Address, Address, Address, Address, Address]:
|
|
194
|
-
"""
|
|
195
|
-
|
|
196
|
-
returns: src_addr, dst_addr, addr_0, addr_1, addr_2
|
|
207
|
+
"""Parse address fields from a 30-character address fragment.
|
|
197
208
|
|
|
198
|
-
|
|
209
|
+
:param addr_fragment: The 30-char fragment (e.g., '01:078710 --:------ 01:144246')
|
|
210
|
+
:type addr_fragment: str
|
|
211
|
+
:return: A tuple of (src_addr, dst_addr, addr_0, addr_1, addr_2)
|
|
212
|
+
:rtype: tuple[Address, Address, Address, Address, Address]
|
|
213
|
+
:raises PacketAddrSetInvalid: If the address fields are not valid.
|
|
199
214
|
"""
|
|
200
215
|
# for debug: print(pkt_addrs.cache_info())
|
|
201
216
|
|
ramses_tx/command.py
CHANGED
|
@@ -2254,7 +2254,13 @@ class Command(Frame):
|
|
|
2254
2254
|
|
|
2255
2255
|
@classmethod # constructor for RQ|2E04
|
|
2256
2256
|
def get_system_mode(cls, ctl_id: DeviceIdT | str) -> Command:
|
|
2257
|
-
"""
|
|
2257
|
+
"""Get the mode of a system (c.f. parser_2e04).
|
|
2258
|
+
|
|
2259
|
+
:param ctl_id: The device ID of the controller
|
|
2260
|
+
:type ctl_id: DeviceIdT | str
|
|
2261
|
+
:return: A Command object for the RQ|2E04 message
|
|
2262
|
+
:rtype: Command
|
|
2263
|
+
"""
|
|
2258
2264
|
|
|
2259
2265
|
return cls.from_attrs(RQ, ctl_id, Code._2E04, FF)
|
|
2260
2266
|
|
|
@@ -2453,7 +2459,17 @@ class Command(Frame):
|
|
|
2453
2459
|
datetime: dt | str,
|
|
2454
2460
|
is_dst: bool = False,
|
|
2455
2461
|
) -> Command:
|
|
2456
|
-
"""
|
|
2462
|
+
"""Set the datetime of a system (c.f. parser_313f).
|
|
2463
|
+
|
|
2464
|
+
:param ctl_id: The device ID of the controller
|
|
2465
|
+
:type ctl_id: DeviceIdT | str
|
|
2466
|
+
:param datetime: The target date and time
|
|
2467
|
+
:type datetime: dt | str
|
|
2468
|
+
:param is_dst: Whether Daylight Saving Time is active, defaults to False
|
|
2469
|
+
:type is_dst: bool
|
|
2470
|
+
:return: A Command object for the W|313F message
|
|
2471
|
+
:rtype: Command
|
|
2472
|
+
"""
|
|
2457
2473
|
# .W --- 30:185469 01:037519 --:------ 313F 009 0060003A0C1B0107E5
|
|
2458
2474
|
|
|
2459
2475
|
dt_str = hex_from_dtm(datetime, is_dst=is_dst, incl_seconds=True)
|