ramses-rf 0.22.40__py3-none-any.whl → 0.51.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ramses_cli/__init__.py +18 -0
- ramses_cli/client.py +597 -0
- ramses_cli/debug.py +20 -0
- ramses_cli/discovery.py +405 -0
- ramses_cli/utils/cat_slow.py +17 -0
- ramses_cli/utils/convert.py +60 -0
- ramses_rf/__init__.py +31 -10
- ramses_rf/binding_fsm.py +787 -0
- ramses_rf/const.py +124 -105
- ramses_rf/database.py +297 -0
- ramses_rf/device/__init__.py +69 -39
- ramses_rf/device/base.py +187 -376
- ramses_rf/device/heat.py +540 -552
- ramses_rf/device/hvac.py +279 -171
- ramses_rf/dispatcher.py +153 -177
- ramses_rf/entity_base.py +478 -361
- ramses_rf/exceptions.py +82 -0
- ramses_rf/gateway.py +377 -513
- ramses_rf/helpers.py +57 -19
- ramses_rf/py.typed +0 -0
- ramses_rf/schemas.py +148 -194
- ramses_rf/system/__init__.py +16 -23
- ramses_rf/system/faultlog.py +363 -0
- ramses_rf/system/heat.py +295 -302
- ramses_rf/system/schedule.py +312 -198
- ramses_rf/system/zones.py +318 -238
- ramses_rf/version.py +2 -8
- ramses_rf-0.51.2.dist-info/METADATA +72 -0
- ramses_rf-0.51.2.dist-info/RECORD +55 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info}/WHEEL +1 -2
- ramses_rf-0.51.2.dist-info/entry_points.txt +2 -0
- {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info/licenses}/LICENSE +1 -1
- ramses_tx/__init__.py +160 -0
- {ramses_rf/protocol → ramses_tx}/address.py +65 -59
- ramses_tx/command.py +1454 -0
- ramses_tx/const.py +903 -0
- ramses_tx/exceptions.py +92 -0
- {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
- {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
- ramses_tx/gateway.py +338 -0
- ramses_tx/helpers.py +883 -0
- {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
- {ramses_rf/protocol → ramses_tx}/message.py +155 -191
- ramses_tx/opentherm.py +1260 -0
- ramses_tx/packet.py +210 -0
- {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
- ramses_tx/protocol.py +801 -0
- ramses_tx/protocol_fsm.py +672 -0
- ramses_tx/py.typed +0 -0
- {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
- {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
- ramses_tx/transport.py +1471 -0
- ramses_tx/typed_dicts.py +492 -0
- ramses_tx/typing.py +181 -0
- ramses_tx/version.py +4 -0
- ramses_rf/discovery.py +0 -398
- ramses_rf/protocol/__init__.py +0 -59
- ramses_rf/protocol/backports.py +0 -42
- ramses_rf/protocol/command.py +0 -1576
- ramses_rf/protocol/const.py +0 -697
- ramses_rf/protocol/exceptions.py +0 -111
- ramses_rf/protocol/helpers.py +0 -390
- ramses_rf/protocol/opentherm.py +0 -1170
- ramses_rf/protocol/packet.py +0 -235
- ramses_rf/protocol/protocol.py +0 -613
- ramses_rf/protocol/transport.py +0 -1011
- ramses_rf/protocol/version.py +0 -10
- ramses_rf/system/hvac.py +0 -82
- ramses_rf-0.22.40.dist-info/METADATA +0 -64
- ramses_rf-0.22.40.dist-info/RECORD +0 -42
- ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
ramses_rf/version.py
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
__version__ = "0.22.40"
|
|
3
|
+
__version__ = "0.51.2"
|
|
10
4
|
VERSION = __version__
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ramses_rf
|
|
3
|
+
Version: 0.51.2
|
|
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
|
+

|
|
23
|
+

|
|
24
|
+

|
|
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=kfKNt7MEhCXc_zJbeZAfydNUVN8mvGKtTksdzTdVqaE,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=zs_P-3QwMi6Qi81lR1KIaoUqqdS1-UdMFWGXwGtkgtg,23438
|
|
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=jxe-qug9YpQ_VntHrnzpUANyyfPmcBb0dH8rlX5Bl0Y,123
|
|
51
|
+
ramses_rf-0.51.2.dist-info/METADATA,sha256=JXBcobOqMtmAPEMqnSZRONz6BEpU2jRr-r93VenhSBM,3906
|
|
52
|
+
ramses_rf-0.51.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
53
|
+
ramses_rf-0.51.2.dist-info/entry_points.txt,sha256=NnyK29baOCNg8DinPYiZ368h7MTH7bgTW26z2A1NeIE,50
|
|
54
|
+
ramses_rf-0.51.2.dist-info/licenses/LICENSE,sha256=-Kc35W7l1UkdiQ4314_yVWv7vDDrg7IrJfMLUiq6Nfs,1074
|
|
55
|
+
ramses_rf-0.51.2.dist-info/RECORD,,
|
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
|
|
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
|
-
|
|
11
|
-
from .
|
|
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:
|
|
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 (
|
|
20
|
+
if k not in (DevType.JIM, DevType.JST)
|
|
23
21
|
}
|
|
24
22
|
DEVICE_LOOKUP |= {"NUL": "63", "---": "--"}
|
|
25
|
-
DEV_TYPE_MAP:
|
|
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" #
|
|
29
|
-
NON_DEVICE_ID = "--:------"
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
79
|
+
)
|
|
76
80
|
|
|
77
81
|
@classmethod
|
|
78
|
-
def _friendly(cls, device_id:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
return False
|
|
179
|
+
return not _DBG_DISABLE_DEV_HVAC or value.split(":", 1)[0] in DEV_TYPE_MAP
|
|
180
180
|
|
|
181
|
-
#
|
|
182
|
-
#
|
|
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
|
-
#
|
|
186
|
-
|
|
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
|
-
|
|
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 =
|
|
200
|
-
except ValueError as
|
|
201
|
-
raise
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
240
|
+
return src_addr, dst_addr, addrs[0], addrs[1], addrs[2]
|