wavin-sentio-connect 0.2.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 HairingX
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,259 @@
1
+ Metadata-Version: 2.4
2
+ Name: wavin_sentio_connect
3
+ Version: 0.2.0
4
+ Summary: Event-driven, async client for the Wavin Sentio floor heating controller: every room and peripheral, typed, and told when a value changes - over Modbus TCP.
5
+ Home-page: https://github.com/HairingX/wavin_sentio_connect
6
+ Author: HairingX
7
+ License: MIT
8
+ Project-URL: Source, https://github.com/HairingX/wavin_sentio_connect
9
+ Project-URL: Issues, https://github.com/HairingX/wavin_sentio_connect/issues
10
+ Project-URL: Releases, https://github.com/HairingX/wavin_sentio_connect/releases
11
+ Keywords: wavin,sentio,floor heating,modbus,modbus-tcp,asyncio
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Home Automation
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.12
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: modbus_event_connect<0.3,>=0.2.0
29
+ Dynamic: license-file
30
+
31
+ # Wavin Sentio Connect
32
+
33
+ An event-driven Python client for the **Wavin Sentio** floor heating controller over Modbus TCP,
34
+ built on [modbus_event_connect](https://github.com/HairingX/modbus_event_connect).
35
+
36
+ The register map is complete for the location, all 16 rooms and all 64 peripheral slots,
37
+ modelled from the official Sentio Modbus manual. Connecting finds out which rooms and
38
+ peripherals your installation actually has; you subscribe to the values you care about and are
39
+ told when one changes - with its quality, so "offline", "no reading" and a real value never look
40
+ alike.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install wavin-sentio-connect
46
+ ```
47
+
48
+ ## Enabling Modbus on the controller
49
+
50
+ Modbus is **disabled by default**. Enable it from a Sentio Display:
51
+
52
+ `System | Installer settings | Modbus configuration | Modbus TCP`
53
+
54
+ The controller restarts afterwards. It uses DHCP; its hostname is
55
+ `Wavin Sentio CCU#[last four digits of the serial number]`.
56
+
57
+ ## Usage
58
+
59
+ ```python
60
+ import asyncio
61
+ from wavin_sentio_connect import RoomPointKey, create_client, room_key, rooms
62
+
63
+ def on_change(key, old, new):
64
+ print(f"{key}: {new.value} ({new.quality.name})")
65
+
66
+ async def main():
67
+ client = create_client("<device-ip>")
68
+ await client.connect() # finds the installed rooms and peripherals
69
+
70
+ for room in rooms(client):
71
+ print(room.number, room.name, "dummy" if room.is_dummy else "")
72
+ client.subscribe(room_key(room.number, RoomPointKey.TEMP_AIR_CURRENT), on_change)
73
+
74
+ while True: # you own the clock; the library owns the plan
75
+ await client.poll() # reads only what is due - free when nothing is
76
+ await asyncio.sleep(1)
77
+
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ When `connect()` returns, `client.points` holds exactly what this installation has - a room that
82
+ was never set up is not there, so nothing is built for it. Nor is what the controller says a
83
+ room lacks: a dummy room ("no thermostat or sensor installed") has no temperature, humidity or
84
+ dew point, and a room not associated with radiators, underfloor heating, drying, thermal
85
+ integration or ventilation has no state or blocking source for it. Nor has a peripheral what
86
+ its type is known to lack, as `NOT_SUPPORTED` lists: the manual's wired RT-201, RS-211 and
87
+ ET-210, the LCD-200 and the EU-208-A have no signal strength.
88
+ `rooms(client)` and `peripherals(client)` describe the installation from values already read,
89
+ with no extra requests.
90
+
91
+ When the installation changes, `poll()` finds it; nobody needs to connect again. What each scan
92
+ read is read again at `PollRate.SCAN`, and where it has changed, only that part is scanned again:
93
+
94
+ | What changed | What is scanned again |
95
+ |---|---|
96
+ | a room's type, or what it is associated with | that room |
97
+ | the peripheral in a slot, its serial number or the room it belongs to | that slot |
98
+ | the address space, the software version or the controller's serial number | everything |
99
+
100
+ `client.subscribe_points(callback)` is told the keys gained and lost - a room set up, a
101
+ peripheral paired or removed.
102
+
103
+ Every value is a `DataValue`: `value`, `quality` and `timestamp` (UTC).
104
+
105
+ | Quality | Meaning |
106
+ |---|---|
107
+ | `GOOD` | the controller answered with a valid value |
108
+ | `NO_DATA` | it answered "no reading" - a missing sensor, an unconfigured limit |
109
+ | `OFFLINE` | the register exists but what is behind it is not answering |
110
+ | `STALE` | the last read failed; the value is the last good one |
111
+
112
+ ## How often values are read
113
+
114
+ Every point has a poll rate, and the library reads it when it is due:
115
+
116
+ | Poll rate | Default | Sentio uses it for |
117
+ |---|---|---|
118
+ | `FAST` | 10 s | room states and blocking sources |
119
+ | `MEDIUM` | 30 s | temperatures, humidity, dew point |
120
+ | `SLOW` | 60 s | settings |
121
+ | `RARE` | 15 min | peripheral signal strength |
122
+ | `STATIC` | at connect | versions, serial numbers, names, room types |
123
+ | `SCAN` | 15 min | what the scans read, to find changes to the installation |
124
+
125
+ Override any of them, or a single key:
126
+
127
+ ```python
128
+ from modbus_event_connect import PollRate
129
+ client.set_poll_interval(PollRate.FAST, 5)
130
+ client.set_poll_interval("room_4_temp_air_current", 2)
131
+ await client.refresh(PollRate.STATIC) # re-read the static values now
132
+ ```
133
+
134
+ Only what something wants is read: a subscriber, or `client.set_polling(key)`.
135
+
136
+ ## Writing
137
+
138
+ ```python
139
+ await client.write(room_key(1, RoomPointKey.TEMP_AIR_TARGET), 21.5)
140
+ await client.write(room_key(1, RoomPointKey.LOCK), RoomLock.HOTEL) # LOCKED / HOTEL / UNLOCKED
141
+ ```
142
+
143
+ - A value of the wrong type is a type error before the program runs: the lock takes a
144
+ `RoomLock`, a temperature a `float`.
145
+ - A value is also checked before anything is sent: its range, its states, and the controller's
146
+ "no reading" sentinel, which could never be read back.
147
+ - Writes are sent in order. Tapping + five times sends the first value and the last, not all
148
+ five.
149
+ - The written point is read back, so subscribers see what the controller holds rather than what
150
+ was asked for. A write that changes the rooms' regulated targets - vacation, standby, a room's
151
+ setpoint, mode or preset - reads those targets again too.
152
+ - The controller answers `SERVER_DEVICE_BUSY` (`0x06`) while it stores a change; the manual says
153
+ such a request "shall be repeated again", and the library does, with backoff.
154
+ - `client.status(Status.WRITE_PENDING)` - which `client.subscribe_status` follows - is true from
155
+ the moment a write is asked for until the last one has finished.
156
+
157
+ For monitoring only, create the client read-only; every write is then refused before it reaches
158
+ the controller:
159
+
160
+ ```python
161
+ client = create_client("<device-ip>", read_only=True)
162
+ ```
163
+
164
+ ## Alarms
165
+
166
+ Every alarm and warning the manual documents for the location, the rooms and the peripherals is
167
+ a key: `system_warning`, `system_error`, and per room and peripheral `..._warning`,
168
+ `..._error`, `..._low_battery` and `room_{n}_peripheral_lost` / `peripheral_{n}_lost`.
169
+
170
+ The manual describes the location's two bits as covering the whole system ("A problem is
171
+ pending in whole system"). Those two are always read at the `FAST` rate; when either changes,
172
+ every alarm someone subscribes to is read at once. Each alarm is also read by itself at the
173
+ `RARE` rate, in case a controller does not reflect it in the system's bits.
174
+
175
+ ## Sharing a connection
176
+
177
+ To put the controller on a connection the host already owns - a gateway shared with other
178
+ devices - use `create_client_on`:
179
+
180
+ ```python
181
+ from modbus_event_connect.modbus import ModbusTcpConnection
182
+ from wavin_sentio_connect import create_client_on
183
+
184
+ connection = ModbusTcpConnection("<device-ip>")
185
+ client = create_client_on(connection, unit_id=1)
186
+ ```
187
+
188
+ The client is asynchronous end to end - no threads, nothing that blocks the event loop - and
189
+ owns no timer: the application calls `poll()` from its own loop.
190
+
191
+ ## Keys and addressing
192
+
193
+ Every point has its key in `LocationPointKey`, `RoomPointKey` or `PeripheralPointKey`, with the
194
+ type of its value. A location point's is its whole key; a room's or a peripheral's becomes one
195
+ with the instance:
196
+
197
+ ```python
198
+ from wavin_sentio_connect import ROOM, LocationPointKey, RoomPointKey, room_key
199
+
200
+ client.value(LocationPointKey.VACATION_ENABLE) # key "vacation_enable"
201
+ for n in client.instances(ROOM): # the rooms this installation has
202
+ client.value(room_key(n, RoomPointKey.TEMP_AIR_CURRENT)) # key "room_4_temp_air_current"
203
+ client.value(room_key(n, RoomPointKey.STATE)) # a RoomState, such as HEATING
204
+ ```
205
+
206
+ A room point's key is the same in every room, so code that handles
207
+ `RoomPointKey.TEMP_AIR_CURRENT` once handles it in all of them; `RoomPointKey.all()` lists them.
208
+ `UNITS` is every unit a Sentio point has. Key strings never change; new points only add keys.
209
+
210
+ A state reads as its member of the enum the manual's values give: `RoomState`, `BlockingSource`,
211
+ `RoomType`, `RoomMode`, `RoomModeOverride`, `TemperaturePreset`, `RoomLock`, `HeatingCoolingMode`,
212
+ `HeatingCoolingModeOverride`, `DeviceType`, `ModbusMode`, `UpdateMode` or `PeripheralType`, and is
213
+ written as one. A number the manual does not name reads as `NO_DATA`, and the value's `.raw` holds
214
+ the number the controller sent. Standby, vacation and daylight saving, which the manual gives as
215
+ 0 and 1, are `bool`; their "no value", 255, reads as `NO_DATA` too.
216
+
217
+ The model is in [`_model.py`](https://github.com/HairingX/wavin_sentio_connect/blob/main/src/wavin_sentio_connect/_model.py); every key is declared there
218
+ with its address and encoding.
219
+
220
+ The manual's "Modbus Address" column holds the addresses themselves, so its numbers are used unchanged:
221
+
222
+ | Object | Base | Instances |
223
+ |---|---|---|
224
+ | Location | `0` | one |
225
+ | Room *N* | `N * 100` | 1–16 |
226
+ | Peripheral *N* | `51100 + N * 100` | 1–64 |
227
+
228
+ Peripheral slots are **not stable identities** - the controller reorders them when peripherals
229
+ are learned or unlearned. Use `peripheral_{n}_serial_number` to recognise a device, and
230
+ `peripheral_{n}_owner` (`0` = location, `1`–`16` = room) to find the room it belongs to.
231
+
232
+ Two room values are easy to confuse: `room_{n}_temp_air_target` is the user's setting, and
233
+ `room_{n}_temp_air_target_active` is the target the controller is regulating to right now -
234
+ different under standby, vacation or a schedule.
235
+
236
+ ## Documentation
237
+
238
+ - [`docs/sentio-modbus-reference.md`](https://github.com/HairingX/wavin_sentio_connect/blob/main/docs/sentio-modbus-reference.md) - the protocol: register
239
+ tables, data types, error handling, enumerations.
240
+ - [`docs/sentio-registers.csv`](https://github.com/HairingX/wavin_sentio_connect/blob/main/docs/sentio-registers.csv) - all 344 documented registers, one
241
+ row each.
242
+
243
+ ## Known gaps
244
+
245
+ - Only the location, room and peripheral objects are modelled. Outdoor, DHW, ITC, HCC, buffer
246
+ tank, ventilation and dehumidifier objects are documented in the CSV but not yet wired.
247
+
248
+ ## Disclaimer
249
+
250
+ Wavin Sentio Connect is provided "as is", without warranty of any kind. The authors and
251
+ contributors are not responsible for any damage or data loss that may occur from using this
252
+ library. Users are solely responsible for ensuring the proper and safe operation of their
253
+ Modbus devices.
254
+
255
+ This project is not affiliated with or endorsed by Wavin.
256
+
257
+ ## License
258
+
259
+ MIT. See [LICENSE](https://github.com/HairingX/wavin_sentio_connect/blob/main/LICENSE).
@@ -0,0 +1,229 @@
1
+ # Wavin Sentio Connect
2
+
3
+ An event-driven Python client for the **Wavin Sentio** floor heating controller over Modbus TCP,
4
+ built on [modbus_event_connect](https://github.com/HairingX/modbus_event_connect).
5
+
6
+ The register map is complete for the location, all 16 rooms and all 64 peripheral slots,
7
+ modelled from the official Sentio Modbus manual. Connecting finds out which rooms and
8
+ peripherals your installation actually has; you subscribe to the values you care about and are
9
+ told when one changes - with its quality, so "offline", "no reading" and a real value never look
10
+ alike.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install wavin-sentio-connect
16
+ ```
17
+
18
+ ## Enabling Modbus on the controller
19
+
20
+ Modbus is **disabled by default**. Enable it from a Sentio Display:
21
+
22
+ `System | Installer settings | Modbus configuration | Modbus TCP`
23
+
24
+ The controller restarts afterwards. It uses DHCP; its hostname is
25
+ `Wavin Sentio CCU#[last four digits of the serial number]`.
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ import asyncio
31
+ from wavin_sentio_connect import RoomPointKey, create_client, room_key, rooms
32
+
33
+ def on_change(key, old, new):
34
+ print(f"{key}: {new.value} ({new.quality.name})")
35
+
36
+ async def main():
37
+ client = create_client("<device-ip>")
38
+ await client.connect() # finds the installed rooms and peripherals
39
+
40
+ for room in rooms(client):
41
+ print(room.number, room.name, "dummy" if room.is_dummy else "")
42
+ client.subscribe(room_key(room.number, RoomPointKey.TEMP_AIR_CURRENT), on_change)
43
+
44
+ while True: # you own the clock; the library owns the plan
45
+ await client.poll() # reads only what is due - free when nothing is
46
+ await asyncio.sleep(1)
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ When `connect()` returns, `client.points` holds exactly what this installation has - a room that
52
+ was never set up is not there, so nothing is built for it. Nor is what the controller says a
53
+ room lacks: a dummy room ("no thermostat or sensor installed") has no temperature, humidity or
54
+ dew point, and a room not associated with radiators, underfloor heating, drying, thermal
55
+ integration or ventilation has no state or blocking source for it. Nor has a peripheral what
56
+ its type is known to lack, as `NOT_SUPPORTED` lists: the manual's wired RT-201, RS-211 and
57
+ ET-210, the LCD-200 and the EU-208-A have no signal strength.
58
+ `rooms(client)` and `peripherals(client)` describe the installation from values already read,
59
+ with no extra requests.
60
+
61
+ When the installation changes, `poll()` finds it; nobody needs to connect again. What each scan
62
+ read is read again at `PollRate.SCAN`, and where it has changed, only that part is scanned again:
63
+
64
+ | What changed | What is scanned again |
65
+ |---|---|
66
+ | a room's type, or what it is associated with | that room |
67
+ | the peripheral in a slot, its serial number or the room it belongs to | that slot |
68
+ | the address space, the software version or the controller's serial number | everything |
69
+
70
+ `client.subscribe_points(callback)` is told the keys gained and lost - a room set up, a
71
+ peripheral paired or removed.
72
+
73
+ Every value is a `DataValue`: `value`, `quality` and `timestamp` (UTC).
74
+
75
+ | Quality | Meaning |
76
+ |---|---|
77
+ | `GOOD` | the controller answered with a valid value |
78
+ | `NO_DATA` | it answered "no reading" - a missing sensor, an unconfigured limit |
79
+ | `OFFLINE` | the register exists but what is behind it is not answering |
80
+ | `STALE` | the last read failed; the value is the last good one |
81
+
82
+ ## How often values are read
83
+
84
+ Every point has a poll rate, and the library reads it when it is due:
85
+
86
+ | Poll rate | Default | Sentio uses it for |
87
+ |---|---|---|
88
+ | `FAST` | 10 s | room states and blocking sources |
89
+ | `MEDIUM` | 30 s | temperatures, humidity, dew point |
90
+ | `SLOW` | 60 s | settings |
91
+ | `RARE` | 15 min | peripheral signal strength |
92
+ | `STATIC` | at connect | versions, serial numbers, names, room types |
93
+ | `SCAN` | 15 min | what the scans read, to find changes to the installation |
94
+
95
+ Override any of them, or a single key:
96
+
97
+ ```python
98
+ from modbus_event_connect import PollRate
99
+ client.set_poll_interval(PollRate.FAST, 5)
100
+ client.set_poll_interval("room_4_temp_air_current", 2)
101
+ await client.refresh(PollRate.STATIC) # re-read the static values now
102
+ ```
103
+
104
+ Only what something wants is read: a subscriber, or `client.set_polling(key)`.
105
+
106
+ ## Writing
107
+
108
+ ```python
109
+ await client.write(room_key(1, RoomPointKey.TEMP_AIR_TARGET), 21.5)
110
+ await client.write(room_key(1, RoomPointKey.LOCK), RoomLock.HOTEL) # LOCKED / HOTEL / UNLOCKED
111
+ ```
112
+
113
+ - A value of the wrong type is a type error before the program runs: the lock takes a
114
+ `RoomLock`, a temperature a `float`.
115
+ - A value is also checked before anything is sent: its range, its states, and the controller's
116
+ "no reading" sentinel, which could never be read back.
117
+ - Writes are sent in order. Tapping + five times sends the first value and the last, not all
118
+ five.
119
+ - The written point is read back, so subscribers see what the controller holds rather than what
120
+ was asked for. A write that changes the rooms' regulated targets - vacation, standby, a room's
121
+ setpoint, mode or preset - reads those targets again too.
122
+ - The controller answers `SERVER_DEVICE_BUSY` (`0x06`) while it stores a change; the manual says
123
+ such a request "shall be repeated again", and the library does, with backoff.
124
+ - `client.status(Status.WRITE_PENDING)` - which `client.subscribe_status` follows - is true from
125
+ the moment a write is asked for until the last one has finished.
126
+
127
+ For monitoring only, create the client read-only; every write is then refused before it reaches
128
+ the controller:
129
+
130
+ ```python
131
+ client = create_client("<device-ip>", read_only=True)
132
+ ```
133
+
134
+ ## Alarms
135
+
136
+ Every alarm and warning the manual documents for the location, the rooms and the peripherals is
137
+ a key: `system_warning`, `system_error`, and per room and peripheral `..._warning`,
138
+ `..._error`, `..._low_battery` and `room_{n}_peripheral_lost` / `peripheral_{n}_lost`.
139
+
140
+ The manual describes the location's two bits as covering the whole system ("A problem is
141
+ pending in whole system"). Those two are always read at the `FAST` rate; when either changes,
142
+ every alarm someone subscribes to is read at once. Each alarm is also read by itself at the
143
+ `RARE` rate, in case a controller does not reflect it in the system's bits.
144
+
145
+ ## Sharing a connection
146
+
147
+ To put the controller on a connection the host already owns - a gateway shared with other
148
+ devices - use `create_client_on`:
149
+
150
+ ```python
151
+ from modbus_event_connect.modbus import ModbusTcpConnection
152
+ from wavin_sentio_connect import create_client_on
153
+
154
+ connection = ModbusTcpConnection("<device-ip>")
155
+ client = create_client_on(connection, unit_id=1)
156
+ ```
157
+
158
+ The client is asynchronous end to end - no threads, nothing that blocks the event loop - and
159
+ owns no timer: the application calls `poll()` from its own loop.
160
+
161
+ ## Keys and addressing
162
+
163
+ Every point has its key in `LocationPointKey`, `RoomPointKey` or `PeripheralPointKey`, with the
164
+ type of its value. A location point's is its whole key; a room's or a peripheral's becomes one
165
+ with the instance:
166
+
167
+ ```python
168
+ from wavin_sentio_connect import ROOM, LocationPointKey, RoomPointKey, room_key
169
+
170
+ client.value(LocationPointKey.VACATION_ENABLE) # key "vacation_enable"
171
+ for n in client.instances(ROOM): # the rooms this installation has
172
+ client.value(room_key(n, RoomPointKey.TEMP_AIR_CURRENT)) # key "room_4_temp_air_current"
173
+ client.value(room_key(n, RoomPointKey.STATE)) # a RoomState, such as HEATING
174
+ ```
175
+
176
+ A room point's key is the same in every room, so code that handles
177
+ `RoomPointKey.TEMP_AIR_CURRENT` once handles it in all of them; `RoomPointKey.all()` lists them.
178
+ `UNITS` is every unit a Sentio point has. Key strings never change; new points only add keys.
179
+
180
+ A state reads as its member of the enum the manual's values give: `RoomState`, `BlockingSource`,
181
+ `RoomType`, `RoomMode`, `RoomModeOverride`, `TemperaturePreset`, `RoomLock`, `HeatingCoolingMode`,
182
+ `HeatingCoolingModeOverride`, `DeviceType`, `ModbusMode`, `UpdateMode` or `PeripheralType`, and is
183
+ written as one. A number the manual does not name reads as `NO_DATA`, and the value's `.raw` holds
184
+ the number the controller sent. Standby, vacation and daylight saving, which the manual gives as
185
+ 0 and 1, are `bool`; their "no value", 255, reads as `NO_DATA` too.
186
+
187
+ The model is in [`_model.py`](https://github.com/HairingX/wavin_sentio_connect/blob/main/src/wavin_sentio_connect/_model.py); every key is declared there
188
+ with its address and encoding.
189
+
190
+ The manual's "Modbus Address" column holds the addresses themselves, so its numbers are used unchanged:
191
+
192
+ | Object | Base | Instances |
193
+ |---|---|---|
194
+ | Location | `0` | one |
195
+ | Room *N* | `N * 100` | 1–16 |
196
+ | Peripheral *N* | `51100 + N * 100` | 1–64 |
197
+
198
+ Peripheral slots are **not stable identities** - the controller reorders them when peripherals
199
+ are learned or unlearned. Use `peripheral_{n}_serial_number` to recognise a device, and
200
+ `peripheral_{n}_owner` (`0` = location, `1`–`16` = room) to find the room it belongs to.
201
+
202
+ Two room values are easy to confuse: `room_{n}_temp_air_target` is the user's setting, and
203
+ `room_{n}_temp_air_target_active` is the target the controller is regulating to right now -
204
+ different under standby, vacation or a schedule.
205
+
206
+ ## Documentation
207
+
208
+ - [`docs/sentio-modbus-reference.md`](https://github.com/HairingX/wavin_sentio_connect/blob/main/docs/sentio-modbus-reference.md) - the protocol: register
209
+ tables, data types, error handling, enumerations.
210
+ - [`docs/sentio-registers.csv`](https://github.com/HairingX/wavin_sentio_connect/blob/main/docs/sentio-registers.csv) - all 344 documented registers, one
211
+ row each.
212
+
213
+ ## Known gaps
214
+
215
+ - Only the location, room and peripheral objects are modelled. Outdoor, DHW, ITC, HCC, buffer
216
+ tank, ventilation and dehumidifier objects are documented in the CSV but not yet wired.
217
+
218
+ ## Disclaimer
219
+
220
+ Wavin Sentio Connect is provided "as is", without warranty of any kind. The authors and
221
+ contributors are not responsible for any damage or data loss that may occur from using this
222
+ library. Users are solely responsible for ensuring the proper and safe operation of their
223
+ Modbus devices.
224
+
225
+ This project is not affiliated with or endorsed by Wavin.
226
+
227
+ ## License
228
+
229
+ MIT. See [LICENSE](https://github.com/HairingX/wavin_sentio_connect/blob/main/LICENSE).
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.pytest.ini_options]
6
+ asyncio_mode = "auto"
7
+ asyncio_default_fixture_loop_scope = "function"
8
+ markers = [
9
+ "live: talks to the real controller; skipped unless one is configured. Exclude with -m \"not live\".",
10
+ ]
11
+ log_cli = true
12
+ log_cli_level = "DEBUG"
13
+ log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
14
+ log_cli_date_format = "%Y-%m-%d %H:%M:%S"
15
+
16
+ [tool.pyright]
17
+ # The standard belongs to the repository, not to a personal editor setting: an IDE and a CI
18
+ # run must agree on what counts as an error.
19
+ include = ["src", "tests"]
20
+ exclude = ["**/__pycache__", "**/.venv", "**/.claude"]
21
+ typeCheckingMode = "strict"
22
+ reportMissingTypeStubs = "none"
23
+
24
+ [[tool.pyright.executionEnvironments]]
25
+ # Tests import the installed package, as its users do (pip install -e .), and each other as
26
+ # `conftest`, as pytest resolves them from the tests directory.
27
+ root = "tests"
28
+ extraPaths = ["tests"]
29
+ reportPrivateUsage = "none"
@@ -0,0 +1,48 @@
1
+ [metadata]
2
+ name = wavin_sentio_connect
3
+ version = attr: wavin_sentio_connect.__version__
4
+ author = HairingX
5
+ license = MIT
6
+ description = Event-driven, async client for the Wavin Sentio floor heating controller: every room and peripheral, typed, and told when a value changes - over Modbus TCP.
7
+ keywords = wavin, sentio, floor heating, modbus, modbus-tcp, asyncio
8
+ url = https://github.com/HairingX/wavin_sentio_connect
9
+ project_urls =
10
+ Source = https://github.com/HairingX/wavin_sentio_connect
11
+ Issues = https://github.com/HairingX/wavin_sentio_connect/issues
12
+ Releases = https://github.com/HairingX/wavin_sentio_connect/releases
13
+ long_description = file: README.md
14
+ long_description_content_type = text/markdown
15
+ classifiers =
16
+ Development Status :: 3 - Alpha
17
+ Framework :: AsyncIO
18
+ Intended Audience :: Developers
19
+ License :: OSI Approved :: MIT License
20
+ Operating System :: OS Independent
21
+ Programming Language :: Python :: 3
22
+ Programming Language :: Python :: 3 :: Only
23
+ Programming Language :: Python :: 3.12
24
+ Programming Language :: Python :: 3.13
25
+ Programming Language :: Python :: 3.14
26
+ Topic :: Software Development :: Libraries
27
+ Topic :: Home Automation
28
+ Typing :: Typed
29
+
30
+ [options]
31
+ python_requires = >=3.12
32
+ packages = find:
33
+ package_dir =
34
+ =src
35
+ zip_safe = False
36
+ install_requires =
37
+ modbus_event_connect >=0.2.0,<0.3
38
+
39
+ [options.packages.find]
40
+ where = src
41
+
42
+ [options.package_data]
43
+ wavin_sentio_connect = py.typed
44
+
45
+ [egg_info]
46
+ tag_build =
47
+ tag_date = 0
48
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python
2
+
3
+ import setuptools
4
+
5
+ if __name__ == "__main__":
6
+ setuptools.setup() # type: ignore
@@ -0,0 +1,73 @@
1
+ """Wavin Sentio over Modbus TCP, built on modbus_event_connect."""
2
+ from ._model import (
3
+ NOT_SUPPORTED,
4
+ PERIPHERAL,
5
+ ROOM,
6
+ SENTIO,
7
+ UNITS,
8
+ BlockingSource,
9
+ DeviceType,
10
+ HeatingCoolingMode,
11
+ HeatingCoolingModeOverride,
12
+ LocationPointKey,
13
+ ModbusMode,
14
+ PeripheralPointKey,
15
+ PeripheralType,
16
+ PointKey,
17
+ RoomLock,
18
+ RoomMode,
19
+ RoomModeOverride,
20
+ RoomPointKey,
21
+ RoomState,
22
+ RoomType,
23
+ TemperaturePreset,
24
+ UpdateMode,
25
+ peripheral_key,
26
+ room_key,
27
+ )
28
+ from ._sentio import (
29
+ DEFAULT_PORT,
30
+ DEFAULT_UNIT_ID,
31
+ SentioPeripheral,
32
+ SentioRoom,
33
+ create_client,
34
+ create_client_on,
35
+ peripherals,
36
+ rooms,
37
+ )
38
+
39
+ __version__ = "0.2.0"
40
+ __all__ = [
41
+ "DEFAULT_PORT",
42
+ "DEFAULT_UNIT_ID",
43
+ "NOT_SUPPORTED",
44
+ "PERIPHERAL",
45
+ "ROOM",
46
+ "SENTIO",
47
+ "UNITS",
48
+ "BlockingSource",
49
+ "DeviceType",
50
+ "HeatingCoolingMode",
51
+ "HeatingCoolingModeOverride",
52
+ "LocationPointKey",
53
+ "ModbusMode",
54
+ "PeripheralPointKey",
55
+ "PeripheralType",
56
+ "PointKey",
57
+ "RoomLock",
58
+ "RoomMode",
59
+ "RoomModeOverride",
60
+ "RoomPointKey",
61
+ "RoomState",
62
+ "RoomType",
63
+ "TemperaturePreset",
64
+ "UpdateMode",
65
+ "SentioPeripheral",
66
+ "SentioRoom",
67
+ "create_client",
68
+ "create_client_on",
69
+ "peripheral_key",
70
+ "peripherals",
71
+ "room_key",
72
+ "rooms",
73
+ ]