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