husdata-h66-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.
- husdata_h66_modbus-0.1.0/LICENSE +21 -0
- husdata_h66_modbus-0.1.0/PKG-INFO +199 -0
- husdata_h66_modbus-0.1.0/README.md +170 -0
- husdata_h66_modbus-0.1.0/README.rst +217 -0
- husdata_h66_modbus-0.1.0/pyproject.toml +78 -0
- husdata_h66_modbus-0.1.0/setup.cfg +4 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66/__init__.py +29 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66/__main__.py +72 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66/device.py +162 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66/py.typed +0 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66/registers.py +279 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66/web.py +74 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66_modbus.egg-info/PKG-INFO +199 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66_modbus.egg-info/SOURCES.txt +18 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66_modbus.egg-info/dependency_links.txt +1 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66_modbus.egg-info/entry_points.txt +2 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66_modbus.egg-info/requires.txt +4 -0
- husdata_h66_modbus-0.1.0/src/husdata_h66_modbus.egg-info/top_level.txt +1 -0
- husdata_h66_modbus-0.1.0/tests/test_device.py +156 -0
- husdata_h66_modbus-0.1.0/tests/test_registers.py +77 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonny Bergdahl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: husdata-h66-modbus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Modbus TCP library for the Husdata H66 heat pump gateway
|
|
5
|
+
Author-email: Jonny Bergdahl <bergdahl@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jonnybergdahl/Python-husdata-h66-modbus
|
|
8
|
+
Project-URL: Repository, https://github.com/jonnybergdahl/Python-husdata-h66-modbus
|
|
9
|
+
Project-URL: Documentation, https://jonnybergdahl.github.io/husdata-h66-modbus/
|
|
10
|
+
Project-URL: Issues, https://github.com/jonnybergdahl/Python-husdata-h66-modbus/issues
|
|
11
|
+
Keywords: husdata,h66,modbus,heat pump,ivt,rego,home assistant
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Home Automation
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.13
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: modbus-connection>=4.12
|
|
26
|
+
Provides-Extra: cli
|
|
27
|
+
Requires-Dist: modbus-connection[pymodbus]>=4.12; extra == "cli"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# husdata-h66-modbus
|
|
31
|
+
|
|
32
|
+
Async Python library for the Modbus TCP interface of the [Husdata H66](https://husdata.se/produkt/h66-wifi-eth-gateway-styr-din-varmepump/) heat pump gateway.
|
|
33
|
+
|
|
34
|
+
It is built on [modbus-connection](https://github.com/home-assistant-libs/modbus-connection), the Modbus layer Home Assistant uses from 2026.9. The library never opens a connection itself: you give it a `ModbusUnit`, either from `modbus_connection` or from Home Assistant's `async_get_unit`. It has no Home Assistant dependency.
|
|
35
|
+
|
|
36
|
+
Tested with an H66 on firmware 360801 connected to an IVT Greenline (Rego 600 controller).
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install husdata-h66-modbus
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Add the `cli` extra to get the pymodbus backend and the `husdata-h66` command line tool:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install "husdata-h66-modbus[cli]"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
The full library documentation can be [found here](https://jonnybergdahl.github.io/husdata-h66-modbus/).
|
|
53
|
+
|
|
54
|
+
## Enable Modbus on the H66
|
|
55
|
+
|
|
56
|
+
Modbus TCP is off by default. On the H66 web page, open **Config** > **Modbus**, set `MODBUS_PORT` to `502` and restart the H66. `MODBUS_IP` can limit which client may connect. The H66 accepts only one Modbus client at a time.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
|
|
63
|
+
from modbus_connection import ModbusTcpParams
|
|
64
|
+
from modbus_connection.pymodbus import ModbusConnection
|
|
65
|
+
|
|
66
|
+
from husdata_h66 import HusdataH66
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def main() -> None:
|
|
70
|
+
connection = ModbusConnection(ModbusTcpParams(host="h66.local"))
|
|
71
|
+
try:
|
|
72
|
+
device = HusdataH66(connection.for_unit(1))
|
|
73
|
+
await device.async_update()
|
|
74
|
+
print(device.get("outdoor")) # 3.5 (°C)
|
|
75
|
+
print(device.get("compressor")) # True
|
|
76
|
+
print(device.values) # everything, keyed by register key
|
|
77
|
+
|
|
78
|
+
await device.async_set("room_setpoint", 21.5)
|
|
79
|
+
finally:
|
|
80
|
+
await connection.close()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In Home Assistant, pass the unit from `async_get_unit` instead:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
unit = async_get_unit(
|
|
90
|
+
hass, entry, ModbusTcpParams(host=entry.data[CONF_HOST]), entry.data[CONF_UNIT_ID]
|
|
91
|
+
)
|
|
92
|
+
entry.runtime_data = HusdataH66(unit)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`async_update()` reads all registers in three requests and returns an `UpdateReport`. It raises `ModbusConnectionError` or `ModbusTimeoutError` when the gateway cannot be reached.
|
|
96
|
+
|
|
97
|
+
`async_set()` takes the value in the register's own unit and checks it against the register's range and step. It raises `ValueError` for an invalid value and `AttributeError` for a read-only register. `async_reset_alarm()` writes 1 to **Alarm reset**.
|
|
98
|
+
|
|
99
|
+
From the command line (read-only):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
husdata-h66 h66.local
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Register map
|
|
106
|
+
|
|
107
|
+
The H66 numbers its Modbus registers one per parameter, in the order its home page lists them. Read-only parameters are INPUT registers, settings are HOLDING registers, and every register is a signed or unsigned 16-bit value. The addresses depend on the heat pump controller and can move after a firmware update or when you add custom registers in the H66.
|
|
108
|
+
|
|
109
|
+
`REGO600_REGISTERS` is the default map, for a Rego 600 controller on firmware 360801. To follow the H66 instead, fetch its home page and parse it:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from husdata_h66 import HusdataH66, parse_firmware_version, parse_registers
|
|
113
|
+
|
|
114
|
+
page = await (await session.get("http://h66.local/")).text()
|
|
115
|
+
device = HusdataH66(unit, parse_registers(page))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`husdata-h66 h66.local --from-web` does the same from the command line.
|
|
119
|
+
|
|
120
|
+
Each parameter has a Husdata variable ID. Its first hex digit gives the type and scaling:
|
|
121
|
+
|
|
122
|
+
| First digit | Type | Scale | Unit |
|
|
123
|
+
|---|---|---|---|
|
|
124
|
+
| 0 | Temperature | 0.1 | °C |
|
|
125
|
+
| 1 | Status | 1 | on/off |
|
|
126
|
+
| 2 | Number | 0.1 | |
|
|
127
|
+
| 3 | Percent | 0.1 | % |
|
|
128
|
+
| 5 | Energy | 0.1 | kWh |
|
|
129
|
+
| 6 | Hours | 1 | h |
|
|
130
|
+
| 7 | Minutes | 1 | min |
|
|
131
|
+
| B | Integer | 1 | |
|
|
132
|
+
| C | Power | 1 | W |
|
|
133
|
+
|
|
134
|
+
Temperatures and numbers are signed, the others unsigned. A temperature sensor that isn't connected reads `None` (the H66 shows −48.3 °C).
|
|
135
|
+
|
|
136
|
+
### Rego 600 (default map)
|
|
137
|
+
|
|
138
|
+
| Address | Type | ID | Key | Range |
|
|
139
|
+
|---|---|---|---|---|
|
|
140
|
+
| 0 | Input | 0001 | `radiator_return` | |
|
|
141
|
+
| 1 | Input | 0002 | `radiator_forward` | |
|
|
142
|
+
| 2 | Input | 0003 | `heat_carrier_return` | |
|
|
143
|
+
| 3 | Input | 0004 | `heat_carrier_forward` | |
|
|
144
|
+
| 4 | Input | 0005 | `brine_in` | |
|
|
145
|
+
| 5 | Input | 0006 | `brine_out` | |
|
|
146
|
+
| 6 | Input | 0007 | `outdoor` | |
|
|
147
|
+
| 7 | Input | 0008 | `indoor` | |
|
|
148
|
+
| 8 | Input | 0009 | `hot_water_top` | |
|
|
149
|
+
| 9 | Input | 000A | `hot_water_mid` | |
|
|
150
|
+
| 10 | Input | 000B | `hot_gas` | |
|
|
151
|
+
| 11 | Input | 3104 | `add_heat_status` | |
|
|
152
|
+
| 12 | Input | 0107 | `heating_setpoint` | |
|
|
153
|
+
| 13 | Input | 0111 | `hot_water_setpoint` | |
|
|
154
|
+
| 14 | Holding | 0203 | `room_setpoint` | 5–29 °C |
|
|
155
|
+
| 15 | Holding | 2204 | `room_sensor_influence` | |
|
|
156
|
+
| 16 | Holding | 2205 | `heat_curve` | 0–10 |
|
|
157
|
+
| 17 | Holding | 0207 | `heat_curve_parallel` | −10–10 °C |
|
|
158
|
+
| 18 | Holding | 0208 | `hot_water_stop_temperature` | 20–60 °C |
|
|
159
|
+
| 19 | Holding | 020B | `hot_water_difference` | 2–15 °C |
|
|
160
|
+
| 20 | Holding | 7209 | `extra_hot_water` | 0–1440 min |
|
|
161
|
+
| 21 | Holding | 1215 | `electric_heater_allowed` | on/off |
|
|
162
|
+
| 22 | Holding | B21B | `electric_heater_max_power` | 1–4 |
|
|
163
|
+
| 23 | Holding | 1233 | `external_control` | on/off |
|
|
164
|
+
| 24 | Holding | 020A | `summer_mode` | 0–30 °C |
|
|
165
|
+
| 25 | Holding | 2210 | `holiday_mode` | 0–30 |
|
|
166
|
+
| 26 | Holding | 12F2 | `alarm_reset` | write 1 |
|
|
167
|
+
| 27 | Input | 1A01 | `compressor` | |
|
|
168
|
+
| 28 | Input | 1A02 | `add_heat_step_1` | |
|
|
169
|
+
| 29 | Input | 1A03 | `add_heat_step_2` | |
|
|
170
|
+
| 30 | Input | 1A04 | `pump_cold_circuit` | |
|
|
171
|
+
| 31 | Input | 1A05 | `pump_heat_circuit` | |
|
|
172
|
+
| 32 | Input | 1A06 | `pump_radiator` | |
|
|
173
|
+
| 33 | Input | 1A07 | `switch_valve_1` | |
|
|
174
|
+
| 34 | Input | 1A20 | `alarm` | |
|
|
175
|
+
| 35 | Input | BA91 | `alarm_code` | |
|
|
176
|
+
| 36 | Input | 6C55 | `compressor_hours_heating` | |
|
|
177
|
+
| 37 | Input | 6C56 | `compressor_hours_hot_water` | |
|
|
178
|
+
| 38 | Input | 6C58 | `aux_heater_hours_heating` | |
|
|
179
|
+
| 39 | Input | 6C59 | `aux_heater_hours_hot_water` | |
|
|
180
|
+
| 40 | Input | CFAA | `power_consumption` | |
|
|
181
|
+
| 41 | Input | 5FAB | `accumulated_consumption` | |
|
|
182
|
+
|
|
183
|
+
`power_consumption` and `accumulated_consumption` are calculated by the H66 from the wattages set for each status variable on its web page. `accumulated_consumption` restarts at 0 when the H66 boots.
|
|
184
|
+
|
|
185
|
+
Not yet verified: `room_sensor_influence` is read unscaled because the H66 web page shows it that way, and the unit and scale of `holiday_mode` are unknown.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
uv sync
|
|
191
|
+
uv run pytest
|
|
192
|
+
uv run ruff check src tests
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`build.sh` publishes a release: it converts this README to `README.rst`, builds the documentation, copies it to the `jonnybergdahl.github.io` repository and pushes it, then builds the package and uploads it to PyPI.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# husdata-h66-modbus
|
|
2
|
+
|
|
3
|
+
Async Python library for the Modbus TCP interface of the [Husdata H66](https://husdata.se/produkt/h66-wifi-eth-gateway-styr-din-varmepump/) heat pump gateway.
|
|
4
|
+
|
|
5
|
+
It is built on [modbus-connection](https://github.com/home-assistant-libs/modbus-connection), the Modbus layer Home Assistant uses from 2026.9. The library never opens a connection itself: you give it a `ModbusUnit`, either from `modbus_connection` or from Home Assistant's `async_get_unit`. It has no Home Assistant dependency.
|
|
6
|
+
|
|
7
|
+
Tested with an H66 on firmware 360801 connected to an IVT Greenline (Rego 600 controller).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install husdata-h66-modbus
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Add the `cli` extra to get the pymodbus backend and the `husdata-h66` command line tool:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "husdata-h66-modbus[cli]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Documentation
|
|
22
|
+
|
|
23
|
+
The full library documentation can be [found here](https://jonnybergdahl.github.io/husdata-h66-modbus/).
|
|
24
|
+
|
|
25
|
+
## Enable Modbus on the H66
|
|
26
|
+
|
|
27
|
+
Modbus TCP is off by default. On the H66 web page, open **Config** > **Modbus**, set `MODBUS_PORT` to `502` and restart the H66. `MODBUS_IP` can limit which client may connect. The H66 accepts only one Modbus client at a time.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
|
|
34
|
+
from modbus_connection import ModbusTcpParams
|
|
35
|
+
from modbus_connection.pymodbus import ModbusConnection
|
|
36
|
+
|
|
37
|
+
from husdata_h66 import HusdataH66
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def main() -> None:
|
|
41
|
+
connection = ModbusConnection(ModbusTcpParams(host="h66.local"))
|
|
42
|
+
try:
|
|
43
|
+
device = HusdataH66(connection.for_unit(1))
|
|
44
|
+
await device.async_update()
|
|
45
|
+
print(device.get("outdoor")) # 3.5 (°C)
|
|
46
|
+
print(device.get("compressor")) # True
|
|
47
|
+
print(device.values) # everything, keyed by register key
|
|
48
|
+
|
|
49
|
+
await device.async_set("room_setpoint", 21.5)
|
|
50
|
+
finally:
|
|
51
|
+
await connection.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
In Home Assistant, pass the unit from `async_get_unit` instead:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
unit = async_get_unit(
|
|
61
|
+
hass, entry, ModbusTcpParams(host=entry.data[CONF_HOST]), entry.data[CONF_UNIT_ID]
|
|
62
|
+
)
|
|
63
|
+
entry.runtime_data = HusdataH66(unit)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`async_update()` reads all registers in three requests and returns an `UpdateReport`. It raises `ModbusConnectionError` or `ModbusTimeoutError` when the gateway cannot be reached.
|
|
67
|
+
|
|
68
|
+
`async_set()` takes the value in the register's own unit and checks it against the register's range and step. It raises `ValueError` for an invalid value and `AttributeError` for a read-only register. `async_reset_alarm()` writes 1 to **Alarm reset**.
|
|
69
|
+
|
|
70
|
+
From the command line (read-only):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
husdata-h66 h66.local
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Register map
|
|
77
|
+
|
|
78
|
+
The H66 numbers its Modbus registers one per parameter, in the order its home page lists them. Read-only parameters are INPUT registers, settings are HOLDING registers, and every register is a signed or unsigned 16-bit value. The addresses depend on the heat pump controller and can move after a firmware update or when you add custom registers in the H66.
|
|
79
|
+
|
|
80
|
+
`REGO600_REGISTERS` is the default map, for a Rego 600 controller on firmware 360801. To follow the H66 instead, fetch its home page and parse it:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from husdata_h66 import HusdataH66, parse_firmware_version, parse_registers
|
|
84
|
+
|
|
85
|
+
page = await (await session.get("http://h66.local/")).text()
|
|
86
|
+
device = HusdataH66(unit, parse_registers(page))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`husdata-h66 h66.local --from-web` does the same from the command line.
|
|
90
|
+
|
|
91
|
+
Each parameter has a Husdata variable ID. Its first hex digit gives the type and scaling:
|
|
92
|
+
|
|
93
|
+
| First digit | Type | Scale | Unit |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
| 0 | Temperature | 0.1 | °C |
|
|
96
|
+
| 1 | Status | 1 | on/off |
|
|
97
|
+
| 2 | Number | 0.1 | |
|
|
98
|
+
| 3 | Percent | 0.1 | % |
|
|
99
|
+
| 5 | Energy | 0.1 | kWh |
|
|
100
|
+
| 6 | Hours | 1 | h |
|
|
101
|
+
| 7 | Minutes | 1 | min |
|
|
102
|
+
| B | Integer | 1 | |
|
|
103
|
+
| C | Power | 1 | W |
|
|
104
|
+
|
|
105
|
+
Temperatures and numbers are signed, the others unsigned. A temperature sensor that isn't connected reads `None` (the H66 shows −48.3 °C).
|
|
106
|
+
|
|
107
|
+
### Rego 600 (default map)
|
|
108
|
+
|
|
109
|
+
| Address | Type | ID | Key | Range |
|
|
110
|
+
|---|---|---|---|---|
|
|
111
|
+
| 0 | Input | 0001 | `radiator_return` | |
|
|
112
|
+
| 1 | Input | 0002 | `radiator_forward` | |
|
|
113
|
+
| 2 | Input | 0003 | `heat_carrier_return` | |
|
|
114
|
+
| 3 | Input | 0004 | `heat_carrier_forward` | |
|
|
115
|
+
| 4 | Input | 0005 | `brine_in` | |
|
|
116
|
+
| 5 | Input | 0006 | `brine_out` | |
|
|
117
|
+
| 6 | Input | 0007 | `outdoor` | |
|
|
118
|
+
| 7 | Input | 0008 | `indoor` | |
|
|
119
|
+
| 8 | Input | 0009 | `hot_water_top` | |
|
|
120
|
+
| 9 | Input | 000A | `hot_water_mid` | |
|
|
121
|
+
| 10 | Input | 000B | `hot_gas` | |
|
|
122
|
+
| 11 | Input | 3104 | `add_heat_status` | |
|
|
123
|
+
| 12 | Input | 0107 | `heating_setpoint` | |
|
|
124
|
+
| 13 | Input | 0111 | `hot_water_setpoint` | |
|
|
125
|
+
| 14 | Holding | 0203 | `room_setpoint` | 5–29 °C |
|
|
126
|
+
| 15 | Holding | 2204 | `room_sensor_influence` | |
|
|
127
|
+
| 16 | Holding | 2205 | `heat_curve` | 0–10 |
|
|
128
|
+
| 17 | Holding | 0207 | `heat_curve_parallel` | −10–10 °C |
|
|
129
|
+
| 18 | Holding | 0208 | `hot_water_stop_temperature` | 20–60 °C |
|
|
130
|
+
| 19 | Holding | 020B | `hot_water_difference` | 2–15 °C |
|
|
131
|
+
| 20 | Holding | 7209 | `extra_hot_water` | 0–1440 min |
|
|
132
|
+
| 21 | Holding | 1215 | `electric_heater_allowed` | on/off |
|
|
133
|
+
| 22 | Holding | B21B | `electric_heater_max_power` | 1–4 |
|
|
134
|
+
| 23 | Holding | 1233 | `external_control` | on/off |
|
|
135
|
+
| 24 | Holding | 020A | `summer_mode` | 0–30 °C |
|
|
136
|
+
| 25 | Holding | 2210 | `holiday_mode` | 0–30 |
|
|
137
|
+
| 26 | Holding | 12F2 | `alarm_reset` | write 1 |
|
|
138
|
+
| 27 | Input | 1A01 | `compressor` | |
|
|
139
|
+
| 28 | Input | 1A02 | `add_heat_step_1` | |
|
|
140
|
+
| 29 | Input | 1A03 | `add_heat_step_2` | |
|
|
141
|
+
| 30 | Input | 1A04 | `pump_cold_circuit` | |
|
|
142
|
+
| 31 | Input | 1A05 | `pump_heat_circuit` | |
|
|
143
|
+
| 32 | Input | 1A06 | `pump_radiator` | |
|
|
144
|
+
| 33 | Input | 1A07 | `switch_valve_1` | |
|
|
145
|
+
| 34 | Input | 1A20 | `alarm` | |
|
|
146
|
+
| 35 | Input | BA91 | `alarm_code` | |
|
|
147
|
+
| 36 | Input | 6C55 | `compressor_hours_heating` | |
|
|
148
|
+
| 37 | Input | 6C56 | `compressor_hours_hot_water` | |
|
|
149
|
+
| 38 | Input | 6C58 | `aux_heater_hours_heating` | |
|
|
150
|
+
| 39 | Input | 6C59 | `aux_heater_hours_hot_water` | |
|
|
151
|
+
| 40 | Input | CFAA | `power_consumption` | |
|
|
152
|
+
| 41 | Input | 5FAB | `accumulated_consumption` | |
|
|
153
|
+
|
|
154
|
+
`power_consumption` and `accumulated_consumption` are calculated by the H66 from the wattages set for each status variable on its web page. `accumulated_consumption` restarts at 0 when the H66 boots.
|
|
155
|
+
|
|
156
|
+
Not yet verified: `room_sensor_influence` is read unscaled because the H66 web page shows it that way, and the unit and scale of `holiday_mode` are unknown.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
uv sync
|
|
162
|
+
uv run pytest
|
|
163
|
+
uv run ruff check src tests
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`build.sh` publishes a release: it converts this README to `README.rst`, builds the documentation, copies it to the `jonnybergdahl.github.io` repository and pushes it, then builds the package and uploads it to PyPI.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
husdata-h66-modbus
|
|
2
|
+
==================
|
|
3
|
+
|
|
4
|
+
Async Python library for the Modbus TCP interface of the `Husdata
|
|
5
|
+
H66 <https://husdata.se/produkt/h66-wifi-eth-gateway-styr-din-varmepump/>`__
|
|
6
|
+
heat pump gateway.
|
|
7
|
+
|
|
8
|
+
It is built on
|
|
9
|
+
`modbus-connection <https://github.com/home-assistant-libs/modbus-connection>`__,
|
|
10
|
+
the Modbus layer Home Assistant uses from 2026.9. The library never
|
|
11
|
+
opens a connection itself: you give it a ``ModbusUnit``, either from
|
|
12
|
+
``modbus_connection`` or from Home Assistant’s ``async_get_unit``. It
|
|
13
|
+
has no Home Assistant dependency.
|
|
14
|
+
|
|
15
|
+
Tested with an H66 on firmware 360801 connected to an IVT Greenline
|
|
16
|
+
(Rego 600 controller).
|
|
17
|
+
|
|
18
|
+
Installation
|
|
19
|
+
------------
|
|
20
|
+
|
|
21
|
+
.. code:: bash
|
|
22
|
+
|
|
23
|
+
pip install husdata-h66-modbus
|
|
24
|
+
|
|
25
|
+
Add the ``cli`` extra to get the pymodbus backend and the
|
|
26
|
+
``husdata-h66`` command line tool:
|
|
27
|
+
|
|
28
|
+
.. code:: bash
|
|
29
|
+
|
|
30
|
+
pip install "husdata-h66-modbus[cli]"
|
|
31
|
+
|
|
32
|
+
Documentation
|
|
33
|
+
-------------
|
|
34
|
+
|
|
35
|
+
The full library documentation can be `found
|
|
36
|
+
here <https://jonnybergdahl.github.io/husdata-h66-modbus/>`__.
|
|
37
|
+
|
|
38
|
+
Enable Modbus on the H66
|
|
39
|
+
------------------------
|
|
40
|
+
|
|
41
|
+
Modbus TCP is off by default. On the H66 web page, open **Config** >
|
|
42
|
+
**Modbus**, set ``MODBUS_PORT`` to ``502`` and restart the H66.
|
|
43
|
+
``MODBUS_IP`` can limit which client may connect. The H66 accepts only
|
|
44
|
+
one Modbus client at a time.
|
|
45
|
+
|
|
46
|
+
Usage
|
|
47
|
+
-----
|
|
48
|
+
|
|
49
|
+
.. code:: python
|
|
50
|
+
|
|
51
|
+
import asyncio
|
|
52
|
+
|
|
53
|
+
from modbus_connection import ModbusTcpParams
|
|
54
|
+
from modbus_connection.pymodbus import ModbusConnection
|
|
55
|
+
|
|
56
|
+
from husdata_h66 import HusdataH66
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def main() -> None:
|
|
60
|
+
connection = ModbusConnection(ModbusTcpParams(host="h66.local"))
|
|
61
|
+
try:
|
|
62
|
+
device = HusdataH66(connection.for_unit(1))
|
|
63
|
+
await device.async_update()
|
|
64
|
+
print(device.get("outdoor")) # 3.5 (°C)
|
|
65
|
+
print(device.get("compressor")) # True
|
|
66
|
+
print(device.values) # everything, keyed by register key
|
|
67
|
+
|
|
68
|
+
await device.async_set("room_setpoint", 21.5)
|
|
69
|
+
finally:
|
|
70
|
+
await connection.close()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
|
|
75
|
+
In Home Assistant, pass the unit from ``async_get_unit`` instead:
|
|
76
|
+
|
|
77
|
+
.. code:: python
|
|
78
|
+
|
|
79
|
+
unit = async_get_unit(
|
|
80
|
+
hass, entry, ModbusTcpParams(host=entry.data[CONF_HOST]), entry.data[CONF_UNIT_ID]
|
|
81
|
+
)
|
|
82
|
+
entry.runtime_data = HusdataH66(unit)
|
|
83
|
+
|
|
84
|
+
``async_update()`` reads all registers in three requests and returns an
|
|
85
|
+
``UpdateReport``. It raises ``ModbusConnectionError`` or
|
|
86
|
+
``ModbusTimeoutError`` when the gateway cannot be reached.
|
|
87
|
+
|
|
88
|
+
``async_set()`` takes the value in the register’s own unit and checks it
|
|
89
|
+
against the register’s range and step. It raises ``ValueError`` for an
|
|
90
|
+
invalid value and ``AttributeError`` for a read-only register.
|
|
91
|
+
``async_reset_alarm()`` writes 1 to **Alarm reset**.
|
|
92
|
+
|
|
93
|
+
From the command line (read-only):
|
|
94
|
+
|
|
95
|
+
.. code:: bash
|
|
96
|
+
|
|
97
|
+
husdata-h66 h66.local
|
|
98
|
+
|
|
99
|
+
Register map
|
|
100
|
+
------------
|
|
101
|
+
|
|
102
|
+
The H66 numbers its Modbus registers one per parameter, in the order its
|
|
103
|
+
home page lists them. Read-only parameters are INPUT registers, settings
|
|
104
|
+
are HOLDING registers, and every register is a signed or unsigned 16-bit
|
|
105
|
+
value. The addresses depend on the heat pump controller and can move
|
|
106
|
+
after a firmware update or when you add custom registers in the H66.
|
|
107
|
+
|
|
108
|
+
``REGO600_REGISTERS`` is the default map, for a Rego 600 controller on
|
|
109
|
+
firmware 360801. To follow the H66 instead, fetch its home page and
|
|
110
|
+
parse it:
|
|
111
|
+
|
|
112
|
+
.. code:: python
|
|
113
|
+
|
|
114
|
+
from husdata_h66 import HusdataH66, parse_firmware_version, parse_registers
|
|
115
|
+
|
|
116
|
+
page = await (await session.get("http://h66.local/")).text()
|
|
117
|
+
device = HusdataH66(unit, parse_registers(page))
|
|
118
|
+
|
|
119
|
+
``husdata-h66 h66.local --from-web`` does the same from the command
|
|
120
|
+
line.
|
|
121
|
+
|
|
122
|
+
Each parameter has a Husdata variable ID. Its first hex digit gives the
|
|
123
|
+
type and scaling:
|
|
124
|
+
|
|
125
|
+
=========== =========== ===== ======
|
|
126
|
+
First digit Type Scale Unit
|
|
127
|
+
=========== =========== ===== ======
|
|
128
|
+
0 Temperature 0.1 °C
|
|
129
|
+
1 Status 1 on/off
|
|
130
|
+
2 Number 0.1
|
|
131
|
+
3 Percent 0.1 %
|
|
132
|
+
5 Energy 0.1 kWh
|
|
133
|
+
6 Hours 1 h
|
|
134
|
+
7 Minutes 1 min
|
|
135
|
+
B Integer 1
|
|
136
|
+
C Power 1 W
|
|
137
|
+
=========== =========== ===== ======
|
|
138
|
+
|
|
139
|
+
Temperatures and numbers are signed, the others unsigned. A temperature
|
|
140
|
+
sensor that isn’t connected reads ``None`` (the H66 shows −48.3 °C).
|
|
141
|
+
|
|
142
|
+
Rego 600 (default map)
|
|
143
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
144
|
+
|
|
145
|
+
======= ======= ==== ============================== ==========
|
|
146
|
+
Address Type ID Key Range
|
|
147
|
+
======= ======= ==== ============================== ==========
|
|
148
|
+
0 Input 0001 ``radiator_return``
|
|
149
|
+
1 Input 0002 ``radiator_forward``
|
|
150
|
+
2 Input 0003 ``heat_carrier_return``
|
|
151
|
+
3 Input 0004 ``heat_carrier_forward``
|
|
152
|
+
4 Input 0005 ``brine_in``
|
|
153
|
+
5 Input 0006 ``brine_out``
|
|
154
|
+
6 Input 0007 ``outdoor``
|
|
155
|
+
7 Input 0008 ``indoor``
|
|
156
|
+
8 Input 0009 ``hot_water_top``
|
|
157
|
+
9 Input 000A ``hot_water_mid``
|
|
158
|
+
10 Input 000B ``hot_gas``
|
|
159
|
+
11 Input 3104 ``add_heat_status``
|
|
160
|
+
12 Input 0107 ``heating_setpoint``
|
|
161
|
+
13 Input 0111 ``hot_water_setpoint``
|
|
162
|
+
14 Holding 0203 ``room_setpoint`` 5–29 °C
|
|
163
|
+
15 Holding 2204 ``room_sensor_influence``
|
|
164
|
+
16 Holding 2205 ``heat_curve`` 0–10
|
|
165
|
+
17 Holding 0207 ``heat_curve_parallel`` −10–10 °C
|
|
166
|
+
18 Holding 0208 ``hot_water_stop_temperature`` 20–60 °C
|
|
167
|
+
19 Holding 020B ``hot_water_difference`` 2–15 °C
|
|
168
|
+
20 Holding 7209 ``extra_hot_water`` 0–1440 min
|
|
169
|
+
21 Holding 1215 ``electric_heater_allowed`` on/off
|
|
170
|
+
22 Holding B21B ``electric_heater_max_power`` 1–4
|
|
171
|
+
23 Holding 1233 ``external_control`` on/off
|
|
172
|
+
24 Holding 020A ``summer_mode`` 0–30 °C
|
|
173
|
+
25 Holding 2210 ``holiday_mode`` 0–30
|
|
174
|
+
26 Holding 12F2 ``alarm_reset`` write 1
|
|
175
|
+
27 Input 1A01 ``compressor``
|
|
176
|
+
28 Input 1A02 ``add_heat_step_1``
|
|
177
|
+
29 Input 1A03 ``add_heat_step_2``
|
|
178
|
+
30 Input 1A04 ``pump_cold_circuit``
|
|
179
|
+
31 Input 1A05 ``pump_heat_circuit``
|
|
180
|
+
32 Input 1A06 ``pump_radiator``
|
|
181
|
+
33 Input 1A07 ``switch_valve_1``
|
|
182
|
+
34 Input 1A20 ``alarm``
|
|
183
|
+
35 Input BA91 ``alarm_code``
|
|
184
|
+
36 Input 6C55 ``compressor_hours_heating``
|
|
185
|
+
37 Input 6C56 ``compressor_hours_hot_water``
|
|
186
|
+
38 Input 6C58 ``aux_heater_hours_heating``
|
|
187
|
+
39 Input 6C59 ``aux_heater_hours_hot_water``
|
|
188
|
+
40 Input CFAA ``power_consumption``
|
|
189
|
+
41 Input 5FAB ``accumulated_consumption``
|
|
190
|
+
======= ======= ==== ============================== ==========
|
|
191
|
+
|
|
192
|
+
``power_consumption`` and ``accumulated_consumption`` are calculated by
|
|
193
|
+
the H66 from the wattages set for each status variable on its web page.
|
|
194
|
+
``accumulated_consumption`` restarts at 0 when the H66 boots.
|
|
195
|
+
|
|
196
|
+
Not yet verified: ``room_sensor_influence`` is read unscaled because the
|
|
197
|
+
H66 web page shows it that way, and the unit and scale of
|
|
198
|
+
``holiday_mode`` are unknown.
|
|
199
|
+
|
|
200
|
+
Development
|
|
201
|
+
-----------
|
|
202
|
+
|
|
203
|
+
.. code:: bash
|
|
204
|
+
|
|
205
|
+
uv sync
|
|
206
|
+
uv run pytest
|
|
207
|
+
uv run ruff check src tests
|
|
208
|
+
|
|
209
|
+
``build.sh`` publishes a release: it converts this README to
|
|
210
|
+
``README.rst``, builds the documentation, copies it to the
|
|
211
|
+
``jonnybergdahl.github.io`` repository and pushes it, then builds the
|
|
212
|
+
package and uploads it to PyPI.
|
|
213
|
+
|
|
214
|
+
License
|
|
215
|
+
-------
|
|
216
|
+
|
|
217
|
+
MIT
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "husdata-h66-modbus"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async Modbus TCP library for the Husdata H66 heat pump gateway"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.13"
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Jonny Bergdahl", email = "bergdahl@users.noreply.github.com"},
|
|
15
|
+
]
|
|
16
|
+
keywords = ["husdata", "h66", "modbus", "heat pump", "ivt", "rego", "home assistant"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Framework :: AsyncIO",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Home Automation",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"modbus-connection>=4.12",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
cli = ["modbus-connection[pymodbus]>=4.12"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/jonnybergdahl/Python-husdata-h66-modbus"
|
|
38
|
+
Repository = "https://github.com/jonnybergdahl/Python-husdata-h66-modbus"
|
|
39
|
+
Documentation = "https://jonnybergdahl.github.io/husdata-h66-modbus/"
|
|
40
|
+
Issues = "https://github.com/jonnybergdahl/Python-husdata-h66-modbus/issues"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
husdata-h66 = "husdata_h66.__main__:main"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"modbus-connection[pymodbus]>=4.12",
|
|
48
|
+
"pytest>=8",
|
|
49
|
+
"pytest-asyncio>=0.24",
|
|
50
|
+
"ruff>=0.8",
|
|
51
|
+
]
|
|
52
|
+
docs = [
|
|
53
|
+
"sphinx>=8",
|
|
54
|
+
"sphinx-autodoc-typehints>=2",
|
|
55
|
+
"sphinx-rtd-theme>=3",
|
|
56
|
+
]
|
|
57
|
+
release = [
|
|
58
|
+
"build>=1.2",
|
|
59
|
+
"twine>=6",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.setuptools.packages.find]
|
|
63
|
+
where = ["src"]
|
|
64
|
+
|
|
65
|
+
[tool.setuptools.package-data]
|
|
66
|
+
husdata_h66 = ["py.typed"]
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
asyncio_mode = "auto"
|
|
70
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 88
|
|
75
|
+
target-version = "py313"
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
|