ramses-rf 0.22.2__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 (72) 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 +378 -514
  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.2.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.2.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_tx/parsers.py +2957 -0
  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 -1561
  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/parsers.py +0 -2673
  66. ramses_rf/protocol/protocol.py +0 -613
  67. ramses_rf/protocol/transport.py +0 -1011
  68. ramses_rf/protocol/version.py +0 -10
  69. ramses_rf/system/hvac.py +0 -82
  70. ramses_rf-0.22.2.dist-info/METADATA +0 -64
  71. ramses_rf-0.22.2.dist-info/RECORD +0 -42
  72. ramses_rf-0.22.2.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