ramses-rf 0.52.4__py3-none-any.whl → 0.53.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ramses_cli/client.py +168 -54
- ramses_cli/debug.py +1 -1
- ramses_cli/py.typed +0 -0
- ramses_cli/utils/convert.py +2 -2
- ramses_rf/__init__.py +2 -0
- 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 +9 -7
- ramses_rf/gateway.py +214 -27
- ramses_rf/schemas.py +2 -1
- ramses_rf/system/zones.py +22 -2
- ramses_rf/version.py +1 -1
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.53.0.dist-info}/METADATA +1 -1
- ramses_rf-0.53.0.dist-info/RECORD +56 -0
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.53.0.dist-info}/WHEEL +1 -1
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.53.0.dist-info}/licenses/LICENSE +1 -1
- ramses_tx/address.py +21 -6
- ramses_tx/command.py +19 -3
- ramses_tx/const.py +110 -23
- 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 +112 -23
- ramses_tx/protocol_fsm.py +28 -10
- ramses_tx/schemas.py +2 -2
- ramses_tx/transport.py +529 -47
- ramses_tx/version.py +1 -1
- ramses_rf-0.52.4.dist-info/RECORD +0 -55
- {ramses_rf-0.52.4.dist-info → ramses_rf-0.53.0.dist-info}/entry_points.txt +0 -0
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,10 +1045,12 @@ 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
|
+
# CRITICAL FIX: self.tcs might be None during early discovery
|
|
1052
|
+
if self.tcs:
|
|
1053
|
+
msgs += [self.tcs._msgz[task[_SZ_COMMAND].code][I_][True]]
|
|
1052
1054
|
# raise NotImplementedError
|
|
1053
1055
|
except KeyError:
|
|
1054
1056
|
pass
|
ramses_rf/gateway.py
CHANGED
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import logging
|
|
14
|
+
from collections.abc import Awaitable, Callable
|
|
14
15
|
from types import SimpleNamespace
|
|
15
16
|
from typing import TYPE_CHECKING, Any
|
|
16
17
|
|
|
@@ -83,7 +84,12 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
class Gateway(Engine):
|
|
86
|
-
"""The gateway class.
|
|
87
|
+
"""The gateway class.
|
|
88
|
+
|
|
89
|
+
This class serves as the primary interface for the RAMSES RF network. It manages
|
|
90
|
+
the serial connection (via ``Engine``), device discovery, schema maintenance,
|
|
91
|
+
and message dispatching.
|
|
92
|
+
"""
|
|
87
93
|
|
|
88
94
|
def __init__(
|
|
89
95
|
self,
|
|
@@ -94,8 +100,30 @@ class Gateway(Engine):
|
|
|
94
100
|
block_list: DeviceListT | None = None,
|
|
95
101
|
known_list: DeviceListT | None = None,
|
|
96
102
|
loop: asyncio.AbstractEventLoop | None = None,
|
|
103
|
+
transport_constructor: Callable[..., Awaitable[RamsesTransportT]] | None = None,
|
|
97
104
|
**kwargs: Any,
|
|
98
105
|
) -> None:
|
|
106
|
+
"""Initialize the Gateway instance.
|
|
107
|
+
|
|
108
|
+
:param port_name: The serial port name (e.g., '/dev/ttyUSB0') or None if using a file.
|
|
109
|
+
:type port_name: str | None
|
|
110
|
+
:param input_file: Path to a packet log file for playback, defaults to None.
|
|
111
|
+
:type input_file: str | None, optional
|
|
112
|
+
:param port_config: Configuration dictionary for the serial port, defaults to None.
|
|
113
|
+
:type port_config: PortConfigT | None, optional
|
|
114
|
+
:param packet_log: Configuration for packet logging, defaults to None.
|
|
115
|
+
:type packet_log: PktLogConfigT | None, optional
|
|
116
|
+
:param block_list: A list of device IDs to block/ignore, defaults to None.
|
|
117
|
+
:type block_list: DeviceListT | None, optional
|
|
118
|
+
:param known_list: A list of known device IDs and their traits, defaults to None.
|
|
119
|
+
:type known_list: DeviceListT | None, optional
|
|
120
|
+
:param loop: The asyncio event loop to use, defaults to None.
|
|
121
|
+
:type loop: asyncio.AbstractEventLoop | None, optional
|
|
122
|
+
:param transport_constructor: A factory for creating the transport layer, defaults to None.
|
|
123
|
+
:type transport_constructor: Callable[..., Awaitable[RamsesTransportT]] | None, optional
|
|
124
|
+
:param kwargs: Additional configuration parameters passed to the engine and schema.
|
|
125
|
+
:type kwargs: Any
|
|
126
|
+
"""
|
|
99
127
|
if kwargs.pop("debug_mode", None):
|
|
100
128
|
_LOGGER.setLevel(logging.DEBUG)
|
|
101
129
|
|
|
@@ -110,6 +138,7 @@ class Gateway(Engine):
|
|
|
110
138
|
block_list=block_list,
|
|
111
139
|
known_list=known_list,
|
|
112
140
|
loop=loop,
|
|
141
|
+
transport_constructor=transport_constructor,
|
|
113
142
|
**SCH_ENGINE_CONFIG(config),
|
|
114
143
|
)
|
|
115
144
|
|
|
@@ -132,13 +161,23 @@ class Gateway(Engine):
|
|
|
132
161
|
self.msg_db: MessageIndex | None = None
|
|
133
162
|
|
|
134
163
|
def __repr__(self) -> str:
|
|
164
|
+
"""Return a string representation of the Gateway.
|
|
165
|
+
|
|
166
|
+
:returns: A string describing the gateway's input source (port or file).
|
|
167
|
+
:rtype: str
|
|
168
|
+
"""
|
|
135
169
|
if not self.ser_name:
|
|
136
170
|
return f"Gateway(input_file={self._input_file})"
|
|
137
171
|
return f"Gateway(port_name={self.ser_name}, port_config={self._port_config})"
|
|
138
172
|
|
|
139
173
|
@property
|
|
140
174
|
def hgi(self) -> HgiGateway | None:
|
|
141
|
-
"""Return the active HGI80-compatible gateway device, if known.
|
|
175
|
+
"""Return the active HGI80-compatible gateway device, if known.
|
|
176
|
+
|
|
177
|
+
:returns: The gateway device instance or None if the transport is not set up
|
|
178
|
+
or the HGI ID is not found.
|
|
179
|
+
:rtype: HgiGateway | None
|
|
180
|
+
"""
|
|
142
181
|
if not self._transport:
|
|
143
182
|
return None
|
|
144
183
|
if device_id := self._transport.get_extra_info(SZ_ACTIVE_HGI):
|
|
@@ -152,10 +191,21 @@ class Gateway(Engine):
|
|
|
152
191
|
start_discovery: bool = True,
|
|
153
192
|
cached_packets: dict[str, str] | None = None,
|
|
154
193
|
) -> None:
|
|
155
|
-
"""Start the Gateway and Initiate discovery as required.
|
|
194
|
+
"""Start the Gateway and Initiate discovery as required.
|
|
195
|
+
|
|
196
|
+
This method initializes packet logging, the SQLite index, loads the schema,
|
|
197
|
+
and optionally restores state from cached packets before starting the transport.
|
|
198
|
+
|
|
199
|
+
:param start_discovery: Whether to initiate the discovery process after start, defaults to True.
|
|
200
|
+
:type start_discovery: bool, optional
|
|
201
|
+
:param cached_packets: A dictionary of packet strings used to restore state, defaults to None.
|
|
202
|
+
:type cached_packets: dict[str, str] | None, optional
|
|
203
|
+
:returns: None
|
|
204
|
+
:rtype: None
|
|
205
|
+
"""
|
|
156
206
|
|
|
157
207
|
def initiate_discovery(dev_list: list[Device], sys_list: list[Evohome]) -> None:
|
|
158
|
-
_LOGGER.debug("
|
|
208
|
+
_LOGGER.debug("Engine: Initiating/enabling discovery...")
|
|
159
209
|
|
|
160
210
|
# [d._start_discovery_poller() for d in devs]
|
|
161
211
|
for device in dev_list:
|
|
@@ -201,10 +251,21 @@ class Gateway(Engine):
|
|
|
201
251
|
initiate_discovery(self.devices, self.systems)
|
|
202
252
|
|
|
203
253
|
def create_sqlite_message_index(self) -> None:
|
|
254
|
+
"""Initialize the SQLite MessageIndex.
|
|
255
|
+
|
|
256
|
+
:returns: None
|
|
257
|
+
:rtype: None
|
|
258
|
+
"""
|
|
204
259
|
self.msg_db = MessageIndex() # start the index
|
|
205
260
|
|
|
206
261
|
async def stop(self) -> None:
|
|
207
|
-
"""Stop the Gateway and tidy up.
|
|
262
|
+
"""Stop the Gateway and tidy up.
|
|
263
|
+
|
|
264
|
+
Stops the message database and the underlying engine/transport.
|
|
265
|
+
|
|
266
|
+
:returns: None
|
|
267
|
+
:rtype: None
|
|
268
|
+
"""
|
|
208
269
|
|
|
209
270
|
if self.msg_db:
|
|
210
271
|
self.msg_db.stop()
|
|
@@ -214,6 +275,12 @@ class Gateway(Engine):
|
|
|
214
275
|
"""Pause the (unpaused) gateway (disables sending/discovery).
|
|
215
276
|
|
|
216
277
|
There is the option to save other objects, as `args`.
|
|
278
|
+
|
|
279
|
+
:param args: Additional objects/state to save during the pause.
|
|
280
|
+
:type args: Any
|
|
281
|
+
:returns: None
|
|
282
|
+
:rtype: None
|
|
283
|
+
:raises RuntimeError: If the engine fails to pause.
|
|
217
284
|
"""
|
|
218
285
|
_LOGGER.debug("Gateway: Pausing engine...")
|
|
219
286
|
|
|
@@ -229,6 +296,9 @@ class Gateway(Engine):
|
|
|
229
296
|
"""Resume the (paused) gateway (enables sending/discovery, if applicable).
|
|
230
297
|
|
|
231
298
|
Will restore other objects, as `args`.
|
|
299
|
+
|
|
300
|
+
:returns: A tuple of arguments saved during the pause.
|
|
301
|
+
:rtype: tuple[Any]
|
|
232
302
|
"""
|
|
233
303
|
args: tuple[Any]
|
|
234
304
|
|
|
@@ -241,7 +311,13 @@ class Gateway(Engine):
|
|
|
241
311
|
def get_state(
|
|
242
312
|
self, include_expired: bool = False
|
|
243
313
|
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
244
|
-
"""Return the current schema & state (may include expired packets).
|
|
314
|
+
"""Return the current schema & state (may include expired packets).
|
|
315
|
+
|
|
316
|
+
:param include_expired: If True, include expired packets in the state, defaults to False.
|
|
317
|
+
:type include_expired: bool, optional
|
|
318
|
+
:returns: A tuple containing the schema dictionary and the packet log dictionary.
|
|
319
|
+
:rtype: tuple[dict[str, Any], dict[str, str]]
|
|
320
|
+
"""
|
|
245
321
|
|
|
246
322
|
self._pause()
|
|
247
323
|
|
|
@@ -287,10 +363,21 @@ class Gateway(Engine):
|
|
|
287
363
|
async def _restore_cached_packets(
|
|
288
364
|
self, packets: dict[str, str], _clear_state: bool = False
|
|
289
365
|
) -> None:
|
|
290
|
-
"""Restore cached packets (may include expired packets).
|
|
366
|
+
"""Restore cached packets (may include expired packets).
|
|
367
|
+
|
|
368
|
+
This process uses a temporary transport to replay the packet history
|
|
369
|
+
into the message handler.
|
|
370
|
+
|
|
371
|
+
:param packets: A dictionary of packet strings.
|
|
372
|
+
:type packets: dict[str, str]
|
|
373
|
+
:param _clear_state: If True, reset internal state before restoration (for testing), defaults to False.
|
|
374
|
+
:type _clear_state: bool, optional
|
|
375
|
+
:returns: None
|
|
376
|
+
:rtype: None
|
|
377
|
+
"""
|
|
291
378
|
|
|
292
379
|
def clear_state() -> None:
|
|
293
|
-
_LOGGER.info("
|
|
380
|
+
_LOGGER.info("Gateway: Clearing existing schema/state...")
|
|
294
381
|
|
|
295
382
|
# self._schema = {}
|
|
296
383
|
|
|
@@ -303,7 +390,7 @@ class Gateway(Engine):
|
|
|
303
390
|
|
|
304
391
|
tmp_transport: RamsesTransportT # mypy hint
|
|
305
392
|
|
|
306
|
-
_LOGGER.debug("
|
|
393
|
+
_LOGGER.debug("Gateway: Restoring a cached packet log...")
|
|
307
394
|
self._pause()
|
|
308
395
|
|
|
309
396
|
if _clear_state: # only intended for test suite use
|
|
@@ -339,11 +426,18 @@ class Gateway(Engine):
|
|
|
339
426
|
|
|
340
427
|
await tmp_transport.get_extra_info(SZ_READER_TASK)
|
|
341
428
|
|
|
342
|
-
_LOGGER.debug("
|
|
429
|
+
_LOGGER.debug("Gateway: Restored, resuming")
|
|
343
430
|
self._resume()
|
|
344
431
|
|
|
345
432
|
def _add_device(self, dev: Device) -> None: # TODO: also: _add_system()
|
|
346
|
-
"""Add a device to the gateway (called by devices during instantiation).
|
|
433
|
+
"""Add a device to the gateway (called by devices during instantiation).
|
|
434
|
+
|
|
435
|
+
:param dev: The device instance to add.
|
|
436
|
+
:type dev: Device
|
|
437
|
+
:returns: None
|
|
438
|
+
:rtype: None
|
|
439
|
+
:raises LookupError: If the device already exists in the gateway.
|
|
440
|
+
"""
|
|
347
441
|
|
|
348
442
|
if dev.id in self.device_by_id:
|
|
349
443
|
raise LookupError(f"Device already exists: {dev.id}")
|
|
@@ -360,13 +454,25 @@ class Gateway(Engine):
|
|
|
360
454
|
child_id: str | None = None,
|
|
361
455
|
is_sensor: bool | None = None,
|
|
362
456
|
) -> Device: # TODO: **schema/traits) -> Device: # may: LookupError
|
|
363
|
-
"""Return a device,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
All devices have traits, but only
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
457
|
+
"""Return a device, creating it if it does not already exist.
|
|
458
|
+
|
|
459
|
+
This method uses provided traits to create or update a device and optionally
|
|
460
|
+
passes a message for it to handle. All devices have traits, but only
|
|
461
|
+
controllers (CTL, UFC) have a schema.
|
|
462
|
+
|
|
463
|
+
:param device_id: The unique identifier for the device (e.g., '01:123456').
|
|
464
|
+
:type device_id: DeviceIdT
|
|
465
|
+
:param msg: An optional initial message for the device to process, defaults to None.
|
|
466
|
+
:type msg: Message | None, optional
|
|
467
|
+
:param parent: The parent entity of this device, if any, defaults to None.
|
|
468
|
+
:type parent: Parent | None, optional
|
|
469
|
+
:param child_id: The specific ID of the child component if applicable, defaults to None.
|
|
470
|
+
:type child_id: str | None, optional
|
|
471
|
+
:param is_sensor: Indicates if this device should be treated as a sensor, defaults to None.
|
|
472
|
+
:type is_sensor: bool | None, optional
|
|
473
|
+
:returns: The existing or newly created device instance.
|
|
474
|
+
:rtype: Device
|
|
475
|
+
:raises LookupError: If the device ID is blocked or not in the allowed known_list.
|
|
370
476
|
"""
|
|
371
477
|
|
|
372
478
|
def check_filter_lists(dev_id: DeviceIdT) -> None: # may: LookupError
|
|
@@ -434,7 +540,21 @@ class Gateway(Engine):
|
|
|
434
540
|
device_id: DeviceIdT,
|
|
435
541
|
create_device: bool = False,
|
|
436
542
|
) -> Device | Fakeable:
|
|
437
|
-
"""Create a faked device.
|
|
543
|
+
"""Create a faked device.
|
|
544
|
+
|
|
545
|
+
Converts an existing device to a fake device, or creates a new fake device
|
|
546
|
+
if it satisfies strict criteria (valid ID, presence in known_list).
|
|
547
|
+
|
|
548
|
+
:param device_id: The ID of the device to fake.
|
|
549
|
+
:type device_id: DeviceIdT
|
|
550
|
+
:param create_device: If True, allow creation of a new device if it doesn't exist, defaults to False.
|
|
551
|
+
:type create_device: bool, optional
|
|
552
|
+
:returns: The faked device instance.
|
|
553
|
+
:rtype: Device | Fakeable
|
|
554
|
+
:raises TypeError: If the device ID is invalid or the device is not fakeable.
|
|
555
|
+
:raises LookupError: If the device does not exist and create_device is False,
|
|
556
|
+
or if create_device is True but the ID is not in known_list.
|
|
557
|
+
"""
|
|
438
558
|
|
|
439
559
|
if not is_valid_dev_id(device_id):
|
|
440
560
|
raise TypeError(f"The device id is not valid: {device_id}")
|
|
@@ -452,7 +572,11 @@ class Gateway(Engine):
|
|
|
452
572
|
|
|
453
573
|
@property
|
|
454
574
|
def tcs(self) -> Evohome | None:
|
|
455
|
-
"""Return the primary TCS, if any.
|
|
575
|
+
"""Return the primary Temperature Control System (TCS), if any.
|
|
576
|
+
|
|
577
|
+
:returns: The primary Evohome system or None.
|
|
578
|
+
:rtype: Evohome | None
|
|
579
|
+
"""
|
|
456
580
|
|
|
457
581
|
if self._tcs is None and self.systems:
|
|
458
582
|
self._tcs = self.systems[0]
|
|
@@ -465,6 +589,9 @@ class Gateway(Engine):
|
|
|
465
589
|
Unlike orphans, which are always instantiated when a schema is loaded, these
|
|
466
590
|
devices may/may not exist. However, if they are ever instantiated, they should
|
|
467
591
|
be given these traits.
|
|
592
|
+
|
|
593
|
+
:returns: A dictionary where keys are device IDs and values are their traits.
|
|
594
|
+
:rtype: DeviceListT
|
|
468
595
|
"""
|
|
469
596
|
|
|
470
597
|
result = self._include # could be devices here, not (yet) in gwy.devices
|
|
@@ -479,6 +606,11 @@ class Gateway(Engine):
|
|
|
479
606
|
|
|
480
607
|
@property
|
|
481
608
|
def system_by_id(self) -> dict[DeviceIdT, Evohome]:
|
|
609
|
+
"""Return a mapping of device IDs to their associated Evohome systems.
|
|
610
|
+
|
|
611
|
+
:returns: A dictionary mapping DeviceId to Evohome instances.
|
|
612
|
+
:rtype: dict[DeviceIdT, Evohome]
|
|
613
|
+
"""
|
|
482
614
|
return {
|
|
483
615
|
d.id: d.tcs
|
|
484
616
|
for d in self.devices
|
|
@@ -487,6 +619,11 @@ class Gateway(Engine):
|
|
|
487
619
|
|
|
488
620
|
@property
|
|
489
621
|
def systems(self) -> list[Evohome]:
|
|
622
|
+
"""Return a list of all identified Evohome systems.
|
|
623
|
+
|
|
624
|
+
:returns: A list of Evohome system instances.
|
|
625
|
+
:rtype: list[Evohome]
|
|
626
|
+
"""
|
|
490
627
|
return list(self.system_by_id.values())
|
|
491
628
|
|
|
492
629
|
@property
|
|
@@ -498,6 +635,9 @@ class Gateway(Engine):
|
|
|
498
635
|
- schema (everything else)
|
|
499
636
|
- known_list
|
|
500
637
|
- block_list
|
|
638
|
+
|
|
639
|
+
:returns: A dictionary representing the current internal configuration state.
|
|
640
|
+
:rtype: dict[str, Any]
|
|
501
641
|
"""
|
|
502
642
|
|
|
503
643
|
return {
|
|
@@ -519,6 +659,9 @@ class Gateway(Engine):
|
|
|
519
659
|
Orphans are devices that 'exist' but don't yet have a place in the schema
|
|
520
660
|
hierarchy (if ever): therefore, they are instantiated when the schema is loaded,
|
|
521
661
|
just like the other devices in the schema.
|
|
662
|
+
|
|
663
|
+
:returns: A dictionary representing the entire system schema structure.
|
|
664
|
+
:rtype: dict[str, Any]
|
|
522
665
|
"""
|
|
523
666
|
|
|
524
667
|
schema: dict[str, Any] = {SZ_MAIN_TCS: self.tcs.ctl.id if self.tcs else None}
|
|
@@ -546,10 +689,20 @@ class Gateway(Engine):
|
|
|
546
689
|
|
|
547
690
|
@property
|
|
548
691
|
def params(self) -> dict[str, Any]:
|
|
692
|
+
"""Return the parameters for all devices.
|
|
693
|
+
|
|
694
|
+
:returns: A dictionary containing parameters for every device in the gateway.
|
|
695
|
+
:rtype: dict[str, Any]
|
|
696
|
+
"""
|
|
549
697
|
return {SZ_DEVICES: {d.id: d.params for d in sorted(self.devices)}}
|
|
550
698
|
|
|
551
699
|
@property
|
|
552
700
|
def status(self) -> dict[str, Any]:
|
|
701
|
+
"""Return the status for all devices and the transport rate.
|
|
702
|
+
|
|
703
|
+
:returns: A dictionary containing device statuses and transmission rate.
|
|
704
|
+
:rtype: dict[str, Any]
|
|
705
|
+
"""
|
|
553
706
|
tx_rate = self._transport.get_extra_info("tx_rate") if self._transport else None
|
|
554
707
|
return {
|
|
555
708
|
SZ_DEVICES: {d.id: d.status for d in sorted(self.devices)},
|
|
@@ -557,7 +710,15 @@ class Gateway(Engine):
|
|
|
557
710
|
}
|
|
558
711
|
|
|
559
712
|
def _msg_handler(self, msg: Message) -> None:
|
|
560
|
-
"""A callback to handle messages from the protocol stack.
|
|
713
|
+
"""A callback to handle messages from the protocol stack.
|
|
714
|
+
|
|
715
|
+
Handles message reassembly (fragmentation) and dispatches the message for processing.
|
|
716
|
+
|
|
717
|
+
:param msg: The incoming message to handle.
|
|
718
|
+
:type msg: Message
|
|
719
|
+
:returns: None
|
|
720
|
+
:rtype: None
|
|
721
|
+
"""
|
|
561
722
|
|
|
562
723
|
super()._msg_handler(msg)
|
|
563
724
|
|
|
@@ -585,9 +746,20 @@ class Gateway(Engine):
|
|
|
585
746
|
) -> asyncio.Task[Packet]:
|
|
586
747
|
"""Wrapper to schedule an async_send_cmd() and return the Task.
|
|
587
748
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
749
|
+
:param cmd: The command object to send.
|
|
750
|
+
:type cmd: Command
|
|
751
|
+
:param gap_duration: The gap between repeats (in seconds), defaults to DEFAULT_GAP_DURATION.
|
|
752
|
+
:type gap_duration: float, optional
|
|
753
|
+
:param num_repeats: Number of times to repeat the command (0 = once, 1 = twice, etc.), defaults to DEFAULT_NUM_REPEATS.
|
|
754
|
+
:type num_repeats: int, optional
|
|
755
|
+
:param priority: The priority of the command, defaults to Priority.DEFAULT.
|
|
756
|
+
:type priority: Priority, optional
|
|
757
|
+
:param timeout: Time to wait for a send to complete, defaults to DEFAULT_SEND_TIMEOUT.
|
|
758
|
+
:type timeout: float, optional
|
|
759
|
+
:param wait_for_reply: Whether to wait for a reply packet, defaults to DEFAULT_WAIT_FOR_REPLY.
|
|
760
|
+
:type wait_for_reply: bool | None, optional
|
|
761
|
+
:returns: The asyncio Task wrapping the send operation.
|
|
762
|
+
:rtype: asyncio.Task[Packet]
|
|
591
763
|
"""
|
|
592
764
|
|
|
593
765
|
coro = self.async_send_cmd(
|
|
@@ -620,9 +792,24 @@ class Gateway(Engine):
|
|
|
620
792
|
If wait_for_reply is True (*and* the Command has a rx_header), return the
|
|
621
793
|
reply Packet. Otherwise, simply return the echo Packet.
|
|
622
794
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
795
|
+
:param cmd: The command object to send.
|
|
796
|
+
:type cmd: Command
|
|
797
|
+
:param gap_duration: The gap between repeats (in seconds), defaults to DEFAULT_GAP_DURATION.
|
|
798
|
+
:type gap_duration: float, optional
|
|
799
|
+
:param num_repeats: Number of times to repeat the command, defaults to DEFAULT_NUM_REPEATS.
|
|
800
|
+
:type num_repeats: int, optional
|
|
801
|
+
:param priority: The priority of the command, defaults to Priority.DEFAULT.
|
|
802
|
+
:type priority: Priority, optional
|
|
803
|
+
:param max_retries: Maximum number of retries if sending fails, defaults to DEFAULT_MAX_RETRIES.
|
|
804
|
+
:type max_retries: int, optional
|
|
805
|
+
:param timeout: Time to wait for the command to send, defaults to DEFAULT_SEND_TIMEOUT.
|
|
806
|
+
:type timeout: float, optional
|
|
807
|
+
:param wait_for_reply: Whether to wait for a reply packet, defaults to DEFAULT_WAIT_FOR_REPLY.
|
|
808
|
+
:type wait_for_reply: bool | None, optional
|
|
809
|
+
:returns: The echo packet or reply packet depending on wait_for_reply.
|
|
810
|
+
:rtype: Packet
|
|
811
|
+
:raises ProtocolSendFailed: If the command was sent but no reply/echo was received.
|
|
812
|
+
:raises ProtocolError: If the system failed to attempt the transmission.
|
|
626
813
|
"""
|
|
627
814
|
|
|
628
815
|
return await super().async_send_cmd(
|
ramses_rf/schemas.py
CHANGED
|
@@ -237,6 +237,7 @@ SCH_GLOBAL_SCHEMAS_DICT = { # System schemas - can be 0-many Heat/HVAC schemas
|
|
|
237
237
|
vol.Optional(SCH_DEVICE_ID_ANY): SCH_VCS, # must be after SCH_DEVICE_ID_CTL
|
|
238
238
|
vol.Optional(SZ_ORPHANS_HEAT): vol.All([SCH_DEVICE_ID_ANY], vol.Unique()),
|
|
239
239
|
vol.Optional(SZ_ORPHANS_HVAC): vol.All([SCH_DEVICE_ID_ANY], vol.Unique()),
|
|
240
|
+
vol.Optional("transport_constructor"): vol.Any(callable, None),
|
|
240
241
|
}
|
|
241
242
|
SCH_GLOBAL_SCHEMAS = vol.Schema(SCH_GLOBAL_SCHEMAS_DICT, extra=vol.PREVENT_EXTRA)
|
|
242
243
|
|
|
@@ -285,7 +286,7 @@ SCH_GLOBAL_CONFIG = (
|
|
|
285
286
|
#
|
|
286
287
|
# 6/7: External Schemas, to be used by clients of this library
|
|
287
288
|
def NormaliseRestoreCache() -> Callable[[bool | dict[str, bool]], dict[str, bool]]:
|
|
288
|
-
"""Convert a
|
|
289
|
+
"""Convert a shorthand restore_cache bool to a dict.
|
|
289
290
|
|
|
290
291
|
restore_cache: bool -> restore_cache:
|
|
291
292
|
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.
|
|
3
|
+
Version: 0.53.0
|
|
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
|