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/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A CLI for the ramses_rf library."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# NOTE: All debug flags should be False for deployment to end-users
|
|
10
|
+
_DBG_FORCE_CLI_DEBUGGING: Final[bool] = (
|
|
11
|
+
False # for debugging of CLI (usu. for click debugging)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if _DBG_FORCE_CLI_DEBUGGING:
|
|
16
|
+
from .debug import start_debugging
|
|
17
|
+
|
|
18
|
+
start_debugging(True)
|
ramses_cli/client.py
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A CLI for the ramses_rf library."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any, Final
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from colorama import Fore, Style, init as colorama_init
|
|
14
|
+
|
|
15
|
+
from ramses_rf import Gateway, GracefulExit, Message, exceptions as exc
|
|
16
|
+
from ramses_rf.const import DONT_CREATE_MESSAGES, SZ_ZONE_IDX
|
|
17
|
+
from ramses_rf.helpers import deep_merge
|
|
18
|
+
from ramses_rf.schemas import (
|
|
19
|
+
SCH_GLOBAL_CONFIG,
|
|
20
|
+
SZ_CONFIG,
|
|
21
|
+
SZ_DISABLE_DISCOVERY,
|
|
22
|
+
SZ_ENABLE_EAVESDROP,
|
|
23
|
+
SZ_REDUCE_PROCESSING,
|
|
24
|
+
)
|
|
25
|
+
from ramses_tx import is_valid_dev_id
|
|
26
|
+
from ramses_tx.logger import CONSOLE_COLS, DEFAULT_DATEFMT, DEFAULT_FMT
|
|
27
|
+
from ramses_tx.schemas import (
|
|
28
|
+
SZ_DISABLE_QOS,
|
|
29
|
+
SZ_DISABLE_SENDING,
|
|
30
|
+
SZ_ENFORCE_KNOWN_LIST,
|
|
31
|
+
SZ_EVOFW_FLAG,
|
|
32
|
+
SZ_FILE_NAME,
|
|
33
|
+
SZ_KNOWN_LIST,
|
|
34
|
+
SZ_PACKET_LOG,
|
|
35
|
+
SZ_SERIAL_PORT,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .debug import SZ_DBG_MODE, start_debugging
|
|
39
|
+
from .discovery import GET_FAULTS, GET_SCHED, SET_SCHED, spawn_scripts
|
|
40
|
+
|
|
41
|
+
from ramses_rf.const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
42
|
+
I_,
|
|
43
|
+
RP,
|
|
44
|
+
RQ,
|
|
45
|
+
W_,
|
|
46
|
+
DEV_TYPE_MAP,
|
|
47
|
+
Code,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_PROFILE_LIBRARY = False # NOTE: for profiling of library
|
|
51
|
+
|
|
52
|
+
if _PROFILE_LIBRARY:
|
|
53
|
+
import cProfile
|
|
54
|
+
import pstats
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
SZ_INPUT_FILE: Final = "input_file"
|
|
58
|
+
|
|
59
|
+
# DEFAULT_SUMMARY can be: True, False, or None
|
|
60
|
+
SHOW_SCHEMA = False
|
|
61
|
+
SHOW_PARAMS = False
|
|
62
|
+
SHOW_STATUS = False
|
|
63
|
+
SHOW_KNOWNS = False
|
|
64
|
+
SHOW_TRAITS = False
|
|
65
|
+
SHOW_CRAZYS = False
|
|
66
|
+
|
|
67
|
+
PRINT_STATE = False # print engine state
|
|
68
|
+
# GET_STATE = False # get engine state
|
|
69
|
+
# SET_STATE = False # set engine state
|
|
70
|
+
|
|
71
|
+
# this is called after import colorlog to ensure its handlers wrap the correct streams
|
|
72
|
+
logging.basicConfig(level=logging.WARNING, format=DEFAULT_FMT, datefmt=DEFAULT_DATEFMT)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
EXECUTE: Final = "execute"
|
|
76
|
+
LISTEN: Final = "listen"
|
|
77
|
+
MONITOR: Final = "monitor"
|
|
78
|
+
PARSE: Final = "parse"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
COLORS = {
|
|
82
|
+
I_: Fore.GREEN,
|
|
83
|
+
RP: Fore.CYAN,
|
|
84
|
+
RQ: Fore.CYAN,
|
|
85
|
+
W_: Style.BRIGHT + Fore.MAGENTA,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
89
|
+
|
|
90
|
+
LIB_KEYS = tuple(SCH_GLOBAL_CONFIG({}).keys()) + (SZ_SERIAL_PORT,)
|
|
91
|
+
LIB_CFG_KEYS = tuple(SCH_GLOBAL_CONFIG({})[SZ_CONFIG].keys()) + (SZ_EVOFW_FLAG,)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def normalise_config(lib_config: dict) -> tuple[str, dict]:
|
|
95
|
+
"""Convert a HA config dict into the client library's own format."""
|
|
96
|
+
|
|
97
|
+
serial_port = lib_config.pop(SZ_SERIAL_PORT, None)
|
|
98
|
+
|
|
99
|
+
# fix for: https://github.com/ramses-rf/ramses_rf/issues/96
|
|
100
|
+
packet_log = lib_config.get(SZ_PACKET_LOG)
|
|
101
|
+
if isinstance(packet_log, str):
|
|
102
|
+
packet_log = {SZ_FILE_NAME: packet_log}
|
|
103
|
+
lib_config[SZ_PACKET_LOG] = packet_log
|
|
104
|
+
|
|
105
|
+
return serial_port, lib_config
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def split_kwargs(obj: tuple[dict, dict], kwargs: dict) -> tuple[dict, dict]:
|
|
109
|
+
"""Split kwargs into cli/library kwargs."""
|
|
110
|
+
cli_kwargs, lib_kwargs = obj
|
|
111
|
+
|
|
112
|
+
cli_kwargs.update(
|
|
113
|
+
{k: v for k, v in kwargs.items() if k not in LIB_KEYS + LIB_CFG_KEYS}
|
|
114
|
+
)
|
|
115
|
+
lib_kwargs.update({k: v for k, v in kwargs.items() if k in LIB_KEYS})
|
|
116
|
+
lib_kwargs[SZ_CONFIG].update({k: v for k, v in kwargs.items() if k in LIB_CFG_KEYS})
|
|
117
|
+
|
|
118
|
+
return cli_kwargs, lib_kwargs
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class DeviceIdParamType(click.ParamType):
|
|
122
|
+
name = "device_id"
|
|
123
|
+
|
|
124
|
+
def convert(self, value: str, param, ctx):
|
|
125
|
+
if is_valid_dev_id(value):
|
|
126
|
+
return value.upper()
|
|
127
|
+
self.fail(f"{value!r} is not a valid device_id", param, ctx)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Args/Params for both RF and file
|
|
131
|
+
@click.group(context_settings=CONTEXT_SETTINGS) # , invoke_without_command=True)
|
|
132
|
+
@click.option("-z", "--debug-mode", count=True, help="enable debugger")
|
|
133
|
+
@click.option("-c", "--config-file", type=click.File("r"))
|
|
134
|
+
@click.option("-rk", "--restore-schema", type=click.File("r"), help="from a HA store")
|
|
135
|
+
@click.option("-rs", "--restore-state", type=click.File("r"), help=" from a HA store")
|
|
136
|
+
@click.option("-r", "--reduce-processing", count=True, help="-rrr will give packets")
|
|
137
|
+
@click.option("-lf", "--long-format", is_flag=True, help="dont truncate STDOUT")
|
|
138
|
+
@click.option("-e/-ne", "--eavesdrop/--no-eavesdrop", default=None)
|
|
139
|
+
@click.option("-g", "--print-state", count=True, help="print state (g=schema, gg=all)")
|
|
140
|
+
# @click.option("--get-state/--no-get-state", default=GET_STATE, help="get the engine state")
|
|
141
|
+
# @click.option("--set-state/--no-set-state", default=SET_STATE, help="set the engine state")
|
|
142
|
+
@click.option( # show_schema
|
|
143
|
+
"-k/-nk",
|
|
144
|
+
"--show-schema/--no-show-schema",
|
|
145
|
+
default=SHOW_SCHEMA,
|
|
146
|
+
help="display system schema",
|
|
147
|
+
)
|
|
148
|
+
@click.option( # show_params
|
|
149
|
+
"-p/-np",
|
|
150
|
+
"--show-params/--no-show-params",
|
|
151
|
+
default=SHOW_PARAMS,
|
|
152
|
+
help="display system params",
|
|
153
|
+
)
|
|
154
|
+
@click.option( # show_status
|
|
155
|
+
"-s/-ns",
|
|
156
|
+
"--show-status/--no-show-status",
|
|
157
|
+
default=SHOW_STATUS,
|
|
158
|
+
help="display system state",
|
|
159
|
+
)
|
|
160
|
+
@click.option( # show_knowns
|
|
161
|
+
"-n/-nn",
|
|
162
|
+
"--show-knowns/--no-show-knowns",
|
|
163
|
+
default=SHOW_KNOWNS,
|
|
164
|
+
help="display known_list (of devices)",
|
|
165
|
+
)
|
|
166
|
+
@click.option( # show_traits
|
|
167
|
+
"-t/-nt",
|
|
168
|
+
"--show-traits/--no-show-traits",
|
|
169
|
+
default=SHOW_TRAITS,
|
|
170
|
+
help="display device traits",
|
|
171
|
+
)
|
|
172
|
+
@click.option( # show_crazys
|
|
173
|
+
"-x/-nx",
|
|
174
|
+
"--show-crazys/--no-show-crazys",
|
|
175
|
+
default=SHOW_CRAZYS,
|
|
176
|
+
help="display crazy things",
|
|
177
|
+
)
|
|
178
|
+
@click.pass_context
|
|
179
|
+
def cli(ctx, config_file=None, eavesdrop: None | bool = None, **kwargs: Any) -> None:
|
|
180
|
+
"""A CLI for the ramses_rf library."""
|
|
181
|
+
|
|
182
|
+
if kwargs[SZ_DBG_MODE] > 0: # Do first
|
|
183
|
+
start_debugging(kwargs[SZ_DBG_MODE] == 1)
|
|
184
|
+
|
|
185
|
+
kwargs, lib_kwargs = split_kwargs(({}, {SZ_CONFIG: {}}), kwargs)
|
|
186
|
+
|
|
187
|
+
if eavesdrop is not None:
|
|
188
|
+
lib_kwargs[SZ_CONFIG][SZ_ENABLE_EAVESDROP] = eavesdrop
|
|
189
|
+
|
|
190
|
+
if config_file: # TODO: validate with voluptuous, use YAML
|
|
191
|
+
lib_kwargs = deep_merge(
|
|
192
|
+
lib_kwargs, json.load(config_file)
|
|
193
|
+
) # CLI takes precedence
|
|
194
|
+
|
|
195
|
+
ctx.obj = kwargs, lib_kwargs
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Args/Params for packet log only
|
|
199
|
+
class FileCommand(click.Command): # client.py parse <file>
|
|
200
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
201
|
+
super().__init__(*args, **kwargs)
|
|
202
|
+
self.params.insert( # input_file name/path only
|
|
203
|
+
0, click.Argument(("input-file",))
|
|
204
|
+
)
|
|
205
|
+
# self.params.insert( # --packet-log # NOTE: useful only for test/dev
|
|
206
|
+
# 1,
|
|
207
|
+
# click.Option(
|
|
208
|
+
# ("-o", "--packet-log"),
|
|
209
|
+
# type=click.Path(),
|
|
210
|
+
# help="Log all packets to this file",
|
|
211
|
+
# ),
|
|
212
|
+
# )
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Args/Params for RF packets only
|
|
216
|
+
class PortCommand(
|
|
217
|
+
click.Command
|
|
218
|
+
): # client.py <command> <port> --packet-log xxx --evofw3-flag xxx
|
|
219
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
220
|
+
super().__init__(*args, **kwargs)
|
|
221
|
+
self.params.insert(0, click.Argument(("serial-port",)))
|
|
222
|
+
""" # self.params.insert( # --no-discover
|
|
223
|
+
# 1,
|
|
224
|
+
# click.Option(
|
|
225
|
+
# ("-d/-nd", "--discover/--no-discover"),
|
|
226
|
+
# is_flag=True,
|
|
227
|
+
# default=False,
|
|
228
|
+
# help="Log all packets to this file",
|
|
229
|
+
# ),
|
|
230
|
+
# )
|
|
231
|
+
# """
|
|
232
|
+
self.params.insert( # --packet-log
|
|
233
|
+
2,
|
|
234
|
+
click.Option(
|
|
235
|
+
("-o", "--packet-log"),
|
|
236
|
+
type=click.Path(),
|
|
237
|
+
help="Log all packets to this file",
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
self.params.insert( # --evofw-flag
|
|
241
|
+
3,
|
|
242
|
+
click.Option(
|
|
243
|
+
("-T", "--evofw-flag"),
|
|
244
|
+
type=click.STRING,
|
|
245
|
+
help="Pass this traceflag to evofw",
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
#
|
|
251
|
+
# 1/4: PARSE (a file, +/- eavesdrop)
|
|
252
|
+
@click.command(cls=FileCommand) # parse a packet log file, then stop
|
|
253
|
+
@click.pass_obj
|
|
254
|
+
def parse(obj, **kwargs: Any):
|
|
255
|
+
"""Command to parse a log file containing messages/packets."""
|
|
256
|
+
config, lib_config = split_kwargs(obj, kwargs)
|
|
257
|
+
|
|
258
|
+
lib_config[SZ_INPUT_FILE] = config.pop(SZ_INPUT_FILE) # just the file path
|
|
259
|
+
|
|
260
|
+
return PARSE, lib_config, config
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
#
|
|
264
|
+
# 2/4: MONITOR (listen to RF, +/- discovery, +/- eavesdrop)
|
|
265
|
+
@click.command(cls=PortCommand) # (optionally) execute a command/script, then monitor
|
|
266
|
+
@click.option("-d/-nd", "--discover/--no-discover", default=None) # --no-discover
|
|
267
|
+
@click.option( # --exec-cmd 'RQ 01:123456 1F09 00'
|
|
268
|
+
"-x", "--exec-cmd", type=click.STRING, help="e.g. 'RQ 01:123456 1F09 00'"
|
|
269
|
+
)
|
|
270
|
+
@click.option( # --execute-scr script device_id
|
|
271
|
+
"-X",
|
|
272
|
+
"--exec-scr",
|
|
273
|
+
type=(str, DeviceIdParamType()),
|
|
274
|
+
help="scan_disc|scan_full|scan_hard|bind device_id",
|
|
275
|
+
)
|
|
276
|
+
@click.option( # --poll-devices device_id, device_id,...
|
|
277
|
+
"--poll-devices", type=click.STRING, help="e.g. 'device_id, device_id, ...'"
|
|
278
|
+
)
|
|
279
|
+
@click.pass_obj
|
|
280
|
+
def monitor(obj, discover: None | bool = None, **kwargs: Any):
|
|
281
|
+
"""Monitor (eavesdrop and/or probe) a serial port for messages/packets."""
|
|
282
|
+
config, lib_config = split_kwargs(obj, kwargs)
|
|
283
|
+
|
|
284
|
+
if discover is None:
|
|
285
|
+
if kwargs["exec_scr"] is None and kwargs["poll_devices"] is None:
|
|
286
|
+
print(" - discovery is enabled")
|
|
287
|
+
lib_config[SZ_CONFIG][SZ_DISABLE_DISCOVERY] = False
|
|
288
|
+
else:
|
|
289
|
+
print(" - discovery is disabled")
|
|
290
|
+
lib_config[SZ_CONFIG][SZ_DISABLE_DISCOVERY] = True
|
|
291
|
+
|
|
292
|
+
return MONITOR, lib_config, config
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
#
|
|
296
|
+
# 3/4: EXECUTE (send cmds to RF, +/- discovery, +/- eavesdrop)
|
|
297
|
+
@click.command(cls=PortCommand) # execute a (complex) script, then stop
|
|
298
|
+
@click.option("-d/-nd", "--discover/--no-discover", default=None) # --no-discover
|
|
299
|
+
@click.option( # --exec-cmd 'RQ 01:123456 1F09 00'
|
|
300
|
+
"-x", "--exec-cmd", type=click.STRING, help="e.g. 'RQ 01:123456 1F09 00'"
|
|
301
|
+
)
|
|
302
|
+
@click.option( # --get-faults ctl_id
|
|
303
|
+
"--get-faults", type=DeviceIdParamType(), help="controller_id"
|
|
304
|
+
)
|
|
305
|
+
@click.option( # --get-schedule ctl_id zone_idx|HW
|
|
306
|
+
"--get-schedule",
|
|
307
|
+
default=[None, None],
|
|
308
|
+
type=(DeviceIdParamType(), str),
|
|
309
|
+
help="controller_id, zone_idx (e.g. '0A', 'HW')",
|
|
310
|
+
)
|
|
311
|
+
@click.option( # --set-schedule ctl_id zone_idx|HW
|
|
312
|
+
"--set-schedule",
|
|
313
|
+
default=[None, None],
|
|
314
|
+
type=(DeviceIdParamType(), click.File("r")),
|
|
315
|
+
help="controller_id, filename.json",
|
|
316
|
+
)
|
|
317
|
+
@click.pass_obj
|
|
318
|
+
def execute(obj, **kwargs: Any):
|
|
319
|
+
"""Execute any specified scripts, return the results, then quit.
|
|
320
|
+
|
|
321
|
+
Disables discovery, and enforces a strict allow_list.
|
|
322
|
+
"""
|
|
323
|
+
config, lib_config = split_kwargs(obj, kwargs)
|
|
324
|
+
|
|
325
|
+
print(" - discovery is force-disabled")
|
|
326
|
+
lib_config[SZ_CONFIG][SZ_DISABLE_DISCOVERY] = True
|
|
327
|
+
lib_config[SZ_CONFIG][SZ_DISABLE_QOS] = False
|
|
328
|
+
|
|
329
|
+
if kwargs[GET_FAULTS]:
|
|
330
|
+
known_list = {kwargs[GET_FAULTS]: {}}
|
|
331
|
+
elif kwargs[GET_SCHED][0]:
|
|
332
|
+
known_list = {kwargs[GET_SCHED][0]: {}}
|
|
333
|
+
elif kwargs[SET_SCHED][0]:
|
|
334
|
+
known_list = {kwargs[SET_SCHED][0]: {}}
|
|
335
|
+
else:
|
|
336
|
+
known_list = {}
|
|
337
|
+
|
|
338
|
+
if known_list:
|
|
339
|
+
print(" - known list is force-configured/enforced")
|
|
340
|
+
lib_config[SZ_KNOWN_LIST] = known_list
|
|
341
|
+
lib_config[SZ_CONFIG][SZ_ENFORCE_KNOWN_LIST] = True
|
|
342
|
+
|
|
343
|
+
return EXECUTE, lib_config, config
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
#
|
|
347
|
+
# 4/4: LISTEN (to RF, +/- eavesdrop - NO sending/discovery)
|
|
348
|
+
@click.command(cls=PortCommand) # (optionally) execute a command, then listen
|
|
349
|
+
@click.pass_obj
|
|
350
|
+
def listen(obj, **kwargs: Any):
|
|
351
|
+
"""Listen to (eavesdrop only) a serial port for messages/packets."""
|
|
352
|
+
config, lib_config = split_kwargs(obj, kwargs)
|
|
353
|
+
|
|
354
|
+
print(" - sending is force-disabled")
|
|
355
|
+
lib_config[SZ_CONFIG][SZ_DISABLE_SENDING] = True
|
|
356
|
+
|
|
357
|
+
return LISTEN, lib_config, config
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def print_results(gwy: Gateway, **kwargs: Any) -> None:
|
|
361
|
+
if kwargs[GET_FAULTS]:
|
|
362
|
+
fault_log = gwy.system_by_id[kwargs[GET_FAULTS]]._faultlog.faultlog
|
|
363
|
+
|
|
364
|
+
if fault_log is None:
|
|
365
|
+
print("No fault log, or failed to get the fault log.")
|
|
366
|
+
else:
|
|
367
|
+
[print(f"{k:02X}", v) for k, v in fault_log.items()]
|
|
368
|
+
|
|
369
|
+
if kwargs[GET_SCHED][0]:
|
|
370
|
+
system_id, zone_idx = kwargs[GET_SCHED]
|
|
371
|
+
if zone_idx == "HW":
|
|
372
|
+
zone = gwy.system_by_id[system_id].dhw
|
|
373
|
+
else:
|
|
374
|
+
zone = gwy.system_by_id[system_id].zone_by_idx[zone_idx]
|
|
375
|
+
schedule = zone.schedule
|
|
376
|
+
|
|
377
|
+
if schedule is None:
|
|
378
|
+
print("Failed to get the schedule.")
|
|
379
|
+
else:
|
|
380
|
+
result = {SZ_ZONE_IDX: zone_idx, "schedule": schedule}
|
|
381
|
+
print(">>> Schedule JSON begins <<<")
|
|
382
|
+
print(json.dumps(result, indent=4))
|
|
383
|
+
print(">>> Schedule JSON ended <<<")
|
|
384
|
+
|
|
385
|
+
if kwargs[SET_SCHED][0]:
|
|
386
|
+
system_id, _ = kwargs[GET_SCHED]
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _save_state(gwy: Gateway) -> None:
|
|
390
|
+
schema, msgs = gwy.get_state()
|
|
391
|
+
|
|
392
|
+
with open("state_msgs.log", "w") as f:
|
|
393
|
+
[f.write(f"{dtm} {pkt}\r\n") for dtm, pkt in msgs.items()] # if not m._expired
|
|
394
|
+
|
|
395
|
+
with open("state_schema.json", "w") as f:
|
|
396
|
+
f.write(json.dumps(schema, indent=4))
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _print_engine_state(gwy: Gateway, **kwargs: Any) -> None:
|
|
400
|
+
(schema, packets) = gwy.get_state(include_expired=True)
|
|
401
|
+
|
|
402
|
+
if kwargs["print_state"] > 0:
|
|
403
|
+
print(f"schema: {json.dumps(schema, indent=4)}\r\n")
|
|
404
|
+
if kwargs["print_state"] > 1:
|
|
405
|
+
print(f"packets: {json.dumps(packets, indent=4)}\r\n")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def print_summary(gwy: Gateway, **kwargs: Any) -> None:
|
|
409
|
+
entity = gwy.tcs or gwy
|
|
410
|
+
|
|
411
|
+
if kwargs.get("show_schema"):
|
|
412
|
+
print(f"Schema[{entity}] = {json.dumps(entity.schema, indent=4)}\r\n")
|
|
413
|
+
|
|
414
|
+
# schema = {d.id: d.schema for d in sorted(gwy.devices)}
|
|
415
|
+
# print(f"Schema[devices] = {json.dumps({'schema': schema}, indent=4)}\r\n")
|
|
416
|
+
|
|
417
|
+
if kwargs.get("show_params"):
|
|
418
|
+
print(f"Params[{entity}] = {json.dumps(entity.params, indent=4)}\r\n")
|
|
419
|
+
|
|
420
|
+
params = {d.id: d.params for d in sorted(gwy.devices)}
|
|
421
|
+
print(f"Params[devices] = {json.dumps({'params': params}, indent=4)}\r\n")
|
|
422
|
+
|
|
423
|
+
if kwargs.get("show_status"):
|
|
424
|
+
print(f"Status[{entity}] = {json.dumps(entity.status, indent=4)}\r\n")
|
|
425
|
+
|
|
426
|
+
status = {d.id: d.status for d in sorted(gwy.devices)}
|
|
427
|
+
print(f"Status[devices] = {json.dumps({'status': status}, indent=4)}\r\n")
|
|
428
|
+
|
|
429
|
+
if kwargs.get("show_knowns"): # show device hints (show-knowns)
|
|
430
|
+
print(f"allow_list (hints) = {json.dumps(gwy._include, indent=4)}\r\n")
|
|
431
|
+
|
|
432
|
+
if kwargs.get("show_traits"): # show device traits
|
|
433
|
+
result = {
|
|
434
|
+
d.id: d.traits # {k: v for k, v in d.traits.items() if k[:1] == "_"}
|
|
435
|
+
for d in sorted(gwy.devices)
|
|
436
|
+
}
|
|
437
|
+
print(json.dumps(result, indent=4), "\r\n")
|
|
438
|
+
|
|
439
|
+
if kwargs.get("show_crazys"):
|
|
440
|
+
for device in [d for d in gwy.devices if d.type == DEV_TYPE_MAP.CTL]:
|
|
441
|
+
for code, verbs in device._msgz.items():
|
|
442
|
+
if code in (Code._0005, Code._000C):
|
|
443
|
+
for verb in verbs.values():
|
|
444
|
+
for pkt in verb.values():
|
|
445
|
+
print(f"{pkt}")
|
|
446
|
+
print()
|
|
447
|
+
for device in [d for d in gwy.devices if d.type == DEV_TYPE_MAP.UFC]:
|
|
448
|
+
for code in device._msgz.values():
|
|
449
|
+
for verb in code.values():
|
|
450
|
+
for pkt in verb.values():
|
|
451
|
+
print(f"{pkt}")
|
|
452
|
+
print()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
async def async_main(command: str, lib_kwargs: dict, **kwargs: Any) -> None:
|
|
456
|
+
"""Do certain things."""
|
|
457
|
+
|
|
458
|
+
def handle_msg(msg: Message) -> None:
|
|
459
|
+
"""Process the message as it arrives (a callback).
|
|
460
|
+
|
|
461
|
+
In this case, the message is merely printed.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
if kwargs["long_format"]: # HACK for test/dev
|
|
465
|
+
print(
|
|
466
|
+
f"{msg.dtm.isoformat(timespec='microseconds')} ... {msg!r}"
|
|
467
|
+
f" # {msg.payload}" # or f' # ("{msg.src!r}", "{msg.dst!r}")'
|
|
468
|
+
)
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
dtm = f"{msg.dtm:%H:%M:%S.%f}"[:-3]
|
|
472
|
+
con_cols = CONSOLE_COLS
|
|
473
|
+
|
|
474
|
+
if msg.code == Code._PUZZ:
|
|
475
|
+
print(f"{Style.BRIGHT}{Fore.YELLOW}{dtm} {msg}"[:con_cols])
|
|
476
|
+
elif msg.src and msg.src.type == DEV_TYPE_MAP.HGI:
|
|
477
|
+
print(f"{Style.BRIGHT}{COLORS.get(msg.verb)}{dtm} {msg}"[:con_cols])
|
|
478
|
+
elif msg.code == Code._1F09 and msg.verb == I_:
|
|
479
|
+
print(f"{Fore.YELLOW}{dtm} {msg}"[:con_cols])
|
|
480
|
+
elif msg.code in (Code._000A, Code._2309, Code._30C9) and msg._has_array:
|
|
481
|
+
print(f"{Fore.YELLOW}{dtm} {msg}"[:con_cols])
|
|
482
|
+
else:
|
|
483
|
+
print(f"{COLORS.get(msg.verb)}{dtm} {msg}"[:con_cols])
|
|
484
|
+
|
|
485
|
+
serial_port, lib_kwargs = normalise_config(lib_kwargs)
|
|
486
|
+
assert isinstance(lib_kwargs.get(SZ_INPUT_FILE), str)
|
|
487
|
+
|
|
488
|
+
if kwargs["restore_schema"]:
|
|
489
|
+
print(" - restoring client schema from a HA cache...")
|
|
490
|
+
state = json.load(kwargs["restore_schema"])["data"]["client_state"]
|
|
491
|
+
lib_kwargs = lib_kwargs | state["schema"]
|
|
492
|
+
|
|
493
|
+
# if serial_port == "/dev/ttyMOCK":
|
|
494
|
+
# from tests.deprecated.mocked_rf import MockGateway # FIXME: for test/dev
|
|
495
|
+
# gwy = MockGateway(serial_port, **lib_kwargs)
|
|
496
|
+
# else:
|
|
497
|
+
gwy = Gateway(serial_port, **lib_kwargs) # passes action to gateway
|
|
498
|
+
|
|
499
|
+
if lib_kwargs[SZ_CONFIG][SZ_REDUCE_PROCESSING] < DONT_CREATE_MESSAGES:
|
|
500
|
+
# library will not send MSGs to STDOUT, so we'll send PKTs instead
|
|
501
|
+
colorama_init(autoreset=True) # WIP: remove strip=True
|
|
502
|
+
gwy.add_msg_handler(handle_msg)
|
|
503
|
+
|
|
504
|
+
if kwargs["restore_state"]:
|
|
505
|
+
print(" - restoring packets from a HA cache...")
|
|
506
|
+
state = json.load(kwargs["restore_state"])["data"]["client_state"]
|
|
507
|
+
await gwy._restore_cached_packets(state["packets"])
|
|
508
|
+
|
|
509
|
+
print("\r\nclient.py: Starting engine...")
|
|
510
|
+
|
|
511
|
+
try: # main code here
|
|
512
|
+
await gwy.start()
|
|
513
|
+
|
|
514
|
+
# TODO:
|
|
515
|
+
# python client.py -rrr listen /dev/ttyUSB0
|
|
516
|
+
# cat *.log | head | python client.py parse
|
|
517
|
+
|
|
518
|
+
if command == EXECUTE:
|
|
519
|
+
tasks = spawn_scripts(gwy, **kwargs)
|
|
520
|
+
await asyncio.gather(*tasks)
|
|
521
|
+
|
|
522
|
+
elif command == MONITOR:
|
|
523
|
+
_ = spawn_scripts(gwy, **kwargs)
|
|
524
|
+
await gwy._protocol._wait_connection_lost
|
|
525
|
+
|
|
526
|
+
elif command in (LISTEN, PARSE):
|
|
527
|
+
await gwy._protocol._wait_connection_lost
|
|
528
|
+
|
|
529
|
+
except asyncio.CancelledError:
|
|
530
|
+
msg = "ended via: CancelledError (e.g. SIGINT)"
|
|
531
|
+
except GracefulExit:
|
|
532
|
+
msg = "ended via: GracefulExit"
|
|
533
|
+
except KeyboardInterrupt: # FIXME: why isn't this captured here? see main
|
|
534
|
+
msg = "ended via: KeyboardInterrupt"
|
|
535
|
+
except exc.RamsesException as err:
|
|
536
|
+
msg = f"ended via: RamsesException: {err}"
|
|
537
|
+
else: # if no Exceptions raised, e.g. EOF when parsing, or Ctrl-C?
|
|
538
|
+
msg = "ended without error (e.g. EOF)"
|
|
539
|
+
finally:
|
|
540
|
+
await gwy.stop() # what happens if we have an exception here?
|
|
541
|
+
|
|
542
|
+
print(f"\r\nclient.py: Engine stopped: {msg}")
|
|
543
|
+
|
|
544
|
+
# if kwargs["save_state"]:
|
|
545
|
+
# _save_state(gwy)
|
|
546
|
+
|
|
547
|
+
if kwargs["print_state"]:
|
|
548
|
+
_print_engine_state(gwy, **kwargs)
|
|
549
|
+
|
|
550
|
+
elif command == EXECUTE:
|
|
551
|
+
print_results(gwy, **kwargs)
|
|
552
|
+
|
|
553
|
+
print_summary(gwy, **kwargs)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
cli.add_command(parse)
|
|
557
|
+
cli.add_command(monitor)
|
|
558
|
+
cli.add_command(execute)
|
|
559
|
+
cli.add_command(listen)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def main() -> None:
|
|
563
|
+
print("\r\nclient.py: Starting ramses_rf...")
|
|
564
|
+
|
|
565
|
+
try:
|
|
566
|
+
result = cli(standalone_mode=False)
|
|
567
|
+
except click.NoSuchOption as err:
|
|
568
|
+
print(f"Error: {err}")
|
|
569
|
+
sys.exit(-1)
|
|
570
|
+
|
|
571
|
+
if isinstance(result, int):
|
|
572
|
+
sys.exit(result)
|
|
573
|
+
|
|
574
|
+
(command, lib_kwargs, kwargs) = result
|
|
575
|
+
|
|
576
|
+
if sys.platform == "win32":
|
|
577
|
+
print(" - event_loop_policy set for win32") # do before asyncio.run()
|
|
578
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
579
|
+
|
|
580
|
+
try:
|
|
581
|
+
if _PROFILE_LIBRARY:
|
|
582
|
+
profile = cProfile.Profile()
|
|
583
|
+
profile.run("asyncio.run(main(command, lib_kwargs, **kwargs))")
|
|
584
|
+
else:
|
|
585
|
+
asyncio.run(async_main(command, lib_kwargs, **kwargs))
|
|
586
|
+
except KeyboardInterrupt: # , SystemExit):
|
|
587
|
+
print("\r\nclient.py: Engine stopped: ended via: KeyboardInterrupt")
|
|
588
|
+
|
|
589
|
+
if _PROFILE_LIBRARY:
|
|
590
|
+
ps = pstats.Stats(profile)
|
|
591
|
+
ps.sort_stats(pstats.SortKey.TIME).print_stats(20)
|
|
592
|
+
|
|
593
|
+
print(" - finished ramses_rf.\r\n")
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
if __name__ == "__main__":
|
|
597
|
+
main()
|
ramses_cli/debug.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A CLI for the ramses_rf library."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
SZ_DBG_MODE = "debug_mode"
|
|
7
|
+
DEBUG_ADDR = "0.0.0.0"
|
|
8
|
+
DEBUG_PORT = 5678
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def start_debugging(wait_for_client: bool) -> None:
|
|
12
|
+
import debugpy # type: ignore[import-untyped]
|
|
13
|
+
|
|
14
|
+
debugpy.listen(address=(DEBUG_ADDR, DEBUG_PORT))
|
|
15
|
+
print(f" - Debugging is enabled, listening on: {DEBUG_ADDR}:{DEBUG_PORT}")
|
|
16
|
+
|
|
17
|
+
if wait_for_client:
|
|
18
|
+
print(" - execution paused, waiting for debugger to attach...")
|
|
19
|
+
debugpy.wait_for_client()
|
|
20
|
+
print(" - debugger is now attached, continuing execution.")
|