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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any