pycomap 1.0.0__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.
- pycomap/__init__.py +56 -0
- pycomap/alarms.py +100 -0
- pycomap/configuration.py +546 -0
- pycomap/controller.py +790 -0
- pycomap/datatypes.py +202 -0
- pycomap/discovery.py +195 -0
- pycomap/exceptions.py +35 -0
- pycomap/history.py +166 -0
- pycomap/protocol/__init__.py +24 -0
- pycomap/protocol/client.py +357 -0
- pycomap/protocol/commands.py +62 -0
- pycomap/protocol/crc.py +20 -0
- pycomap/protocol/crypto.py +90 -0
- pycomap/protocol/framing.py +145 -0
- pycomap/protocol/objects.py +98 -0
- pycomap/protocol/transport.py +89 -0
- pycomap/py.typed +0 -0
- pycomap-1.0.0.dist-info/METADATA +57 -0
- pycomap-1.0.0.dist-info/RECORD +20 -0
- pycomap-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Known ``CommunicationObject`` IDs and ``ControllerError`` codes.
|
|
2
|
+
|
|
3
|
+
Source: decompiled ``ComAp.Controller.dll`` (class ``CommunicationObject`` — a plain class
|
|
4
|
+
with one ``static readonly`` field per object, not a C# ``enum``) and decompiled
|
|
5
|
+
``ComAp.GlobalShared.dll`` (``enum ControllerError : uint``). See ``docs/protocol.md``
|
|
6
|
+
sections 2.5/2.6 for the full table and how to extend it.
|
|
7
|
+
|
|
8
|
+
This is a partial list — only what's been confirmed useful so far. Re-decompile the DLLs
|
|
9
|
+
with ``ilspycmd`` to extend it.
|
|
10
|
+
|
|
11
|
+
Controller commands (``ControllerCommand``, ``Command``) live in
|
|
12
|
+
[pycomap.protocol.commands][].
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import enum
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CommunicationObject(enum.IntEnum):
|
|
21
|
+
"""Known communication object IDs."""
|
|
22
|
+
|
|
23
|
+
VERSION_IB = 24533
|
|
24
|
+
DISCOVERY = 24237
|
|
25
|
+
ECDH_PUBLIC_KEY = 24119
|
|
26
|
+
VERIFY_ACCESS_HASH = 24324
|
|
27
|
+
VERIFY_ACCESS = 24534
|
|
28
|
+
MAX_MESSAGE_DATA_LENGTHS = 24269
|
|
29
|
+
COMAP_PROTOCOL_FEATURES = 24023
|
|
30
|
+
|
|
31
|
+
VALUES_ALL = 24560
|
|
32
|
+
VALUE_STATES_ALL = 24555
|
|
33
|
+
VALUE_STATES_AND_DATA_ALL = 24529
|
|
34
|
+
VALUES_CATEGORY_I = 24563
|
|
35
|
+
VALUES_CATEGORY_II = 24562
|
|
36
|
+
VALUES_CATEGORY_III = 24561
|
|
37
|
+
VALUE_STATES_CATEGORY_I = 24558
|
|
38
|
+
VALUE_STATES_CATEGORY_II = 24557
|
|
39
|
+
VALUE_STATES_CATEGORY_III = 24556
|
|
40
|
+
|
|
41
|
+
SETPOINTS_ALL = 24559
|
|
42
|
+
SETPOINTS_R = 24543
|
|
43
|
+
SETPOINTS_P = 24544
|
|
44
|
+
|
|
45
|
+
ALARM_LIST = 24545
|
|
46
|
+
ALARM_LIST_WITH_VERSION = 24024
|
|
47
|
+
|
|
48
|
+
CONFIGURATION_TABLE = 24575
|
|
49
|
+
TERMINAL_CONFIGURATION_TABLE = 24574
|
|
50
|
+
CONFIGURATION_TABLE_CRC16 = 24573
|
|
51
|
+
TERMINAL_CONFIGURATION_TABLE_CRC16 = 24572
|
|
52
|
+
|
|
53
|
+
SERIAL_NUMBER = 24548
|
|
54
|
+
FIRMWARE_VERSION_TEXT = 24339
|
|
55
|
+
BOOTLOADER_FIRMWARE_VERSION = 24277
|
|
56
|
+
CONTROLLER_FIRMWARE_IDENTIFICATION = 24115
|
|
57
|
+
|
|
58
|
+
COMMAND = 24551
|
|
59
|
+
COMMAND_WITH_ARGUMENT = 23859
|
|
60
|
+
COMMAND_ARGUMENT = 24550
|
|
61
|
+
|
|
62
|
+
CONTROLLER_STATE = 24496
|
|
63
|
+
|
|
64
|
+
HISTORY_LENGTH = 24538
|
|
65
|
+
MAX_HISTORY_RECORDS = 24564
|
|
66
|
+
READ_INDEX_IN_HISTORY = 24565
|
|
67
|
+
WRITE_INDEX_IN_HISTORY = 24566
|
|
68
|
+
OLDER_HISTORY_RECORD = 24567
|
|
69
|
+
YOUNGER_HISTORY_RECORD = 24568
|
|
70
|
+
YOUNGEST_HISTORY_RECORD = 24569
|
|
71
|
+
NEW_HISTORY_RECORDS = 24570
|
|
72
|
+
|
|
73
|
+
COMMUNICATION_STATE = 24571
|
|
74
|
+
CONTROLLER_ADDRESS = 24537
|
|
75
|
+
DISPLAY_CONTRAST = 24547
|
|
76
|
+
GSM_PIN = 24536
|
|
77
|
+
PASSWORD_FOR_WRITE = 24524
|
|
78
|
+
PASSWORD_FOR_WRITE_HASH = 24286
|
|
79
|
+
PASSWORD_DECODE = 24202
|
|
80
|
+
TIME_UNTIL_PASSWORD_ENTERING_UNBLOCKS = 24109
|
|
81
|
+
CHANGE_ACCESS = 24535
|
|
82
|
+
SYSTEM_TIME = 24552
|
|
83
|
+
DATE = 24553 # setpoint FDATE, access_level=1 (requires elevate_access to write)
|
|
84
|
+
TIME = 24554 # setpoint FTIME, access_level=1 (requires elevate_access to write)
|
|
85
|
+
TIME_ZONE = 24366 # setpoint STRING_LIST, access_level=0
|
|
86
|
+
INTELI_MAINS = 24528
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ControllerError(enum.IntEnum):
|
|
90
|
+
"""Known ``ControllerError`` values (``uint32`` on the wire)."""
|
|
91
|
+
|
|
92
|
+
OK = 0
|
|
93
|
+
NO_ANSWER = 1
|
|
94
|
+
ANSWER_POSTPONED = 2
|
|
95
|
+
NON_EXISTING_COMMUNICATION_OBJECT = 100_794_368
|
|
96
|
+
TERMINAL_ACCESS_DISABLED = 134_217_960
|
|
97
|
+
BAD_WRITE_VALUE = 134_217_955
|
|
98
|
+
INVALID_PASSWORD = 134_217_978
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Transport layer abstraction for the ComAp native protocol.
|
|
2
|
+
|
|
3
|
+
``Transport`` is a structural ``typing.Protocol`` — any object with the four async
|
|
4
|
+
methods qualifies, no subclassing required. ``EthernetTransport`` is the only
|
|
5
|
+
implementation for now; AirGate (cloud relay) and serial transports can be added later
|
|
6
|
+
without changing ``ComApClient``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import contextlib
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
from pycomap.exceptions import ComApConnectionError
|
|
17
|
+
|
|
18
|
+
DEFAULT_PORT = 23
|
|
19
|
+
|
|
20
|
+
_log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class Transport(Protocol):
|
|
25
|
+
"""Byte-stream transport used by ``ComApClient``."""
|
|
26
|
+
|
|
27
|
+
async def connect(self) -> None:
|
|
28
|
+
"""Open the underlying connection."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
async def close(self) -> None:
|
|
32
|
+
"""Close the underlying connection, suppressing already-closed errors."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
async def read_exactly(self, n: int) -> bytes:
|
|
36
|
+
"""Read exactly ``n`` bytes, raising ``ComApConnectionError`` if the stream ends."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
async def write(self, data: bytes) -> None:
|
|
40
|
+
"""Write ``data`` and flush."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EthernetTransport:
|
|
45
|
+
"""TCP transport for the ComAp native protocol (port 23).
|
|
46
|
+
|
|
47
|
+
Connects to ``host:port`` via plain TCP; the ``ComApClient`` layer adds the
|
|
48
|
+
ECDH/AES framing on top.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, host: str, port: int = DEFAULT_PORT) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Args:
|
|
54
|
+
host: Controller IP address or hostname.
|
|
55
|
+
port: TCP port; defaults to ``23`` (ComAp native protocol port).
|
|
56
|
+
"""
|
|
57
|
+
self._host = host
|
|
58
|
+
self._port = port
|
|
59
|
+
self._reader: asyncio.StreamReader | None = None
|
|
60
|
+
self._writer: asyncio.StreamWriter | None = None
|
|
61
|
+
|
|
62
|
+
async def connect(self) -> None:
|
|
63
|
+
_log.debug("connecting to %s:%d", self._host, self._port)
|
|
64
|
+
try:
|
|
65
|
+
self._reader, self._writer = await asyncio.open_connection(self._host, self._port)
|
|
66
|
+
except OSError as exc:
|
|
67
|
+
raise ComApConnectionError(f"failed to connect to {self._host}:{self._port}") from exc
|
|
68
|
+
_log.info("connected to %s:%d", self._host, self._port)
|
|
69
|
+
|
|
70
|
+
async def close(self) -> None:
|
|
71
|
+
if self._writer is not None:
|
|
72
|
+
self._writer.close()
|
|
73
|
+
with contextlib.suppress(OSError):
|
|
74
|
+
await self._writer.wait_closed()
|
|
75
|
+
_log.info("disconnected from %s:%d", self._host, self._port)
|
|
76
|
+
self._reader = None
|
|
77
|
+
self._writer = None
|
|
78
|
+
|
|
79
|
+
async def read_exactly(self, n: int) -> bytes:
|
|
80
|
+
assert self._reader is not None
|
|
81
|
+
try:
|
|
82
|
+
return await self._reader.readexactly(n)
|
|
83
|
+
except asyncio.IncompleteReadError as exc:
|
|
84
|
+
raise ComApConnectionError("connection closed while reading a message") from exc
|
|
85
|
+
|
|
86
|
+
async def write(self, data: bytes) -> None:
|
|
87
|
+
assert self._writer is not None
|
|
88
|
+
self._writer.write(data)
|
|
89
|
+
await self._writer.drain()
|
pycomap/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycomap
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Async Python client for ComAp controllers: LAN discovery and the native ECDH/AES-encrypted control protocol
|
|
5
|
+
Author: Igor Panteleyev
|
|
6
|
+
Author-email: Igor Panteleyev <panteleev.igor69@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: cryptography>=42
|
|
9
|
+
Requires-Dist: pytz>=2026.2
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# pycomap
|
|
14
|
+
|
|
15
|
+
Async Python client for ComAp controllers (InteliLite AMF25 and likely compatible
|
|
16
|
+
siblings): LAN discovery and the native ECDH/AES-encrypted control protocol on port 23.
|
|
17
|
+
|
|
18
|
+
Reverse-engineered from `ComAp.Controller.dll` and cross-validated against live hardware.
|
|
19
|
+
|
|
20
|
+
Requires Python 3.13+. Dependencies: `cryptography`, `pytz`.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pycomap import Controller, EthernetTransport
|
|
26
|
+
from pycomap.protocol import ComApClient
|
|
27
|
+
|
|
28
|
+
async with Controller(
|
|
29
|
+
ComApClient(EthernetTransport("192.168.1.9")),
|
|
30
|
+
access_code="0", # factory default (drives ECDH key derivation)
|
|
31
|
+
password=1234, # write-protection password (0-9999); omit for read-only
|
|
32
|
+
) as ctrl:
|
|
33
|
+
values = await ctrl.read_values()
|
|
34
|
+
print(values)
|
|
35
|
+
|
|
36
|
+
await ctrl.set_setpoint("Nominal RPM", 1500)
|
|
37
|
+
await ctrl.set_setpoint("Summer Time Mode", "Winter") # STRING_LIST by label
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
See the [API docs](docs/) for full reference. `just docs-serve` to browse locally.
|
|
41
|
+
|
|
42
|
+
## Development
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
just format # ruff check --fix + ruff format
|
|
46
|
+
just typecheck # ty check
|
|
47
|
+
just unit # tests/unit (no hardware needed)
|
|
48
|
+
just integration # tests/integration (requires .env with PYCOMAP_TEST_HOST)
|
|
49
|
+
just docs-serve # browse API docs locally
|
|
50
|
+
just ai-docs # regenerate llms.txt and CLAUDE.md
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`pre-commit` runs format + typecheck on every commit:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
uv run pre-commit install
|
|
57
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
pycomap/__init__.py,sha256=eXgucOTO_NZvdfAeBUgOIaCY43K6Pqc0V8hxQbEpTA4,1766
|
|
2
|
+
pycomap/alarms.py,sha256=2Yzn8DTfWTtUjuYd3Svfq8h6zyNSZ50haTpQPSS9RH4,3720
|
|
3
|
+
pycomap/configuration.py,sha256=fmdXAw0Xs0jhRxSKGb9ZTt85zUoHyODK1_yVduUxa8k,22009
|
|
4
|
+
pycomap/controller.py,sha256=HCpbxx0aQ01BE21kJna4pDHSfKyc7rA3sEEVacuQn4Y,32734
|
|
5
|
+
pycomap/datatypes.py,sha256=dNLol4tnOxDohraDsm3z9A27PMj_VQrO0e2e6Mm6G1w,6811
|
|
6
|
+
pycomap/discovery.py,sha256=i0PzsQm-wXh7fP_XOumi_L-o8pog5GT0i_QWrskg1yA,6313
|
|
7
|
+
pycomap/exceptions.py,sha256=Zn8Rq4B94L6yFZjaXkgAewNp-NQ5xPXD82uRIBeklIM,1171
|
|
8
|
+
pycomap/history.py,sha256=2AKctZRdRz7B2oPVqHG63UpbpvptW1Azzf_4HTUWNhQ,5904
|
|
9
|
+
pycomap/protocol/__init__.py,sha256=FJOnzdjrrQff7s6CxOiIRtAH7B3CmXnvvT6L4ysqlNQ,714
|
|
10
|
+
pycomap/protocol/client.py,sha256=hE4t9z8PDWH_zX4v0zMs4hCsWZrYniwimVOnOqmo_no,14919
|
|
11
|
+
pycomap/protocol/commands.py,sha256=F_DhytdsFLqmA0y-WcanC-5LRcJBaYVVj-EggGZLe3U,2572
|
|
12
|
+
pycomap/protocol/crc.py,sha256=5J4gQ7PCSc247KxK4RwYm4EZOIKn2p2L-BGLxlq5xAA,584
|
|
13
|
+
pycomap/protocol/crypto.py,sha256=v2ywWRzezqTUXsGvaIdLr-CRsc1Sf010zV5IupxfV5E,3947
|
|
14
|
+
pycomap/protocol/framing.py,sha256=Hsws2G8af9ect2OFksBqc8PbQcrJvPJD_77fiwKg-BU,4541
|
|
15
|
+
pycomap/protocol/objects.py,sha256=wtf8x5FdqyplFRAljg1HCpLfzF-nsWGDIQi4Ew7U72k,2961
|
|
16
|
+
pycomap/protocol/transport.py,sha256=jI5cxeAXP9gdzAsD-Jv-25AciJNKgbd5y5FnjbGXuno,2987
|
|
17
|
+
pycomap/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
pycomap-1.0.0.dist-info/WHEEL,sha256=s49dN1sxqzkgPplo4QuUaKomil-_cbDzeLK4-pZKD-A,81
|
|
19
|
+
pycomap-1.0.0.dist-info/METADATA,sha256=XlA1Sz4jBXFfAsuggtBgFHuEOrD90HCl1rF1tQipfJI,1790
|
|
20
|
+
pycomap-1.0.0.dist-info/RECORD,,
|