spacepak-modbus 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 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "description": "Renovate configuration for spacepak-modbus",
4
+ "dependencyDashboard": false,
5
+ "extends": [
6
+ "config:recommended",
7
+ ":separateMajorReleases",
8
+ ":combinePatchMinorReleases",
9
+ "helpers:pinGitHubActionDigests"
10
+ ],
11
+ "rebaseWhen": "conflicted",
12
+ "ignorePaths": [
13
+ "**/node_modules/**"
14
+ ],
15
+ "github-actions": {
16
+ "enabled": true
17
+ },
18
+ "lockFileMaintenance": {
19
+ "enabled": true,
20
+ "schedule": [
21
+ "before 5am on monday"
22
+ ]
23
+ },
24
+ "packageRules": [
25
+ {
26
+ "matchManagers": [
27
+ "github-actions"
28
+ ],
29
+ "groupName": "GitHub Actions",
30
+ "minimumReleaseAge": "3 days"
31
+ },
32
+ {
33
+ "matchDepTypes": [
34
+ "dev",
35
+ "test"
36
+ ],
37
+ "groupName": "Dev & Test Dependencies",
38
+ "minimumReleaseAge": "3 days"
39
+ },
40
+ {
41
+ "matchPackageNames": [
42
+ "modbus-connection"
43
+ ],
44
+ "groupName": "Core Protocol Dependencies"
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,36 @@
1
+ # Written by Claude, guided by Chris.
2
+ name: CI
3
+
4
+ on:
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ lint:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
18
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
19
+ - name: Install dependencies
20
+ run: uv sync
21
+ - name: Ruff format
22
+ run: uv run ruff format --check .
23
+ - name: Ruff lint
24
+ run: uv run ruff check .
25
+ - name: Mypy
26
+ run: uv run mypy
27
+
28
+ test:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
32
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
33
+ - name: Install dependencies
34
+ run: uv sync
35
+ - name: Run tests
36
+ run: uv run pytest
@@ -0,0 +1,52 @@
1
+ # Written by Claude, guided by Chris.
2
+ name: Publish to PyPI
3
+
4
+ on:
5
+ release:
6
+ types: [published]
7
+ workflow_dispatch:
8
+ inputs:
9
+ tag:
10
+ description: Existing release tag to build and publish (e.g. 4.0.1)
11
+ required: true
12
+ type: string
13
+
14
+ env:
15
+ RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name }}
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ permissions:
21
+ contents: read
22
+ steps:
23
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
24
+ with:
25
+ ref: ${{ env.RELEASE_TAG }}
26
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
27
+ - name: Set package version from release tag
28
+ run: |
29
+ VERSION="${RELEASE_TAG#v}"
30
+ sed -i "s/^version = \"0\\.0\\.0\"$/version = \"${VERSION}\"/" pyproject.toml
31
+ - name: Verify the version was substituted
32
+ run: |
33
+ VERSION="${RELEASE_TAG#v}"
34
+ grep -qx "version = \"${VERSION}\"" pyproject.toml
35
+ - run: uv build
36
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
37
+ with:
38
+ name: dist
39
+ path: dist/
40
+
41
+ publish:
42
+ needs: build
43
+ runs-on: ubuntu-latest
44
+ environment: pypi
45
+ permissions:
46
+ id-token: write
47
+ steps:
48
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
49
+ with:
50
+ name: dist
51
+ path: dist/
52
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .coverage
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.5
2
+ Name: spacepak-modbus
3
+ Version: 0.1.0
4
+ Summary: Read and control SpacePak Solstice Inverter Extreme (ILAHP) heat pumps over Modbus.
5
+ Project-URL: Homepage, https://github.com/cwm12345/spacepak-modbus
6
+ Author: spacepak-modbus contributors
7
+ License: Unlicense
8
+ License-File: LICENSE
9
+ Keywords: air-to-water,heat-pump,hvac,modbus,spacepak
10
+ Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Home Automation
13
+ Requires-Python: >=3.12
14
+ Requires-Dist: modbus-connection[tmodbus]>=4.12.1
15
+ Description-Content-Type: text/markdown
16
+
17
+ <!-- Written by Claude, guided by Chris. -->
18
+
19
+ # spacepak-modbus
20
+
21
+ Read and control SpacePak Solstice Inverter Extreme (ILAHP) air-to-water heat
22
+ pumps over Modbus, as typed Python objects rather than register numbers.
23
+
24
+ The library maps the ILAHP register set onto
25
+ [modbus-connection](https://github.com/home-assistant-libs/modbus-connection)'s
26
+ device model: you hand it a `ModbusUnit`, call `async_update()`, and read
27
+ sub-systems as attributes. It owns no connection and no I/O policy. The caller
28
+ does.
29
+
30
+ ## Status
31
+
32
+ This is a working reference pulled from one real installation, shared in case
33
+ it is useful. It is **not an actively maintained package**: there is no
34
+ commitment to triage issues or pull requests, or to track future
35
+ modbus-connection changes. Forks are welcome, and so is anyone who wants to take
36
+ it over.
37
+
38
+ ## Supported devices
39
+
40
+ The SpacePak Solstice Inverter Extreme (ILAHP) range, per the register map in
41
+ its installation manual (ILHP2-0423). It has been exercised against two units
42
+ behind a Modbus TCP gateway. Other SpacePak models are untested.
43
+
44
+ The unit speaks Modbus RTU at 9600 baud, 8N1, on its RS-485 port. Every
45
+ register this library reads is a holding register.
46
+
47
+ ## What it covers
48
+
49
+ | Component | Registers | What |
50
+ | :--- | :--- | :--- |
51
+ | `controls` | 1011-1012, 1028, 1158-1165 | Power, operating mode, whether the hot water function is enabled, heating and cooling targets, the unit's own setpoint limits |
52
+ | `status` | 2011-2032 | Running, current mode (defrost included), load outputs, compressor hours |
53
+ | `measurements` | 2042-2072 | Water, air, coil and refrigerant temperatures, currents, voltages, compressor frequency |
54
+ | `faults` | 2081-2090 | The nine failure registers, decoded into named faults |
55
+
56
+ The manual lists room temperature (T09, 2058) and water flow (T39, 2077) as not
57
+ used on this unit, so they are left out. The hot water tank temperature (T08)
58
+ only has a sensor behind it when the unit's hot water function is enabled
59
+ (`controls.hot_water_enabled`).
60
+
61
+ The installer parameters (registers 1013-1270: compressor frequency curves, EEV
62
+ steps, defrost timing, weather compensation, timers) are deliberately left out.
63
+ Writing those by mistake would misconfigure the heat pump, not just misreport
64
+ it.
65
+
66
+ Every temperature is stored in tenths of a degree Celsius, whatever the unit's
67
+ display is set to show, so every temperature here is in °C.
68
+
69
+ ## Usage
70
+
71
+ ```python
72
+ import asyncio
73
+
74
+ from modbus_connection import ModbusTcpParams
75
+ from modbus_connection.tmodbus import ModbusConnection
76
+ from spacepak_modbus import IlahpHeatPump
77
+
78
+
79
+ async def main() -> None:
80
+ connection = ModbusConnection(ModbusTcpParams(host="192.168.1.50", port=502))
81
+ try:
82
+ heat_pump = IlahpHeatPump(connection.for_unit(1))
83
+ await heat_pump.async_update()
84
+
85
+ print("Running:", heat_pump.status.running, heat_pump.status.unit_mode)
86
+ print("Outlet:", heat_pump.measurements.outlet_temperature, "°C")
87
+ print("Outdoor:", heat_pump.measurements.ambient_temperature, "°C")
88
+ print("Target:", heat_pump.controls.heating_target_temperature, "°C")
89
+ print("Faults:", [fault.key for fault in heat_pump.faults.active_faults])
90
+ finally:
91
+ await connection.close()
92
+
93
+
94
+ asyncio.run(main())
95
+ ```
96
+
97
+ ### Readings and settings refresh separately
98
+
99
+ - `async_update_readings()` reads `status`, `measurements` and `faults`: what the
100
+ unit measures and reports.
101
+ - `async_update_settings()` reads `controls`: registers that change when
102
+ something writes them. Run one after a write to read back what took effect.
103
+ - `async_update()` does both, in one report.
104
+
105
+ Each returns an `UpdateReport`. A component whose block the unit refuses keeps
106
+ its previous values and is listed in `report.failed`; the rest still refresh. A
107
+ dead link (`ModbusConnectionError`) raises, and so does a timeout before
108
+ anything has answered.
109
+
110
+ ### Writing
111
+
112
+ ```python
113
+ await heat_pump.controls.write("power_on", True)
114
+ await heat_pump.controls.write("heating_target_temperature", 45.0)
115
+ await heat_pump.async_update_settings()
116
+ ```
117
+
118
+ Setpoint writes are checked against the widest range each register accepts.
119
+ The unit applies its own tighter limits too, which it reports as
120
+ `min_heating_setpoint`/`max_heating_setpoint` and
121
+ `min_cooling_setpoint`/`max_cooling_setpoint`. Clamp to those before writing.
122
+
123
+ The operating mode is read-only here. The mode register's values depend on
124
+ whether the installer enabled cooling (parameter H05), and a wrong mode can
125
+ leave a hydronic system without heat.
126
+
127
+ ### Faults
128
+
129
+ The unit reports faults as nine bitmask registers. `faults.active_faults`
130
+ decodes them into `Fault` entries, each with its failure register, bit, a
131
+ stable snake_case `key`, and a description. `FAULTS` is the full table of the
132
+ bits the manual names. A set bit the manual does not name (failure register 3
133
+ has no published bit table) is still reported, as `failure_<n>_bit_<b>`, so
134
+ nothing is hidden. `faults.any_fault` is true when any register has a bit set.
135
+
136
+ ## Checking a real heat pump
137
+
138
+ `script/query.py` reads one unit once and prints every value, which is the
139
+ quickest way to see whether a unit is reachable and addressed correctly:
140
+
141
+ ```bash
142
+ uv run script/query.py 192.168.1.50 --port 502 --unit 1
143
+ uv run script/query.py socket://192.168.1.50:8899 --transport serial --unit 1
144
+ uv run script/query.py /dev/ttyUSB0 --transport serial --baudrate 9600 --unit 1 --raw
145
+ ```
146
+
147
+ `--raw` adds every register it read, undecoded, which is what an issue about a
148
+ wrong value should quote. A raw dump loads straight into modbus-connection's
149
+ mock with `load_raw()`, so it can become a test.
150
+
151
+ ## Development
152
+
153
+ ```bash
154
+ uv sync
155
+ uv run pytest
156
+ uv run ruff check . && uv run ruff format --check . && uv run mypy
157
+ ```
158
+
159
+ The tests run against modbus-connection's in-memory mock backend. No hardware
160
+ is needed.
161
+
162
+ ## Source
163
+
164
+ The register map comes from the SpacePak ILAHP Installation, Operation &
165
+ Maintenance manual (ILHP2-0423), Modbus section. Addresses, scales and the
166
+ enum and bit tables follow it. On live units:
167
+
168
+ - 1011, 1158 and 1159 have been read and written.
169
+ - Every register this library reads has been read from two units and checked
170
+ against the values an existing integration reported for the same units.
171
+ - The operating mode (1012) and the setpoint limits (1162-1165) read as
172
+ expected. The limits are whatever the installer configured, so they differ
173
+ from the manual's defaults.
174
+
175
+ ## License
176
+
177
+ [Unlicense](LICENSE): public domain.
@@ -0,0 +1,161 @@
1
+ <!-- Written by Claude, guided by Chris. -->
2
+
3
+ # spacepak-modbus
4
+
5
+ Read and control SpacePak Solstice Inverter Extreme (ILAHP) air-to-water heat
6
+ pumps over Modbus, as typed Python objects rather than register numbers.
7
+
8
+ The library maps the ILAHP register set onto
9
+ [modbus-connection](https://github.com/home-assistant-libs/modbus-connection)'s
10
+ device model: you hand it a `ModbusUnit`, call `async_update()`, and read
11
+ sub-systems as attributes. It owns no connection and no I/O policy. The caller
12
+ does.
13
+
14
+ ## Status
15
+
16
+ This is a working reference pulled from one real installation, shared in case
17
+ it is useful. It is **not an actively maintained package**: there is no
18
+ commitment to triage issues or pull requests, or to track future
19
+ modbus-connection changes. Forks are welcome, and so is anyone who wants to take
20
+ it over.
21
+
22
+ ## Supported devices
23
+
24
+ The SpacePak Solstice Inverter Extreme (ILAHP) range, per the register map in
25
+ its installation manual (ILHP2-0423). It has been exercised against two units
26
+ behind a Modbus TCP gateway. Other SpacePak models are untested.
27
+
28
+ The unit speaks Modbus RTU at 9600 baud, 8N1, on its RS-485 port. Every
29
+ register this library reads is a holding register.
30
+
31
+ ## What it covers
32
+
33
+ | Component | Registers | What |
34
+ | :--- | :--- | :--- |
35
+ | `controls` | 1011-1012, 1028, 1158-1165 | Power, operating mode, whether the hot water function is enabled, heating and cooling targets, the unit's own setpoint limits |
36
+ | `status` | 2011-2032 | Running, current mode (defrost included), load outputs, compressor hours |
37
+ | `measurements` | 2042-2072 | Water, air, coil and refrigerant temperatures, currents, voltages, compressor frequency |
38
+ | `faults` | 2081-2090 | The nine failure registers, decoded into named faults |
39
+
40
+ The manual lists room temperature (T09, 2058) and water flow (T39, 2077) as not
41
+ used on this unit, so they are left out. The hot water tank temperature (T08)
42
+ only has a sensor behind it when the unit's hot water function is enabled
43
+ (`controls.hot_water_enabled`).
44
+
45
+ The installer parameters (registers 1013-1270: compressor frequency curves, EEV
46
+ steps, defrost timing, weather compensation, timers) are deliberately left out.
47
+ Writing those by mistake would misconfigure the heat pump, not just misreport
48
+ it.
49
+
50
+ Every temperature is stored in tenths of a degree Celsius, whatever the unit's
51
+ display is set to show, so every temperature here is in °C.
52
+
53
+ ## Usage
54
+
55
+ ```python
56
+ import asyncio
57
+
58
+ from modbus_connection import ModbusTcpParams
59
+ from modbus_connection.tmodbus import ModbusConnection
60
+ from spacepak_modbus import IlahpHeatPump
61
+
62
+
63
+ async def main() -> None:
64
+ connection = ModbusConnection(ModbusTcpParams(host="192.168.1.50", port=502))
65
+ try:
66
+ heat_pump = IlahpHeatPump(connection.for_unit(1))
67
+ await heat_pump.async_update()
68
+
69
+ print("Running:", heat_pump.status.running, heat_pump.status.unit_mode)
70
+ print("Outlet:", heat_pump.measurements.outlet_temperature, "°C")
71
+ print("Outdoor:", heat_pump.measurements.ambient_temperature, "°C")
72
+ print("Target:", heat_pump.controls.heating_target_temperature, "°C")
73
+ print("Faults:", [fault.key for fault in heat_pump.faults.active_faults])
74
+ finally:
75
+ await connection.close()
76
+
77
+
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ ### Readings and settings refresh separately
82
+
83
+ - `async_update_readings()` reads `status`, `measurements` and `faults`: what the
84
+ unit measures and reports.
85
+ - `async_update_settings()` reads `controls`: registers that change when
86
+ something writes them. Run one after a write to read back what took effect.
87
+ - `async_update()` does both, in one report.
88
+
89
+ Each returns an `UpdateReport`. A component whose block the unit refuses keeps
90
+ its previous values and is listed in `report.failed`; the rest still refresh. A
91
+ dead link (`ModbusConnectionError`) raises, and so does a timeout before
92
+ anything has answered.
93
+
94
+ ### Writing
95
+
96
+ ```python
97
+ await heat_pump.controls.write("power_on", True)
98
+ await heat_pump.controls.write("heating_target_temperature", 45.0)
99
+ await heat_pump.async_update_settings()
100
+ ```
101
+
102
+ Setpoint writes are checked against the widest range each register accepts.
103
+ The unit applies its own tighter limits too, which it reports as
104
+ `min_heating_setpoint`/`max_heating_setpoint` and
105
+ `min_cooling_setpoint`/`max_cooling_setpoint`. Clamp to those before writing.
106
+
107
+ The operating mode is read-only here. The mode register's values depend on
108
+ whether the installer enabled cooling (parameter H05), and a wrong mode can
109
+ leave a hydronic system without heat.
110
+
111
+ ### Faults
112
+
113
+ The unit reports faults as nine bitmask registers. `faults.active_faults`
114
+ decodes them into `Fault` entries, each with its failure register, bit, a
115
+ stable snake_case `key`, and a description. `FAULTS` is the full table of the
116
+ bits the manual names. A set bit the manual does not name (failure register 3
117
+ has no published bit table) is still reported, as `failure_<n>_bit_<b>`, so
118
+ nothing is hidden. `faults.any_fault` is true when any register has a bit set.
119
+
120
+ ## Checking a real heat pump
121
+
122
+ `script/query.py` reads one unit once and prints every value, which is the
123
+ quickest way to see whether a unit is reachable and addressed correctly:
124
+
125
+ ```bash
126
+ uv run script/query.py 192.168.1.50 --port 502 --unit 1
127
+ uv run script/query.py socket://192.168.1.50:8899 --transport serial --unit 1
128
+ uv run script/query.py /dev/ttyUSB0 --transport serial --baudrate 9600 --unit 1 --raw
129
+ ```
130
+
131
+ `--raw` adds every register it read, undecoded, which is what an issue about a
132
+ wrong value should quote. A raw dump loads straight into modbus-connection's
133
+ mock with `load_raw()`, so it can become a test.
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ uv sync
139
+ uv run pytest
140
+ uv run ruff check . && uv run ruff format --check . && uv run mypy
141
+ ```
142
+
143
+ The tests run against modbus-connection's in-memory mock backend. No hardware
144
+ is needed.
145
+
146
+ ## Source
147
+
148
+ The register map comes from the SpacePak ILAHP Installation, Operation &
149
+ Maintenance manual (ILHP2-0423), Modbus section. Addresses, scales and the
150
+ enum and bit tables follow it. On live units:
151
+
152
+ - 1011, 1158 and 1159 have been read and written.
153
+ - Every register this library reads has been read from two units and checked
154
+ against the values an existing integration reported for the same units.
155
+ - The operating mode (1012) and the setpoint limits (1162-1165) read as
156
+ expected. The limits are whatever the installer configured, so they differ
157
+ from the manual's defaults.
158
+
159
+ ## License
160
+
161
+ [Unlicense](LICENSE): public domain.
@@ -0,0 +1,26 @@
1
+ <!-- Written by Claude, guided by Chris. -->
2
+
3
+ # Releasing `spacepak-modbus`
4
+
5
+ This document describes how to release a new version of `spacepak-modbus` to PyPI.
6
+
7
+ ## Tagging Convention
8
+
9
+ > [!IMPORTANT]
10
+ > **Release tags must be bare semantic versions without a leading `v`** (e.g. `0.1.5`, `1.0.0`).
11
+ > Do not use `v`-prefixed tags like `v0.1.5`.
12
+
13
+ Release tags are directly substituted into `pyproject.toml` during the release workflow to set the package version according to PEP 440.
14
+
15
+ ## Release Process
16
+
17
+ 1. **Verify CI**: Ensure that the `CI` workflow is green on the `main` branch.
18
+ 2. **Draft a GitHub Release**:
19
+ - Go to GitHub Releases: **Draft a new release**.
20
+ - **Choose a tag**: Enter the new version number as a bare tag (e.g. `0.1.5`).
21
+ - **Target**: `main`.
22
+ - **Title**: Enter the version number (e.g. `0.1.5`).
23
+ - **Description**: Provide release notes / changelog.
24
+ 3. **Publish the Release**:
25
+ - Click **Publish release**.
26
+ - The `.github/workflows/publish.yml` workflow will automatically trigger, build the wheel and source distribution, and publish the package to PyPI via Trusted Publishing.
@@ -0,0 +1,61 @@
1
+ # Written by Claude, guided by Chris.
2
+ [build-system]
3
+ requires = ["hatchling"]
4
+ build-backend = "hatchling.build"
5
+
6
+ [project]
7
+ name = "spacepak-modbus"
8
+ version = "0.1.0"
9
+ description = "Read and control SpacePak Solstice Inverter Extreme (ILAHP) heat pumps over Modbus."
10
+ readme = "README.md"
11
+ requires-python = ">=3.12"
12
+ license = { text = "Unlicense" }
13
+ authors = [{ name = "spacepak-modbus contributors" }]
14
+ keywords = ["modbus", "spacepak", "heat-pump", "air-to-water", "hvac"]
15
+ classifiers = [
16
+ "License :: OSI Approved :: The Unlicense (Unlicense)",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Home Automation",
19
+ ]
20
+ # The caller injects a ModbusUnit; tmodbus is the backend this is exercised against.
21
+ dependencies = ["modbus-connection[tmodbus]>=4.12.1"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/cwm12345/spacepak-modbus"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "mypy>=1.11",
29
+ "pytest>=8",
30
+ "pytest-asyncio>=0.24",
31
+ "ruff>=0.6",
32
+ ]
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/spacepak_modbus"]
36
+
37
+ [tool.pytest.ini_options]
38
+ asyncio_mode = "auto"
39
+ testpaths = ["tests"]
40
+
41
+ [tool.ruff]
42
+ target-version = "py312"
43
+ src = ["src"]
44
+
45
+ [tool.ruff.lint]
46
+ select = ["E", "F", "W", "I", "UP", "B", "D"]
47
+ # D401 wants imperative docstrings; this codebase names things, matching
48
+ # modbus-connection's own style. D107 is covered by the class docstring.
49
+ ignore = ["D107", "D401"]
50
+
51
+ [tool.ruff.lint.pydocstyle]
52
+ convention = "pep257"
53
+
54
+ [tool.ruff.lint.per-file-ignores]
55
+ # A test's name is its documentation; docstrings are for the ones that need a why.
56
+ "tests/*" = ["D100", "D103", "D104"]
57
+
58
+ [tool.mypy]
59
+ python_version = "3.12"
60
+ files = ["src", "tests", "script"]
61
+ strict = true