luxmodbus 0.1.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.
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.3
2
+ Name: luxmodbus
3
+ Version: 0.1.0
4
+ Summary: Framing, register map, and discovery for the LuxPower inverter Modbus protocol (HA-free).
5
+ Keywords: luxpower,modbus,inverter,solar,home-assistant,lumen
6
+ Author: Steven Marks
7
+ Author-email: Steven Marks <marksie1988@users.noreply.github.com>
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Natural Language :: English
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Home Automation
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.12
21
+ Project-URL: homepage, https://github.com/totaldebug/luxmodbus
22
+ Project-URL: repository, https://github.com/totaldebug/luxmodbus
23
+ Description-Content-Type: text/markdown
24
+
25
+ <a name="readme-top"></a>
26
+
27
+ [![Release][release-shield]][release-url]
28
+ [![Stargazers][stars-shield]][stars-url]
29
+ ![codecov][codecov-shield]
30
+
31
+ [![Contributors][contributors-shield]][contributors-url]
32
+ [![Forks][forks-shield]][forks-url]
33
+ [![Issues][issues-shield]][issues-url]
34
+
35
+ [![MIT License][license-shield]][license-url]
36
+
37
+ <!-- PROJECT HEADER -->
38
+ <br />
39
+ <div align="center">
40
+ <a href="https://github.com/totaldebug/luxmodbus">
41
+ <h3 align="center">luxmodbus</h3>
42
+ </a>
43
+
44
+ <p align="center">
45
+ Framing, register map, and discovery for the LuxPower inverter Modbus protocol.
46
+ </p>
47
+ <br />
48
+ <a href="https://github.com/totaldebug/luxmodbus/issues/new?labels=type%2Fbug&template=bug_report.yml">Report Bug</a>
49
+ ·
50
+ <a href="https://github.com/totaldebug/luxmodbus/issues/new?labels=type%2Ffeature&template=feature_request.yml">Request Feature</a>
51
+ </div>
52
+
53
+ <!-- TABLE OF CONTENTS -->
54
+ <details>
55
+ <summary>Table of Contents</summary>
56
+ <ol>
57
+ <li><a href="#about-the-project">About The Project</a></li>
58
+ <li><a href="#getting-started">Getting Started</a></li>
59
+ <li><a href="#usage">Usage</a></li>
60
+ <li><a href="#the-frame">The Frame</a></li>
61
+ <li><a href="#provenance">Provenance</a></li>
62
+ <li><a href="#contributing">Contributing</a></li>
63
+ <li><a href="#license">License</a></li>
64
+ </ol>
65
+ </details>
66
+
67
+ ## About The Project
68
+
69
+ `luxmodbus` is a small, dependency-free Python library for the **LuxPower
70
+ inverter Modbus protocol** — packet framing, the declarative register map, and
71
+ register discovery. It has **no Home Assistant dependency** and is the protocol
72
+ core consumed by the [Lumen](https://github.com/totaldebug/lumen) Home Assistant
73
+ integration. Because it imports nothing from Home Assistant, it can be tested
74
+ entirely offline against captured packet bytes.
75
+
76
+ ### Status
77
+
78
+ Early. Implemented so far:
79
+
80
+ - `protocol.py` — frame encode/decode (LuxPower TCP envelope + inner Modbus RTU
81
+ data frame), read/write request builders, and read-response unpacking. No I/O,
82
+ no register-meaning knowledge.
83
+ - `registers.py` — declarative address → meaning map (the single source of
84
+ truth) with a decode engine and a bounds-checked `encode_value` for writes.
85
+ - `discovery.py` — passive diff-and-log engine: compares observed registers
86
+ against the known map and records the unknown with a rolling value history.
87
+
88
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
89
+
90
+ ## Getting Started
91
+
92
+ This project uses [uv](https://docs.astral.sh/uv/).
93
+
94
+ ```bash
95
+ git clone https://github.com/totaldebug/luxmodbus.git
96
+ cd luxmodbus
97
+ uv sync
98
+ uv run nox -s tests
99
+ ```
100
+
101
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
102
+
103
+ ## Usage
104
+
105
+ ```python
106
+ from luxmodbus import Frame, decode_inputs
107
+
108
+ frame = Frame.decode(raw_bytes) # validates prefix + CRC
109
+ data = frame.data_frame() # inner Modbus frame
110
+ # raw register values -> {key: scaled value}
111
+ values = decode_inputs({1: 2503, 4: 530, 5: (90 << 8) | 88})
112
+ # {"pv1_voltage": 250.3, "battery_voltage": 53.0, "soc": 88, "soh": 90}
113
+ ```
114
+
115
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
116
+
117
+ ## The Frame
118
+
119
+ LuxPower wraps a modified Modbus RTU "data frame" inside a TCP envelope:
120
+
121
+ ```
122
+ prefix(2)=A1 1A | protocol(u16 LE) | frame_length(u16 LE) | reserved(1)=01 |
123
+ tcp_function(1) | dongle_serial(10) | data_length(u16 LE) | data_frame(N) | crc16(u16 LE)
124
+ ```
125
+
126
+ - `frame_length = total_len - 6`
127
+ - `data_length = len(data_frame) + 2` (the trailing CRC)
128
+ - `crc16` is the standard Modbus CRC (poly `0xA001`, init `0xFFFF`) over the
129
+ data frame, appended little-endian.
130
+
131
+ The inner data frame:
132
+
133
+ ```
134
+ action(1) | device_function(1) | inverter_serial(10) | register(u16 LE) | value
135
+ ```
136
+
137
+ See [`docs/capturing-packets.md`](docs/capturing-packets.md) for how to capture
138
+ real packets and turn them into test fixtures.
139
+
140
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
141
+
142
+ ## Provenance
143
+
144
+ Clean-room: the protocol *facts* (field layout, CRC algorithm, function codes,
145
+ register meanings) are taken from the official Lux Power Modbus RTU
146
+ specification and validated against real packet bytes. No code is copied from
147
+ other implementations.
148
+
149
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
150
+
151
+ ## Contributing
152
+
153
+ Contributions are welcome. Please open an issue first to discuss changes, then
154
+ ensure `uv run nox -s tests` passes (style, types, docstring coverage, and the
155
+ test suite) before opening a PR.
156
+
157
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
158
+
159
+ ## License
160
+
161
+ Distributed under the MIT License. See `LICENSE` for more information.
162
+
163
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
164
+
165
+ <!-- MARKDOWN LINKS & IMAGES -->
166
+ [release-shield]: https://img.shields.io/github/v/release/totaldebug/luxmodbus?style=for-the-badge
167
+ [release-url]: https://github.com/totaldebug/luxmodbus/releases
168
+ [stars-shield]: https://img.shields.io/github/stars/totaldebug/luxmodbus.svg?style=for-the-badge
169
+ [stars-url]: https://github.com/totaldebug/luxmodbus/stargazers
170
+ [codecov-shield]: https://img.shields.io/codecov/c/github/totaldebug/luxmodbus?style=for-the-badge
171
+ [contributors-shield]: https://img.shields.io/github/contributors/totaldebug/luxmodbus.svg?style=for-the-badge
172
+ [contributors-url]: https://github.com/totaldebug/luxmodbus/graphs/contributors
173
+ [forks-shield]: https://img.shields.io/github/forks/totaldebug/luxmodbus.svg?style=for-the-badge
174
+ [forks-url]: https://github.com/totaldebug/luxmodbus/network/members
175
+ [issues-shield]: https://img.shields.io/github/issues/totaldebug/luxmodbus.svg?style=for-the-badge
176
+ [issues-url]: https://github.com/totaldebug/luxmodbus/issues
177
+ [license-shield]: https://img.shields.io/github/license/totaldebug/luxmodbus.svg?style=for-the-badge
178
+ [license-url]: https://github.com/totaldebug/luxmodbus/blob/main/LICENSE
@@ -0,0 +1,154 @@
1
+ <a name="readme-top"></a>
2
+
3
+ [![Release][release-shield]][release-url]
4
+ [![Stargazers][stars-shield]][stars-url]
5
+ ![codecov][codecov-shield]
6
+
7
+ [![Contributors][contributors-shield]][contributors-url]
8
+ [![Forks][forks-shield]][forks-url]
9
+ [![Issues][issues-shield]][issues-url]
10
+
11
+ [![MIT License][license-shield]][license-url]
12
+
13
+ <!-- PROJECT HEADER -->
14
+ <br />
15
+ <div align="center">
16
+ <a href="https://github.com/totaldebug/luxmodbus">
17
+ <h3 align="center">luxmodbus</h3>
18
+ </a>
19
+
20
+ <p align="center">
21
+ Framing, register map, and discovery for the LuxPower inverter Modbus protocol.
22
+ </p>
23
+ <br />
24
+ <a href="https://github.com/totaldebug/luxmodbus/issues/new?labels=type%2Fbug&template=bug_report.yml">Report Bug</a>
25
+ ·
26
+ <a href="https://github.com/totaldebug/luxmodbus/issues/new?labels=type%2Ffeature&template=feature_request.yml">Request Feature</a>
27
+ </div>
28
+
29
+ <!-- TABLE OF CONTENTS -->
30
+ <details>
31
+ <summary>Table of Contents</summary>
32
+ <ol>
33
+ <li><a href="#about-the-project">About The Project</a></li>
34
+ <li><a href="#getting-started">Getting Started</a></li>
35
+ <li><a href="#usage">Usage</a></li>
36
+ <li><a href="#the-frame">The Frame</a></li>
37
+ <li><a href="#provenance">Provenance</a></li>
38
+ <li><a href="#contributing">Contributing</a></li>
39
+ <li><a href="#license">License</a></li>
40
+ </ol>
41
+ </details>
42
+
43
+ ## About The Project
44
+
45
+ `luxmodbus` is a small, dependency-free Python library for the **LuxPower
46
+ inverter Modbus protocol** — packet framing, the declarative register map, and
47
+ register discovery. It has **no Home Assistant dependency** and is the protocol
48
+ core consumed by the [Lumen](https://github.com/totaldebug/lumen) Home Assistant
49
+ integration. Because it imports nothing from Home Assistant, it can be tested
50
+ entirely offline against captured packet bytes.
51
+
52
+ ### Status
53
+
54
+ Early. Implemented so far:
55
+
56
+ - `protocol.py` — frame encode/decode (LuxPower TCP envelope + inner Modbus RTU
57
+ data frame), read/write request builders, and read-response unpacking. No I/O,
58
+ no register-meaning knowledge.
59
+ - `registers.py` — declarative address → meaning map (the single source of
60
+ truth) with a decode engine and a bounds-checked `encode_value` for writes.
61
+ - `discovery.py` — passive diff-and-log engine: compares observed registers
62
+ against the known map and records the unknown with a rolling value history.
63
+
64
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
65
+
66
+ ## Getting Started
67
+
68
+ This project uses [uv](https://docs.astral.sh/uv/).
69
+
70
+ ```bash
71
+ git clone https://github.com/totaldebug/luxmodbus.git
72
+ cd luxmodbus
73
+ uv sync
74
+ uv run nox -s tests
75
+ ```
76
+
77
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
78
+
79
+ ## Usage
80
+
81
+ ```python
82
+ from luxmodbus import Frame, decode_inputs
83
+
84
+ frame = Frame.decode(raw_bytes) # validates prefix + CRC
85
+ data = frame.data_frame() # inner Modbus frame
86
+ # raw register values -> {key: scaled value}
87
+ values = decode_inputs({1: 2503, 4: 530, 5: (90 << 8) | 88})
88
+ # {"pv1_voltage": 250.3, "battery_voltage": 53.0, "soc": 88, "soh": 90}
89
+ ```
90
+
91
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
92
+
93
+ ## The Frame
94
+
95
+ LuxPower wraps a modified Modbus RTU "data frame" inside a TCP envelope:
96
+
97
+ ```
98
+ prefix(2)=A1 1A | protocol(u16 LE) | frame_length(u16 LE) | reserved(1)=01 |
99
+ tcp_function(1) | dongle_serial(10) | data_length(u16 LE) | data_frame(N) | crc16(u16 LE)
100
+ ```
101
+
102
+ - `frame_length = total_len - 6`
103
+ - `data_length = len(data_frame) + 2` (the trailing CRC)
104
+ - `crc16` is the standard Modbus CRC (poly `0xA001`, init `0xFFFF`) over the
105
+ data frame, appended little-endian.
106
+
107
+ The inner data frame:
108
+
109
+ ```
110
+ action(1) | device_function(1) | inverter_serial(10) | register(u16 LE) | value
111
+ ```
112
+
113
+ See [`docs/capturing-packets.md`](docs/capturing-packets.md) for how to capture
114
+ real packets and turn them into test fixtures.
115
+
116
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
117
+
118
+ ## Provenance
119
+
120
+ Clean-room: the protocol *facts* (field layout, CRC algorithm, function codes,
121
+ register meanings) are taken from the official Lux Power Modbus RTU
122
+ specification and validated against real packet bytes. No code is copied from
123
+ other implementations.
124
+
125
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
126
+
127
+ ## Contributing
128
+
129
+ Contributions are welcome. Please open an issue first to discuss changes, then
130
+ ensure `uv run nox -s tests` passes (style, types, docstring coverage, and the
131
+ test suite) before opening a PR.
132
+
133
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
134
+
135
+ ## License
136
+
137
+ Distributed under the MIT License. See `LICENSE` for more information.
138
+
139
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
140
+
141
+ <!-- MARKDOWN LINKS & IMAGES -->
142
+ [release-shield]: https://img.shields.io/github/v/release/totaldebug/luxmodbus?style=for-the-badge
143
+ [release-url]: https://github.com/totaldebug/luxmodbus/releases
144
+ [stars-shield]: https://img.shields.io/github/stars/totaldebug/luxmodbus.svg?style=for-the-badge
145
+ [stars-url]: https://github.com/totaldebug/luxmodbus/stargazers
146
+ [codecov-shield]: https://img.shields.io/codecov/c/github/totaldebug/luxmodbus?style=for-the-badge
147
+ [contributors-shield]: https://img.shields.io/github/contributors/totaldebug/luxmodbus.svg?style=for-the-badge
148
+ [contributors-url]: https://github.com/totaldebug/luxmodbus/graphs/contributors
149
+ [forks-shield]: https://img.shields.io/github/forks/totaldebug/luxmodbus.svg?style=for-the-badge
150
+ [forks-url]: https://github.com/totaldebug/luxmodbus/network/members
151
+ [issues-shield]: https://img.shields.io/github/issues/totaldebug/luxmodbus.svg?style=for-the-badge
152
+ [issues-url]: https://github.com/totaldebug/luxmodbus/issues
153
+ [license-shield]: https://img.shields.io/github/license/totaldebug/luxmodbus.svg?style=for-the-badge
154
+ [license-url]: https://github.com/totaldebug/luxmodbus/blob/main/LICENSE
@@ -0,0 +1,95 @@
1
+ [project]
2
+ name = "luxmodbus"
3
+ version = "0.1.0"
4
+ description = "Framing, register map, and discovery for the LuxPower inverter Modbus protocol (HA-free)."
5
+ authors = [
6
+ { name = "Steven Marks", email = "marksie1988@users.noreply.github.com" }
7
+ ]
8
+ license = { text = "MIT" }
9
+ readme = "README.md"
10
+ keywords = ["luxpower", "modbus", "inverter", "solar", "home-assistant", "lumen"]
11
+ requires-python = ">=3.12"
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Natural Language :: English",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Home Automation",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.urls]
28
+ homepage = "https://github.com/totaldebug/luxmodbus"
29
+ repository = "https://github.com/totaldebug/luxmodbus"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "ruff>=0.9.0",
34
+ "mypy>=1.10.0",
35
+ "pre-commit>=3.7.1",
36
+ "interrogate>=1.5.0",
37
+ "pytest>=8.2.2",
38
+ "pytest-asyncio>=0.23",
39
+ "pytest-cov>=5.0.0",
40
+ "nox>=2024.4.15",
41
+ ]
42
+
43
+ [build-system]
44
+ requires = ["uv_build>=0.10.0,<0.11.0"]
45
+ build-backend = "uv_build"
46
+
47
+ [tool.uv.build-backend]
48
+ module-name = "luxmodbus"
49
+ module-root = "src"
50
+
51
+ [tool.ruff]
52
+ line-length = 120
53
+ target-version = "py312"
54
+ exclude = ["tests"]
55
+
56
+ [tool.ruff.lint]
57
+ select = ["E", "F", "I", "UP", "B"]
58
+ ignore = []
59
+
60
+ [tool.ruff.lint.isort]
61
+ known-first-party = ["luxmodbus"]
62
+ section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
63
+
64
+ [tool.mypy]
65
+ python_version = "3.12"
66
+ warn_return_any = true
67
+ warn_unused_configs = true
68
+ mypy_path = "src"
69
+
70
+ [[tool.mypy.overrides]]
71
+ module = "luxmodbus.*"
72
+ disallow_untyped_defs = true
73
+
74
+ [tool.interrogate]
75
+ ignore-init-method = true
76
+ ignore-init-module = false
77
+ ignore-magic = false
78
+ ignore-semiprivate = false
79
+ ignore-private = false
80
+ ignore-property-decorators = false
81
+ ignore-module = true
82
+ ignore-nested-functions = false
83
+ ignore-nested-classes = true
84
+ ignore-setters = false
85
+ fail-under = 100
86
+ exclude = ["build", ".devcontainer", ".nox", ".cache", "tests"]
87
+ ignore-regex = ["^get$", "^mock_.*", ".*BaseClass.*"]
88
+ verbose = 0
89
+ quiet = false
90
+ color = true
91
+
92
+ [tool.pytest.ini_options]
93
+ testpaths = ["tests"]
94
+ asyncio_mode = "auto"
95
+ addopts = "-q"
@@ -0,0 +1,111 @@
1
+ """luxmodbus — HA-free framing, register map, and discovery for LuxPower inverters."""
2
+
3
+ from luxmodbus.discovery import (
4
+ DEFAULT_HISTORY_LIMIT,
5
+ DiscoveryStore,
6
+ UnknownRegister,
7
+ )
8
+ from luxmodbus.protocol import (
9
+ CrcError,
10
+ DataFrame,
11
+ DeviceFunction,
12
+ Frame,
13
+ PrefixError,
14
+ ProtocolError,
15
+ TcpFunction,
16
+ TruncatedFrameError,
17
+ crc16,
18
+ decode_read_response,
19
+ extract_frames,
20
+ )
21
+ from luxmodbus.registers import (
22
+ FLAG_REGISTERS,
23
+ HOLD_REGISTERS,
24
+ INPUT_REGISTERS,
25
+ SELECT_REGISTERS,
26
+ TIME_REGISTERS,
27
+ ByteSelect,
28
+ FlagDef,
29
+ FlagRegister,
30
+ Measurement,
31
+ RegisterBank,
32
+ RegisterDef,
33
+ SelectRegister,
34
+ TimeRegister,
35
+ ValueType,
36
+ decode_flags,
37
+ decode_holds,
38
+ decode_inputs,
39
+ decode_select,
40
+ decode_time,
41
+ decode_value,
42
+ encode_time,
43
+ encode_value,
44
+ find_hold,
45
+ find_input,
46
+ mapped_hold_addresses,
47
+ mapped_input_addresses,
48
+ set_flag,
49
+ set_select,
50
+ )
51
+ from luxmodbus.transport import (
52
+ ClientTransport,
53
+ ServerTransport,
54
+ Transport,
55
+ TransportConnectError,
56
+ TransportError,
57
+ TransportNotConnectedError,
58
+ )
59
+
60
+ __version__ = "0.1.0"
61
+
62
+ __all__ = [
63
+ "DEFAULT_HISTORY_LIMIT",
64
+ "FLAG_REGISTERS",
65
+ "HOLD_REGISTERS",
66
+ "INPUT_REGISTERS",
67
+ "ByteSelect",
68
+ "ClientTransport",
69
+ "CrcError",
70
+ "DataFrame",
71
+ "DeviceFunction",
72
+ "DiscoveryStore",
73
+ "FlagDef",
74
+ "FlagRegister",
75
+ "Frame",
76
+ "Measurement",
77
+ "PrefixError",
78
+ "ProtocolError",
79
+ "RegisterBank",
80
+ "RegisterDef",
81
+ "SELECT_REGISTERS",
82
+ "TIME_REGISTERS",
83
+ "SelectRegister",
84
+ "ServerTransport",
85
+ "TcpFunction",
86
+ "TimeRegister",
87
+ "Transport",
88
+ "TransportConnectError",
89
+ "TransportError",
90
+ "TransportNotConnectedError",
91
+ "TruncatedFrameError",
92
+ "UnknownRegister",
93
+ "ValueType",
94
+ "crc16",
95
+ "decode_flags",
96
+ "decode_holds",
97
+ "decode_inputs",
98
+ "decode_read_response",
99
+ "decode_select",
100
+ "decode_time",
101
+ "decode_value",
102
+ "encode_time",
103
+ "encode_value",
104
+ "extract_frames",
105
+ "find_hold",
106
+ "find_input",
107
+ "mapped_hold_addresses",
108
+ "mapped_input_addresses",
109
+ "set_flag",
110
+ "set_select",
111
+ ]