ramses-rf 0.22.40__py3-none-any.whl → 0.51.2__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/__init__.py +18 -0
- ramses_cli/client.py +597 -0
- ramses_cli/debug.py +20 -0
- ramses_cli/discovery.py +405 -0
- ramses_cli/utils/cat_slow.py +17 -0
- ramses_cli/utils/convert.py +60 -0
- ramses_rf/__init__.py +31 -10
- ramses_rf/binding_fsm.py +787 -0
- ramses_rf/const.py +124 -105
- ramses_rf/database.py +297 -0
- ramses_rf/device/__init__.py +69 -39
- ramses_rf/device/base.py +187 -376
- ramses_rf/device/heat.py +540 -552
- ramses_rf/device/hvac.py +279 -171
- ramses_rf/dispatcher.py +153 -177
- ramses_rf/entity_base.py +478 -361
- ramses_rf/exceptions.py +82 -0
- ramses_rf/gateway.py +377 -513
- ramses_rf/helpers.py +57 -19
- ramses_rf/py.typed +0 -0
- ramses_rf/schemas.py +148 -194
- ramses_rf/system/__init__.py +16 -23
- ramses_rf/system/faultlog.py +363 -0
- ramses_rf/system/heat.py +295 -302
- ramses_rf/system/schedule.py +312 -198
- ramses_rf/system/zones.py +318 -238
- ramses_rf/version.py +2 -8
- ramses_rf-0.51.2.dist-info/METADATA +72 -0
- ramses_rf-0.51.2.dist-info/RECORD +55 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info}/WHEEL +1 -2
- ramses_rf-0.51.2.dist-info/entry_points.txt +2 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info/licenses}/LICENSE +1 -1
- ramses_tx/__init__.py +160 -0
- {ramses_rf/protocol → ramses_tx}/address.py +65 -59
- ramses_tx/command.py +1454 -0
- ramses_tx/const.py +903 -0
- ramses_tx/exceptions.py +92 -0
- {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
- {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
- ramses_tx/gateway.py +338 -0
- ramses_tx/helpers.py +883 -0
- {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
- {ramses_rf/protocol → ramses_tx}/message.py +155 -191
- ramses_tx/opentherm.py +1260 -0
- ramses_tx/packet.py +210 -0
- {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
- ramses_tx/protocol.py +801 -0
- ramses_tx/protocol_fsm.py +672 -0
- ramses_tx/py.typed +0 -0
- {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
- {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
- ramses_tx/transport.py +1471 -0
- ramses_tx/typed_dicts.py +492 -0
- ramses_tx/typing.py +181 -0
- ramses_tx/version.py +4 -0
- ramses_rf/discovery.py +0 -398
- ramses_rf/protocol/__init__.py +0 -59
- ramses_rf/protocol/backports.py +0 -42
- ramses_rf/protocol/command.py +0 -1576
- ramses_rf/protocol/const.py +0 -697
- ramses_rf/protocol/exceptions.py +0 -111
- ramses_rf/protocol/helpers.py +0 -390
- ramses_rf/protocol/opentherm.py +0 -1170
- ramses_rf/protocol/packet.py +0 -235
- ramses_rf/protocol/protocol.py +0 -613
- ramses_rf/protocol/transport.py +0 -1011
- ramses_rf/protocol/version.py +0 -10
- ramses_rf/system/hvac.py +0 -82
- ramses_rf-0.22.40.dist-info/METADATA +0 -64
- ramses_rf-0.22.40.dist-info/RECORD +0 -42
- ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
ramses_tx/packet.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""RAMSES RF - a RAMSES-II protocol decoder & analyser.
|
|
3
|
+
|
|
4
|
+
Decode/process a packet (packet that was received).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime as dt, timedelta as td
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from . import exceptions as exc
|
|
13
|
+
from .command import Command
|
|
14
|
+
from .frame import Frame
|
|
15
|
+
from .logger import getLogger # overridden logger.getLogger
|
|
16
|
+
from .opentherm import PARAMS_DATA_IDS, SCHEMA_DATA_IDS, STATUS_DATA_IDS
|
|
17
|
+
from .ramses import CODES_SCHEMA, SZ_LIFESPAN
|
|
18
|
+
|
|
19
|
+
from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
20
|
+
I_,
|
|
21
|
+
RP,
|
|
22
|
+
RQ,
|
|
23
|
+
W_,
|
|
24
|
+
Code,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# these trade memory for speed
|
|
29
|
+
_TD_SECS_000 = td(seconds=0)
|
|
30
|
+
_TD_SECS_003 = td(seconds=3)
|
|
31
|
+
_TD_SECS_360 = td(seconds=360)
|
|
32
|
+
_TD_MINS_005 = td(minutes=5)
|
|
33
|
+
_TD_MINS_060 = td(minutes=60)
|
|
34
|
+
_TD_MINS_360 = td(minutes=360)
|
|
35
|
+
_TD_DAYS_001 = td(minutes=60 * 24)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
PKT_LOGGER = getLogger(f"{__name__}_log", pkt_log=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Packet(Frame):
|
|
42
|
+
"""The Packet class (pkts that were received); will trap/log invalid pkts.
|
|
43
|
+
|
|
44
|
+
They have a datetime (when received) an RSSI, and other meta-fields.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_dtm: dt
|
|
48
|
+
_rssi: str
|
|
49
|
+
|
|
50
|
+
def __init__(self, dtm: dt, frame: str, **kwargs: Any) -> None:
|
|
51
|
+
"""Create a packet from a string (actually from f"{RSSI} {frame}").
|
|
52
|
+
|
|
53
|
+
Will raise InvalidPacketError if it is invalid.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
super().__init__(frame[4:]) # remove RSSI
|
|
57
|
+
|
|
58
|
+
self._dtm: dt = dtm
|
|
59
|
+
|
|
60
|
+
self._rssi: str = frame[0:3]
|
|
61
|
+
|
|
62
|
+
self.comment: str = kwargs.get("comment", "")
|
|
63
|
+
self.error_text: str = kwargs.get("err_msg", "")
|
|
64
|
+
self.raw_frame: str = kwargs.get("raw_frame", "")
|
|
65
|
+
|
|
66
|
+
self._lifespan: bool | td = pkt_lifespan(self) or False
|
|
67
|
+
|
|
68
|
+
self._validate(strict_checking=False)
|
|
69
|
+
|
|
70
|
+
def _validate(self, *, strict_checking: bool = False) -> None:
|
|
71
|
+
"""Validate the packet, and parse the addresses if so (will log all packets).
|
|
72
|
+
|
|
73
|
+
Raise an exception InvalidPacketError (InvalidAddrSetError) if it is not valid.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
if self.error_text:
|
|
78
|
+
raise exc.PacketInvalid(self.error_text)
|
|
79
|
+
|
|
80
|
+
if not self._frame and self.comment: # log null pkts only if has a comment
|
|
81
|
+
raise exc.PacketInvalid("Null packet")
|
|
82
|
+
|
|
83
|
+
super()._validate(strict_checking=strict_checking) # no RSSI
|
|
84
|
+
|
|
85
|
+
# FIXME: this is messy
|
|
86
|
+
PKT_LOGGER.info("", extra=self.__dict__) # the packet.log line
|
|
87
|
+
|
|
88
|
+
except exc.PacketInvalid as err: # incl. InvalidAddrSetError
|
|
89
|
+
if self._frame or self.error_text:
|
|
90
|
+
PKT_LOGGER.warning("%s", err, extra=self.__dict__)
|
|
91
|
+
raise err
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
"""Return an unambiguous string representation of this object."""
|
|
95
|
+
# e.g.: RQ --- 18:000730 01:145038 --:------ 000A 002 0800 # 000A|RQ|01:145038|08
|
|
96
|
+
try:
|
|
97
|
+
hdr = f" # {self._hdr}{f' ({self._ctx})' if self._ctx else ''}"
|
|
98
|
+
except (exc.PacketInvalid, NotImplementedError):
|
|
99
|
+
hdr = ""
|
|
100
|
+
try:
|
|
101
|
+
dtm = self.dtm.isoformat(timespec="microseconds")
|
|
102
|
+
except AttributeError:
|
|
103
|
+
dtm = dt.min.isoformat(timespec="microseconds")
|
|
104
|
+
return f"{dtm} ... {self}{hdr}"
|
|
105
|
+
|
|
106
|
+
def __str__(self) -> str:
|
|
107
|
+
"""Return a brief readable string representation of this object."""
|
|
108
|
+
# e.g.: 000A|RQ|01:145038|08
|
|
109
|
+
return super().__repr__() # TODO: self._hdr
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def dtm(self) -> dt:
|
|
113
|
+
return self._dtm
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _partition(pkt_line: str) -> tuple[str, str, str]: # map[str]
|
|
117
|
+
"""Partition a packet line into its three parts.
|
|
118
|
+
|
|
119
|
+
Format: packet[ < parser-hint: ...][ * evofw3-err_msg][ # evofw3-comment]
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
fragment, _, comment = pkt_line.partition("#")
|
|
123
|
+
fragment, _, err_msg = fragment.partition("*")
|
|
124
|
+
pkt_str, _, _ = fragment.partition("<") # discard any parser hints
|
|
125
|
+
return map(str.strip, (pkt_str, err_msg, comment)) # type: ignore[return-value]
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def _from_cmd(cls, cmd: Command, dtm: dt | None = None) -> Packet:
|
|
129
|
+
"""Create a Packet from a Command."""
|
|
130
|
+
if dtm is None:
|
|
131
|
+
dtm = dt.now()
|
|
132
|
+
return cls.from_port(dtm, f"... {cmd._frame}")
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_dict(cls, dtm: str, pkt_line: str) -> Packet:
|
|
136
|
+
"""Create a packet from a saved state (a curated dict)."""
|
|
137
|
+
frame, _, comment = cls._partition(pkt_line)
|
|
138
|
+
return cls(dt.fromisoformat(dtm), frame, comment=comment)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_file(cls, dtm: str, pkt_line: str) -> Packet:
|
|
142
|
+
"""Create a packet from a log file line."""
|
|
143
|
+
frame, err_msg, comment = cls._partition(pkt_line)
|
|
144
|
+
if not frame:
|
|
145
|
+
raise ValueError(f"null frame: >>>{frame}<<<")
|
|
146
|
+
return cls(dt.fromisoformat(dtm), frame, err_msg=err_msg, comment=comment)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_port(cls, dtm: dt, pkt_line: str, raw_line: bytes | None = None) -> Packet:
|
|
150
|
+
"""Create a packet from a USB port (HGI80, evofw3)."""
|
|
151
|
+
frame, err_msg, comment = cls._partition(pkt_line)
|
|
152
|
+
if not frame:
|
|
153
|
+
raise ValueError(f"null frame: >>>{frame}<<<")
|
|
154
|
+
return cls(dtm, frame, err_msg=err_msg, comment=comment, raw_frame=raw_line)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# TODO: remove None as a possible return value
|
|
158
|
+
def pkt_lifespan(pkt: Packet) -> td: # import OtbGateway??
|
|
159
|
+
"""Return the pkt lifespan, or dt.max() if the packet does not expire.
|
|
160
|
+
|
|
161
|
+
Some codes require a valid payload to best determine lifespan (e.g. 1F09).
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
if pkt.verb in (RQ, W_):
|
|
165
|
+
return _TD_SECS_000
|
|
166
|
+
|
|
167
|
+
if pkt.code in (Code._0005, Code._000C):
|
|
168
|
+
return _TD_DAYS_001
|
|
169
|
+
|
|
170
|
+
if pkt.code == Code._0006:
|
|
171
|
+
return _TD_MINS_060
|
|
172
|
+
|
|
173
|
+
if pkt.code == Code._0404: # 0404 tombstoned by incremented 0006
|
|
174
|
+
return _TD_DAYS_001
|
|
175
|
+
|
|
176
|
+
if pkt.code == Code._000A and pkt._has_array:
|
|
177
|
+
return _TD_MINS_060 # sends I /1h
|
|
178
|
+
|
|
179
|
+
if pkt.code == Code._10E0: # but: what if valid pkt with a corrupt src_id
|
|
180
|
+
return _TD_DAYS_001
|
|
181
|
+
|
|
182
|
+
if pkt.code == Code._1F09: # sends I /sync_cycle
|
|
183
|
+
# can't do better than 300s with reading the payload
|
|
184
|
+
return _TD_SECS_360 if pkt.verb == I_ else _TD_SECS_000
|
|
185
|
+
|
|
186
|
+
if pkt.code == Code._1FC9 and pkt.verb == RP:
|
|
187
|
+
return _TD_DAYS_001 # TODO: check other verbs, they seem variable
|
|
188
|
+
|
|
189
|
+
if pkt.code in (Code._2309, Code._30C9) and pkt._has_array: # sends I /sync_cycle
|
|
190
|
+
return _TD_SECS_360
|
|
191
|
+
|
|
192
|
+
if pkt.code == Code._3220: # FIXME: 2.1 means we can miss two packets
|
|
193
|
+
# if pkt.payload[4:6] in WRITE_MSG_IDS: # and Write-Data: # TODO
|
|
194
|
+
# return _TD_SECS_003 * 2.1
|
|
195
|
+
if int(pkt.payload[4:6], 16) in SCHEMA_DATA_IDS:
|
|
196
|
+
return _TD_MINS_360 * 2.1
|
|
197
|
+
if int(pkt.payload[4:6], 16) in PARAMS_DATA_IDS:
|
|
198
|
+
return _TD_MINS_060 * 2.1
|
|
199
|
+
if int(pkt.payload[4:6], 16) in STATUS_DATA_IDS:
|
|
200
|
+
return _TD_MINS_005 * 2.1
|
|
201
|
+
return _TD_MINS_005 * 2.1
|
|
202
|
+
|
|
203
|
+
# if pkt.code in (Code._3B00, Code._3EF0, ): # TODO: 0008, 3EF0, 3EF1
|
|
204
|
+
# return td(minutes=6.7) # TODO: WIP
|
|
205
|
+
|
|
206
|
+
if (code := CODES_SCHEMA.get(pkt.code)) and SZ_LIFESPAN in code:
|
|
207
|
+
result: bool | td | None = CODES_SCHEMA[pkt.code][SZ_LIFESPAN]
|
|
208
|
+
return result if isinstance(result, td) else _TD_MINS_060
|
|
209
|
+
|
|
210
|
+
return _TD_MINS_060 # applies to lots of HVAC packets
|