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_rf/version.py CHANGED
@@ -1,10 +1,4 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
- """RAMSES RF - a RAMSES-II protocol decoder & analyser.
1
+ """RAMSES RF - a RAMSES-II protocol decoder & analyser (application layer)."""
5
2
 
6
- The RAMSES-II system (controllers, zones, devices).
7
- """
8
-
9
- __version__ = "0.22.40"
3
+ __version__ = "0.51.1"
10
4
  VERSION = __version__
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: ramses_rf
3
+ Version: 0.51.1
4
+ Summary: A stateful RAMSES-II protocol decoder & analyser.
5
+ Project-URL: Homepage, https://github.com/ramses-rf/ramses_rf
6
+ Project-URL: Bug Tracker, https://github.com/ramses-rf/ramses_rf/issues
7
+ Project-URL: Wiki, https://github.com/ramses-rf/ramses_rf/wiki
8
+ Author-email: David Bonnes <zxdavb@bonnes.me>, Egbert Broerse <dcc2@ebroerse.nl>
9
+ Maintainer-email: Egbert Broerse <dcc2@ebroerse.nl>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: airios,chronotherm,climarad,evohome,hometronics,honeywell,itho,nuaire,orcon,ramses,resideo,round thermostat,sundial,vasco
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Home Automation
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: colorlog>=6.9.0
17
+ Requires-Dist: paho-mqtt>=2.1.0
18
+ Requires-Dist: pyserial-asyncio-fast>=0.16
19
+ Requires-Dist: voluptuous>=0.15.2
20
+ Description-Content-Type: text/markdown
21
+
22
+ ![Linting](https://github.com/ramses-rf/ramses_rf/actions/workflows/check-lint.yml/badge.svg)
23
+ ![Typing](https://github.com/ramses-rf/ramses_rf/actions/workflows/check-type.yml/badge.svg)
24
+ ![Testing](https://github.com/ramses-rf/ramses_rf/actions/workflows/check-test.yml/badge.svg)
25
+
26
+ ## Overview
27
+
28
+ **ramses_rf** is a Python client library/CLI utility used to interface with some Honeywell-compatible HVAC & CH/DHW systems that use 868MHz RF, such as:
29
+ - (Heat) **evohome**, **Sundial**, **Hometronic**, **Chronotherm**
30
+ - (HVAC) **Itho**, **Orcon**, **Nuaire**, **Vasco**, **ClimaRad**
31
+
32
+ It requires a USB-to-RF device, either a Honeywell HGI80 (somewhat rare, expensive) or something running the [evofw3](https://github.com/ghoti57/evofw3) firmware, such as the one from [here](https://indalo-tech.onlineweb.shop/) or your own ESP32-S3-WROOM-1 N16R8 with a CC1100 transponder.
33
+
34
+ It does four things:
35
+ - decodes RAMSES II-compatible packets and converts them into useful JSON
36
+ - builds a picture (schema, config & state) of evohome-compatible CH/DHW systems - either passively (by eavesdropping), or actively (probing)
37
+ - allows you to send commands to CH/DHW and HVAC systems, or monitor for state changes
38
+ - allows you to emulate some hardware devices
39
+
40
+ > [!WARNING]
41
+ > This library is not affiliated with Honeywell, Airios nor any final manufacturer. The developers take no responsibility for anything that may happen to your devices because of this library.
42
+
43
+ For CH/DHW, the simplest way to know if it will work with your system is to identify the box connected to your boiler/HVAC appliance as one of:
44
+ - **R8810A**: OpenTherm Bridge
45
+ - **BDR91A**: Wireless Relay (also BDR91T)
46
+ - **HC60NG**: Wireless Relay (older hardware)
47
+
48
+ Other systems may well work, such as some Itho Daalderop HVAC systems, use this protocol, YMMV.
49
+
50
+ It includes a CLI and can be used as a standalone tool, but also is used as a client library by:
51
+ - [ramses_cc](https://github.com/ramses-rf/ramses_cc), a Home Assistant integration
52
+ - [evohome-Listener](https://github.com/smar000/evohome-Listener), an MQTT gateway
53
+
54
+ ## Installation
55
+
56
+ To use the `ramses_cc` Integration in Home Assistant, just install `Ramses RF` from HACS. It will take care of installing this library. See the [`Ramses_cc wiki`](https://github.com/ramses-rf/ramses_cc/wiki/1.-Installation) for details.
57
+
58
+ ### Ramses_rf CLI
59
+
60
+ To install the `ramses_rf` command line client:
61
+ ```
62
+ git clone https://github.com/ramses-rf/ramses_rf
63
+ cd ramses_rf
64
+ pip install -r requirements.txt
65
+ pip install -e .
66
+ ```
67
+
68
+ The CLI is called ``client.py`` and is included in the code root.
69
+ It has options to monitor and parse Ramses-II traffic to screen or a log file, and to parse a file containing Ramses-II messages to the screen.
70
+ See the [client.py CLI wiki page](https://github.com/ramses-rf/ramses_rf/wiki/The-client.py-command-line) for instructions.
71
+
72
+ For code development, some more setup is required. Please follow the steps in our [Developer's Resource](README-developers.md)
@@ -0,0 +1,55 @@
1
+ ramses_cli/__init__.py,sha256=uvGzWqOf4avvgzxJNSLFWEelIWqSZ-AeLAZzg5x58bc,397
2
+ ramses_cli/client.py,sha256=QOmPKjCQHHOZwLBWEB438zabI9k38-ELRwisLvbvxSU,19782
3
+ ramses_cli/debug.py,sha256=vgR0lOHoYjWarN948dI617WZZGNuqHbeq6Tc16Da7b4,608
4
+ ramses_cli/discovery.py,sha256=81XbmpNiCpUHVZBwo2g1eRwyJG-wZhpSsc44G3hHlFA,12972
5
+ ramses_cli/utils/cat_slow.py,sha256=AhUpM5gnegCitNKU-JGHn-DrRzSi-49ZR1Qw6lxe_t8,607
6
+ ramses_cli/utils/convert.py,sha256=D_YiCyX5na9pgC-_NhBlW9N1dgRKUK-uLtLBfofjzZM,1804
7
+ ramses_rf/__init__.py,sha256=zONFBiRdf07cPTSxzr2V3t-b3CGokZjT9SGit4JUKRA,1055
8
+ ramses_rf/binding_fsm.py,sha256=uZAOl3i19KCXqqlaLJWkEqMMP7NJBhVPW3xTikQD1fY,25996
9
+ ramses_rf/const.py,sha256=DSo4ROWDlOlcdXQdrpAF17vOsTLgmf2u0UppjYa5qJI,5390
10
+ ramses_rf/database.py,sha256=6k5MLtK5Lplz8THfluQoQU-eniUkqSwEUMvVW7VyGhI,9880
11
+ ramses_rf/dispatcher.py,sha256=b7Cg1vAP6FECC6GeZsJ0BZVqy-ZjJTXhZquzcwE87WI,11221
12
+ ramses_rf/entity_base.py,sha256=BYHatdUuz_BSQsUlzsC52qPMLZw7h_M6UTb3A6PBPH4,39473
13
+ ramses_rf/exceptions.py,sha256=rzVZDcYxFH7BjUAQ3U1fHWtgBpwk3BgjX1TO1L1iM8c,2538
14
+ ramses_rf/gateway.py,sha256=vqoTEb6QXnwaIMa66oed_3LEVvlyQ3flsAAMliEEvVA,20921
15
+ ramses_rf/helpers.py,sha256=LcrVLqnF2qJWqXrC7UXKOQE8khCT3OhoTpZ_ZVBjw3A,4249
16
+ ramses_rf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ ramses_rf/schemas.py,sha256=mYOUZOH5OIDNBxRM2vd8POzDWEEmLhxh5UtqjTpFNek,13287
18
+ ramses_rf/version.py,sha256=F1An7mWrZUZVyxhkApeCPk3xFAau34Pc6hzHu6fqvk8,125
19
+ ramses_rf/device/__init__.py,sha256=sUbH5dhbYFXSoM_TPFRutpRutBRpup7_cQ9smPtDTy8,4858
20
+ ramses_rf/device/base.py,sha256=V2YzRhdxrTqfHYrCBq6pJsYdTgAx8gGzfdo8pkbeEo8,17450
21
+ ramses_rf/device/heat.py,sha256=2sCsggySVcuTzyXDmgWy76QbhlU5MQWSejy3zgI5BDE,54242
22
+ ramses_rf/device/hvac.py,sha256=5K0FsTTu506OByd_oMoNJ7FePvjfQaxlT4eehXUAEC8,23642
23
+ ramses_rf/system/__init__.py,sha256=uZLKio3gLlBzePa2aDQ1nxkcp1YXOGrn6iHTG8LiNIw,711
24
+ ramses_rf/system/faultlog.py,sha256=GdGmVGT3137KsTlV_nhccgIFEmYu6DFsLTn4S-8JSok,12799
25
+ ramses_rf/system/heat.py,sha256=dARzcwL39JGwOBJkKJBi0_i7rr8IvY-qaNmWmgJLpdo,39223
26
+ ramses_rf/system/schedule.py,sha256=Ts6tdZPTQLV5NkgwA73tPa5QUsnZNIIuYoKC-8VsXDk,18808
27
+ ramses_rf/system/zones.py,sha256=QwRtSHY5c-Amcs6JD16uQcimOsEQTZcMm1dW-pqEFqM,36041
28
+ ramses_tx/__init__.py,sha256=wJ7Ntx-0AyJwYwSG8OrFMpxDLXs6GbECbCcYhq98mSA,3162
29
+ ramses_tx/address.py,sha256=2640K3sXzogZtd4-tSxwVjYEEXcFE1DgmtvZlTMM5mE,8444
30
+ ramses_tx/command.py,sha256=g5PBf9JnuygveyaYrqIuV8wIn7grm0evuqKy9Cp1oaA,53844
31
+ ramses_tx/const.py,sha256=B2db8Yxks-lMNsQAK1DoPkF1gvwNIacLmKwXuApUyLk,30221
32
+ ramses_tx/exceptions.py,sha256=FJSU9YkvpKjs3yeTqUJX1o3TPFSe_B01gRGIh9b3PNc,2632
33
+ ramses_tx/fingerprints.py,sha256=nfftA1E62HQnb-eLt2EqjEi_la0DAoT0wt-PtTMie0s,11974
34
+ ramses_tx/frame.py,sha256=9lUVh8gAMXNRAolfFw2WuWANjn24AWkmscuM9Tm5imE,22036
35
+ ramses_tx/gateway.py,sha256=FE5MWA1eIE9JATA2vRoBSQ8fAzqp7TqAm3Ds3k1KnKE,11267
36
+ ramses_tx/helpers.py,sha256=WJ5JtAT9iyhkcW53AIPNPuvGEUWFwLumZc-mCG2kIOc,32236
37
+ ramses_tx/logger.py,sha256=7vUpcfOFMW95juMWDx5dhUOqV8DTsindZ-Qz2aCmEoA,11073
38
+ ramses_tx/message.py,sha256=J1wvVkLPJQr2ffKCUQYSWwLPzRTZBC0zUU5W9DkN3hU,13190
39
+ ramses_tx/opentherm.py,sha256=58PXz9l5x8Ou6Fm3y-R_UnGHCYahoi2RKIDdYStUMzk,42378
40
+ ramses_tx/packet.py,sha256=NGunaGCkEjhTp9t4mARK5e7kbqT-Z_JKCH7ibMYMJXU,7357
41
+ ramses_tx/parsers.py,sha256=R-oFcRUe7HPsm9n4196hFUD1tULbFdKBAWo8HvzGyjw,109218
42
+ ramses_tx/protocol.py,sha256=ifj3qwcQivjQDaQUwM94xp-U8Pmef6zwSH7mav8DLWA,28867
43
+ ramses_tx/protocol_fsm.py,sha256=YhHkTqbl8w-myimsOjV50uIFgg9HiApwPU7xA_jg5nU,26827
44
+ ramses_tx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ ramses_tx/ramses.py,sha256=yPSdhxGRYQ9AuTWoKs9Cb3YQn5YnEKZVs7BYkQFmqCw,52037
46
+ ramses_tx/schemas.py,sha256=h2AcArVROy1_C4n6F0Crj4e-2BxXxH74xogFlc6nKHI,12769
47
+ ramses_tx/transport.py,sha256=28CtiqNltcJhLr4VIHCW9uCohWXrvxmx6ySUSIuRQ9c,52892
48
+ ramses_tx/typed_dicts.py,sha256=4ZT50M-Cuwy2ljAIorwoxEJ9c737xUHrUxX9wTh79xE,10834
49
+ ramses_tx/typing.py,sha256=eF2SlPWhNhEFQj6WX2AhTXiyRQVXYnFutiepllYl2rI,5042
50
+ ramses_tx/version.py,sha256=PyOf332e4cHU33OjhzPo9s_NpQJZFU36TeBDdC0fF3M,123
51
+ ramses_rf-0.51.1.dist-info/METADATA,sha256=cJUJq-2CnXserwY3AwjXZrRf_xUe-xRPLtAm069GCU8,3906
52
+ ramses_rf-0.51.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
+ ramses_rf-0.51.1.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
54
+ ramses_rf-0.51.1.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
55
+ ramses_rf-0.51.1.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.37.1)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ client = ramses_cli.client:main
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 David Bonnes
3
+ Copyright (c) 2021-2025 David Bonnes
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
ramses_tx/__init__.py ADDED
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env python3
2
+ """RAMSES RF - a RAMSES-II protocol decoder & analyser."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ from functools import partial
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from .address import (
11
+ ALL_DEV_ADDR,
12
+ ALL_DEVICE_ID,
13
+ NON_DEV_ADDR,
14
+ NON_DEVICE_ID,
15
+ Address,
16
+ is_valid_dev_id,
17
+ )
18
+ from .command import CODE_API_MAP, Command
19
+ from .const import (
20
+ DEV_ROLE_MAP,
21
+ DEV_TYPE_MAP,
22
+ F9,
23
+ FA,
24
+ FC,
25
+ FF,
26
+ SZ_ACTIVE_HGI,
27
+ SZ_DEVICE_ROLE,
28
+ SZ_DOMAIN_ID,
29
+ SZ_ZONE_CLASS,
30
+ SZ_ZONE_IDX,
31
+ SZ_ZONE_MASK,
32
+ SZ_ZONE_TYPE,
33
+ ZON_ROLE_MAP,
34
+ DevRole,
35
+ DevType,
36
+ IndexT,
37
+ Priority,
38
+ VerbT,
39
+ ZoneRole,
40
+ )
41
+ from .gateway import Engine
42
+ from .logger import set_pkt_logging
43
+ from .message import Message
44
+ from .packet import PKT_LOGGER, Packet
45
+ from .protocol import PortProtocol, ReadProtocol, protocol_factory
46
+ from .ramses import CODES_BY_DEV_SLUG, CODES_SCHEMA
47
+ from .schemas import SZ_SERIAL_PORT, DeviceIdT, DeviceListT
48
+ from .transport import (
49
+ FileTransport,
50
+ PortTransport,
51
+ RamsesTransportT,
52
+ is_hgi80,
53
+ transport_factory,
54
+ )
55
+ from .typing import QosParams
56
+ from .version import VERSION
57
+
58
+ from .const import ( # isort: skip
59
+ I_,
60
+ RP,
61
+ RQ,
62
+ W_,
63
+ Code,
64
+ )
65
+
66
+
67
+ __all__ = [
68
+ "VERSION",
69
+ "Engine",
70
+ #
71
+ "SZ_ACTIVE_HGI",
72
+ "SZ_DEVICE_ROLE",
73
+ "SZ_DOMAIN_ID",
74
+ "SZ_SERIAL_PORT",
75
+ "SZ_ZONE_CLASS",
76
+ "SZ_ZONE_IDX",
77
+ "SZ_ZONE_MASK",
78
+ "SZ_ZONE_TYPE",
79
+ #
80
+ "ALL_DEV_ADDR",
81
+ "ALL_DEVICE_ID",
82
+ "NON_DEV_ADDR",
83
+ "NON_DEVICE_ID",
84
+ #
85
+ "CODE_API_MAP",
86
+ "CODES_BY_DEV_SLUG", # shouldn't export this
87
+ "CODES_SCHEMA",
88
+ "DEV_ROLE_MAP",
89
+ "DEV_TYPE_MAP",
90
+ "ZON_ROLE_MAP",
91
+ #
92
+ "I_",
93
+ "RP",
94
+ "RQ",
95
+ "W_",
96
+ "F9",
97
+ "FA",
98
+ "FC",
99
+ "FF",
100
+ #
101
+ "DeviceIdT",
102
+ "DeviceListT",
103
+ "DevRole",
104
+ "DevType",
105
+ "IndexT",
106
+ "VerbT",
107
+ "ZoneRole",
108
+ #
109
+ "Address",
110
+ "Code",
111
+ "Command",
112
+ "Message",
113
+ "Packet",
114
+ "Priority",
115
+ "QosParams",
116
+ #
117
+ "PortProtocol",
118
+ "ReadProtocol",
119
+ "RamsesProtocolT",
120
+ "extract_known_hgi_id",
121
+ "protocol_factory",
122
+ #
123
+ "FileTransport",
124
+ "PortTransport",
125
+ "RamsesTransportT",
126
+ "is_hgi80",
127
+ "transport_factory",
128
+ #
129
+ "is_valid_dev_id",
130
+ "set_pkt_logging_config",
131
+ ]
132
+
133
+
134
+ if TYPE_CHECKING:
135
+ from logging import Logger
136
+
137
+
138
+ async def set_pkt_logging_config(**config: Any) -> Logger:
139
+ """
140
+ Set up ramses packet logging to a file or port.
141
+ Must runs async in executor to prevent HA blocking call opening packet log file (issue #200)
142
+
143
+ :param config: if file_name is included, opens packet_log file
144
+ :return: a logging.Logger
145
+ """
146
+ loop = asyncio.get_running_loop()
147
+ await loop.run_in_executor(None, partial(set_pkt_logging, PKT_LOGGER, **config))
148
+ return PKT_LOGGER
149
+
150
+
151
+ def extract_known_hgi_id(
152
+ include_list: DeviceListT,
153
+ /,
154
+ *,
155
+ disable_warnings: bool = False,
156
+ strick_checking: bool = False,
157
+ ) -> DeviceIdT | None:
158
+ return PortProtocol._extract_known_hgi_id(
159
+ include_list, disable_warnings=disable_warnings, strick_checking=strick_checking
160
+ )
@@ -1,39 +1,44 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
2
  """RAMSES RF - a RAMSES-II protocol decoder & analyser."""
3
+
5
4
  from __future__ import annotations
6
5
 
7
6
  from functools import lru_cache
8
- from typing import Dict
7
+ from typing import TYPE_CHECKING, Final
8
+
9
+ from . import exceptions as exc
10
+ from .const import DEV_TYPE_MAP as _DEV_TYPE_MAP, DEVICE_ID_REGEX, DevType
11
+ from .schemas import DeviceIdT
9
12
 
10
- from .const import DEV_TYPE
11
- from .const import DEV_TYPE_MAP as _DEV_TYPE_MAP
12
- from .const import DEVICE_ID_REGEX, __dev_mode__
13
- from .exceptions import InvalidAddrSetError
14
- from .helpers import typechecked
13
+ if TYPE_CHECKING:
14
+ from .schemas import DeviceIdT
15
15
 
16
- DEV_MODE = __dev_mode__ and False
17
- DEV_HVAC = True
18
16
 
19
- DEVICE_LOOKUP: Dict[str, str] = {
17
+ DEVICE_LOOKUP: dict[str, str] = {
20
18
  k: _DEV_TYPE_MAP._hex(k)
21
19
  for k in _DEV_TYPE_MAP.SLUGS
22
- if k not in (DEV_TYPE.JIM, DEV_TYPE.JST)
20
+ if k not in (DevType.JIM, DevType.JST)
23
21
  }
24
22
  DEVICE_LOOKUP |= {"NUL": "63", "---": "--"}
25
- DEV_TYPE_MAP: Dict[str, str] = {v: k for k, v in DEVICE_LOOKUP.items()}
23
+ DEV_TYPE_MAP: dict[str, str] = {v: k for k, v in DEVICE_LOOKUP.items()}
26
24
 
27
25
 
28
- HGI_DEVICE_ID = "18:000730" # default type and address of HGI, 18:013393
29
- NON_DEVICE_ID = "--:------"
30
- NUL_DEVICE_ID = "63:262142" # FFFFFE - send here if not bound?
26
+ HGI_DEVICE_ID: DeviceIdT = "18:000730" # type: ignore[assignment]
27
+ NON_DEVICE_ID: DeviceIdT = "--:------" # type: ignore[assignment]
28
+ ALL_DEVICE_ID: DeviceIdT = "63:262142" # type: ignore[assignment] # aka 'FFFFFE'
29
+
30
+ #
31
+ # NOTE: All debug flags should be False for deployment to end-users
32
+ _DBG_DISABLE_STRICT_CHECKING: Final[bool] = False # a convenience for the test suite
33
+ _DBG_DISABLE_DEV_HVAC = False
31
34
 
32
35
 
33
36
  class Address:
34
37
  """The device Address class."""
35
38
 
36
- def __init__(self, device_id) -> None:
39
+ _SLUG = None
40
+
41
+ def __init__(self, device_id: DeviceIdT) -> None:
37
42
  """Create an address from a valid device id."""
38
43
 
39
44
  # if device_id is None:
@@ -52,30 +57,29 @@ class Address:
52
57
  def __str__(self) -> str:
53
58
  return self._friendly(self.id).strip()
54
59
 
55
- def __eq__(self, other) -> bool:
60
+ def __eq__(self, other: object) -> bool:
56
61
  if not hasattr(other, "id"): # can compare Address with Device
57
62
  return NotImplemented
58
- return self.id == other.id
63
+ return self.id == other.id # type: ignore[no-any-return]
59
64
 
60
65
  @property
61
66
  def hex_id(self) -> str:
62
67
  if self._hex_id is not None:
63
68
  return self._hex_id
64
- self._hex_id = self.convert_to_hex(self.id)
69
+ self._hex_id = self.convert_to_hex(self.id) # type: ignore[unreachable]
65
70
  return self._hex_id
66
71
 
67
72
  @staticmethod
68
73
  def is_valid(value: str) -> bool: # Union[str, Match[str], None]:
69
-
70
74
  # if value[:2] not in DEV_TYPE_MAP:
71
75
  # return False
72
76
 
73
77
  return isinstance(value, str) and (
74
78
  value == NON_DEVICE_ID or DEVICE_ID_REGEX.ANY.match(value)
75
- ) # type: ignore[return-value]
79
+ )
76
80
 
77
81
  @classmethod
78
- def _friendly(cls, device_id: str) -> str:
82
+ def _friendly(cls, device_id: DeviceIdT) -> str:
79
83
  """Convert (say) '01:145038' to 'CTL:145038'."""
80
84
 
81
85
  if not cls.is_valid(device_id):
@@ -90,18 +94,18 @@ class Address:
90
94
  """Convert (say) '06368E' to '01:145038' (or 'CTL:145038')."""
91
95
 
92
96
  if device_hex == "FFFFFE": # aka '63:262142'
93
- return ">null dev<" if friendly_id else NUL_DEVICE_ID
97
+ return ">null dev<" if friendly_id else ALL_DEVICE_ID
94
98
 
95
99
  if not device_hex.strip(): # aka '--:------'
96
100
  return f"{'':10}" if friendly_id else NON_DEVICE_ID
97
101
 
98
102
  _tmp = int(device_hex, 16)
99
- device_id = f"{(_tmp & 0xFC0000) >> 18:02d}:{_tmp & 0x03FFFF:06d}"
103
+ device_id: DeviceIdT = f"{(_tmp & 0xFC0000) >> 18:02d}:{_tmp & 0x03FFFF:06d}" # type: ignore[assignment]
100
104
 
101
105
  return cls._friendly(device_id) if friendly_id else device_id
102
106
 
103
107
  @classmethod
104
- def convert_to_hex(cls, device_id: str) -> str:
108
+ def convert_to_hex(cls, device_id: DeviceIdT) -> str:
105
109
  """Convert (say) '01:145038' (or 'CTL:145038') to '06368E'."""
106
110
 
107
111
  if not cls.is_valid(device_id):
@@ -116,25 +120,24 @@ class Address:
116
120
  return f"{(int(dev_type) << 18) + int(device_id[-6:]):0>6X}" # no preceding 0x
117
121
 
118
122
  # @classmethod
119
- # def from_hex(cls, hex_id: str):
123
+ # def from_hex(cls, hex_id: DeviceIdT):
120
124
  # """Call as: d = Address.from_hex('06368E')."""
121
125
 
122
126
  # return cls(cls.convert_from_hex(hex_id))
123
127
 
124
128
 
125
129
  @lru_cache(maxsize=256)
126
- def id_to_address(device_id) -> Address:
130
+ def id_to_address(device_id: DeviceIdT) -> Address:
127
131
  """Factory method to cache & return device Address from device ID."""
128
132
  return Address(device_id=device_id)
129
133
 
130
134
 
131
- HGI_DEV_ADDR = Address(HGI_DEVICE_ID)
132
- NON_DEV_ADDR = Address(NON_DEVICE_ID)
133
- NUL_DEV_ADDR = Address(NUL_DEVICE_ID)
135
+ HGI_DEV_ADDR = Address(HGI_DEVICE_ID) # 18:000730
136
+ NON_DEV_ADDR = Address(NON_DEVICE_ID) # --:------
137
+ ALL_DEV_ADDR = Address(ALL_DEVICE_ID) # 63:262142
134
138
 
135
139
 
136
- @typechecked
137
- def dev_id_to_hex_id(device_id: str) -> str:
140
+ def dev_id_to_hex_id(device_id: DeviceIdT) -> str:
138
141
  """Convert (say) '01:145038' (or 'CTL:145038') to '06368E'."""
139
142
 
140
143
  if len(device_id) == 9: # e.g. '01:123456'
@@ -149,14 +152,13 @@ def dev_id_to_hex_id(device_id: str) -> str:
149
152
  return f"{(int(dev_type) << 18) + int(device_id[-6:]):0>6X}"
150
153
 
151
154
 
152
- @typechecked
153
- def hex_id_to_dev_id(device_hex: str, friendly_id: bool = False) -> str:
155
+ def hex_id_to_dev_id(device_hex: str, friendly_id: bool = False) -> DeviceIdT:
154
156
  """Convert (say) '06368E' to '01:145038' (or 'CTL:145038')."""
155
157
  if device_hex == "FFFFFE": # aka '63:262142'
156
- return "NUL:262142" if friendly_id else NUL_DEVICE_ID
158
+ return "NUL:262142" if friendly_id else ALL_DEVICE_ID # type: ignore[return-value]
157
159
 
158
160
  if not device_hex.strip(): # aka '--:------'
159
- return f"{'':10}" if friendly_id else NON_DEVICE_ID
161
+ return f"{'':10}" if friendly_id else NON_DEVICE_ID # type: ignore[return-value]
160
162
 
161
163
  _tmp = int(device_hex, 16)
162
164
  dev_type = f"{(_tmp & 0xFC0000) >> 18:02d}"
@@ -164,65 +166,69 @@ def hex_id_to_dev_id(device_hex: str, friendly_id: bool = False) -> str:
164
166
  if friendly_id:
165
167
  dev_type = DEV_TYPE_MAP.get(dev_type, f"{dev_type:<3}")
166
168
 
167
- return f"{dev_type}:{_tmp & 0x03FFFF:06d}"
169
+ return f"{dev_type}:{_tmp & 0x03FFFF:06d}" # type: ignore[return-value]
168
170
 
169
171
 
170
172
  @lru_cache(maxsize=128)
171
- @typechecked
172
- def is_valid_dev_id(value: str, dev_class: str = None) -> bool:
173
+ def is_valid_dev_id(value: str, dev_class: None | str = None) -> bool:
173
174
  """Return True if a device_id is valid."""
174
175
 
175
176
  if not isinstance(value, str) or not DEVICE_ID_REGEX.ANY.match(value):
176
177
  return False
177
178
 
178
- if not DEV_HVAC and value.split(":", 1)[0] not in DEV_TYPE_MAP:
179
- return False
179
+ return not _DBG_DISABLE_DEV_HVAC or value.split(":", 1)[0] in DEV_TYPE_MAP
180
180
 
181
- # TODO: specify device type (for HVAC)
182
- # elif dev_type is not None and dev_type != value.split(":", maxsplit=1)[0]:
183
- # raise TypeError(f"The device type does not match '{dev_type}'")
181
+ # if _DBG_DISABLE_DEV_HVAC and value.split(":", 1)[0] not in DEV_TYPE_MAP:
182
+ # return False
184
183
 
185
- # assert value == hex_id_to_dev_id(dev_id_to_hex_id(value))
186
- return True
184
+ # # TODO: specify device type (for HVAC)
185
+ # # elif dev_type is not None and dev_type != value.split(":", maxsplit=1)[0]:
186
+ # # raise TypeError(f"The device type does not match '{dev_type}'")
187
+
188
+ # # assert value == hex_id_to_dev_id(dev_id_to_hex_id(value))
189
+ # return True
187
190
 
188
191
 
189
192
  @lru_cache(maxsize=256) # there is definite benefit in caching this
190
- @typechecked
191
- def pkt_addrs(addr_fragment: str) -> tuple[Address, ...]:
193
+ def pkt_addrs(addr_fragment: str) -> tuple[Address, Address, Address, Address, Address]:
192
194
  """Return the address fields from (e.g): '01:078710 --:------ 01:144246'.
193
195
 
196
+ returns: src_addr, dst_addr, addr_0, addr_1, addr_2
197
+
194
198
  Will raise an InvalidAddrSetError is the address fields are not valid.
195
199
  """
196
200
  # for debug: print(pkt_addrs.cache_info())
197
201
 
198
202
  try:
199
- addrs = [id_to_address(addr_fragment[i : i + 9]) for i in range(0, 30, 10)]
200
- except ValueError as exc:
201
- raise InvalidAddrSetError(f"Invalid addr set: {addr_fragment}: {exc}")
203
+ addrs = tuple(id_to_address(addr_fragment[i : i + 9]) for i in range(0, 30, 10))
204
+ except ValueError as err:
205
+ raise exc.PacketAddrSetInvalid(
206
+ f"Invalid address set: {addr_fragment}: {err}"
207
+ ) from None
202
208
 
203
- if (
209
+ if not _DBG_DISABLE_STRICT_CHECKING and (
204
210
  not (
205
211
  # .I --- 01:145038 --:------ 01:145038 1F09 003 FF073F # valid
206
212
  # .I --- 04:108173 --:------ 01:155341 2309 003 0001F4 # valid
207
- addrs[0] not in (NON_DEV_ADDR, NUL_DEV_ADDR)
213
+ addrs[0] not in (NON_DEV_ADDR, ALL_DEV_ADDR)
208
214
  and addrs[1] == NON_DEV_ADDR
209
215
  and addrs[2] != NON_DEV_ADDR
210
216
  )
211
217
  and not (
212
- # .I --- 32:206250 30:082155 --:------ 22F1 003 00020A # valid
218
+ # .I --- 32:206250 30:082155 --:------ 22F1 003 00020A # valid
213
219
  # .I --- 29:151550 29:237552 --:------ 22F3 007 00023C03040000 # valid
214
- addrs[0] not in (NON_DEV_ADDR, NUL_DEV_ADDR)
220
+ addrs[0] not in (NON_DEV_ADDR, ALL_DEV_ADDR)
215
221
  and addrs[1] not in (NON_DEV_ADDR, addrs[0])
216
222
  and addrs[2] == NON_DEV_ADDR
217
223
  )
218
224
  and not (
219
225
  # .I --- --:------ --:------ 10:105624 1FD4 003 00AAD4 # valid
220
- addrs[2] not in (NON_DEV_ADDR, NUL_DEV_ADDR)
226
+ addrs[2] not in (NON_DEV_ADDR, ALL_DEV_ADDR)
221
227
  and addrs[0] == NON_DEV_ADDR
222
228
  and addrs[1] == NON_DEV_ADDR
223
229
  )
224
230
  ):
225
- raise InvalidAddrSetError(f"Invalid addr set: {addr_fragment}")
231
+ raise exc.PacketAddrSetInvalid(f"Invalid address set: {addr_fragment}")
226
232
 
227
233
  device_addrs = list(filter(lambda a: a.type != "--", addrs)) # dex
228
234
  src_addr = device_addrs[0]
@@ -231,4 +237,4 @@ def pkt_addrs(addr_fragment: str) -> tuple[Address, ...]:
231
237
  if src_addr.id == dst_addr.id: # incl. HGI_DEV_ADDR == HGI_DEV_ADDR
232
238
  src_addr = dst_addr
233
239
 
234
- return src_addr, dst_addr, *addrs
240
+ return src_addr, dst_addr, addrs[0], addrs[1], addrs[2]