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.
Files changed (71) 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 +377 -513
  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.40.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.40.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_rf/protocol → ramses_tx}/parsers.py +1266 -1003
  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 -1576
  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/protocol.py +0 -613
  66. ramses_rf/protocol/transport.py +0 -1011
  67. ramses_rf/protocol/version.py +0 -10
  68. ramses_rf/system/hvac.py +0 -82
  69. ramses_rf-0.22.40.dist-info/METADATA +0 -64
  70. ramses_rf-0.22.40.dist-info/RECORD +0 -42
  71. 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.")