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.
- 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 +286 -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.1.dist-info/METADATA +72 -0
- ramses_rf-0.51.1.dist-info/RECORD +55 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.dist-info}/WHEEL +1 -2
- ramses_rf-0.51.1.dist-info/entry_points.txt +2 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.1.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_cli/discovery.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""RAMSES RF - discovery scripts."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import functools
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
13
|
+
|
|
14
|
+
from ramses_rf import exceptions as exc
|
|
15
|
+
from ramses_rf.const import SZ_SCHEDULE, SZ_ZONE_IDX
|
|
16
|
+
from ramses_rf.device import Fakeable
|
|
17
|
+
from ramses_tx import CODES_SCHEMA, Command, DeviceIdT, Priority
|
|
18
|
+
from ramses_tx.opentherm import OTB_DATA_IDS
|
|
19
|
+
|
|
20
|
+
# Beware, none of this is reliable - it is all subject to random change
|
|
21
|
+
# However, these serve as examples how to use the other modules
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
from ramses_rf.const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
25
|
+
I_,
|
|
26
|
+
RP,
|
|
27
|
+
RQ,
|
|
28
|
+
W_,
|
|
29
|
+
Code,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from ramses_rf import Gateway, IndexT
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
EXEC_CMD: Final = "exec_cmd"
|
|
37
|
+
GET_FAULTS: Final = "get_faults"
|
|
38
|
+
GET_SCHED: Final = "get_schedule"
|
|
39
|
+
SET_SCHED: Final = "set_schedule"
|
|
40
|
+
|
|
41
|
+
EXEC_SCR: Final = "exec_scr"
|
|
42
|
+
SCAN_DISC: Final = "scan_disc"
|
|
43
|
+
SCAN_FULL: Final = "scan_full"
|
|
44
|
+
SCAN_HARD: Final = "scan_hard"
|
|
45
|
+
SCAN_XXXX: Final = "scan_xxxx"
|
|
46
|
+
|
|
47
|
+
# DEVICE_ID_REGEX = re.compile(DEVICE_ID_REGEX.ANY)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_LOGGER = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def script_decorator(fnc: Callable[..., Any]) -> Callable[..., Any]:
|
|
54
|
+
@functools.wraps(fnc)
|
|
55
|
+
def wrapper(gwy: Gateway, *args: Any, **kwargs: Any) -> None:
|
|
56
|
+
gwy.send_cmd(
|
|
57
|
+
Command._puzzle(message="Script begins:"),
|
|
58
|
+
priority=Priority.HIGHEST,
|
|
59
|
+
num_repeats=3,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
fnc(gwy, *args, **kwargs)
|
|
63
|
+
|
|
64
|
+
gwy.send_cmd(
|
|
65
|
+
Command._puzzle(message="Script done."),
|
|
66
|
+
priority=Priority.LOWEST,
|
|
67
|
+
num_repeats=3,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
return wrapper
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def spawn_scripts(gwy: Gateway, **kwargs: Any) -> list[asyncio.Task[None]]:
|
|
76
|
+
tasks = []
|
|
77
|
+
|
|
78
|
+
if kwargs.get(EXEC_CMD):
|
|
79
|
+
tasks += [asyncio.create_task(exec_cmd(gwy, **kwargs))]
|
|
80
|
+
|
|
81
|
+
if kwargs.get(GET_FAULTS):
|
|
82
|
+
tasks += [asyncio.create_task(get_faults(gwy, kwargs[GET_FAULTS]))]
|
|
83
|
+
|
|
84
|
+
elif kwargs.get(GET_SCHED) and kwargs[GET_SCHED][0]:
|
|
85
|
+
tasks += [asyncio.create_task(get_schedule(gwy, *kwargs[GET_SCHED]))]
|
|
86
|
+
|
|
87
|
+
elif kwargs.get(SET_SCHED) and kwargs[SET_SCHED][0]:
|
|
88
|
+
tasks += [asyncio.create_task(set_schedule(gwy, *kwargs[SET_SCHED]))]
|
|
89
|
+
|
|
90
|
+
elif kwargs.get(EXEC_SCR):
|
|
91
|
+
script = SCRIPTS.get(f"{kwargs[EXEC_SCR][0]}")
|
|
92
|
+
if script is None:
|
|
93
|
+
_LOGGER.warning(f"Script: {kwargs[EXEC_SCR][0]}() - unknown script")
|
|
94
|
+
else:
|
|
95
|
+
_LOGGER.info(f"Script: {kwargs[EXEC_SCR][0]}().- starts...")
|
|
96
|
+
tasks += [asyncio.create_task(script(gwy, kwargs[EXEC_SCR][1]))]
|
|
97
|
+
|
|
98
|
+
gwy._tasks.extend(tasks)
|
|
99
|
+
return tasks
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def exec_cmd(gwy: Gateway, **kwargs: Any) -> None:
|
|
103
|
+
cmd = Command.from_cli(kwargs[EXEC_CMD])
|
|
104
|
+
await gwy.async_send_cmd(cmd, priority=Priority.HIGH, wait_for_reply=True)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# @script_decorator
|
|
108
|
+
# async def script_scan_001(gwy: Gateway, dev_id: DeviceIdT):
|
|
109
|
+
# _LOGGER.warning("scan_001() invoked - expect a lot of nonsense")
|
|
110
|
+
# for idx in range(0x10):
|
|
111
|
+
# gwy.send_cmd(Command.from_attrs(W_, dev_id, Code._000E, f"{idx:02X}0050"))
|
|
112
|
+
# gwy.send_cmd(Command.from_attrs(RQ, dev_id, Code._000E, f"{idx:02X}00C8"))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def get_faults(
|
|
116
|
+
gwy: Gateway, ctl_id: DeviceIdT, start: int = 0, limit: int = 0x3F
|
|
117
|
+
) -> None:
|
|
118
|
+
ctl = gwy.get_device(ctl_id)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
await ctl.tcs.get_faultlog(start=start, limit=limit) # 0418
|
|
122
|
+
except exc.ExpiredCallbackError as err:
|
|
123
|
+
_LOGGER.error("get_faults(): Function timed out: %s", err)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def get_schedule(gwy: Gateway, ctl_id: DeviceIdT, zone_idx: str) -> None:
|
|
127
|
+
zone = gwy.get_device(ctl_id).tcs.get_htg_zone(zone_idx)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
await zone.get_schedule()
|
|
131
|
+
except exc.ExpiredCallbackError as err:
|
|
132
|
+
_LOGGER.error("get_schedule(): Function timed out: %s", err)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def set_schedule(gwy: Gateway, ctl_id: DeviceIdT, schedule: str) -> None:
|
|
136
|
+
schedule_ = json.loads(schedule)
|
|
137
|
+
zone_idx = schedule_[SZ_ZONE_IDX]
|
|
138
|
+
|
|
139
|
+
zone = gwy.get_device(ctl_id).tcs.get_htg_zone(zone_idx)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
await zone.set_schedule(schedule_[SZ_SCHEDULE]) # 0404
|
|
143
|
+
except exc.ExpiredCallbackError as err:
|
|
144
|
+
_LOGGER.error("set_schedule(): Function timed out: %s", err)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def script_bind_req(
|
|
148
|
+
gwy: Gateway, dev_id: DeviceIdT, code: Code = Code._2309
|
|
149
|
+
) -> None:
|
|
150
|
+
dev = gwy.get_device(dev_id)
|
|
151
|
+
assert isinstance(dev, Fakeable) # mypy
|
|
152
|
+
dev._make_fake()
|
|
153
|
+
await dev._initiate_binding_process([code])
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def script_bind_wait(
|
|
157
|
+
gwy: Gateway, dev_id: DeviceIdT, code: Code = Code._2309, idx: IndexT = "00"
|
|
158
|
+
) -> None:
|
|
159
|
+
dev = gwy.get_device(dev_id)
|
|
160
|
+
assert isinstance(dev, Fakeable) # mypy
|
|
161
|
+
dev._make_fake()
|
|
162
|
+
await dev._wait_for_binding_request([code], idx=idx)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def script_poll_device(gwy: Gateway, dev_id: DeviceIdT) -> list[asyncio.Task[None]]:
|
|
166
|
+
async def periodic_send(
|
|
167
|
+
gwy: Gateway,
|
|
168
|
+
cmd: Command,
|
|
169
|
+
count: int = 1,
|
|
170
|
+
interval: float | None = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
async def periodic_(interval_: float) -> None:
|
|
173
|
+
await asyncio.sleep(interval_)
|
|
174
|
+
gwy.send_cmd(cmd, priority=Priority.LOW)
|
|
175
|
+
|
|
176
|
+
if interval is None:
|
|
177
|
+
interval = 0 if count == 1 else 60
|
|
178
|
+
|
|
179
|
+
if count <= 0:
|
|
180
|
+
while True:
|
|
181
|
+
await periodic_(interval)
|
|
182
|
+
else:
|
|
183
|
+
for _ in range(count):
|
|
184
|
+
await periodic_(interval)
|
|
185
|
+
|
|
186
|
+
_LOGGER.warning("poll_device() invoked...")
|
|
187
|
+
|
|
188
|
+
tasks = []
|
|
189
|
+
|
|
190
|
+
for code in (Code._0016, Code._1FC9):
|
|
191
|
+
cmd = Command.from_attrs(RQ, dev_id, code, "00")
|
|
192
|
+
tasks.append(asyncio.create_task(periodic_send(gwy, cmd, count=0)))
|
|
193
|
+
|
|
194
|
+
gwy._tasks.extend(tasks)
|
|
195
|
+
return tasks
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@script_decorator
|
|
199
|
+
async def script_scan_disc(gwy: Gateway, dev_id: DeviceIdT) -> None:
|
|
200
|
+
_LOGGER.warning("scan_disc() invoked...")
|
|
201
|
+
|
|
202
|
+
await gwy.get_device(dev_id).discover() # discover_flag=Discover.DEFAULT)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@script_decorator
|
|
206
|
+
async def script_scan_full(gwy: Gateway, dev_id: DeviceIdT) -> None:
|
|
207
|
+
_LOGGER.warning("scan_full() invoked - expect a lot of Warnings")
|
|
208
|
+
|
|
209
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, Code._0016, "0000"), num_repeats=3)
|
|
210
|
+
|
|
211
|
+
for code in sorted(CODES_SCHEMA):
|
|
212
|
+
if code == Code._0005:
|
|
213
|
+
for zone_type in range(20): # known up to 18
|
|
214
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, code, f"00{zone_type:02X}"))
|
|
215
|
+
|
|
216
|
+
elif code == Code._000C:
|
|
217
|
+
for zone_idx in range(16): # also: FA-FF?
|
|
218
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, code, f"{zone_idx:02X}00"))
|
|
219
|
+
|
|
220
|
+
elif code == Code._0016:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
elif code in (Code._01D0, Code._01E9):
|
|
224
|
+
for zone_idx in ("00", "01", "FC"): # type: ignore[assignment]
|
|
225
|
+
gwy.send_cmd(Command.from_attrs(W_, dev_id, code, f"{zone_idx}00"))
|
|
226
|
+
gwy.send_cmd(Command.from_attrs(W_, dev_id, code, f"{zone_idx}03"))
|
|
227
|
+
|
|
228
|
+
elif code == Code._0404: # FIXME
|
|
229
|
+
gwy.send_cmd(Command.get_schedule_fragment(dev_id, "HW", 1, 0))
|
|
230
|
+
gwy.send_cmd(Command.get_schedule_fragment(dev_id, "00", 1, 0))
|
|
231
|
+
|
|
232
|
+
elif code == Code._0418:
|
|
233
|
+
for log_idx in range(2):
|
|
234
|
+
gwy.send_cmd(Command.get_system_log_entry(dev_id, log_idx))
|
|
235
|
+
|
|
236
|
+
elif code == Code._1100:
|
|
237
|
+
gwy.send_cmd(Command.get_tpi_params(dev_id))
|
|
238
|
+
|
|
239
|
+
elif code == Code._2E04:
|
|
240
|
+
gwy.send_cmd(Command.get_system_mode(dev_id))
|
|
241
|
+
|
|
242
|
+
elif code == Code._3220:
|
|
243
|
+
for data_id in (0, 3): # these are mandatory READ_DATA data_ids
|
|
244
|
+
gwy.send_cmd(Command.get_opentherm_data(dev_id, data_id))
|
|
245
|
+
|
|
246
|
+
elif code == Code._PUZZ:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
elif (
|
|
250
|
+
code in CODES_SCHEMA
|
|
251
|
+
and RQ in CODES_SCHEMA[code]
|
|
252
|
+
and re.match(CODES_SCHEMA[code][RQ], "00")
|
|
253
|
+
):
|
|
254
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, code, "00"))
|
|
255
|
+
|
|
256
|
+
else:
|
|
257
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, code, "0000"))
|
|
258
|
+
|
|
259
|
+
# these are possible/difficult codes
|
|
260
|
+
for code in (Code._0150, Code._2389):
|
|
261
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, code, "0000"))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@script_decorator
|
|
265
|
+
async def script_scan_hard(
|
|
266
|
+
gwy: Gateway, dev_id: DeviceIdT, *, start_code: None | int = None
|
|
267
|
+
) -> None:
|
|
268
|
+
_LOGGER.warning("scan_hard() invoked - expect some Warnings")
|
|
269
|
+
|
|
270
|
+
start_code = start_code or 0
|
|
271
|
+
|
|
272
|
+
for code in range(start_code, 0x5000):
|
|
273
|
+
await gwy.async_send_cmd(
|
|
274
|
+
Command.from_attrs(RQ, dev_id, f"{code:04X}", "0000"), # type:ignore[arg-type]
|
|
275
|
+
priority=Priority.LOW,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@script_decorator
|
|
280
|
+
async def script_scan_fan(gwy: Gateway, dev_id: DeviceIdT) -> None:
|
|
281
|
+
_LOGGER.warning("scan_fan() invoked - expect a lot of nonsense")
|
|
282
|
+
|
|
283
|
+
from ramses_tx.ramses import _DEV_KLASSES_HVAC
|
|
284
|
+
|
|
285
|
+
OUT_CODES = (
|
|
286
|
+
Code._0016,
|
|
287
|
+
Code._1470,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
OLD_CODES = dict.fromkeys(
|
|
291
|
+
c for k in _DEV_KLASSES_HVAC.values() for c in k if c not in OUT_CODES
|
|
292
|
+
)
|
|
293
|
+
for code in OLD_CODES:
|
|
294
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, code, "00"))
|
|
295
|
+
|
|
296
|
+
NEW_CODES = (
|
|
297
|
+
Code._0150,
|
|
298
|
+
Code._042F,
|
|
299
|
+
Code._1030,
|
|
300
|
+
Code._10D0,
|
|
301
|
+
Code._10E1,
|
|
302
|
+
Code._2210,
|
|
303
|
+
Code._22B0,
|
|
304
|
+
Code._22E0,
|
|
305
|
+
Code._22E5,
|
|
306
|
+
Code._22E9,
|
|
307
|
+
Code._22F1,
|
|
308
|
+
Code._22F2,
|
|
309
|
+
Code._22F3,
|
|
310
|
+
Code._22F4,
|
|
311
|
+
Code._22F7,
|
|
312
|
+
Code._22F8,
|
|
313
|
+
Code._2400,
|
|
314
|
+
Code._2410,
|
|
315
|
+
Code._2420,
|
|
316
|
+
Code._313E,
|
|
317
|
+
Code._3221,
|
|
318
|
+
Code._3222,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
for code in NEW_CODES:
|
|
322
|
+
if code not in OLD_CODES and code not in OUT_CODES:
|
|
323
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, code, "00"))
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@script_decorator
|
|
327
|
+
async def script_scan_otb(gwy: Gateway, dev_id: DeviceIdT) -> None:
|
|
328
|
+
_LOGGER.warning("script_scan_otb_full invoked - expect a lot of nonsense")
|
|
329
|
+
|
|
330
|
+
for msg_id in OTB_DATA_IDS:
|
|
331
|
+
gwy.send_cmd(Command.get_opentherm_data(dev_id, msg_id))
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@script_decorator
|
|
335
|
+
async def script_scan_otb_hard(gwy: Gateway, dev_id: DeviceIdT) -> None:
|
|
336
|
+
_LOGGER.warning("script_scan_otb_hard invoked - expect a lot of nonsense")
|
|
337
|
+
|
|
338
|
+
for msg_id in range(0x80):
|
|
339
|
+
gwy.send_cmd(Command.get_opentherm_data(dev_id, msg_id), priority=Priority.LOW)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@script_decorator
|
|
343
|
+
async def script_scan_otb_map(
|
|
344
|
+
gwy: Gateway, dev_id: DeviceIdT
|
|
345
|
+
) -> None: # Tested only upon a R8820A
|
|
346
|
+
_LOGGER.warning("script_scan_otb_map invoked - expect a lot of nonsense")
|
|
347
|
+
|
|
348
|
+
RAMSES_TO_OPENTHERM = {
|
|
349
|
+
Code._22D9: "01", # boiler setpoint / ControlSetpoint
|
|
350
|
+
Code._3EF1: "11", # rel. modulation level / RelativeModulationLevel
|
|
351
|
+
Code._1300: "12", # cv water pressure / CHWaterPressure
|
|
352
|
+
Code._12F0: "13", # dhw_flow_rate / DHWFlowRate
|
|
353
|
+
Code._3200: "19", # boiler output temp / BoilerWaterTemperature
|
|
354
|
+
Code._1260: "1A", # dhw temp / DHWTemperature
|
|
355
|
+
Code._1290: "1B", # outdoor temp / OutsideTemperature
|
|
356
|
+
Code._3210: "1C", # boiler return temp / ReturnWaterTemperature
|
|
357
|
+
Code._10A0: "38", # dhw params[SZ_SETPOINT] / DHWSetpoint
|
|
358
|
+
Code._1081: "39", # max ch setpoint / MaxCHWaterSetpoint
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for code, msg_id in RAMSES_TO_OPENTHERM.items():
|
|
362
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, code, "00"), priority=Priority.LOW)
|
|
363
|
+
gwy.send_cmd(Command.get_opentherm_data(dev_id, msg_id), priority=Priority.LOW)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@script_decorator
|
|
367
|
+
async def script_scan_otb_ramses(
|
|
368
|
+
gwy: Gateway, dev_id: DeviceIdT
|
|
369
|
+
) -> None: # Tested only upon a R8820A
|
|
370
|
+
_LOGGER.warning("script_scan_otb_ramses invoked - expect a lot of nonsense")
|
|
371
|
+
|
|
372
|
+
_CODES = (
|
|
373
|
+
Code._042F,
|
|
374
|
+
Code._10E0, # device_info
|
|
375
|
+
Code._10E1, # device_id
|
|
376
|
+
Code._1FD0,
|
|
377
|
+
Code._2400,
|
|
378
|
+
Code._2401,
|
|
379
|
+
Code._2410,
|
|
380
|
+
Code._2420,
|
|
381
|
+
Code._1300, # cv water pressure / CHWaterPressure
|
|
382
|
+
Code._1081, # max ch setpoint / MaxCHWaterSetpoint
|
|
383
|
+
Code._10A0, # dhw params[SZ_SETPOINT] / DHWSetpoint
|
|
384
|
+
Code._22D9, # boiler setpoint / ControlSetpoint
|
|
385
|
+
Code._1260, # dhw temp / DHWTemperature
|
|
386
|
+
Code._1290, # outdoor temp / OutsideTemperature
|
|
387
|
+
Code._3200, # boiler output temp / BoilerWaterTemperature
|
|
388
|
+
Code._3210, # boiler return temp / ReturnWaterTemperature
|
|
389
|
+
Code._0150,
|
|
390
|
+
Code._12F0, # dhw flow rate / DHWFlowRate
|
|
391
|
+
Code._1098,
|
|
392
|
+
Code._10B0,
|
|
393
|
+
Code._3221,
|
|
394
|
+
Code._3223,
|
|
395
|
+
Code._3EF0, # rel. modulation level / RelativeModulationLevel (also, below)
|
|
396
|
+
Code._3EF1, # rel. modulation level / RelativeModulationLevel
|
|
397
|
+
) # excl. 3220
|
|
398
|
+
|
|
399
|
+
for c in _CODES:
|
|
400
|
+
gwy.send_cmd(Command.from_attrs(RQ, dev_id, c, "00"), priority=Priority.LOW)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
SCRIPTS = {
|
|
404
|
+
k[7:]: v for k, v in locals().items() if callable(v) and k.startswith("script_")
|
|
405
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
from time import sleep
|
|
4
|
+
|
|
5
|
+
# python utils/cat_slow.py -i packet.log | tee /dev/pts/0
|
|
6
|
+
# cat packet.log | cut -d ' ' -f 2- | unix2dos | pv --quiet --line-mode --rate-limit 1 | tee /dev/pts/3
|
|
7
|
+
|
|
8
|
+
parser = argparse.ArgumentParser(description="Echo a file slowly")
|
|
9
|
+
parser.add_argument("-i", "--input-file", type=argparse.FileType("r"), default="-")
|
|
10
|
+
parser.add_argument("-d", "--delay-in-ms", type=int, default="100")
|
|
11
|
+
args = parser.parse_args()
|
|
12
|
+
|
|
13
|
+
for line in args.input_file:
|
|
14
|
+
if "RQ" in line:
|
|
15
|
+
continue
|
|
16
|
+
print(line.rstrip()[27:])
|
|
17
|
+
sleep(args.delay_in_ms / 1000.0)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""RAMSES RF - a RAMSES-II protocol decoder & analyser.
|
|
3
|
+
|
|
4
|
+
WIP - Utility to a configuration file from JSON to YAML & back.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from ramses_rf.helpers import shrink
|
|
13
|
+
from ramses_rf.schemas import load_config
|
|
14
|
+
|
|
15
|
+
parser = argparse.ArgumentParser(description="Convert a file JSON <-> YAML")
|
|
16
|
+
parser.add_argument("-i", "--input-file", type=argparse.FileType("r"), default="-")
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def convert_json_to_yaml(data: dict) -> str:
|
|
21
|
+
"""Convert from json (client.py -C config.json) to yaml (HA configuration.yaml)."""
|
|
22
|
+
(config, schema, include, exclude) = load_config("/dev/ttyMOCK", None, **data)
|
|
23
|
+
|
|
24
|
+
config = vars(config)
|
|
25
|
+
config["use_regex"]["inbound"].pop("( 03:.* 03:.* (1060|2389|30C9) 003) ..") # HACK
|
|
26
|
+
|
|
27
|
+
result = {
|
|
28
|
+
"serial_port": "/dev/ttyMOCK",
|
|
29
|
+
"packet_log": None,
|
|
30
|
+
"restore_cache": False,
|
|
31
|
+
"ramses_rf": shrink(config),
|
|
32
|
+
"schema": shrink(schema),
|
|
33
|
+
"known_list": {k: shrink(v) for k, v in include.items()},
|
|
34
|
+
"block_list": shrink(exclude),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
print(yaml.dump({"ramses_cc": result}, sort_keys=False))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def convert_yaml_to_json(data: dict) -> str:
|
|
41
|
+
"""Convert from yaml (HA configuration.yaml) to json (client.py -C config.json)."""
|
|
42
|
+
|
|
43
|
+
result = data["ramses_cc"]
|
|
44
|
+
result["config"] = result.pop("ramses_rf", {})
|
|
45
|
+
result.update(result.pop("schema", {}))
|
|
46
|
+
|
|
47
|
+
result["orphans_heat"] = {}
|
|
48
|
+
result["orphans_hvac"] = {}
|
|
49
|
+
result["known_list"] = result.pop("known_list", {})
|
|
50
|
+
result["block_list"] = result.pop("block_list", {})
|
|
51
|
+
|
|
52
|
+
print(json.dumps(result, indent=4))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
data_file = args.input_file.read()
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
convert_json_to_yaml(json.loads(data_file))
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
convert_yaml_to_json(yaml.safe_load(data_file))
|
ramses_rf/__init__.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
#
|
|
4
2
|
"""RAMSES RF - a RAMSES-II protocol decoder & analyser.
|
|
5
3
|
|
|
6
4
|
Works with (amongst others):
|
|
@@ -8,30 +6,53 @@ Works with (amongst others):
|
|
|
8
6
|
- sundial (up to 2 zones)
|
|
9
7
|
- chronotherm (CM60xNG can do 4 zones)
|
|
10
8
|
- hometronics (16? zones)
|
|
9
|
+
- vision pro
|
|
11
10
|
"""
|
|
11
|
+
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from ramses_tx import Address, Command, Message, Packet # noqa: F401
|
|
15
18
|
|
|
16
|
-
from .
|
|
19
|
+
from .device import Device # noqa: F401
|
|
17
20
|
from .gateway import Gateway # noqa: F401
|
|
18
|
-
from .protocol import is_valid_dev_id # noqa: F401
|
|
19
21
|
from .version import VERSION # noqa: F401
|
|
20
22
|
|
|
21
|
-
#
|
|
22
|
-
from .protocol import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
23
|
+
from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
23
24
|
I_,
|
|
24
25
|
RP,
|
|
25
26
|
RQ,
|
|
26
27
|
W_,
|
|
28
|
+
Code,
|
|
27
29
|
)
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from .const import IndexT, VerbT # noqa: F401, pylint: disable=unused-import
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"VERSION",
|
|
37
|
+
"Gateway",
|
|
38
|
+
#
|
|
39
|
+
"Address",
|
|
40
|
+
"Command",
|
|
41
|
+
"Device",
|
|
42
|
+
"Message",
|
|
43
|
+
"Packet",
|
|
44
|
+
#
|
|
45
|
+
"I_",
|
|
46
|
+
"RP",
|
|
47
|
+
"RQ",
|
|
48
|
+
"W_",
|
|
49
|
+
#
|
|
50
|
+
"Code",
|
|
51
|
+
"IndexT",
|
|
52
|
+
"VerbT",
|
|
53
|
+
]
|
|
31
54
|
|
|
32
55
|
_LOGGER = logging.getLogger(__name__)
|
|
33
|
-
if DEV_MODE:
|
|
34
|
-
_LOGGER.setLevel(logging.DEBUG)
|
|
35
56
|
|
|
36
57
|
|
|
37
58
|
class GracefulExit(SystemExit):
|