pycomap 1.0.0__tar.gz
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-1.0.0/PKG-INFO +57 -0
- pycomap-1.0.0/README.md +45 -0
- pycomap-1.0.0/pyproject.toml +73 -0
- pycomap-1.0.0/src/pycomap/__init__.py +56 -0
- pycomap-1.0.0/src/pycomap/alarms.py +100 -0
- pycomap-1.0.0/src/pycomap/configuration.py +546 -0
- pycomap-1.0.0/src/pycomap/controller.py +790 -0
- pycomap-1.0.0/src/pycomap/datatypes.py +202 -0
- pycomap-1.0.0/src/pycomap/discovery.py +195 -0
- pycomap-1.0.0/src/pycomap/exceptions.py +35 -0
- pycomap-1.0.0/src/pycomap/history.py +166 -0
- pycomap-1.0.0/src/pycomap/protocol/__init__.py +24 -0
- pycomap-1.0.0/src/pycomap/protocol/client.py +357 -0
- pycomap-1.0.0/src/pycomap/protocol/commands.py +62 -0
- pycomap-1.0.0/src/pycomap/protocol/crc.py +20 -0
- pycomap-1.0.0/src/pycomap/protocol/crypto.py +90 -0
- pycomap-1.0.0/src/pycomap/protocol/framing.py +145 -0
- pycomap-1.0.0/src/pycomap/protocol/objects.py +98 -0
- pycomap-1.0.0/src/pycomap/protocol/transport.py +89 -0
- pycomap-1.0.0/src/pycomap/py.typed +0 -0
pycomap-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
pycomap-1.0.0/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# pycomap
|
|
2
|
+
|
|
3
|
+
Async Python client for ComAp controllers (InteliLite AMF25 and likely compatible
|
|
4
|
+
siblings): LAN discovery and the native ECDH/AES-encrypted control protocol on port 23.
|
|
5
|
+
|
|
6
|
+
Reverse-engineered from `ComAp.Controller.dll` and cross-validated against live hardware.
|
|
7
|
+
|
|
8
|
+
Requires Python 3.13+. Dependencies: `cryptography`, `pytz`.
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from pycomap import Controller, EthernetTransport
|
|
14
|
+
from pycomap.protocol import ComApClient
|
|
15
|
+
|
|
16
|
+
async with Controller(
|
|
17
|
+
ComApClient(EthernetTransport("192.168.1.9")),
|
|
18
|
+
access_code="0", # factory default (drives ECDH key derivation)
|
|
19
|
+
password=1234, # write-protection password (0-9999); omit for read-only
|
|
20
|
+
) as ctrl:
|
|
21
|
+
values = await ctrl.read_values()
|
|
22
|
+
print(values)
|
|
23
|
+
|
|
24
|
+
await ctrl.set_setpoint("Nominal RPM", 1500)
|
|
25
|
+
await ctrl.set_setpoint("Summer Time Mode", "Winter") # STRING_LIST by label
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
See the [API docs](docs/) for full reference. `just docs-serve` to browse locally.
|
|
29
|
+
|
|
30
|
+
## Development
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
just format # ruff check --fix + ruff format
|
|
34
|
+
just typecheck # ty check
|
|
35
|
+
just unit # tests/unit (no hardware needed)
|
|
36
|
+
just integration # tests/integration (requires .env with PYCOMAP_TEST_HOST)
|
|
37
|
+
just docs-serve # browse API docs locally
|
|
38
|
+
just ai-docs # regenerate llms.txt and CLAUDE.md
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`pre-commit` runs format + typecheck on every commit:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uv run pre-commit install
|
|
45
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pycomap"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Async Python client for ComAp controllers: LAN discovery and the native ECDH/AES-encrypted control protocol"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Igor Panteleyev", email = "panteleev.igor69@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"cryptography>=42",
|
|
13
|
+
"pytz>=2026.2",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"mkdocs>=1.6,<2",
|
|
23
|
+
"mkdocs-material>=9.5",
|
|
24
|
+
"mkdocstrings[python]>=0.25",
|
|
25
|
+
"pre-commit>=4.6.0",
|
|
26
|
+
"pytest>=8",
|
|
27
|
+
"pytest-asyncio>=0.24",
|
|
28
|
+
"pytest-cov>=7.1.0",
|
|
29
|
+
"pytest-env>=1.6.0",
|
|
30
|
+
"pytest-mock>=3.15.1",
|
|
31
|
+
"ruff>=0.15",
|
|
32
|
+
"ty>=0.0.16",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
addopts = "-ra"
|
|
37
|
+
asyncio_mode = "auto"
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
markers = [
|
|
40
|
+
"integration: requires real ComAp hardware reachable on the network (set PYCOMAP_TEST_HOST, see .env.example)",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.pytest_env]
|
|
44
|
+
env_files = [".env"]
|
|
45
|
+
|
|
46
|
+
[tool.coverage.run]
|
|
47
|
+
source = ["src/pycomap"]
|
|
48
|
+
omit = ["tests/*"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 100
|
|
52
|
+
target-version = "py313"
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = [
|
|
56
|
+
"E", # pycodestyle errors
|
|
57
|
+
"F", # pyflakes
|
|
58
|
+
"I", # isort
|
|
59
|
+
"UP", # pyupgrade
|
|
60
|
+
"B", # flake8-bugbear
|
|
61
|
+
"SIM", # flake8-simplify
|
|
62
|
+
"C4", # flake8-comprehensions
|
|
63
|
+
"RUF", # ruff-specific
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.isort]
|
|
67
|
+
known-first-party = ["pycomap"]
|
|
68
|
+
|
|
69
|
+
[tool.ty.environment]
|
|
70
|
+
python-version = "3.13"
|
|
71
|
+
|
|
72
|
+
[tool.uv]
|
|
73
|
+
publish-url = "https://upload.pypi.org/legacy/"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""pycomap — async Python client for ComAp controllers (InteliLite/AMF25 and others).
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from pycomap import Command, Controller, EthernetTransport, discover
|
|
6
|
+
|
|
7
|
+
async with ComApClient(EthernetTransport("192.168.1.9")) as client:
|
|
8
|
+
await client.authenticate("0")
|
|
9
|
+
await client.elevate_access(password)
|
|
10
|
+
result = await client.execute_command(Command.FAULT_RESET)
|
|
11
|
+
|
|
12
|
+
Public API surface:
|
|
13
|
+
|
|
14
|
+
- [ComApClient][pycomap.protocol.ComApClient] — low-level protocol client
|
|
15
|
+
- [Command][pycomap.protocol.Command] — named controller commands
|
|
16
|
+
- [EthernetTransport][pycomap.protocol.EthernetTransport] /
|
|
17
|
+
[Transport][pycomap.protocol.transport.Transport]
|
|
18
|
+
- [discover][pycomap.discovery.discover] — UDP controller discovery
|
|
19
|
+
- [pycomap.configuration][] — `ConfigurationTable` parsing and value/setpoint decode
|
|
20
|
+
- [pycomap.alarms][] — alarm list parsing
|
|
21
|
+
- [pycomap.history][] — history record parsing
|
|
22
|
+
- [pycomap.datatypes][] — wire types and BCD codec
|
|
23
|
+
|
|
24
|
+
See ``docs/protocol.md`` in the project repository for the full reverse-engineering notes.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
|
|
31
|
+
from pycomap.controller import Controller
|
|
32
|
+
from pycomap.discovery import discover
|
|
33
|
+
from pycomap.exceptions import (
|
|
34
|
+
ComApAuthError,
|
|
35
|
+
ComApConnectionError,
|
|
36
|
+
ComApControllerError,
|
|
37
|
+
ComApError,
|
|
38
|
+
ComApProtocolError,
|
|
39
|
+
)
|
|
40
|
+
from pycomap.protocol import ComApClient, Command, ControllerCommand, EthernetTransport
|
|
41
|
+
|
|
42
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"ComApAuthError",
|
|
46
|
+
"ComApClient",
|
|
47
|
+
"ComApConnectionError",
|
|
48
|
+
"ComApControllerError",
|
|
49
|
+
"ComApError",
|
|
50
|
+
"ComApProtocolError",
|
|
51
|
+
"Command",
|
|
52
|
+
"Controller",
|
|
53
|
+
"ControllerCommand",
|
|
54
|
+
"EthernetTransport",
|
|
55
|
+
"discover",
|
|
56
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Alarm list parsing for IL3 controllers.
|
|
2
|
+
|
|
3
|
+
Source: ``AlarmListRecord.LoadIL3`` + ``LoadCommonPart`` in ``ComAp.Controller.dll``.
|
|
4
|
+
See ``docs/protocol.md`` for the full binary format notes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import struct
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from pycomap.configuration import NamesCategory, parse_names_heap
|
|
13
|
+
from pycomap.datatypes import get_bits
|
|
14
|
+
|
|
15
|
+
# IL3 alarm list: 112 bytes = up to 16 x 7-byte records.
|
|
16
|
+
# Record: uint32 dw + uint16 flags + uint8 source.
|
|
17
|
+
# IsUsed: bit 31 of dw must be 1 AND not all fields 0xFF.
|
|
18
|
+
# DiagnosticCodeType.ComAp = 7 (bits 0-2 of flags).
|
|
19
|
+
|
|
20
|
+
_IL3_ALARM_RECORD_SIZE = 7
|
|
21
|
+
_DIAGNOSTIC_CODE_TYPE_COMAP = 7
|
|
22
|
+
_ALARM_RECORD_STRUCT = struct.Struct("<IHB") # uint32 dw, uint16 flags, uint8 source
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(slots=True, frozen=True)
|
|
26
|
+
class AlarmRecord:
|
|
27
|
+
"""One entry from the controller's alarm list (``CommunicationObject.ALARM_LIST``,
|
|
28
|
+
C.O. 24545).
|
|
29
|
+
|
|
30
|
+
Source: ``AlarmListRecord.LoadIL3`` + ``LoadCommonPart`` in ``ComAp.Controller.dll``.
|
|
31
|
+
The IL3 format is 112 bytes = up to 16 x 7-byte records; records stop at the first
|
|
32
|
+
unused slot (all fields at max value) or end of buffer.
|
|
33
|
+
|
|
34
|
+
``reason`` is resolved from ``AlarmReasonNames`` and matches the value's display name
|
|
35
|
+
(e.g. ``"Generator Voltage L1-N"``). ``prefix`` comes from ``HistoryPrefixNames`` and
|
|
36
|
+
encodes the protection type (e.g. ``"Wrn"``, ``"Sd"``).
|
|
37
|
+
|
|
38
|
+
For non-ComAp diagnostic codes (ECU alarms from CAN bus), ``reason`` and ``prefix``
|
|
39
|
+
will be empty strings and ``fault_code`` carries the raw ECU fault code.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
is_active: bool
|
|
43
|
+
is_confirmed: bool
|
|
44
|
+
reason: str
|
|
45
|
+
prefix: str
|
|
46
|
+
occurred: int
|
|
47
|
+
source: int
|
|
48
|
+
fault_code: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_alarm_list(config_data: bytes, alarm_data: bytes) -> list[AlarmRecord]:
|
|
52
|
+
"""Parse the controller's ``ALARM_LIST`` blob (C.O. 24545) into a list of
|
|
53
|
+
``AlarmRecord`` objects, in list order (most recent first per controller behaviour).
|
|
54
|
+
|
|
55
|
+
Only active (``IsUsed``) slots are returned; the list will be empty if there are no
|
|
56
|
+
current alarms. Pass the full raw ``ConfigurationTable`` blob as ``config_data`` —
|
|
57
|
+
it is used to resolve the ``AlarmReasonNames`` and ``HistoryPrefixNames`` heaps.
|
|
58
|
+
"""
|
|
59
|
+
reason_names = parse_names_heap(config_data, NamesCategory.ALARM_REASON_NAMES)
|
|
60
|
+
prefix_names = parse_names_heap(config_data, NamesCategory.HISTORY_PREFIX_NAMES)
|
|
61
|
+
|
|
62
|
+
records: list[AlarmRecord] = []
|
|
63
|
+
for offset in range(0, len(alarm_data) - _IL3_ALARM_RECORD_SIZE + 1, _IL3_ALARM_RECORD_SIZE):
|
|
64
|
+
dw, flags, source = _ALARM_RECORD_STRUCT.unpack_from(alarm_data, offset)
|
|
65
|
+
|
|
66
|
+
is_used = bool(get_bits(dw, 31, 1)) and not (
|
|
67
|
+
dw == 0xFFFFFFFF and flags == 0xFFFF and source == 0xFF
|
|
68
|
+
)
|
|
69
|
+
if not is_used:
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
kind = get_bits(flags, 0, 3)
|
|
73
|
+
reason_index = get_bits(flags, 3, 11)
|
|
74
|
+
is_active = bool(get_bits(flags, 14, 1))
|
|
75
|
+
is_confirmed = bool(get_bits(flags, 15, 1))
|
|
76
|
+
|
|
77
|
+
fault_code = get_bits(dw, 0, 19)
|
|
78
|
+
prefix_index = get_bits(dw, 19, 5)
|
|
79
|
+
occurred = get_bits(dw, 24, 7)
|
|
80
|
+
|
|
81
|
+
if kind == _DIAGNOSTIC_CODE_TYPE_COMAP:
|
|
82
|
+
reason = reason_names[reason_index] if reason_index < len(reason_names) else ""
|
|
83
|
+
prefix = prefix_names[prefix_index] if prefix_index < len(prefix_names) else ""
|
|
84
|
+
else:
|
|
85
|
+
reason = ""
|
|
86
|
+
prefix = ""
|
|
87
|
+
|
|
88
|
+
records.append(
|
|
89
|
+
AlarmRecord(
|
|
90
|
+
is_active=is_active,
|
|
91
|
+
is_confirmed=is_confirmed,
|
|
92
|
+
reason=reason,
|
|
93
|
+
prefix=prefix,
|
|
94
|
+
occurred=occurred,
|
|
95
|
+
source=source,
|
|
96
|
+
fault_code=fault_code,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return records
|