ramses-rf 0.51.4__py3-none-any.whl → 0.51.6__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 CHANGED
@@ -483,7 +483,6 @@ async def async_main(command: str, lib_kwargs: dict, **kwargs: Any) -> None:
483
483
  print(f"{COLORS.get(msg.verb)}{dtm} {msg}"[:con_cols])
484
484
 
485
485
  serial_port, lib_kwargs = normalise_config(lib_kwargs)
486
- assert isinstance(lib_kwargs.get(SZ_INPUT_FILE), str)
487
486
 
488
487
  if kwargs["restore_schema"]:
489
488
  print(" - restoring client schema from a HA cache...")
ramses_rf/const.py CHANGED
@@ -61,6 +61,8 @@ from ramses_tx.const import ( # noqa: F401
61
61
  SZ_REMAINING_DAYS as SZ_REMAINING_DAYS,
62
62
  SZ_REMAINING_MINS as SZ_REMAINING_MINS,
63
63
  SZ_REMAINING_PERCENT as SZ_REMAINING_PERCENT,
64
+ SZ_REQ_REASON as SZ_REQ_REASON,
65
+ SZ_REQ_SPEED as SZ_REQ_SPEED,
64
66
  SZ_SCHEDULE as SZ_SCHEDULE,
65
67
  SZ_SENSOR as SZ_SENSOR,
66
68
  SZ_SETPOINT as SZ_SETPOINT,
ramses_rf/database.py CHANGED
@@ -29,6 +29,22 @@ class Params(TypedDict):
29
29
  _LOGGER = logging.getLogger(__name__)
30
30
 
31
31
 
32
+ def _setup_db_adapters() -> None:
33
+ """Set up the database adapters and converters."""
34
+
35
+ def adapt_datetime_iso(val: dt) -> str:
36
+ """Adapt datetime.datetime to timezone-naive ISO 8601 datetime."""
37
+ return val.isoformat(timespec="microseconds")
38
+
39
+ sqlite3.register_adapter(dt, adapt_datetime_iso)
40
+
41
+ def convert_datetime(val: bytes) -> dt:
42
+ """Convert ISO 8601 datetime to datetime.datetime object."""
43
+ return dt.fromisoformat(val.decode())
44
+
45
+ sqlite3.register_converter("dtm", convert_datetime)
46
+
47
+
32
48
  class MessageIndex:
33
49
  """A simple in-memory SQLite3 database for indexing messages."""
34
50
 
@@ -40,7 +56,7 @@ class MessageIndex:
40
56
  self._cx = sqlite3.connect(":memory:") # Connect to a SQLite DB in memory
41
57
  self._cu = self._cx.cursor() # Create a cursor
42
58
 
43
- self._setup_db_adapters() # dtm adapter/converter
59
+ _setup_db_adapters() # dtm adapter/converter
44
60
  self._setup_db_schema()
45
61
 
46
62
  self._lock = asyncio.Lock()
@@ -76,23 +92,19 @@ class MessageIndex:
76
92
  """Return the messages in the index in a threadsafe way."""
77
93
  return self._msgs
78
94
 
79
- def _setup_db_adapters(self) -> None:
80
- """Setup the database adapters and converters."""
81
-
82
- def adapt_datetime_iso(val: dt) -> str:
83
- """Adapt datetime.datetime to timezone-naive ISO 8601 datetime."""
84
- return val.isoformat(timespec="microseconds")
85
-
86
- sqlite3.register_adapter(dt, adapt_datetime_iso)
87
-
88
- def convert_datetime(val: bytes) -> dt:
89
- """Convert ISO 8601 datetime to datetime.datetime object."""
90
- return dt.fromisoformat(val.decode())
95
+ def _setup_db_schema(self) -> None:
96
+ """Set up the message database schema.
91
97
 
92
- sqlite3.register_converter("dtm", convert_datetime)
98
+ Fields:
93
99
 
94
- def _setup_db_schema(self) -> None:
95
- """Setup the dayabase schema."""
100
+ - dtm message timestamp
101
+ - verb _I, RQ etc.
102
+ - src message origin address
103
+ - dst message destination address
104
+ - code packet code aka command class e.g. _0005, _31DA
105
+ - ctx message context, created from payload as index + extra markers (Heat)
106
+ - hdr packet header e.g. 000C|RP|01:223036|0208 (see: src/ramses_tx/frame.py)
107
+ """
96
108
 
97
109
  self._cu.execute(
98
110
  """
@@ -120,14 +132,14 @@ class MessageIndex:
120
132
  async def _housekeeping_loop(self) -> None:
121
133
  """Periodically remove stale messages from the index."""
122
134
 
123
- def housekeeping(dt_now: dt, _cutoff: td = td(days=1)) -> None:
135
+ async def housekeeping(dt_now: dt, _cutoff: td = td(days=1)) -> None:
124
136
  dtm = (dt_now - _cutoff).isoformat(timespec="microseconds")
125
137
 
126
138
  self._cu.execute("SELECT dtm FROM messages WHERE dtm => ?", (dtm,))
127
139
  rows = self._cu.fetchall()
128
140
 
129
141
  try: # make this operation atomic, i.e. update self._msgs only on success
130
- # await self._lock.acquire()
142
+ await self._lock.acquire()
131
143
  self._cu.execute("DELETE FROM messages WHERE dtm < ?", (dtm,))
132
144
  msgs = OrderedDict({row[0]: self._msgs[row[0]] for row in rows})
133
145
  self._cx.commit()
@@ -137,19 +149,19 @@ class MessageIndex:
137
149
  else:
138
150
  self._msgs = msgs
139
151
  finally:
140
- pass # self._lock.release()
152
+ self._lock.release()
141
153
 
142
154
  while True:
143
155
  self._last_housekeeping = dt.now()
144
156
  await asyncio.sleep(3600)
145
- housekeeping(self._last_housekeeping)
157
+ await housekeeping(self._last_housekeeping)
146
158
 
147
159
  def add(self, msg: Message) -> Message | None:
148
160
  """Add a single message to the index.
149
161
 
150
162
  Returns any message that was removed because it had the same header.
151
163
 
152
- Throws a warning is there is a duplicate dtm.
164
+ Throws a warning if there is a duplicate dtm.
153
165
  """ # TODO: eventually, may be better to use SqlAlchemy
154
166
 
155
167
  dup: tuple[Message, ...] = tuple() # avoid UnboundLocalError
@@ -204,7 +216,9 @@ class MessageIndex:
204
216
 
205
217
  return msgs[0] if msgs else None
206
218
 
207
- def rem(self, msg: Message | None = None, **kwargs: str) -> tuple[Message, ...]:
219
+ def rem(
220
+ self, msg: Message | None = None, **kwargs: str
221
+ ) -> tuple[Message, ...] | None:
208
222
  """Remove a set of message(s) from the index.
209
223
 
210
224
  Returns any messages that were removed.
@@ -215,6 +229,7 @@ class MessageIndex:
215
229
  if msg:
216
230
  kwargs["dtm"] = msg.dtm.isoformat(timespec="microseconds")
217
231
 
232
+ msgs = None
218
233
  try: # make this operation atomic, i.e. update self._msgs only on success
219
234
  # await self._lock.acquire()
220
235
  msgs = self._delete_from(**kwargs)
@@ -277,8 +292,8 @@ class MessageIndex:
277
292
  def all(self, include_expired: bool = False) -> tuple[Message, ...]:
278
293
  """Return all messages from the index."""
279
294
 
280
- # self.cursor.execute("SELECT * FROM messages")
281
- # return [self._megs[row[0]] for row in self.cursor.fetchall()]
295
+ # self._cu.execute("SELECT * FROM messages")
296
+ # return tuple(self._msgs[row[0]] for row in self._cu.fetchall())
282
297
 
283
298
  return tuple(
284
299
  m for m in self._msgs.values() if include_expired or not m._expired
ramses_rf/device/base.py CHANGED
@@ -62,7 +62,7 @@ class DeviceBase(Entity):
62
62
 
63
63
  # FIXME: ZZZ entities must know their parent device ID and their own idx
64
64
  self._z_id = dev_addr.id # the responsible device is itself
65
- self._z_idx = None # depends upon it's location in the schema
65
+ self._z_idx = None # depends upon its location in the schema
66
66
 
67
67
  self.id: DeviceIdT = dev_addr.id
68
68
 
@@ -95,7 +95,7 @@ class DeviceBase(Entity):
95
95
 
96
96
  if traits.get(SZ_FAKED): # class & alias are done elsewhere
97
97
  if not isinstance(self, Fakeable):
98
- raise TypeError(f"Device is not fakable: {self} (traits={traits})")
98
+ raise TypeError(f"Device is not fakeable: {self} (traits={traits})")
99
99
  self._make_fake()
100
100
 
101
101
  self._scheme = traits.get(SZ_SCHEME)
ramses_rf/device/hvac.py CHANGED
@@ -31,6 +31,8 @@ from ramses_rf.const import (
31
31
  SZ_REMAINING_DAYS,
32
32
  SZ_REMAINING_MINS,
33
33
  SZ_REMAINING_PERCENT,
34
+ SZ_REQ_REASON,
35
+ SZ_REQ_SPEED,
34
36
  SZ_SPEED_CAPABILITIES,
35
37
  SZ_SUPPLY_FAN_SPEED,
36
38
  SZ_SUPPLY_FLOW,
@@ -447,10 +449,10 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
447
449
  def fan_info(self) -> str | None:
448
450
  """
449
451
  Extract fan info description from _31D9 or _31DA message payload, e.g. "speed 2, medium".
450
- By its name, the result is automatically displayed in HA Climate UI.
452
+ By its name, the result is picked up by a sensor in HA Climate UI.
451
453
  Some manufacturers (Orcon, Vasco) include the fan mode (auto, manual), others don't (Itho).
452
454
 
453
- :return: a string describing mode, speed
455
+ :return: a string describing fan mode, speed
454
456
  """
455
457
  if Code._31D9 in self._msgs:
456
458
  # Itho, Vasco D60 and ClimaRad (MiniBox fan) send mode/speed in _31D9
@@ -464,9 +466,10 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
464
466
  @property
465
467
  def indoor_humidity(self) -> float | None:
466
468
  """
467
- Extract humidity value from _12A0 or _31DA JSON message payload
469
+ Extract indoor_humidity from MessageIndex _12A0 or _31DA payload
470
+ Just a demo for SQLite query helper at the moment.
468
471
 
469
- :return: percentage <= 1.0
472
+ :return: float RH value from 0.0 to 1.0 = 100%
470
473
  """
471
474
  if Code._12A0 in self._msgs and isinstance(
472
475
  self._msgs[Code._12A0].payload, list
@@ -506,6 +509,18 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
506
509
  def remaining_mins(self) -> int | None:
507
510
  return self._msg_value(Code._31DA, key=SZ_REMAINING_MINS)
508
511
 
512
+ @property
513
+ def request_fan_speed(self) -> float | None:
514
+ return self._msg_value(Code._2210, key=SZ_REQ_SPEED)
515
+
516
+ @property
517
+ def request_src(self) -> str | None:
518
+ """
519
+ Orcon, others?
520
+ :return: source sensor of auto speed request: IDL, CO2 or HUM
521
+ """
522
+ return self._msg_value(Code._2210, key=SZ_REQ_REASON)
523
+
509
524
  @property
510
525
  def speed_cap(self) -> int | None:
511
526
  return self._msg_value(Code._31DA, key=SZ_SPEED_CAPABILITIES)
ramses_rf/dispatcher.py CHANGED
@@ -91,7 +91,7 @@ def _create_devices_from_addrs(gwy: Gateway, this: Message) -> None:
91
91
  if not gwy.config.enable_eavesdrop:
92
92
  return
93
93
 
94
- if not isinstance(this.dst, Device) and this.src is not gwy.hgi: # type: ignore[unreachable]
94
+ if not isinstance(this.dst, Device) and this.src != gwy.hgi: # type: ignore[unreachable]
95
95
  with contextlib.suppress(LookupError):
96
96
  this.dst = gwy.get_device(this.dst.id) # type: ignore[assignment]
97
97
 
@@ -188,9 +188,9 @@ def process_msg(gwy: Gateway, msg: Message) -> None:
188
188
  def logger_xxxx(msg: Message) -> None:
189
189
  if _DBG_FORCE_LOG_MESSAGES:
190
190
  _LOGGER.warning(msg)
191
- elif msg.src is not gwy.hgi or (msg.code != Code._PUZZ and msg.verb != RQ):
191
+ elif msg.src != gwy.hgi or (msg.code != Code._PUZZ and msg.verb != RQ):
192
192
  _LOGGER.info(msg)
193
- elif msg.src is not gwy.hgi or msg.verb != RQ:
193
+ elif msg.src != gwy.hgi or msg.verb != RQ:
194
194
  _LOGGER.info(msg)
195
195
  elif _LOGGER.getEffectiveLevel() == logging.DEBUG:
196
196
  _LOGGER.info(msg)
@@ -215,7 +215,7 @@ def process_msg(gwy: Gateway, msg: Message) -> None:
215
215
  if (
216
216
  msg.src._SLUG != DevType.HGI # avoid: msg.src.id != gwy.hgi.id
217
217
  and msg.verb != I_
218
- and msg.dst is not msg.src
218
+ and msg.dst != msg.src
219
219
  ):
220
220
  # HGI80 can do what it likes
221
221
  # receiving an I isn't currently in the schema & so can't yet be tested
@@ -234,10 +234,10 @@ def process_msg(gwy: Gateway, msg: Message) -> None:
234
234
  # TODO: only be for fully-faked (not Fakable) dst (it picks up via RF if not)
235
235
 
236
236
  if msg.code == Code._1FC9 and msg.payload[SZ_PHASE] == SZ_OFFER:
237
- devices = [d for d in gwy.devices if d is not msg.src and d._is_binding]
237
+ devices = [d for d in gwy.devices if d != msg.src and d._is_binding]
238
238
 
239
239
  elif msg.dst == ALL_DEV_ADDR: # some offers use dst=63:, so after 1FC9 offer
240
- devices = [d for d in gwy.devices if d is not msg.src and d.is_faked]
240
+ devices = [d for d in gwy.devices if d != msg.src and d.is_faked]
241
241
 
242
242
  elif msg.dst is not msg.src and isinstance(msg.dst, Fakeable): # type: ignore[unreachable]
243
243
  # to eavesdrop pkts from other devices, but relevant to this device
@@ -268,8 +268,8 @@ def process_msg(gwy: Gateway, msg: Message) -> None:
268
268
 
269
269
  else:
270
270
  logger_xxxx(msg)
271
- if gwy._zzz:
272
- gwy._zzz.add(msg)
271
+ if gwy.msg_db:
272
+ gwy.msg_db.add(msg)
273
273
 
274
274
 
275
275
  # TODO: this needs cleaning up (e.g. handle intervening packet)
ramses_rf/entity_base.py CHANGED
@@ -190,7 +190,7 @@ class _MessageDB(_Entity):
190
190
  def __init__(self, gwy: Gateway) -> None:
191
191
  super().__init__(gwy)
192
192
 
193
- self._msgs_: dict[Code, Message] = {} # code, should be code/ctx? ?deprecate
193
+ self._msgs_: dict[Code, Message] = {} # code, should be code/ctx?
194
194
  self._msgz_: dict[
195
195
  Code, dict[VerbT, dict[bool | str | None, Message]]
196
196
  ] = {} # code/verb/ctx, should be code/ctx/verb?
@@ -205,19 +205,19 @@ class _MessageDB(_Entity):
205
205
  ):
206
206
  return # ZZZ: don't store these
207
207
 
208
+ # Store msg by code in flat self._msgs_ Dict (deprecated since 0.50.3)
208
209
  if msg.verb in (I_, RP):
209
210
  self._msgs_[msg.code] = msg
210
211
 
211
212
  if msg.code not in self._msgz_:
213
+ # Store msg verb + ctx by code in nested self._msgz_ Dict (deprecated)
212
214
  self._msgz_[msg.code] = {msg.verb: {msg._pkt._ctx: msg}}
213
215
  elif msg.verb not in self._msgz_[msg.code]:
216
+ # Same, 1 level deeper
214
217
  self._msgz_[msg.code][msg.verb] = {msg._pkt._ctx: msg}
215
- else: # if not self._gwy._zzz:
218
+ else:
219
+ # Same, replacing previous message
216
220
  self._msgz_[msg.code][msg.verb][msg._pkt._ctx] = msg
217
- # elif (
218
- # self._msgz[msg.code][msg.verb][msg._pkt._ctx] is not msg
219
- # ): # MsgIdx ensures this
220
- # assert False # TODO: remove
221
221
 
222
222
  @property
223
223
  def _msg_db(self) -> list[Message]: # flattened version of _msgz[code][verb][indx]
@@ -228,7 +228,7 @@ class _MessageDB(_Entity):
228
228
  - a compound ctx (e.g. 0005/000C/0418)
229
229
  - True (an array of elements, each with its own idx),
230
230
  - False (no idx, is usu. 00),
231
- - None (not deteminable, rare)
231
+ - None (not determinable, rare)
232
232
  """
233
233
  return [m for c in self._msgz.values() for v in c.values() for m in v.values()]
234
234
 
@@ -239,8 +239,8 @@ class _MessageDB(_Entity):
239
239
 
240
240
  obj: _MessageDB
241
241
 
242
- if self._gwy._zzz:
243
- self._gwy._zzz.rem(msg)
242
+ if self._gwy.msg_db: # central SQLite MessageIndex
243
+ self._gwy.msg_db.rem(msg)
244
244
 
245
245
  entities: list[_MessageDB] = []
246
246
  if isinstance(msg.src, Device):
@@ -261,8 +261,8 @@ class _MessageDB(_Entity):
261
261
  def _get_msg_by_hdr(self, hdr: HeaderT) -> Message | None:
262
262
  """Return a msg, if any, that matches a header."""
263
263
 
264
- # if self._gwy._zzz:
265
- # msgs = self._gwy._zzz.get(hdr=hdr)
264
+ # if self._gwy.msg_db: # central SQLite MessageIndex
265
+ # msgs = self._gwy.msg_db.get(hdr=hdr)
266
266
  # return msgs[0] if msgs else None
267
267
 
268
268
  msg: Message
@@ -387,26 +387,36 @@ class _MessageDB(_Entity):
387
387
  codes = {
388
388
  k: (CODES_SCHEMA[k][SZ_NAME] if k in CODES_SCHEMA else None)
389
389
  for k in sorted(self._msgs)
390
- if self._msgs[k].src is (self if hasattr(self, "addr") else self.ctl)
390
+ if self._msgs[k].src == (self if hasattr(self, "addr") else self.ctl)
391
391
  }
392
392
 
393
393
  return {"_sent": list(codes.keys())}
394
394
 
395
395
  @property
396
396
  def _msgs(self) -> dict[Code, Message]:
397
- if not self._gwy._zzz:
397
+ """
398
+ Get a flat Dict af all I/RP messages logged with this device as src or dst.
399
+
400
+ :return: nested Dict of messages by Code
401
+ """
402
+ if not self._gwy.msg_db: # no central SQLite MessageIndex
398
403
  return self._msgs_
399
404
 
400
405
  sql = """
401
406
  SELECT dtm from messages WHERE verb in (' I', 'RP') AND (src = ? OR dst = ?)
402
407
  """
403
- return { # ? use context instead?
404
- m.code: m for m in self._gwy._zzz.qry(sql, (self.id[:9], self.id[:9]))
408
+ return { # ? use ctx (context) instead of just the address?
409
+ m.code: m for m in self._gwy.msg_db.qry(sql, (self.id[:9], self.id[:9]))
405
410
  } # e.g. 01:123456_HW
406
411
 
407
412
  @property
408
413
  def _msgz(self) -> dict[Code, dict[VerbT, dict[bool | str | None, Message]]]:
409
- if not self._gwy._zzz:
414
+ """
415
+ Get a nested Dict of all I/RP messages logged with this device as src or dst.
416
+
417
+ :return: Dict of messages, nested by Code, Verb, Context
418
+ """
419
+ if not self._gwy.msg_db: # no central SQLite MessageIndex
410
420
  return self._msgz_
411
421
 
412
422
  msgs_1: dict[Code, dict[VerbT, dict[bool | str | None, Message]]] = {}
@@ -437,7 +447,7 @@ class _Discovery(_MessageDB):
437
447
  self._supported_cmds_ctx: dict[str, bool | None] = {}
438
448
 
439
449
  if not gwy.config.disable_discovery:
440
- # self._start_discovery_poller() # Can't use derived classes dont exist yet
450
+ # self._start_discovery_poller() # Can't use derived classes don't exist yet
441
451
  gwy._loop.call_soon(self._start_discovery_poller)
442
452
 
443
453
  @property # TODO: needs tidy up
@@ -464,7 +474,7 @@ class _Discovery(_MessageDB):
464
474
  def _to_data_id(msg_id: MsgId | str) -> OtDataId:
465
475
  return int(msg_id, 16) # type: ignore[return-value]
466
476
 
467
- def _to_msg_id(data_id: OtDataId | int) -> MsgId:
477
+ def _to_msg_id(data_id: OtDataId | int) -> MsgId: # not used
468
478
  return f"{data_id:02X}" # type: ignore[return-value]
469
479
 
470
480
  return {
@@ -639,7 +649,7 @@ class _Discovery(_MessageDB):
639
649
  task[_SZ_NEXT_DUE] = msg.dtm + task[_SZ_INTERVAL]
640
650
 
641
651
  if task[_SZ_NEXT_DUE] > dt_now:
642
- continue # if (most recent) last_msg is is not yet due...
652
+ continue # if (most recent) last_msg is not yet due...
643
653
 
644
654
  # since we may do I/O, check if the code|msg_id is deprecated
645
655
  task[_SZ_NEXT_DUE] = dt_now + task[_SZ_INTERVAL] # might undeprecate later
@@ -733,25 +743,6 @@ class Parent(Entity): # A System, Zone, DhwZone or a UfhController
733
743
  self.child_by_id: dict[str, Child] = {}
734
744
  self.childs: list[Child] = []
735
745
 
736
- # def _handle_msg(self, msg: Message) -> None:
737
- # def eavesdrop_ufh_circuits():
738
- # if msg.code == Code._22C9:
739
- # # .I --- 02:044446 --:------ 02:044446 22C9 024 00-076C0A28-01 01-06720A28-01 02-06A40A28-01 03-06A40A2-801 # NOTE: fragments
740
- # # .I --- 02:044446 --:------ 02:044446 22C9 006 04-07D00A28-01 # [{'ufh_idx': '04',...
741
- # circuit_idxs = [c[SZ_UFH_IDX] for c in msg.payload]
742
-
743
- # for cct_idx in circuit_idxs:
744
- # self.get_circuit(cct_idx, msg=msg)
745
-
746
- # # BUG: this will fail with > 4 circuits, as uses two pkts for this msg
747
- # # if [c for c in self.child_by_id if c not in circuit_idxs]:
748
- # # raise CorruptStateError
749
-
750
- # super()._handle_msg(msg)
751
-
752
- # if self._gwy.config.enable_eavesdrop:
753
- # eavesdrop_ufh_circuits()
754
-
755
746
  @property
756
747
  def zone_idx(self) -> str:
757
748
  """Return the domain id.
ramses_rf/gateway.py CHANGED
@@ -129,7 +129,7 @@ class Gateway(Engine):
129
129
  self.devices: list[Device] = []
130
130
  self.device_by_id: dict[DeviceIdT, Device] = {}
131
131
 
132
- self._zzz: MessageIndex | None = None # MessageIndex()
132
+ self.msg_db: MessageIndex | None = None # MessageIndex()
133
133
 
134
134
  def __repr__(self) -> str:
135
135
  if not self.ser_name:
@@ -196,8 +196,8 @@ class Gateway(Engine):
196
196
  async def stop(self) -> None:
197
197
  """Stop the Gateway and tidy up."""
198
198
 
199
- if self._zzz:
200
- self._zzz.stop()
199
+ if self.msg_db:
200
+ self.msg_db.stop()
201
201
  await super().stop()
202
202
 
203
203
  def _pause(self, *args: Any) -> None:
@@ -255,10 +255,10 @@ class Gateway(Engine):
255
255
  msgs.extend([m for z in system.zones for m in z._msgs.values()])
256
256
  # msgs.extend([m for z in system.dhw for m in z._msgs.values()]) # TODO
257
257
 
258
- if self._zzz:
258
+ if self.msg_db:
259
259
  pkts = {
260
260
  f"{repr(msg._pkt)[:26]}": f"{repr(msg._pkt)[27:]}"
261
- for msg in self._zzz.all(include_expired=True)
261
+ for msg in self.msg_db.all(include_expired=True)
262
262
  if wanted_msg(msg, include_expired=include_expired)
263
263
  }
264
264
 
@@ -405,7 +405,7 @@ class Gateway(Engine):
405
405
  _LOGGER.warning(f"The device is not fakeable: {dev}")
406
406
 
407
407
  # TODO: the exact order of the following may need refining...
408
- # TODO: some will be done my devices themselves?
408
+ # TODO: some will be done by devices themselves?
409
409
 
410
410
  # if schema: # Step 2: Only controllers have a schema...
411
411
  # dev._update_schema(**schema) # TODO: schema/traits
@@ -422,7 +422,7 @@ class Gateway(Engine):
422
422
  self,
423
423
  device_id: DeviceIdT,
424
424
  create_device: bool = False,
425
- ) -> Device:
425
+ ) -> Device | Fakeable:
426
426
  """Create a faked device."""
427
427
 
428
428
  if not is_valid_dev_id(device_id):
@@ -437,7 +437,7 @@ class Gateway(Engine):
437
437
  dev._make_fake()
438
438
  return dev
439
439
 
440
- raise TypeError(f"The device is not fakable: {device_id}")
440
+ raise TypeError(f"The device is not fakeable: {device_id}")
441
441
 
442
442
  @property
443
443
  def tcs(self) -> Evohome | None:
@@ -547,14 +547,6 @@ class Gateway(Engine):
547
547
 
548
548
  def _msg_handler(self, msg: Message) -> None:
549
549
  """A callback to handle messages from the protocol stack."""
550
- # TODO: Remove this
551
- # # HACK: if CLI, double-logging with client.py proc_msg() & setLevel(DEBUG)
552
- # if (log_level := _LOGGER.getEffectiveLevel()) < logging.INFO:
553
- # _LOGGER.info(msg)
554
- # elif log_level <= logging.INFO and not (
555
- # msg.verb == RQ and msg.src.type == DEV_TYPE_MAP.HGI
556
- # ):
557
- # _LOGGER.info(msg)
558
550
 
559
551
  super()._msg_handler(msg)
560
552
 
ramses_rf/system/heat.py CHANGED
@@ -193,19 +193,19 @@ class SystemBase(Parent, Entity): # 3B00 (multi-relay)
193
193
  this.code in (Code._22D9, Code._3220) and this.verb == RQ
194
194
  ): # TODO: RPs too?
195
195
  # dst could be an Address...
196
- if this.src is self.ctl and isinstance(this.dst, OtbGateway): # type: ignore[unreachable]
196
+ if this.src == self.ctl and isinstance(this.dst, OtbGateway): # type: ignore[unreachable]
197
197
  app_cntrl = this.dst # type: ignore[unreachable]
198
198
 
199
199
  elif this.code == Code._3EF0 and this.verb == RQ:
200
200
  # dst could be an Address...
201
- if this.src is self.ctl and isinstance(
201
+ if this.src == self.ctl and isinstance(
202
202
  this.dst, # type: ignore[unreachable]
203
203
  BdrSwitch | OtbGateway,
204
204
  ):
205
205
  app_cntrl = this.dst # type: ignore[unreachable]
206
206
 
207
207
  elif this.code == Code._3B00 and this.verb == I_ and prev is not None:
208
- if this.src is self.ctl and isinstance(prev.src, BdrSwitch): # type: ignore[unreachable]
208
+ if this.src == self.ctl and isinstance(prev.src, BdrSwitch): # type: ignore[unreachable]
209
209
  if prev.code == this.code and prev.verb == this.verb: # type: ignore[unreachable]
210
210
  app_cntrl = prev.src
211
211
 
ramses_rf/system/zones.py CHANGED
@@ -266,7 +266,7 @@ class DhwZone(ZoneSchedule): # CS92A
266
266
  # self._get_dhw(sensor=this.dst)
267
267
 
268
268
  assert (
269
- msg.src is self.ctl
269
+ msg.src == self.ctl
270
270
  and msg.code in (Code._0005, Code._000C, Code._10A0, Code._1260, Code._1F41)
271
271
  or msg.payload.get(SZ_DOMAIN_ID) in (F9, FA)
272
272
  or msg.payload.get(SZ_ZONE_IDX) == "HW"
@@ -633,13 +633,13 @@ class Zone(ZoneSchedule):
633
633
  self._update_schema(**{SZ_CLASS: ZON_ROLE_MAP[ZoneRole.UFH]})
634
634
 
635
635
  assert (
636
- msg.src is self.ctl or msg.src.type == DEV_TYPE_MAP.UFC
636
+ msg.src == self.ctl or msg.src.type == DEV_TYPE_MAP.UFC
637
637
  ) and ( # DEX
638
638
  isinstance(msg.payload, dict)
639
639
  or [d for d in msg.payload if d.get(SZ_ZONE_IDX) == self.idx]
640
640
  ), f"msg inappropriately routed to {self}"
641
641
 
642
- assert (msg.src is self.ctl or msg.src.type == DEV_TYPE_MAP.UFC) and ( # DEX
642
+ assert (msg.src == self.ctl or msg.src.type == DEV_TYPE_MAP.UFC) and ( # DEX
643
643
  isinstance(msg.payload, list)
644
644
  or msg.code == Code._0005
645
645
  or msg.payload.get(SZ_ZONE_IDX) == self.idx
@@ -699,8 +699,10 @@ class Zone(ZoneSchedule):
699
699
  def name(self) -> str | None: # 0004
700
700
  """Return the name of the zone."""
701
701
 
702
- if self._gwy._zzz:
703
- msgs = self._gwy._zzz.get(code=Code._0004, src=self._z_id, ctx=self._z_idx)
702
+ if self._gwy.msg_db:
703
+ msgs = self._gwy.msg_db.get(
704
+ code=Code._0004, src=self._z_id, ctx=self._z_idx
705
+ )
704
706
  return msgs[0].payload.get(SZ_NAME) if msgs else None
705
707
 
706
708
  return self._msg_value(Code._0004, key=SZ_NAME) # type: ignore[no-any-return]
ramses_rf/version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  """RAMSES RF - a RAMSES-II protocol decoder & analyser (application layer)."""
2
2
 
3
- __version__ = "0.51.4"
3
+ __version__ = "0.51.6"
4
4
  VERSION = __version__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ramses_rf
3
- Version: 0.51.4
3
+ Version: 0.51.6
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
@@ -67,6 +67,6 @@ pip install -e .
67
67
 
68
68
  The CLI is called ``client.py`` and is included in the code root.
69
69
  It has options to monitor and parse Ramses-II traffic to screen or a log file, and to parse a file containing Ramses-II messages to the screen.
70
- See the [client.py CLI wiki page](https://github.com/ramses-rf/ramses_rf/wiki/The-client.py-command-line) for instructions.
70
+ See the [client.py CLI wiki page](https://github.com/ramses-rf/ramses_rf/wiki/2.-The-client.py-command-line) for instructions.
71
71
 
72
72
  For code development, some more setup is required. Please follow the steps in our [Developer's Resource](README-developers.md)
@@ -0,0 +1,55 @@
1
+ ramses_cli/__init__.py,sha256=uvGzWqOf4avvgzxJNSLFWEelIWqSZ-AeLAZzg5x58bc,397
2
+ ramses_cli/client.py,sha256=vbKS3KVPiGsDWLp5cR3SVBtXrs-TinzlxSbTgcb4G2k,19724
3
+ ramses_cli/debug.py,sha256=vgR0lOHoYjWarN948dI617WZZGNuqHbeq6Tc16Da7b4,608
4
+ ramses_cli/discovery.py,sha256=81XbmpNiCpUHVZBwo2g1eRwyJG-wZhpSsc44G3hHlFA,12972
5
+ ramses_cli/utils/cat_slow.py,sha256=AhUpM5gnegCitNKU-JGHn-DrRzSi-49ZR1Qw6lxe_t8,607
6
+ ramses_cli/utils/convert.py,sha256=D_YiCyX5na9pgC-_NhBlW9N1dgRKUK-uLtLBfofjzZM,1804
7
+ ramses_rf/__init__.py,sha256=zONFBiRdf07cPTSxzr2V3t-b3CGokZjT9SGit4JUKRA,1055
8
+ ramses_rf/binding_fsm.py,sha256=uZAOl3i19KCXqqlaLJWkEqMMP7NJBhVPW3xTikQD1fY,25996
9
+ ramses_rf/const.py,sha256=L3z31CZ-xqno6oZp_h-67CB_5tDDqTwSWXsqRtsjMcs,5460
10
+ ramses_rf/database.py,sha256=ZZZgucyuU1IHsSewGukZfDg2gu8KeNaEFriWKM0TUHs,10287
11
+ ramses_rf/dispatcher.py,sha256=JGkqSi1o-YhQ2rj8tNkXwYLLeJIC7F061xpHoH8sSsM,11201
12
+ ramses_rf/entity_base.py,sha256=V9m_Q5SOLP5ko3sok0NDvyz3YdYch1QsxM6tHCIE7cA,39212
13
+ ramses_rf/exceptions.py,sha256=rzVZDcYxFH7BjUAQ3U1fHWtgBpwk3BgjX1TO1L1iM8c,2538
14
+ ramses_rf/gateway.py,sha256=WdIIGgs87CYfXwSCSVb2YzqOgLC7W4bkpulWQb7PFNw,20564
15
+ ramses_rf/helpers.py,sha256=LcrVLqnF2qJWqXrC7UXKOQE8khCT3OhoTpZ_ZVBjw3A,4249
16
+ ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ ramses_rf/schemas.py,sha256=mYOUZOH5OIDNBxRM2vd8POzDWEEmLhxh5UtqjTpFNek,13287
18
+ ramses_rf/version.py,sha256=QtONmxsLFzHSx9Z1ddbRf5_x-XAddalTD9dH8-naWpo,125
19
+ ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
20
+ ramses_rf/device/base.py,sha256=WGkBTUNjRUEe-phxdtdiXVCZnTi6-i1i_YT6g689UTM,17450
21
+ ramses_rf/device/heat.py,sha256=2sCsggySVcuTzyXDmgWy76QbhlU5MQWSejy3zgI5BDE,54242
22
+ ramses_rf/device/hvac.py,sha256=H_PUfG_jrrvJgtnu6Bco6PLxHn7CHALwebZzZI1ygFo,23917
23
+ ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
24
+ ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
25
+ ramses_rf/system/heat.py,sha256=3jaFEChU-HlWCRMY1y7u09s7AH4hT0pC63hnqwdmZOc,39223
26
+ ramses_rf/system/schedule.py,sha256=Ts6tdZPTQLV5NkgwA73tPa5QUsnZNIIuYoKC-8VsXDk,18808
27
+ ramses_rf/system/zones.py,sha256=9AH7ooN5QfiqvWuor2P1Dn8aILjQb2RWL9rWqDH1IjA,36075
28
+ ramses_tx/__init__.py,sha256=4FsVOzICJ4H80LJ0MknZCN0_V-g0k1nMkHUQ0IdrJW8,3161
29
+ ramses_tx/address.py,sha256=5swDr_SvOs1CxBmT-iJpldf8R00mOb7gKPMiEnxLz84,8452
30
+ ramses_tx/command.py,sha256=y69y9NYgQHuPbm7h6xC0osf3e1YIKY9jwmsfPiJ8N6U,58348
31
+ ramses_tx/const.py,sha256=QmwSS4BIN3ZFrLUiiFScP1RCUHuJ782V3ycRPQTtB_c,30297
32
+ ramses_tx/exceptions.py,sha256=FJSU9YkvpKjs3yeTqUJX1o3TPFSe_B01gRGIh9b3PNc,2632
33
+ ramses_tx/fingerprints.py,sha256=nfftA1E62HQnb-eLt2EqjEi_la0DAoT0wt-PtTMie0s,11974
34
+ ramses_tx/frame.py,sha256=9lUVh8gAMXNRAolfFw2WuWANjn24AWkmscuM9Tm5imE,22036
35
+ ramses_tx/gateway.py,sha256=TXLYwT6tFpmSokD29Qyj1ze7UGCxKidooeyP557Jfoo,11266
36
+ ramses_tx/helpers.py,sha256=0zIvMY2zZ5V52fDAyVGXxNBND1HI-9fBN3dlgDNKNyo,33040
37
+ ramses_tx/logger.py,sha256=9xTxKVdZEoNZvvbjKSeKtMmlSzLhocTRW-61O6WpVsk,11397
38
+ ramses_tx/message.py,sha256=hl_gLfwrF79ftUNnsgNt3XGsIhM2Pts0MtZZuGjfaxk,13169
39
+ ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
40
+ ramses_tx/packet.py,sha256=NGunaGCkEjhTp9t4mARK5e7kbqT-Z_JKCH7ibMYMJXU,7357
41
+ ramses_tx/parsers.py,sha256=ngKmZNFPp4k-HmpOtX8_zWnrjM4O68OzjqAOEMznHFE,109863
42
+ ramses_tx/protocol.py,sha256=ifj3qwcQivjQDaQUwM94xp-U8Pmef6zwSH7mav8DLWA,28867
43
+ ramses_tx/protocol_fsm.py,sha256=YhHkTqbl8w-myimsOjV50uIFgg9HiApwPU7xA_jg5nU,26827
44
+ ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ ramses_tx/ramses.py,sha256=9R-JrInORWUNMPklrAPQWwtr_2aaruQmFqQPw5mFkrE,52223
46
+ ramses_tx/schemas.py,sha256=h2AcArVROy1_C4n6F0Crj4e-2BxXxH74xogFlc6nKHI,12769
47
+ ramses_tx/transport.py,sha256=aLpULRSivoJqzH8GDPRDcbehETOhFflEqmHbaniGLvg,56210
48
+ ramses_tx/typed_dicts.py,sha256=w-0V5t2Q3GiNUOrRAWiW9GtSwbta_7luME6GfIb1zhI,10869
49
+ ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
50
+ ramses_tx/version.py,sha256=Jvb0klvS67N7SxexepgSmtxSwb8sKcIR8MBBeUXTfSM,123
51
+ ramses_rf-0.51.6.dist-info/METADATA,sha256=OzEjMdl75qcH3T8URoBIKgSNQKAualdYRRD8giwa034,3909
52
+ ramses_rf-0.51.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
+ ramses_rf-0.51.6.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
+ ramses_rf-0.51.6.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
+ ramses_rf-0.51.6.dist-info/RECORD,,
ramses_tx/__init__.py CHANGED
@@ -138,7 +138,7 @@ if TYPE_CHECKING:
138
138
  async def set_pkt_logging_config(**config: Any) -> Logger:
139
139
  """
140
140
  Set up ramses packet logging to a file or port.
141
- Must runs async in executor to prevent HA blocking call opening packet log file (issue #200)
141
+ Must run async in executor to prevent HA blocking call opening packet log file (issue #200)
142
142
 
143
143
  :param config: if file_name is included, opens packet_log file
144
144
  :return: a logging.Logger