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 ADDED
@@ -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
+ ]
pycomap/alarms.py ADDED
@@ -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