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_rf/protocol/exceptions.py
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
#
|
|
4
|
-
"""RAMSES RF - exceptions."""
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class EvohomeError(Exception):
|
|
9
|
-
"""Base class for exceptions in this module."""
|
|
10
|
-
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class ExpiredCallbackError(EvohomeError):
|
|
15
|
-
"""Raised when the callback has expired."""
|
|
16
|
-
|
|
17
|
-
def __init__(self, *args, **kwargs):
|
|
18
|
-
super().__init__(*args, **kwargs)
|
|
19
|
-
self.message = args[0] if args else None
|
|
20
|
-
|
|
21
|
-
def __str__(self) -> str:
|
|
22
|
-
err_msg = "The callback has expired"
|
|
23
|
-
err_tip = "(no hint)"
|
|
24
|
-
if self.message:
|
|
25
|
-
return f"{err_msg}: {self.message} {err_tip}"
|
|
26
|
-
return f"{err_msg} {err_tip}"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class CorruptEvohomeError(EvohomeError):
|
|
30
|
-
"""Base class for exceptions in this module."""
|
|
31
|
-
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class InvalidPacketError(CorruptEvohomeError):
|
|
36
|
-
"""Raised when the packet is inconsistent."""
|
|
37
|
-
|
|
38
|
-
def __init__(self, *args, **kwargs):
|
|
39
|
-
super().__init__(*args, **kwargs)
|
|
40
|
-
self.message = args[0] if args else None
|
|
41
|
-
|
|
42
|
-
def __str__(self) -> str:
|
|
43
|
-
err_msg = "Corrupt packet"
|
|
44
|
-
err_tip = ""
|
|
45
|
-
if self.message:
|
|
46
|
-
return f"{err_msg}: {self.message}{err_tip}"
|
|
47
|
-
return f"{err_msg} {err_tip}"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class InvalidAddrSetError(InvalidPacketError):
|
|
51
|
-
"""Raised when the packet's address set is inconsistent."""
|
|
52
|
-
|
|
53
|
-
def __init__(self, *args, **kwargs):
|
|
54
|
-
super().__init__(*args, **kwargs)
|
|
55
|
-
self.message = args[0] if args else None
|
|
56
|
-
|
|
57
|
-
def __str__(self) -> str:
|
|
58
|
-
err_msg = "Corrupt addresses"
|
|
59
|
-
err_tip = ""
|
|
60
|
-
if self.message:
|
|
61
|
-
return f"{err_msg}: {self.message}{err_tip}"
|
|
62
|
-
return f"{err_msg} {err_tip}"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class InvalidPayloadError(InvalidPacketError):
|
|
66
|
-
"""Raised when the packet's payload is inconsistent."""
|
|
67
|
-
|
|
68
|
-
def __init__(self, *args, **kwargs):
|
|
69
|
-
super().__init__(*args, **kwargs)
|
|
70
|
-
self.message = args[0] if args else None
|
|
71
|
-
|
|
72
|
-
def __str__(self) -> str:
|
|
73
|
-
err_msg = "Corrupt payload"
|
|
74
|
-
err_tip = ""
|
|
75
|
-
if self.message:
|
|
76
|
-
return f"{err_msg}: {self.message}{err_tip}"
|
|
77
|
-
return f"{err_msg} {err_tip}"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class CorruptStateError(CorruptEvohomeError):
|
|
81
|
-
"""Raised when the system state (usu. schema) is inconsistent."""
|
|
82
|
-
|
|
83
|
-
def __init__(self, *args, **kwargs):
|
|
84
|
-
super().__init__(*args, **kwargs)
|
|
85
|
-
self.message = args[0] if args else None
|
|
86
|
-
|
|
87
|
-
def __str__(self) -> str:
|
|
88
|
-
err_msg = "Inconsistent schema"
|
|
89
|
-
err_tip = "(try restarting the client library)"
|
|
90
|
-
if self.message:
|
|
91
|
-
return f"{err_msg}: {self.message}{err_tip}"
|
|
92
|
-
return f"{err_msg} {err_tip}"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
class ForeignGatewayError(EvohomeError):
|
|
96
|
-
"""Raised when a foreign gateway is detected.
|
|
97
|
-
|
|
98
|
-
These devices may not be gateways (set a class), or belong to a neighbout (exclude
|
|
99
|
-
via block_list/known_list), or should be allowed (known_list).
|
|
100
|
-
"""
|
|
101
|
-
|
|
102
|
-
def __init__(self, *args, **kwargs):
|
|
103
|
-
super().__init__(*args, **kwargs)
|
|
104
|
-
self.message = args[0] if args else None
|
|
105
|
-
|
|
106
|
-
def __str__(self) -> str:
|
|
107
|
-
err_msg = "There is more than one HGI80-compatible gateway"
|
|
108
|
-
err_tip = " (consider enforcing a known_list)"
|
|
109
|
-
if self.message:
|
|
110
|
-
return f"{err_msg}: {self.message}{err_tip}"
|
|
111
|
-
return f"{err_msg} {err_tip}"
|
ramses_rf/protocol/helpers.py
DELETED
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
#
|
|
4
|
-
"""RAMSES RF - Protocol/Transport layer.
|
|
5
|
-
|
|
6
|
-
Helper functions.
|
|
7
|
-
"""
|
|
8
|
-
# from __future__ import annotations # incompatible with @typechecked
|
|
9
|
-
|
|
10
|
-
import ctypes
|
|
11
|
-
import sys
|
|
12
|
-
import time
|
|
13
|
-
from datetime import datetime as dt
|
|
14
|
-
from typing import ( # typeguard doesn't support PEP604 on 3.9.x
|
|
15
|
-
Iterable,
|
|
16
|
-
Optional,
|
|
17
|
-
Union,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
from typeguard import typechecked # type: ignore[reportMissingImports]
|
|
22
|
-
|
|
23
|
-
except ImportError:
|
|
24
|
-
|
|
25
|
-
def typechecked(fnc): # type: ignore[no-redef]
|
|
26
|
-
def wrapper(*args, **kwargs):
|
|
27
|
-
return fnc(*args, **kwargs)
|
|
28
|
-
|
|
29
|
-
return wrapper
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class _FILE_TIME(ctypes.Structure):
|
|
33
|
-
"""Data structure for GetSystemTimePreciseAsFileTime()."""
|
|
34
|
-
|
|
35
|
-
_fields_ = [("dwLowDateTime", ctypes.c_uint), ("dwHighDateTime", ctypes.c_uint)]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
file_time = _FILE_TIME()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@typechecked
|
|
42
|
-
def timestamp() -> float:
|
|
43
|
-
"""Return the number of seconds since the Unix epoch.
|
|
44
|
-
|
|
45
|
-
Return an accurate value, even for Windows-based systems.
|
|
46
|
-
""" # see: https://www.python.org/dev/peps/pep-0564/
|
|
47
|
-
if sys.platform != "win32":
|
|
48
|
-
return time.time_ns() / 1e9 # since 1970-01-01T00:00:00Z, time.gmtime(0)
|
|
49
|
-
ctypes.windll.kernel32.GetSystemTimePreciseAsFileTime(ctypes.byref(file_time))
|
|
50
|
-
_time = (file_time.dwLowDateTime + (file_time.dwHighDateTime << 32)) / 1e7
|
|
51
|
-
return _time - 134774 * 24 * 60 * 60 # otherwise, is since 1601-01-01T00:00:00Z
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@typechecked
|
|
55
|
-
def dt_now() -> dt:
|
|
56
|
-
"""Return the current datetime as a local/naive datetime object.
|
|
57
|
-
|
|
58
|
-
This is slower, but potentially more accurate, than dt.now(), and is used mainly for
|
|
59
|
-
packet timestamps.
|
|
60
|
-
"""
|
|
61
|
-
if sys.platform == "win32":
|
|
62
|
-
return dt.fromtimestamp(timestamp())
|
|
63
|
-
return dt.now()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@typechecked
|
|
67
|
-
def dt_str() -> str:
|
|
68
|
-
"""Return the current datetime as a isoformat string."""
|
|
69
|
-
return dt_now().isoformat(timespec="microseconds")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@typechecked
|
|
73
|
-
def bool_from_hex(value: str) -> Optional[bool]: # either False, True or None
|
|
74
|
-
"""Convert a 2-char hex string into a boolean."""
|
|
75
|
-
if not isinstance(value, str) or len(value) != 2:
|
|
76
|
-
raise ValueError(f"Invalid value: {value}, is not a 2-char hex string")
|
|
77
|
-
if value == "FF":
|
|
78
|
-
return None
|
|
79
|
-
return {"00": False, "C8": True}[value]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@typechecked
|
|
83
|
-
def bool_to_hex(value: Optional[bool]) -> str: # either 00, C8 or FF
|
|
84
|
-
"""Convert a boolean into a 2-char hex string."""
|
|
85
|
-
if value is None:
|
|
86
|
-
return "FF"
|
|
87
|
-
if not isinstance(value, bool):
|
|
88
|
-
raise ValueError(f"Invalid value: {value}, is not bool")
|
|
89
|
-
return {False: "00", True: "C8"}[value]
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@typechecked
|
|
93
|
-
def date_from_hex(value: str) -> Optional[str]: # YY-MM-DD
|
|
94
|
-
"""Convert am 8-char hex string into a date, format YY-MM-DD."""
|
|
95
|
-
if not isinstance(value, str) or len(value) != 8:
|
|
96
|
-
raise ValueError(f"Invalid value: {value}, is not an 8-char hex string")
|
|
97
|
-
if value == "FFFFFFFF":
|
|
98
|
-
return None
|
|
99
|
-
return dt(
|
|
100
|
-
year=int(value[4:8], 16),
|
|
101
|
-
month=int(value[2:4], 16),
|
|
102
|
-
day=int(value[:2], 16) & 0b11111, # 1st 3 bits: DayOfWeek
|
|
103
|
-
).strftime("%Y-%m-%d")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@typechecked
|
|
107
|
-
def double_from_hex(value: str, factor: int = 1) -> Optional[float]:
|
|
108
|
-
"""Convert a 4-char hex string into a double."""
|
|
109
|
-
if not isinstance(value, str) or len(value) != 4:
|
|
110
|
-
raise ValueError(f"Invalid value: {value}, is not a 4-char hex string")
|
|
111
|
-
if value == "7FFF":
|
|
112
|
-
return None
|
|
113
|
-
return int(value, 16) / factor
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@typechecked
|
|
117
|
-
def double_to_hex(value: Optional[float], factor: int = 1) -> str:
|
|
118
|
-
"""Convert a double into 4-char hex string."""
|
|
119
|
-
if value is None:
|
|
120
|
-
return "7FFF"
|
|
121
|
-
if not isinstance(value, float):
|
|
122
|
-
raise ValueError(f"Invalid value: {value}, is not a double (a float)")
|
|
123
|
-
return f"{int(value * factor):04X}"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@typechecked
|
|
127
|
-
def dtm_from_hex(value: str) -> Optional[str]: # from parsers
|
|
128
|
-
"""Convert a 12/14-char hex string to an isoformat datetime (naive, local)."""
|
|
129
|
-
# 00141B0A07E3 (...HH:MM:00) for system_mode, zone_mode (schedules?)
|
|
130
|
-
# 0400041C0A07E3 (...HH:MM:SS) for sync_datetime
|
|
131
|
-
|
|
132
|
-
if not isinstance(value, str) or len(value) not in (12, 14):
|
|
133
|
-
raise ValueError(f"Invalid value: {value}, is not a 12/14-char hex string")
|
|
134
|
-
if value[-12:] == "FF" * 6:
|
|
135
|
-
return None
|
|
136
|
-
if len(value) == 12:
|
|
137
|
-
value = f"00{value}"
|
|
138
|
-
return dt(
|
|
139
|
-
year=int(value[10:14], 16),
|
|
140
|
-
month=int(value[8:10], 16),
|
|
141
|
-
day=int(value[6:8], 16),
|
|
142
|
-
hour=int(value[4:6], 16) & 0b11111, # 1st 3 bits: DayOfWeek
|
|
143
|
-
minute=int(value[2:4], 16),
|
|
144
|
-
second=int(value[:2], 16) & 0b1111111, # 1st bit: used for DST
|
|
145
|
-
).isoformat(timespec="seconds")
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
@typechecked
|
|
149
|
-
def dtm_to_hex(dtm: Union[None, dt, str], is_dst=False, incl_seconds=False) -> str:
|
|
150
|
-
"""Convert a datetime (isoformat str, or naive dtm) to a 12/14-char hex str."""
|
|
151
|
-
|
|
152
|
-
def _dtm_to_hex(tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, *args):
|
|
153
|
-
return (
|
|
154
|
-
f"{tm_sec:02X}{tm_min:02X}{tm_hour:02X}"
|
|
155
|
-
f"{tm_mday:02X}{tm_mon:02X}{tm_year:04X}"
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
if dtm is None:
|
|
159
|
-
return "FF" * (7 if incl_seconds else 6)
|
|
160
|
-
if isinstance(dtm, str):
|
|
161
|
-
dtm = dt.fromisoformat(dtm)
|
|
162
|
-
dtm_str = _dtm_to_hex(*dtm.timetuple()) # TODO: add DST for tm_isdst
|
|
163
|
-
if is_dst:
|
|
164
|
-
dtm_str = f"{int(dtm_str[:2], 16) | 0x80:02X}" + dtm_str[2:]
|
|
165
|
-
return dtm_str if incl_seconds else dtm_str[2:]
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
@typechecked
|
|
169
|
-
def dts_from_hex(value: str) -> Optional[str]:
|
|
170
|
-
"""YY-MM-DD HH:MM:SS."""
|
|
171
|
-
if not isinstance(value, str) or len(value) != 12:
|
|
172
|
-
raise ValueError(f"Invalid value: {value}, is not a 12-char hex string")
|
|
173
|
-
if value == "00000000007F":
|
|
174
|
-
return None
|
|
175
|
-
_seqx = int(value, 16)
|
|
176
|
-
return dt(
|
|
177
|
-
year=(_seqx & 0b1111111 << 24) >> 24,
|
|
178
|
-
month=(_seqx & 0b1111 << 36) >> 36,
|
|
179
|
-
day=(_seqx & 0b11111 << 31) >> 31,
|
|
180
|
-
hour=(_seqx & 0b11111 << 19) >> 19,
|
|
181
|
-
minute=(_seqx & 0b111111 << 13) >> 13,
|
|
182
|
-
second=(_seqx & 0b111111 << 7) >> 7,
|
|
183
|
-
).strftime("%y-%m-%dT%H:%M:%S")
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
@typechecked
|
|
187
|
-
def dts_to_hex(dtm: Union[None, dt, str]) -> str: # TODO: WIP
|
|
188
|
-
"""Convert a datetime (isoformat str, or dtm) to a packed 12-char hex str."""
|
|
189
|
-
"""YY-MM-DD HH:MM:SS."""
|
|
190
|
-
if dtm is None:
|
|
191
|
-
return "00000000007F"
|
|
192
|
-
if isinstance(dtm, str):
|
|
193
|
-
dtm = dt.fromisoformat(dtm) # TODO: YY-MM-DD, not YYYY-MM-DD
|
|
194
|
-
(tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec, *_) = dtm.timetuple()
|
|
195
|
-
result = sum(
|
|
196
|
-
(
|
|
197
|
-
tm_year % 100 << 24,
|
|
198
|
-
tm_mon << 36,
|
|
199
|
-
tm_mday << 31,
|
|
200
|
-
tm_hour << 19,
|
|
201
|
-
tm_min << 13,
|
|
202
|
-
tm_sec << 7,
|
|
203
|
-
)
|
|
204
|
-
)
|
|
205
|
-
return f"{result:012X}"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@typechecked
|
|
209
|
-
def flag8_from_hex(byte: str, lsb: bool = False) -> list[int]: # TODO: use tuple
|
|
210
|
-
"""Split a hex str (a byte) into a list of 8 bits, MSB as first bit by default.
|
|
211
|
-
|
|
212
|
-
If lsb==True, then the LSB is first.
|
|
213
|
-
The `lsb` boolean is used so that flag[0] is `zone_idx["00"]`, etc.
|
|
214
|
-
"""
|
|
215
|
-
if not isinstance(byte, str) or len(byte) != 2:
|
|
216
|
-
raise ValueError(f"Invalid value: '{byte}', is not a 2-char hex string")
|
|
217
|
-
if lsb: # make LSB is first bit
|
|
218
|
-
return list((int(byte, 16) & (1 << x)) >> x for x in range(8))
|
|
219
|
-
return list((int(byte, 16) & (1 << x)) >> x for x in reversed(range(8)))
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@typechecked
|
|
223
|
-
def flag8_to_hex(flags: Iterable[int], lsb: bool = False) -> str:
|
|
224
|
-
"""Convert a list of 8 bits, MSB as first bit by default, into an ASCII hex string.
|
|
225
|
-
|
|
226
|
-
The `lsb` boolean is used so that flag[0] is `zone_idx["00"]`, etc.
|
|
227
|
-
"""
|
|
228
|
-
if not isinstance(flags, list) or len(flags) != 8:
|
|
229
|
-
raise ValueError(f"Invalid value: '{flags}', is not a list of 8 bits")
|
|
230
|
-
if lsb: # LSB is first bit
|
|
231
|
-
return f"{sum(x<<idx for idx, x in enumerate(flags)):02X}"
|
|
232
|
-
return f"{sum(x<<idx for idx, x in enumerate(reversed(flags))):02X}"
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
# TODO: add a wrapper for EF, & 0xF0
|
|
236
|
-
@typechecked
|
|
237
|
-
def percent_from_hex(
|
|
238
|
-
value: str, high_res: bool = True
|
|
239
|
-
) -> Optional[float]: # c.f. valve_demand
|
|
240
|
-
"""Convert a 2-char hex string into a percentage.
|
|
241
|
-
|
|
242
|
-
The range is 0-100%, with resolution of 0.5% (high_res) or 1%.
|
|
243
|
-
"""
|
|
244
|
-
if not isinstance(value, str) or len(value) != 2:
|
|
245
|
-
raise ValueError(f"Invalid value: {value}, is not a 2-char hex string")
|
|
246
|
-
if value == "EF": # TODO: when EF, when 7F?
|
|
247
|
-
return None # TODO: raise NotImplementedError
|
|
248
|
-
if (raw_result := int(value, 16)) & 0xF0 == 0xF0:
|
|
249
|
-
return None # TODO: raise errors
|
|
250
|
-
result = float(raw_result) / (200 if high_res else 100)
|
|
251
|
-
if result > 1.0:
|
|
252
|
-
raise ValueError(f"Invalid result: {result} (0x{value}) is > 1")
|
|
253
|
-
return result
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
@typechecked
|
|
257
|
-
def str_from_hex(value: str) -> Optional[str]: # printable ASCII characters
|
|
258
|
-
"""Return a string of printable ASCII characters."""
|
|
259
|
-
# result = bytearray.fromhex(value).split(b"\x7F")[0] # TODO: needs checking
|
|
260
|
-
if not isinstance(value, str):
|
|
261
|
-
raise ValueError(f"Invalid value: {value}, is not a string")
|
|
262
|
-
result = bytearray([x for x in bytearray.fromhex(value) if 31 < x < 127])
|
|
263
|
-
return result.decode("ascii").strip() if result else None
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
@typechecked
|
|
267
|
-
def str_to_hex(value: str) -> str:
|
|
268
|
-
"""Convert a string to a variable-length ASCII hex string."""
|
|
269
|
-
if not isinstance(value, str):
|
|
270
|
-
raise ValueError(f"Invalid value: {value}, is not a string")
|
|
271
|
-
return "".join(f"{ord(x):02X}" for x in value) # or: value.encode().hex()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
@typechecked
|
|
275
|
-
def temp_from_hex(value: str) -> Union[None, bool, float]:
|
|
276
|
-
"""Convert a 2's complement 4-byte hex string to an float."""
|
|
277
|
-
if not isinstance(value, str) or len(value) != 4:
|
|
278
|
-
raise ValueError(f"Invalid value: {value}, is not a 4-char hex string")
|
|
279
|
-
if value == "31FF": # means: N/A (== 127.99, 2s complement), signed?
|
|
280
|
-
return None
|
|
281
|
-
if value == "7EFF": # possibly only for setpoints? unsigned?
|
|
282
|
-
return False
|
|
283
|
-
if value == "7FFF": # also: FFFF?, means: N/A (== 327.67)
|
|
284
|
-
return None
|
|
285
|
-
temp = int(value, 16)
|
|
286
|
-
return (temp if temp < 2**15 else temp - 2**16) / 100
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
@typechecked
|
|
290
|
-
def temp_to_hex(value: Optional[float]) -> str:
|
|
291
|
-
"""Convert a float to a 2's complement 4-byte hex string."""
|
|
292
|
-
if value is None:
|
|
293
|
-
return "7FFF" # or: "31FF"?
|
|
294
|
-
if value is False:
|
|
295
|
-
return "7EFF"
|
|
296
|
-
if not isinstance(value, (float, int)):
|
|
297
|
-
raise TypeError(f"Invalid temp: {value} is not a float")
|
|
298
|
-
if not -(2**7) <= value < 2**7: # TODO: tighten range
|
|
299
|
-
raise ValueError(f"Invalid temp: {value} is out of range")
|
|
300
|
-
temp = int(value * 100)
|
|
301
|
-
return f"{temp if temp >= 0 else temp + 2 ** 16:04X}"
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
@typechecked
|
|
305
|
-
def valve_demand(value: str) -> Optional[dict]: # c.f. percent_from_hex()
|
|
306
|
-
"""Convert a 2-char hex string into a percentage.
|
|
307
|
-
|
|
308
|
-
The range is 0-100%, with resolution of 0.5% (high_res) or 1%.
|
|
309
|
-
""" # for a damper (restricts flow), or a valve (permits flow)
|
|
310
|
-
if not isinstance(value, str) or len(value) != 2:
|
|
311
|
-
raise ValueError(f"Invalid value: {value}, is not a 2-char hex string")
|
|
312
|
-
if value == "EF":
|
|
313
|
-
return None # TODO: raise NotImplementedError
|
|
314
|
-
result = int(value, 16)
|
|
315
|
-
if result & 0xF0 == 0xF0:
|
|
316
|
-
STATE_3150 = {
|
|
317
|
-
"F0": "open_circuit",
|
|
318
|
-
"F1": "short_circuit",
|
|
319
|
-
"FD": "valve_stuck", # damper/valve stuck
|
|
320
|
-
"FE": "actuator_stuck",
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
"heat_demand": None,
|
|
324
|
-
"fault": STATE_3150.get(value, "malfunction"),
|
|
325
|
-
}
|
|
326
|
-
result = result / 200 # type: ignore[assignment]
|
|
327
|
-
if result > 1:
|
|
328
|
-
raise ValueError(f"Invalid result: {result} (0x{value}) is > 1")
|
|
329
|
-
return {"heat_demand": result}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def _precision_v_cost():
|
|
333
|
-
import math
|
|
334
|
-
|
|
335
|
-
#
|
|
336
|
-
LOOPS = 10**6
|
|
337
|
-
#
|
|
338
|
-
print("time.time_ns(): %s" % time.time_ns())
|
|
339
|
-
print("time.time(): %s\r\n" % time.time())
|
|
340
|
-
#
|
|
341
|
-
starts = time.time_ns()
|
|
342
|
-
min_dt = [abs(time.time_ns() - time.time_ns()) for _ in range(LOOPS)]
|
|
343
|
-
min_dt = min(filter(bool, min_dt))
|
|
344
|
-
print("min delta time_ns(): %s ns" % min_dt)
|
|
345
|
-
print("duration time_ns(): %s ns\r\n" % (time.time_ns() - starts))
|
|
346
|
-
#
|
|
347
|
-
starts = time.time_ns()
|
|
348
|
-
min_dt = [abs(time.time() - time.time()) for _ in range(LOOPS)]
|
|
349
|
-
min_dt = min(filter(bool, min_dt))
|
|
350
|
-
print("min delta time(): %s ns" % math.ceil(min_dt * 1e9))
|
|
351
|
-
print("duration time(): %s ns\r\n" % (time.time_ns() - starts))
|
|
352
|
-
#
|
|
353
|
-
starts = time.time_ns()
|
|
354
|
-
min_dt = [abs(timestamp() - timestamp()) for _ in range(LOOPS)]
|
|
355
|
-
min_dt = min(filter(bool, min_dt))
|
|
356
|
-
print("min delta timestamp(): %s ns" % math.ceil(min_dt * 1e9))
|
|
357
|
-
print("duration timestamp(): %s ns\r\n" % (time.time_ns() - starts))
|
|
358
|
-
#
|
|
359
|
-
LOOPS = 10**4
|
|
360
|
-
#
|
|
361
|
-
starts = time.time_ns()
|
|
362
|
-
min_td = [abs(dt.now() - dt.now()) for _ in range(LOOPS)]
|
|
363
|
-
min_td = min(filter(bool, min_td))
|
|
364
|
-
print("min delta dt.now(): %s ns" % math.ceil(min_dt * 1e9))
|
|
365
|
-
print("duration dt.now(): %s ns\r\n" % (time.time_ns() - starts))
|
|
366
|
-
#
|
|
367
|
-
starts = time.time_ns()
|
|
368
|
-
min_td = [abs(dt_now() - dt_now()) for _ in range(LOOPS)]
|
|
369
|
-
min_td = min(filter(bool, min_td))
|
|
370
|
-
print("min delta dt_now(): %s ns" % math.ceil(min_dt * 1e9))
|
|
371
|
-
print("duration dt_now(): %s ns\r\n" % (time.time_ns() - starts))
|
|
372
|
-
#
|
|
373
|
-
starts = time.time_ns()
|
|
374
|
-
min_td = [
|
|
375
|
-
abs(
|
|
376
|
-
(dt_now if sys.platform == "win32" else dt.now)()
|
|
377
|
-
- (dt_now if sys.platform == "win32" else dt.now)()
|
|
378
|
-
)
|
|
379
|
-
for _ in range(LOOPS)
|
|
380
|
-
]
|
|
381
|
-
min_td = min(filter(bool, min_td))
|
|
382
|
-
print("min delta dt_now(): %s ns" % math.ceil(min_dt * 1e9))
|
|
383
|
-
print("duration dt_now(): %s ns\r\n" % (time.time_ns() - starts))
|
|
384
|
-
#
|
|
385
|
-
dt_nov = dt_now if sys.platform == "win32" else dt.now
|
|
386
|
-
starts = time.time_ns()
|
|
387
|
-
min_td = [abs(dt_nov() - dt_nov()) for _ in range(LOOPS)]
|
|
388
|
-
min_td = min(filter(bool, min_td))
|
|
389
|
-
print("min delta dt_now(): %s ns" % math.ceil(min_dt * 1e9))
|
|
390
|
-
print("duration dt_now(): %s ns\r\n" % (time.time_ns() - starts))
|