ramses-rf 0.22.40__py3-none-any.whl → 0.51.1__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.
Files changed (71) hide show
  1. ramses_cli/__init__.py +18 -0
  2. ramses_cli/client.py +597 -0
  3. ramses_cli/debug.py +20 -0
  4. ramses_cli/discovery.py +405 -0
  5. ramses_cli/utils/cat_slow.py +17 -0
  6. ramses_cli/utils/convert.py +60 -0
  7. ramses_rf/__init__.py +31 -10
  8. ramses_rf/binding_fsm.py +787 -0
  9. ramses_rf/const.py +124 -105
  10. ramses_rf/database.py +297 -0
  11. ramses_rf/device/__init__.py +69 -39
  12. ramses_rf/device/base.py +187 -376
  13. ramses_rf/device/heat.py +540 -552
  14. ramses_rf/device/hvac.py +286 -171
  15. ramses_rf/dispatcher.py +153 -177
  16. ramses_rf/entity_base.py +478 -361
  17. ramses_rf/exceptions.py +82 -0
  18. ramses_rf/gateway.py +377 -513
  19. ramses_rf/helpers.py +57 -19
  20. ramses_rf/py.typed +0 -0
  21. ramses_rf/schemas.py +148 -194
  22. ramses_rf/system/__init__.py +16 -23
  23. ramses_rf/system/faultlog.py +363 -0
  24. ramses_rf/system/heat.py +295 -302
  25. ramses_rf/system/schedule.py +312 -198
  26. ramses_rf/system/zones.py +318 -238
  27. ramses_rf/version.py +2 -8
  28. ramses_rf-0.51.1.dist-info/METADATA +72 -0
  29. ramses_rf-0.51.1.dist-info/RECORD +55 -0
  30. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info}/WHEEL +1 -2
  31. ramses_rf-0.51.1.dist-info/entry_points.txt +2 -0
  32. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info/licenses}/LICENSE +1 -1
  33. ramses_tx/__init__.py +160 -0
  34. {ramses_rf/protocol → ramses_tx}/address.py +65 -59
  35. ramses_tx/command.py +1454 -0
  36. ramses_tx/const.py +903 -0
  37. ramses_tx/exceptions.py +92 -0
  38. {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
  39. {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
  40. ramses_tx/gateway.py +338 -0
  41. ramses_tx/helpers.py +883 -0
  42. {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
  43. {ramses_rf/protocol → ramses_tx}/message.py +155 -191
  44. ramses_tx/opentherm.py +1260 -0
  45. ramses_tx/packet.py +210 -0
  46. {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
  47. ramses_tx/protocol.py +801 -0
  48. ramses_tx/protocol_fsm.py +672 -0
  49. ramses_tx/py.typed +0 -0
  50. {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
  51. {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
  52. ramses_tx/transport.py +1471 -0
  53. ramses_tx/typed_dicts.py +492 -0
  54. ramses_tx/typing.py +181 -0
  55. ramses_tx/version.py +4 -0
  56. ramses_rf/discovery.py +0 -398
  57. ramses_rf/protocol/__init__.py +0 -59
  58. ramses_rf/protocol/backports.py +0 -42
  59. ramses_rf/protocol/command.py +0 -1576
  60. ramses_rf/protocol/const.py +0 -697
  61. ramses_rf/protocol/exceptions.py +0 -111
  62. ramses_rf/protocol/helpers.py +0 -390
  63. ramses_rf/protocol/opentherm.py +0 -1170
  64. ramses_rf/protocol/packet.py +0 -235
  65. ramses_rf/protocol/protocol.py +0 -613
  66. ramses_rf/protocol/transport.py +0 -1011
  67. ramses_rf/protocol/version.py +0 -10
  68. ramses_rf/system/hvac.py +0 -82
  69. ramses_rf-0.22.40.dist-info/METADATA +0 -64
  70. ramses_rf-0.22.40.dist-info/RECORD +0 -42
  71. ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
ramses_rf/dispatcher.py CHANGED
@@ -1,34 +1,36 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
- """RAMSES RF - a RAMSES-II protocol decoder & analyser.
2
+ """RAMSES RF - Decode/process a message (payload into JSON)."""
3
+
4
+ # TODO:
5
+ # - fix dispatching - what devices (some are Addr) are sent packets, esp. 1FC9s
5
6
 
6
- Decode/process a message (payload into JSON).
7
- """
8
7
  from __future__ import annotations
9
8
 
9
+ import contextlib
10
10
  import logging
11
11
  from datetime import timedelta as td
12
+ from typing import TYPE_CHECKING, Final
13
+
14
+ from ramses_tx import ALL_DEV_ADDR, CODES_BY_DEV_SLUG, Message
15
+ from ramses_tx.ramses import (
16
+ CODES_OF_HEAT_DOMAIN,
17
+ CODES_OF_HEAT_DOMAIN_ONLY,
18
+ CODES_OF_HVAC_DOMAIN_ONLY,
19
+ )
12
20
 
21
+ from . import exceptions as exc
13
22
  from .const import (
14
- DEV_TYPE,
15
23
  DEV_TYPE_MAP,
16
24
  DONT_CREATE_ENTITIES,
17
25
  DONT_UPDATE_ENTITIES,
18
26
  SZ_DEVICES,
19
- __dev_mode__,
20
- )
21
- from .device import Device
22
- from .protocol import CODES_BY_DEV_SLUG, CODES_SCHEMA, Message
23
- from .protocol.exceptions import EvohomeError, InvalidAddrSetError, InvalidPacketError
24
- from .protocol.ramses import (
25
- CODES_OF_HEAT_DOMAIN,
26
- CODES_OF_HEAT_DOMAIN_ONLY,
27
- CODES_OF_HVAC_DOMAIN_ONLY,
27
+ SZ_OFFER,
28
+ SZ_PHASE,
29
+ DevType,
28
30
  )
31
+ from .device import Device, Fakeable
29
32
 
30
- # skipcq: PY-W2000
31
- from .protocol import ( # noqa: F401, isort: skip, pylint: disable=unused-import
33
+ from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
32
34
  I_,
33
35
  RP,
34
36
  RQ,
@@ -36,45 +38,52 @@ from .protocol import ( # noqa: F401, isort: skip, pylint: disable=unused-impor
36
38
  Code,
37
39
  )
38
40
 
39
- __all__ = ["process_msg"]
41
+ if TYPE_CHECKING:
42
+ from . import Gateway
40
43
 
41
- CODE_NAMES = {k: v["name"] for k, v in CODES_SCHEMA.items()}
44
+ #
45
+ # NOTE: All debug flags should be False for deployment to end-users
46
+ _DBG_FORCE_LOG_MESSAGES: Final[bool] = False # useful for dev/test
47
+ _DBG_INCREASE_LOG_LEVELS: Final[bool] = (
48
+ False # set True for developer-friendly log spam
49
+ )
42
50
 
43
- MSG_FORMAT_10 = "|| {:10s} | {:10s} | {:2s} | {:16s} | {:^4s} || {}"
44
- MSG_FORMAT_18 = "|| {:18s} | {:18s} | {:2s} | {:16s} | {:^4s} || {}"
51
+ _LOGGER = logging.getLogger(__name__)
45
52
 
46
- DEV_MODE = __dev_mode__ and False # or True # set True for useful Tracebacks
47
53
 
48
- _LOGGER = logging.getLogger(__name__)
49
- if DEV_MODE:
50
- _LOGGER.setLevel(logging.DEBUG)
54
+ __all__ = ["detect_array_fragment", "process_msg"]
55
+
56
+
57
+ MSG_FORMAT_18 = "|| {:18s} | {:18s} | {:2s} | {:16s} | {:^4s} || {}"
51
58
 
52
- STRICT_MODE = not DEV_MODE and False
59
+ _TD_SECONDS_003 = td(seconds=3)
53
60
 
54
61
 
55
- def _create_devices_from_addrs(gwy, this: Message) -> None:
62
+ def _create_devices_from_addrs(gwy: Gateway, this: Message) -> None:
56
63
  """Discover and create any new devices using the packet addresses (not payload)."""
57
64
 
65
+ # FIXME: changing Address to Devices is messy: ? Protocol for same method signatures
58
66
  # prefer Devices but can continue with Addresses if required...
59
- this.src = gwy.device_by_id.get(this.src.id, this.src)
60
- this.dst = gwy.device_by_id.get(this.dst.id, this.dst)
67
+ this.src = gwy.device_by_id.get(this.src.id, this.src) # type: ignore[assignment]
68
+ this.dst = gwy.device_by_id.get(this.dst.id, this.dst) # type: ignore[assignment]
61
69
 
62
70
  # Devices need to know their controller, ?and their location ('parent' domain)
63
71
  # NB: only addrs processed here, packet metadata is processed elsewhere
64
72
 
65
73
  # Determinging bindings to a controller:
66
- # - configury; As per any schema
74
+ # - configury; As per any schema # codespell:ignore configury
67
75
  # - discovery: If in 000C pkt, or pkt *to* device where src is a controller
68
76
  # - eavesdrop: If pkt *from* device where dst is a controller
69
77
 
70
78
  # Determinging location in a schema (domain/DHW/zone):
71
- # - configury; As per any schema
79
+ # - configury; As per any schema # codespell:ignore configury
72
80
  # - discovery: If in 000C pkt - unable for 10: & 00: (TRVs)
73
81
  # - discovery: from packet fingerprint, excl. payloads (only for 10:)
74
82
  # - eavesdrop: from packet fingerprint, incl. payloads
75
83
 
76
- if not isinstance(this.src, Device):
77
- this.src = gwy.get_device(this.src.id) # may: LookupError (don't swallow)
84
+ if not isinstance(this.src, Device): # type: ignore[unreachable]
85
+ # may: LookupError, but don't suppress
86
+ this.src = gwy.get_device(this.src.id) # type: ignore[assignment]
78
87
  if this.dst.id == this.src.id:
79
88
  this.dst = this.src
80
89
  return
@@ -82,14 +91,12 @@ def _create_devices_from_addrs(gwy, this: Message) -> None:
82
91
  if not gwy.config.enable_eavesdrop:
83
92
  return
84
93
 
85
- if not isinstance(this.dst, Device) and this.src is not gwy.hgi:
86
- try:
87
- this.dst = gwy.get_device(this.dst.id) # may: LookupError (but swallow it)
88
- except LookupError:
89
- pass
94
+ if not isinstance(this.dst, Device) and this.src is not gwy.hgi: # type: ignore[unreachable]
95
+ with contextlib.suppress(LookupError):
96
+ this.dst = gwy.get_device(this.dst.id) # type: ignore[assignment]
90
97
 
91
98
 
92
- def _check_msg_addrs(msg: Message) -> None:
99
+ def _check_msg_addrs(msg: Message) -> None: # TODO
93
100
  """Validate the packet's address set.
94
101
 
95
102
  Raise InvalidAddrSetError if the meta data is invalid, otherwise simply return.
@@ -106,7 +113,9 @@ def _check_msg_addrs(msg: Message) -> None:
106
113
  # .I --- 01:078710 --:------ 01:144246 1F09 003 FF04B5 # invalid
107
114
  # .I --- 29:151550 29:237552 --:------ 22F3 007 00023C03040000 # valid? HVAC
108
115
  if msg.code in CODES_OF_HEAT_DOMAIN_ONLY:
109
- raise InvalidAddrSetError(f"Invalid addr pair: {msg.src!r}/{msg.dst!r}")
116
+ raise exc.PacketAddrSetInvalid(
117
+ f"Invalid addr pair: {msg.src!r}/{msg.dst!r}"
118
+ )
110
119
  elif msg.code in CODES_OF_HEAT_DOMAIN:
111
120
  _LOGGER.warning(
112
121
  f"{msg!r} < Invalid addr pair: {msg.src!r}/{msg.dst!r}, is it HVAC?"
@@ -117,197 +126,164 @@ def _check_msg_addrs(msg: Message) -> None:
117
126
  )
118
127
 
119
128
 
120
- def _check_msg_src(msg: Message, *, slug: str = None) -> None:
121
- """Validate the packet's source device class (type) against its verb/code pair.
122
-
123
- Raise InvalidPacketError if the meta data is invalid, otherwise simply return.
124
- """
129
+ def _check_src_slug(msg: Message, *, slug: str | None = None) -> None:
130
+ """Validate the packet's source device class against its verb/code pair."""
125
131
 
126
132
  if slug is None: # slug = best_dev_role(msg.src, msg=msg)._SLUG
127
- slug = getattr(msg.src, "_SLUG", DEV_TYPE.DEV)
128
- if slug in (DEV_TYPE.HGI, DEV_TYPE.DEV, DEV_TYPE.HEA, DEV_TYPE.HVC):
133
+ slug = getattr(msg.src, "_SLUG", None)
134
+ if slug in (None, DevType.HGI, DevType.DEV, DevType.HEA, DevType.HVC):
129
135
  return # TODO: use DEV_TYPE_MAP.PROMOTABLE_SLUGS
130
136
 
131
137
  if slug not in CODES_BY_DEV_SLUG:
132
- if msg.code != Code._10E0 and msg.code not in CODES_OF_HVAC_DOMAIN_ONLY:
133
- err_msg = f"Unknown src type: {msg.dst}"
134
- if STRICT_MODE:
135
- raise InvalidPacketError(err_msg)
136
- (_LOGGER.warning if DEV_MODE else _LOGGER.info)(f"{msg!r} < {err_msg}")
137
- return
138
- _LOGGER.warning(f"{msg!r} < Unknown src type: {msg.src}, is it HVAC?")
139
- return
138
+ raise exc.PacketInvalid(f"{msg!r} < Unknown src slug ({slug}), is it HVAC?")
140
139
 
141
140
  #
142
- #
143
141
 
144
- if msg.code not in CODES_BY_DEV_SLUG[slug]: # type: ignore[index]
145
- if slug != DEV_TYPE.DEV:
146
- err_msg = f"Invalid code for {msg.src} to Tx: {msg.code}"
147
- if STRICT_MODE:
148
- raise InvalidPacketError(err_msg)
149
- (_LOGGER.warning if DEV_MODE else _LOGGER.info)(f"{msg!r} < {err_msg}")
150
- return
151
- if msg.verb in (RQ, W_):
152
- return
153
- (_LOGGER.warning if DEV_MODE else _LOGGER.info)(
154
- f"{msg!r} < Invalid code for {msg.src} to Tx: {msg.code}"
155
- )
156
- return
142
+ if msg.code not in CODES_BY_DEV_SLUG[slug]:
143
+ raise exc.PacketInvalid(f"{msg!r} < Unexpected code for src ({slug}) to Tx")
157
144
 
158
145
  #
159
146
  #
160
147
 
161
- #
162
- # (code := CODES_BY_DEV_SLUG[slug][msg.code]) and msg.verb not in code:
163
- if msg.verb not in CODES_BY_DEV_SLUG[slug][msg.code]: # type: ignore[index]
164
- err_msg = f"Invalid verb/code for {msg.src} to Tx: {msg.verb}/{msg.code}"
165
- if STRICT_MODE:
166
- raise InvalidPacketError(err_msg)
167
- (_LOGGER.warning if DEV_MODE else _LOGGER.info)(f"{msg!r} < {err_msg}")
168
-
148
+ if msg.verb not in CODES_BY_DEV_SLUG[slug][msg.code]:
149
+ raise exc.PacketInvalid(
150
+ f"{msg!r} < Unexpected verb/code for src ({slug}) to Tx"
151
+ )
169
152
 
170
- def _check_msg_dst(msg: Message, *, slug: str = None) -> None:
171
- """Validate the packet's destination device class (type) against its verb/code pair.
172
153
 
173
- Raise InvalidPacketError if the meta data is invalid, otherwise simply return.
174
- """
154
+ def _check_dst_slug(msg: Message, *, slug: str | None = None) -> None:
155
+ """Validate the packet's destination device class against its verb/code pair."""
175
156
 
176
157
  if slug is None:
177
158
  slug = getattr(msg.dst, "_SLUG", None)
178
- if slug in (None, DEV_TYPE.HGI, DEV_TYPE.DEV, DEV_TYPE.HEA, DEV_TYPE.HVC):
159
+ if slug in (None, DevType.HGI, DevType.DEV, DevType.HEA, DevType.HVC):
179
160
  return # TODO: use DEV_TYPE_MAP.PROMOTABLE_SLUGS
180
161
 
181
162
  if slug not in CODES_BY_DEV_SLUG:
182
- if msg.code not in CODES_OF_HVAC_DOMAIN_ONLY:
183
- err_msg = f"Unknown dst type: {msg.dst}"
184
- if STRICT_MODE:
185
- raise InvalidPacketError(err_msg)
186
- (_LOGGER.warning if DEV_MODE else _LOGGER.info)(f"{msg!r} < {err_msg}")
187
- return
188
- _LOGGER.warning(f"{msg!r} < Unknown dst type: {msg.dst}, is it HVAC?")
189
- return
163
+ raise exc.PacketInvalid(f"{msg!r} < Unknown dst slug ({slug}), is it HVAC?")
190
164
 
191
- if msg.verb == I_: # TODO: not common, unless src=dst
192
- return # receiving an I isn't currently in the schema & cant yet be tested
193
165
  if f"{slug}/{msg.verb}/{msg.code}" in (f"CTL/{RQ}/{Code._3EF1}",):
194
166
  return # HACK: an exception-to-the-rule that need sorting
195
167
 
196
- if msg.code not in CODES_BY_DEV_SLUG[slug]: # type: ignore[index]
197
- if False and slug != DEV_TYPE.HGI: # NOTE: not yet needed because of 1st if
198
- err_msg = f"Invalid code for {msg.dst} to Rx: {msg.code}"
199
- if STRICT_MODE:
200
- raise InvalidPacketError(err_msg)
201
- (_LOGGER.warning if DEV_MODE else _LOGGER.info)(f"{msg!r} < {err_msg}")
202
- return
203
- if msg.verb == RP:
204
- return
205
- (_LOGGER.warning if DEV_MODE else _LOGGER.info)(
206
- f"{msg!r} < Invalid code for {msg.dst} to Rx/Tx: {msg.code}"
207
- )
208
- return
168
+ if msg.code not in CODES_BY_DEV_SLUG[slug]:
169
+ raise exc.PacketInvalid(f"{msg!r} < Unexpected code for dst ({slug}) to Rx")
209
170
 
210
171
  if f"{msg.verb}/{msg.code}" in (f"{W_}/{Code._0001}",):
211
172
  return # HACK: an exception-to-the-rule that need sorting
212
- if f"{slug}/{msg.verb}/{msg.code}" in (f"{DEV_TYPE.BDR}/{RQ}/{Code._3EF0}",):
173
+ if f"{slug}/{msg.verb}/{msg.code}" in (f"{DevType.BDR}/{RQ}/{Code._3EF0}",):
213
174
  return # HACK: an exception-to-the-rule that need sorting
214
175
 
215
- verb = {RQ: RP, RP: RQ, W_: I_}[msg.verb]
216
- # (code := CODES_BY_DEV_SLUG[klass][msg.code]) and verb not in code:
217
- if verb not in CODES_BY_DEV_SLUG[slug][msg.code]: # type: ignore[index]
218
- err_msg = f"Invalid verb/code for {msg.dst} to Rx: {msg.verb}/{msg.code}"
219
- if STRICT_MODE:
220
- raise InvalidPacketError(err_msg)
221
- (_LOGGER.warning if DEV_MODE else _LOGGER.info)(f"{msg!r} < {err_msg}")
176
+ if {RQ: RP, RP: RQ, W_: I_}[msg.verb] not in CODES_BY_DEV_SLUG[slug][msg.code]:
177
+ raise exc.PacketInvalid(
178
+ f"{msg!r} < Unexpected verb/code for dst ({slug}) to Rx"
179
+ )
222
180
 
223
181
 
224
- def process_msg(msg: Message, *, prev_msg: Message = None) -> None:
182
+ def process_msg(gwy: Gateway, msg: Message) -> None:
225
183
  """Decoding the packet payload and route it appropriately."""
226
184
 
227
- # All methods require a valid message (payload), except create_devices(), which
228
- # requires a valid message only for 000C.
229
-
230
- def detect_array_fragment(this, prev) -> dict: # _PayloadT
231
- """Return complete array if this pkt is the latter half of an array."""
232
- # This will work, even if the 2nd pkt._is_array == False as 1st == True
233
- # .I --- 01:158182 --:------ 01:158182 000A 048 001201F409C4011101F409C40...
234
- # .I --- 01:158182 --:------ 01:158182 000A 006 081001F409C4
235
- if (
236
- not prev
237
- or not prev._has_array
238
- or this.code not in (Code._000A, Code._22C9)
239
- or this.code != prev.code
240
- or this.verb != prev.verb != I_
241
- or this.src != prev.src
242
- or this.dtm >= prev.dtm + td(seconds=3)
243
- ):
244
- return this.payload
245
-
246
- this._pkt._force_has_array()
247
- payload = this.payload if isinstance(this.payload, list) else [this.payload]
248
- return prev.payload + payload
185
+ # All methods require msg with a valid payload, except _create_devices_from_addrs(),
186
+ # which requires a valid payload only for 000C.
249
187
 
250
- gwy = msg._gwy # pylint: disable=protected-access, skipcq: PYL-W0212
251
-
252
- # HACK: if CLI, double-logging with client.py proc_msg() & setLevel(DEBUG)
253
- if (log_level := _LOGGER.getEffectiveLevel()) < logging.INFO:
254
- _LOGGER.info(msg)
255
- elif log_level <= logging.INFO and not (
256
- msg.verb == RQ and msg.src.type == DEV_TYPE_MAP.HGI
257
- ):
258
- _LOGGER.info(msg)
259
-
260
- msg._payload = detect_array_fragment(msg, prev_msg) # HACK: needs rethinking?
188
+ def logger_xxxx(msg: Message) -> None:
189
+ if _DBG_FORCE_LOG_MESSAGES:
190
+ _LOGGER.warning(msg)
191
+ elif msg.src is not gwy.hgi or (msg.code != Code._PUZZ and msg.verb != RQ):
192
+ _LOGGER.info(msg)
193
+ elif msg.src is not gwy.hgi or msg.verb != RQ:
194
+ _LOGGER.info(msg)
195
+ elif _LOGGER.getEffectiveLevel() == logging.DEBUG:
196
+ _LOGGER.info(msg)
261
197
 
262
198
  try: # validate / dispatch the packet
263
-
264
- _check_msg_addrs(msg) # ? InvalidAddrSetError
199
+ _check_msg_addrs(msg) # ?InvalidAddrSetError TODO: ?useful at all
265
200
 
266
201
  # TODO: any use in creating a device only if the payload is valid?
267
- if gwy.config.reduce_processing < DONT_CREATE_ENTITIES:
268
- try:
269
- _create_devices_from_addrs(gwy, msg)
270
- except LookupError as exc:
271
- (_LOGGER.error if DEV_MODE else _LOGGER.warning)(
272
- "%s < %s(%s)", msg._pkt, exc.__class__.__name__, exc
273
- )
274
- return
275
-
276
- _check_msg_src(msg) # ? InvalidPacketError
277
- if msg.dst is not msg.src or msg.verb != I_:
278
- _check_msg_dst(msg) # ? InvalidPacketError
202
+ if gwy.config.reduce_processing >= DONT_CREATE_ENTITIES:
203
+ logger_xxxx(msg) # return ensures try's else: clause won't be invoked
204
+ return
205
+
206
+ try:
207
+ _create_devices_from_addrs(gwy, msg)
208
+ except LookupError as err:
209
+ (_LOGGER.error if _DBG_INCREASE_LOG_LEVELS else _LOGGER.warning)(
210
+ "%s < %s(%s)", msg._pkt, err.__class__.__name__, err
211
+ )
212
+ return
213
+
214
+ _check_src_slug(msg) # ? raise exc.PacketInvalid
215
+ if (
216
+ msg.src._SLUG != DevType.HGI # avoid: msg.src.id != gwy.hgi.id
217
+ and msg.verb != I_
218
+ and msg.dst is not msg.src
219
+ ):
220
+ # HGI80 can do what it likes
221
+ # receiving an I isn't currently in the schema & so can't yet be tested
222
+ _check_dst_slug(msg) # ? raise exc.PacketInvalid
279
223
 
280
224
  if gwy.config.reduce_processing >= DONT_UPDATE_ENTITIES:
225
+ logger_xxxx(msg) # return ensures try's else: clause won't be invoked
281
226
  return
282
227
 
283
228
  # NOTE: here, msgs are routed only to devices: routing to other entities (i.e.
284
229
  # systems, zones, circuits) is done by those devices (e.g. UFC to UfhCircuit)
285
230
 
286
- if isinstance(msg.src, Device): # , HgiGateway)): # could use DeviceBase
287
- msg.src._handle_msg(msg)
231
+ if isinstance(msg.src, Device): # type: ignore[unreachable]
232
+ gwy._loop.call_soon(msg.src._handle_msg, msg) # type: ignore[unreachable]
288
233
 
289
- # TODO: should only be for fully-faked dst (as it will pick up via RF if not)
290
- if msg.dst is not msg.src:
291
- devices = (msg.dst,) # dont: msg.dst._handle_msg(msg)
234
+ # TODO: only be for fully-faked (not Fakable) dst (it picks up via RF if not)
292
235
 
293
- elif msg.code == Code._1FC9 and msg.payload["phase"] == "offer":
294
- devices = (d for d in msg._gwy.devices if d is not msg.src)
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]
295
238
 
296
- elif hasattr(msg.src, SZ_DEVICES):
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]
241
+
242
+ elif msg.dst is not msg.src and isinstance(msg.dst, Fakeable): # type: ignore[unreachable]
243
+ # to eavesdrop pkts from other devices, but relevant to this device
244
+ # dont: msg.dst._handle_msg(msg)
245
+ devices = [msg.dst] # type: ignore[unreachable]
246
+
247
+ # TODO: this may not be required...
248
+ elif hasattr(msg.src, SZ_DEVICES): # FIXME: use isinstance()
249
+ # elif isinstance(msg.src, Controller):
297
250
  # .I --- 22:060293 --:------ 22:060293 0008 002 000C
298
251
  # .I --- 01:054173 --:------ 01:054173 0008 002 03AA
299
252
  # needed for (e.g.) faked relays: each device decides if the pkt is useful
300
- devices = msg.src.devices # if d._SLUG = "BDR"
253
+ devices = msg.src.devices
301
254
 
302
255
  else:
303
- return
256
+ devices = []
304
257
 
305
- [d._handle_msg(msg) for d in devices if getattr(d, "_faked", False)]
258
+ for d in devices: # FIXME: some may be Addresses?
259
+ gwy._loop.call_soon(d._handle_msg, msg)
306
260
 
307
- except (AssertionError, EvohomeError, NotImplementedError) as exc:
308
- (_LOGGER.error if DEV_MODE else _LOGGER.warning)(
309
- "%s < %s(%s)", msg._pkt, exc.__class__.__name__, exc
261
+ except (AssertionError, exc.RamsesException, NotImplementedError) as err:
262
+ (_LOGGER.error if _DBG_INCREASE_LOG_LEVELS else _LOGGER.warning)(
263
+ "%s < %s(%s)", msg._pkt, err.__class__.__name__, err
310
264
  )
311
265
 
312
- except (AttributeError, LookupError, TypeError, ValueError) as exc:
313
- _LOGGER.exception("%s < %s(%s)", msg._pkt, exc.__class__.__name__, exc)
266
+ except (AttributeError, LookupError, TypeError, ValueError) as err:
267
+ _LOGGER.exception("%s < %s(%s)", msg._pkt, err.__class__.__name__, err)
268
+
269
+ else:
270
+ logger_xxxx(msg)
271
+ if gwy._zzz:
272
+ gwy._zzz.add(msg)
273
+
274
+
275
+ # TODO: this needs cleaning up (e.g. handle intervening packet)
276
+ def detect_array_fragment(this: Message, prev: Message) -> bool: # _PayloadT
277
+ """Return a merged array if this pkt is the latter half of an array."""
278
+ # This will work, even if the 2nd pkt._is_array == False as 1st == True
279
+ # .I --- 01:158182 --:------ 01:158182 000A 048 001201F409C4011101F409C40...
280
+ # .I --- 01:158182 --:------ 01:158182 000A 006 081001F409C4
281
+
282
+ return bool(
283
+ prev._has_array
284
+ and this.code in (Code._000A, Code._22C9) # TODO: not a complete list
285
+ and this.code == prev.code
286
+ and this.verb == prev.verb == I_
287
+ and this.src == prev.src
288
+ and this.dtm < prev.dtm + _TD_SECONDS_003
289
+ )