ramses-rf 0.51.3__py3-none-any.whl → 0.51.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_rf/const.py +2 -0
- ramses_rf/device/hvac.py +14 -0
- ramses_rf/dispatcher.py +6 -6
- ramses_rf/entity_base.py +1 -1
- ramses_rf/system/heat.py +3 -3
- ramses_rf/system/zones.py +3 -3
- ramses_rf/version.py +1 -1
- {ramses_rf-0.51.3.dist-info → ramses_rf-0.51.5.dist-info}/METADATA +1 -1
- {ramses_rf-0.51.3.dist-info → ramses_rf-0.51.5.dist-info}/RECORD +18 -18
- ramses_tx/const.py +2 -0
- ramses_tx/helpers.py +1 -1
- ramses_tx/parsers.py +28 -14
- ramses_tx/ramses.py +1 -1
- ramses_tx/transport.py +87 -20
- ramses_tx/version.py +1 -1
- {ramses_rf-0.51.3.dist-info → ramses_rf-0.51.5.dist-info}/WHEEL +0 -0
- {ramses_rf-0.51.3.dist-info → ramses_rf-0.51.5.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.51.3.dist-info → ramses_rf-0.51.5.dist-info}/licenses/LICENSE +0 -0
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/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,
|
|
@@ -506,6 +508,18 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
506
508
|
def remaining_mins(self) -> int | None:
|
|
507
509
|
return self._msg_value(Code._31DA, key=SZ_REMAINING_MINS)
|
|
508
510
|
|
|
511
|
+
@property
|
|
512
|
+
def request_fan_speed(self) -> float | None:
|
|
513
|
+
return self._msg_value(Code._2210, key=SZ_REQ_SPEED)
|
|
514
|
+
|
|
515
|
+
@property
|
|
516
|
+
def request_src(self) -> str | None:
|
|
517
|
+
"""
|
|
518
|
+
Orcon, others?
|
|
519
|
+
:return: source sensor of auto speed request: IDL, CO2 or HUM
|
|
520
|
+
"""
|
|
521
|
+
return self._msg_value(Code._2210, key=SZ_REQ_REASON)
|
|
522
|
+
|
|
509
523
|
@property
|
|
510
524
|
def speed_cap(self) -> int | None:
|
|
511
525
|
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
|
|
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
|
|
191
|
+
elif msg.src != gwy.hgi or (msg.code != Code._PUZZ and msg.verb != RQ):
|
|
192
192
|
_LOGGER.info(msg)
|
|
193
|
-
elif msg.src
|
|
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
|
|
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
|
|
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
|
|
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
|
ramses_rf/entity_base.py
CHANGED
|
@@ -387,7 +387,7 @@ 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
|
|
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())}
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
ramses_rf/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ramses_rf
|
|
3
|
-
Version: 0.51.
|
|
3
|
+
Version: 0.51.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
|
|
@@ -6,50 +6,50 @@ ramses_cli/utils/cat_slow.py,sha256=AhUpM5gnegCitNKU-JGHn-DrRzSi-49ZR1Qw6lxe_t8,
|
|
|
6
6
|
ramses_cli/utils/convert.py,sha256=D_YiCyX5na9pgC-_NhBlW9N1dgRKUK-uLtLBfofjzZM,1804
|
|
7
7
|
ramses_rf/__init__.py,sha256=zONFBiRdf07cPTSxzr2V3t-b3CGokZjT9SGit4JUKRA,1055
|
|
8
8
|
ramses_rf/binding_fsm.py,sha256=uZAOl3i19KCXqqlaLJWkEqMMP7NJBhVPW3xTikQD1fY,25996
|
|
9
|
-
ramses_rf/const.py,sha256=
|
|
9
|
+
ramses_rf/const.py,sha256=L3z31CZ-xqno6oZp_h-67CB_5tDDqTwSWXsqRtsjMcs,5460
|
|
10
10
|
ramses_rf/database.py,sha256=6k5MLtK5Lplz8THfluQoQU-eniUkqSwEUMvVW7VyGhI,9880
|
|
11
|
-
ramses_rf/dispatcher.py,sha256=
|
|
12
|
-
ramses_rf/entity_base.py,sha256=
|
|
11
|
+
ramses_rf/dispatcher.py,sha256=L-XQ-mbE3HyyxExkhe5kfD4elKlGZnV0kHT4OHTWzE8,11197
|
|
12
|
+
ramses_rf/entity_base.py,sha256=EaIyGIu3fU1Ks30jdqS69nYmcWRKXaWx81oBGDiaXGw,39602
|
|
13
13
|
ramses_rf/exceptions.py,sha256=rzVZDcYxFH7BjUAQ3U1fHWtgBpwk3BgjX1TO1L1iM8c,2538
|
|
14
14
|
ramses_rf/gateway.py,sha256=vqoTEb6QXnwaIMa66oed_3LEVvlyQ3flsAAMliEEvVA,20921
|
|
15
15
|
ramses_rf/helpers.py,sha256=LcrVLqnF2qJWqXrC7UXKOQE8khCT3OhoTpZ_ZVBjw3A,4249
|
|
16
16
|
ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
ramses_rf/schemas.py,sha256=mYOUZOH5OIDNBxRM2vd8POzDWEEmLhxh5UtqjTpFNek,13287
|
|
18
|
-
ramses_rf/version.py,sha256=
|
|
18
|
+
ramses_rf/version.py,sha256=2CNEExhYqbwGxmDWyifsA3RubG0bCgN6a1NUTKnIfBM,125
|
|
19
19
|
ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
|
|
20
20
|
ramses_rf/device/base.py,sha256=V2YzRhdxrTqfHYrCBq6pJsYdTgAx8gGzfdo8pkbeEo8,17450
|
|
21
21
|
ramses_rf/device/heat.py,sha256=2sCsggySVcuTzyXDmgWy76QbhlU5MQWSejy3zgI5BDE,54242
|
|
22
|
-
ramses_rf/device/hvac.py,sha256=
|
|
22
|
+
ramses_rf/device/hvac.py,sha256=fRzFGQD6zrkii0Ns9EV1uqh8MTXHn2CO3LZ1WOXLjhs,23835
|
|
23
23
|
ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
|
|
24
24
|
ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
|
|
25
|
-
ramses_rf/system/heat.py,sha256=
|
|
25
|
+
ramses_rf/system/heat.py,sha256=3jaFEChU-HlWCRMY1y7u09s7AH4hT0pC63hnqwdmZOc,39223
|
|
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=2_c1YHrGbObUeEqjcqJDO08Fo2Mr1aYn4VorNmfFaKk,36041
|
|
28
28
|
ramses_tx/__init__.py,sha256=wJ7Ntx-0AyJwYwSG8OrFMpxDLXs6GbECbCcYhq98mSA,3162
|
|
29
29
|
ramses_tx/address.py,sha256=2640K3sXzogZtd4-tSxwVjYEEXcFE1DgmtvZlTMM5mE,8444
|
|
30
30
|
ramses_tx/command.py,sha256=g5PBf9JnuygveyaYrqIuV8wIn7grm0evuqKy9Cp1oaA,53844
|
|
31
|
-
ramses_tx/const.py,sha256=
|
|
31
|
+
ramses_tx/const.py,sha256=ILZvbIp9qI2ZTmGDDJ0YFoxOH3GtQ7g9MT3vAUtHWAE,30291
|
|
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=9lUVh8gAMXNRAolfFw2WuWANjn24AWkmscuM9Tm5imE,22036
|
|
35
35
|
ramses_tx/gateway.py,sha256=FE5MWA1eIE9JATA2vRoBSQ8fAzqp7TqAm3Ds3k1KnKE,11267
|
|
36
|
-
ramses_tx/helpers.py,sha256=
|
|
36
|
+
ramses_tx/helpers.py,sha256=qDJTsTU2tfSZrfJuFi1q29efkkHzqRtg85M6ItQH6qA,32247
|
|
37
37
|
ramses_tx/logger.py,sha256=7vUpcfOFMW95juMWDx5dhUOqV8DTsindZ-Qz2aCmEoA,11073
|
|
38
38
|
ramses_tx/message.py,sha256=J1wvVkLPJQr2ffKCUQYSWwLPzRTZBC0zUU5W9DkN3hU,13190
|
|
39
39
|
ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
|
|
40
40
|
ramses_tx/packet.py,sha256=NGunaGCkEjhTp9t4mARK5e7kbqT-Z_JKCH7ibMYMJXU,7357
|
|
41
|
-
ramses_tx/parsers.py,sha256=
|
|
41
|
+
ramses_tx/parsers.py,sha256=uwu_HsMjpsSyMYzoBpVnAxu2pqBJiQDL5ls8_B60V7c,109708
|
|
42
42
|
ramses_tx/protocol.py,sha256=ifj3qwcQivjQDaQUwM94xp-U8Pmef6zwSH7mav8DLWA,28867
|
|
43
43
|
ramses_tx/protocol_fsm.py,sha256=YhHkTqbl8w-myimsOjV50uIFgg9HiApwPU7xA_jg5nU,26827
|
|
44
44
|
ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
-
ramses_tx/ramses.py,sha256=
|
|
45
|
+
ramses_tx/ramses.py,sha256=GnZwvx-HSVFdjXfUen6aWClDtrAmYaKwbrWl-LsyKO4,52045
|
|
46
46
|
ramses_tx/schemas.py,sha256=h2AcArVROy1_C4n6F0Crj4e-2BxXxH74xogFlc6nKHI,12769
|
|
47
|
-
ramses_tx/transport.py,sha256=
|
|
47
|
+
ramses_tx/transport.py,sha256=aLpULRSivoJqzH8GDPRDcbehETOhFflEqmHbaniGLvg,56210
|
|
48
48
|
ramses_tx/typed_dicts.py,sha256=4ZT50M-Cuwy2ljAIorwoxEJ9c737xUHrUxX9wTh79xE,10834
|
|
49
49
|
ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
|
|
50
|
-
ramses_tx/version.py,sha256=
|
|
51
|
-
ramses_rf-0.51.
|
|
52
|
-
ramses_rf-0.51.
|
|
53
|
-
ramses_rf-0.51.
|
|
54
|
-
ramses_rf-0.51.
|
|
55
|
-
ramses_rf-0.51.
|
|
50
|
+
ramses_tx/version.py,sha256=uQWGeAYKPd3vO8uf3sjQlNE_kdbcsQl2FfC3y2J7NJg,123
|
|
51
|
+
ramses_rf-0.51.5.dist-info/METADATA,sha256=5qyM_GCuZGD5rzMMGOjQUfNR4NJAd5TOQwIrBnoSEmY,3906
|
|
52
|
+
ramses_rf-0.51.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
53
|
+
ramses_rf-0.51.5.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
|
|
54
|
+
ramses_rf-0.51.5.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
|
|
55
|
+
ramses_rf-0.51.5.dist-info/RECORD,,
|
ramses_tx/const.py
CHANGED
|
@@ -88,6 +88,8 @@ SZ_REL_HUMIDITY: Final = "rel_humidity"
|
|
|
88
88
|
SZ_REMAINING_DAYS: Final = "days_remaining"
|
|
89
89
|
SZ_REMAINING_MINS: Final = "remaining_mins"
|
|
90
90
|
SZ_REMAINING_PERCENT: Final = "percent_remaining"
|
|
91
|
+
SZ_REQ_REASON: Final = "req_reason"
|
|
92
|
+
SZ_REQ_SPEED: Final = "req_speed"
|
|
91
93
|
SZ_SUPPLY_FAN_SPEED: Final = "supply_fan_speed"
|
|
92
94
|
SZ_SUPPLY_FLOW: Final = "supply_flow"
|
|
93
95
|
SZ_SUPPLY_TEMP: Final = "supply_temp"
|
ramses_tx/helpers.py
CHANGED
|
@@ -757,7 +757,7 @@ def parse_fan_info(value: HexStr2) -> PayDictT.FAN_INFO:
|
|
|
757
757
|
}
|
|
758
758
|
|
|
759
759
|
|
|
760
|
-
# 31DA[38:40]
|
|
760
|
+
# 31DA[38:40], also 2210
|
|
761
761
|
def parse_exhaust_fan_speed(value: HexStr2) -> PayDictT.EXHAUST_FAN_SPEED:
|
|
762
762
|
"""Return the exhaust fan speed (% of max speed)."""
|
|
763
763
|
return _parse_fan_speed(SZ_EXHAUST_FAN_SPEED, value) # type: ignore[return-value]
|
ramses_tx/parsers.py
CHANGED
|
@@ -72,6 +72,7 @@ from .const import (
|
|
|
72
72
|
SZ_RELAY_DEMAND,
|
|
73
73
|
SZ_REMAINING_DAYS,
|
|
74
74
|
SZ_REMAINING_PERCENT,
|
|
75
|
+
SZ_REQ_REASON,
|
|
75
76
|
SZ_SETPOINT,
|
|
76
77
|
SZ_SETPOINT_BOUNDS,
|
|
77
78
|
SZ_SYSTEM_MODE,
|
|
@@ -1378,29 +1379,42 @@ def parser_1fd4(payload: str, msg: Message) -> PayDictT._1FD4:
|
|
|
1378
1379
|
return {"ticker": int(payload[2:], 16)}
|
|
1379
1380
|
|
|
1380
1381
|
|
|
1381
|
-
# WIP:
|
|
1382
|
+
# WIP: HVAC auto requests (confirmed for Orcon, others?)
|
|
1382
1383
|
def parser_2210(payload: str, msg: Message) -> dict[str, Any]:
|
|
1383
1384
|
try:
|
|
1384
1385
|
assert msg.verb in (RP, I_) or payload == "00"
|
|
1385
|
-
assert payload[10:12] == payload[38:40]
|
|
1386
|
-
"
|
|
1387
|
-
|
|
1388
|
-
"FF",
|
|
1389
|
-
), f"expected (58|96|FF), not {payload[10:12]}"
|
|
1386
|
+
assert payload[10:12] == payload[38:40], (
|
|
1387
|
+
f"expected byte 19 {payload[10:12]}, not {payload[38:40]}"
|
|
1388
|
+
) # auto requested fan speed %. Identical [38:40] is for supply?
|
|
1390
1389
|
assert payload[20:22] == payload[48:50] and payload[20:22] in (
|
|
1391
|
-
"00",
|
|
1392
|
-
"
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
assert payload[80
|
|
1396
|
-
|
|
1390
|
+
"00", # idle
|
|
1391
|
+
"02", # requested by CO2 level/sensor
|
|
1392
|
+
"03", # requested by humidity level/sensor
|
|
1393
|
+
), f"expected req_reason (00|02|03), not {payload[20:22]}"
|
|
1394
|
+
assert payload[78:80] in ("00", "02"), (
|
|
1395
|
+
f"expected byte 39 (00|02), not {payload[78:80]}"
|
|
1396
|
+
)
|
|
1397
|
+
assert payload[80:82] in ("01", "08"), (
|
|
1398
|
+
f"expected byte 40 (01|08), not {payload[80:82]}"
|
|
1399
|
+
)
|
|
1400
|
+
assert payload[82:] in ("00", "40"), (
|
|
1401
|
+
f"expected byte 41- (00|40), not {payload[82:]}"
|
|
1402
|
+
)
|
|
1397
1403
|
|
|
1398
1404
|
except AssertionError as err:
|
|
1399
1405
|
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1400
1406
|
|
|
1407
|
+
_req = "IDL"
|
|
1408
|
+
if payload[20:22] == "02":
|
|
1409
|
+
_req = "CO2"
|
|
1410
|
+
elif payload[20:22] == "03":
|
|
1411
|
+
_req = "HUM"
|
|
1412
|
+
|
|
1401
1413
|
return {
|
|
1402
|
-
|
|
1403
|
-
|
|
1414
|
+
**parse_exhaust_fan_speed(
|
|
1415
|
+
payload[10:12]
|
|
1416
|
+
), # for Orcon: 29 hex == 41 decimal divided by 2 gives 20.5 (%)
|
|
1417
|
+
SZ_REQ_REASON: _req,
|
|
1404
1418
|
"unknown_78": payload[78:80],
|
|
1405
1419
|
"unknown_80": payload[80:82],
|
|
1406
1420
|
"unknown_82": payload[82:],
|
ramses_tx/ramses.py
CHANGED
|
@@ -1059,7 +1059,7 @@ _DEV_KLASSES_HVAC: dict[str, dict[Code, dict[VerbT, Any]]] = {
|
|
|
1059
1059
|
Code._12C8: {I_: {}},
|
|
1060
1060
|
Code._1470: {RP: {}},
|
|
1061
1061
|
Code._1F09: {I_: {}, RP: {}},
|
|
1062
|
-
Code._1FC9: {W_: {}},
|
|
1062
|
+
Code._1FC9: {I_: {}, W_: {}},
|
|
1063
1063
|
Code._2210: {I_: {}, RP: {}},
|
|
1064
1064
|
Code._22E0: {RP: {}},
|
|
1065
1065
|
Code._22E5: {RP: {}},
|
ramses_tx/transport.py
CHANGED
|
@@ -778,7 +778,8 @@ class FileTransport(_ReadTransport, _FileTransportAbstractor):
|
|
|
778
778
|
while not self._reading:
|
|
779
779
|
await asyncio.sleep(0.001)
|
|
780
780
|
self._frame_read(dtm_str, pkt_line)
|
|
781
|
-
|
|
781
|
+
await asyncio.sleep(0)
|
|
782
|
+
# NOTE: instable without, big performance penalty if delay >0
|
|
782
783
|
|
|
783
784
|
elif isinstance(self._pkt_source, str): # file_name, used in client parse
|
|
784
785
|
# open file file_name before reading
|
|
@@ -794,7 +795,8 @@ class FileTransport(_ReadTransport, _FileTransportAbstractor):
|
|
|
794
795
|
] != "#":
|
|
795
796
|
self._frame_read(dtm_pkt_line[:26], dtm_pkt_line[27:])
|
|
796
797
|
# this is where the parsing magic happens!
|
|
797
|
-
|
|
798
|
+
await asyncio.sleep(0)
|
|
799
|
+
# NOTE: instable without, big performance penalty if delay >0
|
|
798
800
|
except FileNotFoundError as err:
|
|
799
801
|
_LOGGER.warning(f"Correct the packet file name; {err}")
|
|
800
802
|
elif isinstance(self._pkt_source, TextIOWrapper): # used by client monitor
|
|
@@ -804,7 +806,8 @@ class FileTransport(_ReadTransport, _FileTransportAbstractor):
|
|
|
804
806
|
# can be blank lines in annotated log files
|
|
805
807
|
if (dtm_pkt_line := dtm_pkt_line.strip()) and dtm_pkt_line[:1] != "#":
|
|
806
808
|
self._frame_read(dtm_pkt_line[:26], dtm_pkt_line[27:])
|
|
807
|
-
await asyncio.sleep(0)
|
|
809
|
+
await asyncio.sleep(0)
|
|
810
|
+
# NOTE: instable without, big performance penalty if delay >0
|
|
808
811
|
else:
|
|
809
812
|
raise exc.TransportSourceInvalid(
|
|
810
813
|
f"Packet source is not dict, TextIOWrapper or str: {self._pkt_source:!r}"
|
|
@@ -1035,6 +1038,7 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1035
1038
|
|
|
1036
1039
|
self._connected = False
|
|
1037
1040
|
self._connecting = False
|
|
1041
|
+
self._connection_established = False # Track if initial connection was made
|
|
1038
1042
|
self._extra[SZ_IS_EVOFW3] = True
|
|
1039
1043
|
|
|
1040
1044
|
# Reconnection settings
|
|
@@ -1069,7 +1073,7 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1069
1073
|
self._connecting = True
|
|
1070
1074
|
try:
|
|
1071
1075
|
self.client.connect_async(
|
|
1072
|
-
self._broker_url.hostname
|
|
1076
|
+
str(self._broker_url.hostname or "localhost"),
|
|
1073
1077
|
self._broker_url.port or 1883,
|
|
1074
1078
|
60,
|
|
1075
1079
|
)
|
|
@@ -1136,8 +1140,21 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1136
1140
|
self._reconnect_task.cancel()
|
|
1137
1141
|
self._reconnect_task = None
|
|
1138
1142
|
|
|
1143
|
+
# Subscribe to base topic to see 'online' messages
|
|
1139
1144
|
self.client.subscribe(self._topic_base) # hope to see 'online' message
|
|
1140
1145
|
|
|
1146
|
+
# Also subscribe to data topics with wildcard for reliability after reconnect
|
|
1147
|
+
# This ensures we get data even if we miss the 'online' message
|
|
1148
|
+
if self._topic_base.endswith("/+"):
|
|
1149
|
+
data_wildcard = self._topic_base.replace("/+", "/+/rx")
|
|
1150
|
+
self.client.subscribe(data_wildcard, qos=self._mqtt_qos)
|
|
1151
|
+
_LOGGER.debug(f"Subscribed to data wildcard: {data_wildcard}")
|
|
1152
|
+
|
|
1153
|
+
# If we already have specific topics, re-subscribe to them
|
|
1154
|
+
if hasattr(self, "_topic_sub") and self._topic_sub:
|
|
1155
|
+
self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
|
|
1156
|
+
_LOGGER.debug(f"Re-subscribed to specific topic: {self._topic_sub}")
|
|
1157
|
+
|
|
1141
1158
|
def _on_connect_fail(
|
|
1142
1159
|
self,
|
|
1143
1160
|
client: mqtt.Client,
|
|
@@ -1155,19 +1172,36 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1155
1172
|
self,
|
|
1156
1173
|
client: mqtt.Client,
|
|
1157
1174
|
userdata: Any,
|
|
1158
|
-
|
|
1159
|
-
|
|
1175
|
+
*args: Any,
|
|
1176
|
+
**kwargs: Any,
|
|
1160
1177
|
) -> None:
|
|
1161
|
-
|
|
1178
|
+
# Handle different paho-mqtt callback signatures
|
|
1179
|
+
reason_code = args[0] if len(args) >= 1 else None
|
|
1180
|
+
|
|
1181
|
+
reason_name = (
|
|
1182
|
+
reason_code.getName()
|
|
1183
|
+
if reason_code is not None and hasattr(reason_code, "getName")
|
|
1184
|
+
else str(reason_code)
|
|
1185
|
+
)
|
|
1186
|
+
_LOGGER.warning(f"MQTT disconnected: {reason_name}")
|
|
1162
1187
|
|
|
1188
|
+
was_connected = self._connected
|
|
1163
1189
|
self._connected = False
|
|
1164
1190
|
|
|
1191
|
+
# If we were previously connected and had established communication,
|
|
1192
|
+
# notify that the device is now offline
|
|
1193
|
+
if was_connected and hasattr(self, "_topic_sub") and self._topic_sub:
|
|
1194
|
+
device_topic = self._topic_sub[:-3] # Remove "/rx" suffix
|
|
1195
|
+
_LOGGER.warning(f"{self}: the MQTT device is offline: {device_topic}")
|
|
1196
|
+
|
|
1197
|
+
# Pause writing since device is offline
|
|
1198
|
+
if hasattr(self, "_protocol"):
|
|
1199
|
+
self._protocol.pause_writing()
|
|
1200
|
+
|
|
1165
1201
|
# Only attempt reconnection if we didn't deliberately disconnect
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
elif reason_code.is_failure and not self._closing:
|
|
1170
|
-
# Connection failed, also schedule reconnection
|
|
1202
|
+
|
|
1203
|
+
if not self._closing:
|
|
1204
|
+
# Schedule reconnection for any disconnect (unexpected or failure)
|
|
1171
1205
|
self._schedule_reconnect()
|
|
1172
1206
|
|
|
1173
1207
|
def _create_connection(self, msg: mqtt.MQTTMessage) -> None:
|
|
@@ -1191,7 +1225,12 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1191
1225
|
|
|
1192
1226
|
self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
|
|
1193
1227
|
|
|
1194
|
-
|
|
1228
|
+
# Only call connection_made on first connection, not reconnections
|
|
1229
|
+
if not self._connection_established:
|
|
1230
|
+
self._connection_established = True
|
|
1231
|
+
self._make_connection(gwy_id=msg.topic[-9:]) # type: ignore[arg-type]
|
|
1232
|
+
else:
|
|
1233
|
+
_LOGGER.info("MQTT reconnected - protocol connection already established")
|
|
1195
1234
|
|
|
1196
1235
|
# NOTE: self._frame_read() invoked from here
|
|
1197
1236
|
def _on_message(
|
|
@@ -1211,23 +1250,51 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1211
1250
|
_LOGGER.info("Rx: %s", msg.payload)
|
|
1212
1251
|
|
|
1213
1252
|
if msg.topic[-3:] != "/rx": # then, e.g. 'RAMSES/GATEWAY/18:017804'
|
|
1214
|
-
if msg.payload == b"offline"
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1253
|
+
if msg.payload == b"offline":
|
|
1254
|
+
# Check if this offline message is for our current device
|
|
1255
|
+
if (
|
|
1256
|
+
hasattr(self, "_topic_sub")
|
|
1257
|
+
and self._topic_sub
|
|
1258
|
+
and msg.topic == self._topic_sub[:-3]
|
|
1259
|
+
) or not hasattr(self, "_topic_sub"):
|
|
1260
|
+
_LOGGER.warning(
|
|
1261
|
+
f"{self}: the ESP device is offline (via LWT): {msg.topic}"
|
|
1262
|
+
)
|
|
1263
|
+
# Don't set _connected = False here - that's for MQTT connection, not ESP device
|
|
1264
|
+
if hasattr(self, "_protocol"):
|
|
1265
|
+
self._protocol.pause_writing()
|
|
1220
1266
|
|
|
1221
1267
|
# BUG: using create task (self._loop.ct() & asyncio.ct()) causes the
|
|
1222
1268
|
# BUG: event look to close early
|
|
1223
1269
|
elif msg.payload == b"online":
|
|
1224
1270
|
_LOGGER.info(
|
|
1225
|
-
f"{self}: the
|
|
1271
|
+
f"{self}: the ESP device is online (via status): {msg.topic}"
|
|
1226
1272
|
)
|
|
1227
1273
|
self._create_connection(msg)
|
|
1228
1274
|
|
|
1229
1275
|
return
|
|
1230
1276
|
|
|
1277
|
+
# Handle data messages - if we don't have connection established yet but get data,
|
|
1278
|
+
# we can infer the gateway from the topic
|
|
1279
|
+
if not self._connection_established and msg.topic.endswith("/rx"):
|
|
1280
|
+
# Extract gateway ID from topic like "RAMSES/GATEWAY/18:123456/rx"
|
|
1281
|
+
topic_parts = msg.topic.split("/")
|
|
1282
|
+
if len(topic_parts) >= 3 and topic_parts[-2] not in ("+", "*"):
|
|
1283
|
+
gateway_id = topic_parts[-2] # Should be something like "18:123456"
|
|
1284
|
+
_LOGGER.info(
|
|
1285
|
+
f"Inferring gateway connection from data topic: {gateway_id}"
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
# Set up topics and connection
|
|
1289
|
+
self._topic_pub = f"{'/'.join(topic_parts[:-1])}/tx"
|
|
1290
|
+
self._topic_sub = msg.topic
|
|
1291
|
+
self._extra[SZ_ACTIVE_HGI] = gateway_id
|
|
1292
|
+
|
|
1293
|
+
# Mark as connected and establish protocol connection
|
|
1294
|
+
self._connected = True
|
|
1295
|
+
self._connection_established = True
|
|
1296
|
+
self._make_connection(gwy_id=gateway_id) # type: ignore[arg-type]
|
|
1297
|
+
|
|
1231
1298
|
try:
|
|
1232
1299
|
payload = json.loads(msg.payload)
|
|
1233
1300
|
except json.JSONDecodeError:
|
ramses_tx/version.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|