grundfos-ble 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.
- grundfos_ble-0.1.0/.gitignore +31 -0
- grundfos_ble-0.1.0/CHANGELOG.md +29 -0
- grundfos_ble-0.1.0/LICENSE +21 -0
- grundfos_ble-0.1.0/PKG-INFO +181 -0
- grundfos_ble-0.1.0/README.md +155 -0
- grundfos_ble-0.1.0/docs/protocol.md +102 -0
- grundfos_ble-0.1.0/pyproject.toml +109 -0
- grundfos_ble-0.1.0/src/grundfos_ble/__init__.py +101 -0
- grundfos_ble-0.1.0/src/grundfos_ble/advertisement.py +84 -0
- grundfos_ble-0.1.0/src/grundfos_ble/exceptions.py +75 -0
- grundfos_ble-0.1.0/src/grundfos_ble/genibus/__init__.py +38 -0
- grundfos_ble-0.1.0/src/grundfos_ble/genibus/apdu.py +97 -0
- grundfos_ble-0.1.0/src/grundfos_ble/genibus/classic.py +249 -0
- grundfos_ble-0.1.0/src/grundfos_ble/genibus/crc.py +41 -0
- grundfos_ble-0.1.0/src/grundfos_ble/genibus/objects.py +225 -0
- grundfos_ble-0.1.0/src/grundfos_ble/genibus/telegram.py +188 -0
- grundfos_ble-0.1.0/src/grundfos_ble/pump.py +588 -0
- grundfos_ble-0.1.0/src/grundfos_ble/py.typed +0 -0
- grundfos_ble-0.1.0/src/grundfos_ble/registry/__init__.py +5 -0
- grundfos_ble-0.1.0/src/grundfos_ble/registry/alpha2go.py +698 -0
- grundfos_ble-0.1.0/src/grundfos_ble/registry/base.py +110 -0
- grundfos_ble-0.1.0/src/grundfos_ble/testdata/alpha2go/measured-2026-11-07.hex +15 -0
- grundfos_ble-0.1.0/src/grundfos_ble/testdata/alpha2go/measured-2026-11-09-full.hex +50 -0
- grundfos_ble-0.1.0/src/grundfos_ble/testdata/alpha2go/measured-2026-11-09.hex +14 -0
- grundfos_ble-0.1.0/src/grundfos_ble/testing.py +110 -0
- grundfos_ble-0.1.0/src/grundfos_ble/transport.py +279 -0
- grundfos_ble-0.1.0/tests/__init__.py +0 -0
- grundfos_ble-0.1.0/tests/conftest.py +53 -0
- grundfos_ble-0.1.0/tests/test_advertisement.py +40 -0
- grundfos_ble-0.1.0/tests/test_apdu_objects.py +146 -0
- grundfos_ble-0.1.0/tests/test_classic.py +104 -0
- grundfos_ble-0.1.0/tests/test_crc.py +60 -0
- grundfos_ble-0.1.0/tests/test_pump.py +378 -0
- grundfos_ble-0.1.0/tests/test_readme.py +58 -0
- grundfos_ble-0.1.0/tests/test_registry.py +63 -0
- grundfos_ble-0.1.0/tests/test_telegram.py +133 -0
- grundfos_ble-0.1.0/tests/test_testing.py +47 -0
- grundfos_ble-0.1.0/tests/test_transport.py +345 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
.mypy_cache/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
coverage.xml
|
|
13
|
+
htmlcov/
|
|
14
|
+
|
|
15
|
+
# Nix
|
|
16
|
+
result
|
|
17
|
+
.direnv/
|
|
18
|
+
|
|
19
|
+
# Editors
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
*.swp
|
|
23
|
+
|
|
24
|
+
# Local environments and secrets
|
|
25
|
+
.venv-*/
|
|
26
|
+
.env
|
|
27
|
+
.env.*
|
|
28
|
+
.envrc
|
|
29
|
+
|
|
30
|
+
# macOS
|
|
31
|
+
.DS_Store
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- GENIbus framing: telegrams, APDUs, CRC-16/CCITT, reassembly of notification-sized pieces,
|
|
12
|
+
classic classes 2/3/4/5/7 (GET, SET, INFO, strings, the Grundfos unit table) and class-10
|
|
13
|
+
data objects (GET, SET, headers, float/integer fields with the "not available" and NaN
|
|
14
|
+
sentinels).
|
|
15
|
+
- Advertisement parser for the 0xFE5D service data, including the Bluetooth-button flag that
|
|
16
|
+
marks the pairing window.
|
|
17
|
+
- ALPHA2 GO profile: process values, motor state, operation state, per-mode setpoints and
|
|
18
|
+
factory limits, panel setting and presets, flow limiters, AutoNight, proportional-pressure
|
|
19
|
+
curve, alarms and warnings, statistics, temperatures and energy, each entry tagged with how
|
|
20
|
+
it was verified and where it comes from.
|
|
21
|
+
- `BleTransport` on bleak 3 and bleak-retry-connector 4: pairs on every connection, chunked
|
|
22
|
+
writes, one request in flight. `PairingRequired` means the pump refused pairing (SMP reason 82),
|
|
23
|
+
and `reply_seen` tells a silent link from an answered one. Only ESPHome proxies have been tested.
|
|
24
|
+
- `GrundfosPump`: typed reads and capability probing with raw-frame capture. This is a read-only
|
|
25
|
+
release: the write methods raise `NotSupported`, and no write has ever been sent to a real pump.
|
|
26
|
+
- `grundfos_ble.testing`: a replay transport, a fixture format for recorded frames, and the
|
|
27
|
+
recorded ALPHA2 GO frames as `FIXTURES`.
|
|
28
|
+
|
|
29
|
+
[0.1.0]: https://github.com/83noit/grundfos-ble/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 83noit
|
|
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,181 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: grundfos-ble
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Python library for Grundfos circulator pumps over GENIbus-on-Bluetooth LE (ALPHA2 GO first)
|
|
5
|
+
Project-URL: Homepage, https://github.com/83noit/grundfos-ble
|
|
6
|
+
Project-URL: Repository, https://github.com/83noit/grundfos-ble
|
|
7
|
+
Project-URL: Issues, https://github.com/83noit/grundfos-ble/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/83noit/grundfos-ble/blob/main/CHANGELOG.md
|
|
9
|
+
Author: 83noit
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: alpha2,ble,bluetooth,genibus,grundfos,home-assistant,pump
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Home Automation
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.13
|
|
23
|
+
Requires-Dist: bleak-retry-connector<5,>=4.0
|
|
24
|
+
Requires-Dist: bleak<4,>=3.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# grundfos-ble
|
|
28
|
+
|
|
29
|
+
Async, typed Python library for Grundfos circulator pumps that speak **GENIbus over
|
|
30
|
+
Bluetooth LE**. The only supported pump today is the **ALPHA2 GO**. The framing layer is
|
|
31
|
+
transport-agnostic and the pump profile is data, which should make related Grundfos Bluetooth
|
|
32
|
+
pumps (ALPHA3, ALPHA HWR, SCALA1) easier to add, but none of them is supported yet.
|
|
33
|
+
|
|
34
|
+
**This is a preliminary, read-only release.** It reads a pump's state and values. Writing to a
|
|
35
|
+
pump is not supported yet.
|
|
36
|
+
|
|
37
|
+
The library was written for use under Home Assistant, but it does not depend on it.
|
|
38
|
+
|
|
39
|
+
This project is not affiliated with, endorsed by or supported by Grundfos. Product names are
|
|
40
|
+
used only to identify compatible pumps.
|
|
41
|
+
|
|
42
|
+
## What it does
|
|
43
|
+
|
|
44
|
+
- Decodes the pump's advertisement (service UUID `0xFE5D`), including the flag that says the
|
|
45
|
+
Bluetooth button was just pressed.
|
|
46
|
+
- Connects through bleak and bleak-retry-connector, pairs on every connection, and talks GENIbus
|
|
47
|
+
over the single GENI characteristic. Only ESPHome Bluetooth proxies have been tested. Other
|
|
48
|
+
bleak backends may connect, but BlueZ and WinRT pairing failures are not recognised as
|
|
49
|
+
`PairingRequired` and surface as `GrundfosConnectionError`.
|
|
50
|
+
- Reads flow, head, power, voltage, current, speed, electronics temperature, control and
|
|
51
|
+
operation mode, setpoint, factory limits, the panel setting, flow limiters, AutoNight, alarms,
|
|
52
|
+
warnings and, where the pump has them, statistics, temperatures and energy. The stored panel
|
|
53
|
+
presets have no typed reader. Fetch them with `GrundfosPump.get_object` and
|
|
54
|
+
`registry.alpha2go.panel_preset`.
|
|
55
|
+
- Does **not** support writes yet. The write methods (control mode, setpoint, limiters, AutoNight,
|
|
56
|
+
start/stop, alarm reset) raise `NotSupported`. See [Known limitations](#known-limitations).
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
pip install grundfos-ble
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Python 3.13 or newer; depends on `bleak>=3` and `bleak-retry-connector>=4`.
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
|
|
71
|
+
from bleak import BleakScanner
|
|
72
|
+
|
|
73
|
+
from grundfos_ble import BleTransport, GrundfosPump, PairingRequired
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def main() -> None:
|
|
77
|
+
device = await BleakScanner.find_device_by_address("AA:BB:CC:DD:EE:FF")
|
|
78
|
+
if device is None:
|
|
79
|
+
raise SystemExit("Pump not found")
|
|
80
|
+
|
|
81
|
+
transport = BleTransport(device, name="My pump")
|
|
82
|
+
pump = GrundfosPump(transport)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
await transport.connect() # pairs (encrypts) every time
|
|
86
|
+
except PairingRequired:
|
|
87
|
+
print("Press the pump's Bluetooth button and connect again within 3 seconds")
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
identity = await pump.read_identity()
|
|
92
|
+
values = await pump.read_measurements()
|
|
93
|
+
print(identity.model, values.flow_m3h, "m³/h", values.head_m, "m", values.power_w, "W")
|
|
94
|
+
finally:
|
|
95
|
+
await transport.disconnect()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
asyncio.run(main())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The library never scans; you hand it a `BLEDevice`. Under Home Assistant that device comes
|
|
102
|
+
from the Bluetooth integration and HA chooses the proxy.
|
|
103
|
+
|
|
104
|
+
## Pairing, in one paragraph
|
|
105
|
+
|
|
106
|
+
The pump drops any link that is not encrypted within about two seconds, and accepts pairing
|
|
107
|
+
only for about three seconds after its Bluetooth button is pressed. Once paired, the bond lives
|
|
108
|
+
on the adapter or proxy that paired. When tested, the pump held at least two bonds at once, a
|
|
109
|
+
phone and an ESPHome proxy. Every connection must still call `pair()`; with a bond that is a quick
|
|
110
|
+
re-encrypt. `PairingRequired` is raised when the pump refuses pairing (ESP-IDF reason 82, seen
|
|
111
|
+
through ESPHome proxies). A link that connects but never answers shows up as `reply_seen` staying
|
|
112
|
+
false. A pump losing a bond has never been observed, so how that looks is unknown.
|
|
113
|
+
|
|
114
|
+
## Protocol
|
|
115
|
+
|
|
116
|
+
GENIbus telegram `SD LEN DA SA APDU… CRC16` (CRC-16/CCITT, init 0xFFFF, inverted), requests
|
|
117
|
+
`27 … E7 F8`, replies `24 … F8 E7`, frames written in 20-byte pieces. Most values live in
|
|
118
|
+
class-10 "data objects" addressed by `(id, sub-id)` whose replies carry a
|
|
119
|
+
`status type version size` header and big-endian floats. See [docs/protocol.md](https://github.com/83noit/grundfos-ble/blob/main/docs/protocol.md)
|
|
120
|
+
for the full inventory with the provenance of every item.
|
|
121
|
+
|
|
122
|
+
## Known limitations
|
|
123
|
+
|
|
124
|
+
**Writing to a pump is not supported yet.** The write methods raise `NotSupported`, and this
|
|
125
|
+
project has never sent a write to a pump. The encoders behind them are tested against frames
|
|
126
|
+
captured from the Grundfos GO app, and that is all. Before writes can be enabled, at least these
|
|
127
|
+
need fixing:
|
|
128
|
+
|
|
129
|
+
- `set_flow_limiter` does not check the limit against the pump's range, so it accepts 0, negative,
|
|
130
|
+
NaN and huge values. A near-zero maximum flow could starve a circuit of circulation.
|
|
131
|
+
- `set_setpoint` skips its range check when the factory limits are not available.
|
|
132
|
+
- `Access` and `MODES_SEEN_ON_ALPHA2_GO` are not enforced, so objects whose meaning is unknown
|
|
133
|
+
could be written.
|
|
134
|
+
- `set_setpoint` writes the control mode first. If the second write fails, the mode has changed
|
|
135
|
+
and the error does not say so. The read-back is returned but not compared, and the pump clamps
|
|
136
|
+
silently.
|
|
137
|
+
- Read-modify-write rebuilds objects instead of patching the raw bytes, and does not refuse a
|
|
138
|
+
different object version or size.
|
|
139
|
+
- Cancelling mid-write can leave part of a SET at the pump.
|
|
140
|
+
|
|
141
|
+
Reads have limitations too:
|
|
142
|
+
|
|
143
|
+
- The object version is not checked, so a firmware that changes a layout under the same type
|
|
144
|
+
would decode into wrong values.
|
|
145
|
+
- The advertisement parser checks only the length, not the `03 01 00` prefix.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
nix develop # or: uv sync --group dev && source .venv/bin/activate
|
|
151
|
+
ruff check . && ruff format --check .
|
|
152
|
+
mypy
|
|
153
|
+
pytest --cov
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Measurements, operation state, statistics and the other optional objects are tested against frames
|
|
157
|
+
recorded from a real ALPHA2 GO (`src/grundfos_ble/testdata/`, available to dependants as
|
|
158
|
+
`grundfos_ble.testing.FIXTURES`). Identity and string reads are tested only on hand-built or ALPHA
|
|
159
|
+
HWR frames, and the Bluetooth connection (`establish_connection`) is always mocked. Add new captures
|
|
160
|
+
in the same format (`>` request, `<` notification pieces) rather than hand-built frames.
|
|
161
|
+
|
|
162
|
+
## Status
|
|
163
|
+
|
|
164
|
+
Alpha, read-only. Process and motor values were checked against the panel of one ALPHA2 GO. That pump also
|
|
165
|
+
answered the operation state, panel setting, flow limiters, AutoNight, alarms, warnings, statistics,
|
|
166
|
+
temperatures and energy with plausible values, with no fault active. The remaining objects come from
|
|
167
|
+
captured traffic or documentation for related pumps. Other pump families are unverified.
|
|
168
|
+
|
|
169
|
+
## Acknowledgements
|
|
170
|
+
|
|
171
|
+
This library was written from the GENIbus specification and from frames captured on real pumps.
|
|
172
|
+
No code from these projects is used, but their published protocol research made the work possible:
|
|
173
|
+
|
|
174
|
+
- [esphome-alpha2go](https://github.com/Birkemosen/esphome-alpha2go) (GPL-3.0): ALPHA2 GO traffic captures and object layouts
|
|
175
|
+
- [alpha-hwr](https://eman.github.io/alpha-hwr/) (MIT): ALPHA HWR protocol documentation
|
|
176
|
+
- [gofree](https://github.com/tatutsa888/gofree) (MIT): SCALA1 protocol documentation
|
|
177
|
+
- [esphome-grundfos-alpha3](https://github.com/Seba65439/esphome-grundfos-alpha3) (MIT): ALPHA3 decoders
|
|
178
|
+
|
|
179
|
+
## Licence
|
|
180
|
+
|
|
181
|
+
MIT. See the `LICENSE` file.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# grundfos-ble
|
|
2
|
+
|
|
3
|
+
Async, typed Python library for Grundfos circulator pumps that speak **GENIbus over
|
|
4
|
+
Bluetooth LE**. The only supported pump today is the **ALPHA2 GO**. The framing layer is
|
|
5
|
+
transport-agnostic and the pump profile is data, which should make related Grundfos Bluetooth
|
|
6
|
+
pumps (ALPHA3, ALPHA HWR, SCALA1) easier to add, but none of them is supported yet.
|
|
7
|
+
|
|
8
|
+
**This is a preliminary, read-only release.** It reads a pump's state and values. Writing to a
|
|
9
|
+
pump is not supported yet.
|
|
10
|
+
|
|
11
|
+
The library was written for use under Home Assistant, but it does not depend on it.
|
|
12
|
+
|
|
13
|
+
This project is not affiliated with, endorsed by or supported by Grundfos. Product names are
|
|
14
|
+
used only to identify compatible pumps.
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
- Decodes the pump's advertisement (service UUID `0xFE5D`), including the flag that says the
|
|
19
|
+
Bluetooth button was just pressed.
|
|
20
|
+
- Connects through bleak and bleak-retry-connector, pairs on every connection, and talks GENIbus
|
|
21
|
+
over the single GENI characteristic. Only ESPHome Bluetooth proxies have been tested. Other
|
|
22
|
+
bleak backends may connect, but BlueZ and WinRT pairing failures are not recognised as
|
|
23
|
+
`PairingRequired` and surface as `GrundfosConnectionError`.
|
|
24
|
+
- Reads flow, head, power, voltage, current, speed, electronics temperature, control and
|
|
25
|
+
operation mode, setpoint, factory limits, the panel setting, flow limiters, AutoNight, alarms,
|
|
26
|
+
warnings and, where the pump has them, statistics, temperatures and energy. The stored panel
|
|
27
|
+
presets have no typed reader. Fetch them with `GrundfosPump.get_object` and
|
|
28
|
+
`registry.alpha2go.panel_preset`.
|
|
29
|
+
- Does **not** support writes yet. The write methods (control mode, setpoint, limiters, AutoNight,
|
|
30
|
+
start/stop, alarm reset) raise `NotSupported`. See [Known limitations](#known-limitations).
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pip install grundfos-ble
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Python 3.13 or newer; depends on `bleak>=3` and `bleak-retry-connector>=4`.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
|
|
45
|
+
from bleak import BleakScanner
|
|
46
|
+
|
|
47
|
+
from grundfos_ble import BleTransport, GrundfosPump, PairingRequired
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def main() -> None:
|
|
51
|
+
device = await BleakScanner.find_device_by_address("AA:BB:CC:DD:EE:FF")
|
|
52
|
+
if device is None:
|
|
53
|
+
raise SystemExit("Pump not found")
|
|
54
|
+
|
|
55
|
+
transport = BleTransport(device, name="My pump")
|
|
56
|
+
pump = GrundfosPump(transport)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
await transport.connect() # pairs (encrypts) every time
|
|
60
|
+
except PairingRequired:
|
|
61
|
+
print("Press the pump's Bluetooth button and connect again within 3 seconds")
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
identity = await pump.read_identity()
|
|
66
|
+
values = await pump.read_measurements()
|
|
67
|
+
print(identity.model, values.flow_m3h, "m³/h", values.head_m, "m", values.power_w, "W")
|
|
68
|
+
finally:
|
|
69
|
+
await transport.disconnect()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
asyncio.run(main())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The library never scans; you hand it a `BLEDevice`. Under Home Assistant that device comes
|
|
76
|
+
from the Bluetooth integration and HA chooses the proxy.
|
|
77
|
+
|
|
78
|
+
## Pairing, in one paragraph
|
|
79
|
+
|
|
80
|
+
The pump drops any link that is not encrypted within about two seconds, and accepts pairing
|
|
81
|
+
only for about three seconds after its Bluetooth button is pressed. Once paired, the bond lives
|
|
82
|
+
on the adapter or proxy that paired. When tested, the pump held at least two bonds at once, a
|
|
83
|
+
phone and an ESPHome proxy. Every connection must still call `pair()`; with a bond that is a quick
|
|
84
|
+
re-encrypt. `PairingRequired` is raised when the pump refuses pairing (ESP-IDF reason 82, seen
|
|
85
|
+
through ESPHome proxies). A link that connects but never answers shows up as `reply_seen` staying
|
|
86
|
+
false. A pump losing a bond has never been observed, so how that looks is unknown.
|
|
87
|
+
|
|
88
|
+
## Protocol
|
|
89
|
+
|
|
90
|
+
GENIbus telegram `SD LEN DA SA APDU… CRC16` (CRC-16/CCITT, init 0xFFFF, inverted), requests
|
|
91
|
+
`27 … E7 F8`, replies `24 … F8 E7`, frames written in 20-byte pieces. Most values live in
|
|
92
|
+
class-10 "data objects" addressed by `(id, sub-id)` whose replies carry a
|
|
93
|
+
`status type version size` header and big-endian floats. See [docs/protocol.md](https://github.com/83noit/grundfos-ble/blob/main/docs/protocol.md)
|
|
94
|
+
for the full inventory with the provenance of every item.
|
|
95
|
+
|
|
96
|
+
## Known limitations
|
|
97
|
+
|
|
98
|
+
**Writing to a pump is not supported yet.** The write methods raise `NotSupported`, and this
|
|
99
|
+
project has never sent a write to a pump. The encoders behind them are tested against frames
|
|
100
|
+
captured from the Grundfos GO app, and that is all. Before writes can be enabled, at least these
|
|
101
|
+
need fixing:
|
|
102
|
+
|
|
103
|
+
- `set_flow_limiter` does not check the limit against the pump's range, so it accepts 0, negative,
|
|
104
|
+
NaN and huge values. A near-zero maximum flow could starve a circuit of circulation.
|
|
105
|
+
- `set_setpoint` skips its range check when the factory limits are not available.
|
|
106
|
+
- `Access` and `MODES_SEEN_ON_ALPHA2_GO` are not enforced, so objects whose meaning is unknown
|
|
107
|
+
could be written.
|
|
108
|
+
- `set_setpoint` writes the control mode first. If the second write fails, the mode has changed
|
|
109
|
+
and the error does not say so. The read-back is returned but not compared, and the pump clamps
|
|
110
|
+
silently.
|
|
111
|
+
- Read-modify-write rebuilds objects instead of patching the raw bytes, and does not refuse a
|
|
112
|
+
different object version or size.
|
|
113
|
+
- Cancelling mid-write can leave part of a SET at the pump.
|
|
114
|
+
|
|
115
|
+
Reads have limitations too:
|
|
116
|
+
|
|
117
|
+
- The object version is not checked, so a firmware that changes a layout under the same type
|
|
118
|
+
would decode into wrong values.
|
|
119
|
+
- The advertisement parser checks only the length, not the `03 01 00` prefix.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
nix develop # or: uv sync --group dev && source .venv/bin/activate
|
|
125
|
+
ruff check . && ruff format --check .
|
|
126
|
+
mypy
|
|
127
|
+
pytest --cov
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Measurements, operation state, statistics and the other optional objects are tested against frames
|
|
131
|
+
recorded from a real ALPHA2 GO (`src/grundfos_ble/testdata/`, available to dependants as
|
|
132
|
+
`grundfos_ble.testing.FIXTURES`). Identity and string reads are tested only on hand-built or ALPHA
|
|
133
|
+
HWR frames, and the Bluetooth connection (`establish_connection`) is always mocked. Add new captures
|
|
134
|
+
in the same format (`>` request, `<` notification pieces) rather than hand-built frames.
|
|
135
|
+
|
|
136
|
+
## Status
|
|
137
|
+
|
|
138
|
+
Alpha, read-only. Process and motor values were checked against the panel of one ALPHA2 GO. That pump also
|
|
139
|
+
answered the operation state, panel setting, flow limiters, AutoNight, alarms, warnings, statistics,
|
|
140
|
+
temperatures and energy with plausible values, with no fault active. The remaining objects come from
|
|
141
|
+
captured traffic or documentation for related pumps. Other pump families are unverified.
|
|
142
|
+
|
|
143
|
+
## Acknowledgements
|
|
144
|
+
|
|
145
|
+
This library was written from the GENIbus specification and from frames captured on real pumps.
|
|
146
|
+
No code from these projects is used, but their published protocol research made the work possible:
|
|
147
|
+
|
|
148
|
+
- [esphome-alpha2go](https://github.com/Birkemosen/esphome-alpha2go) (GPL-3.0): ALPHA2 GO traffic captures and object layouts
|
|
149
|
+
- [alpha-hwr](https://eman.github.io/alpha-hwr/) (MIT): ALPHA HWR protocol documentation
|
|
150
|
+
- [gofree](https://github.com/tatutsa888/gofree) (MIT): SCALA1 protocol documentation
|
|
151
|
+
- [esphome-grundfos-alpha3](https://github.com/Seba65439/esphome-grundfos-alpha3) (MIT): ALPHA3 decoders
|
|
152
|
+
|
|
153
|
+
## Licence
|
|
154
|
+
|
|
155
|
+
MIT. See the `LICENSE` file.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# GENIbus over Bluetooth LE, as spoken by the Grundfos ALPHA2 GO
|
|
2
|
+
|
|
3
|
+
What this library implements and how sure we are of each part. "Measured" means read from
|
|
4
|
+
a real ALPHA2 GO by this project and checked against the pump's display or a clock. "Answered"
|
|
5
|
+
means a real ALPHA2 GO returned plausible values with nothing to check them against. "Captured" means
|
|
6
|
+
seen in Grundfos GO app traffic recorded on an ALPHA2 GO 25-90 by the esphome-alpha2go
|
|
7
|
+
author (facts only; that project is GPL and none of its code is used here). "Documented"
|
|
8
|
+
means described for a related pump (ALPHA HWR = family 52 type 7, ALPHA3, SCALA1) and not yet
|
|
9
|
+
confirmed on an ALPHA2 GO. The objects the library reads or writes are defined, with the same
|
|
10
|
+
tags, in `grundfos_ble/registry/alpha2go.py`. The QH coefficients (93/180–184, 93/200) and the
|
|
11
|
+
clock (94/101) are listed here for reference and are not in the registry.
|
|
12
|
+
|
|
13
|
+
## Transport
|
|
14
|
+
|
|
15
|
+
| Item | Value | Status |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Advertised service UUID | 0xFE5D (Bluetooth SIG member UUID of Grundfos A/S) | measured |
|
|
18
|
+
| Service data | `03 01 00 fam type ver mode flags tail(2)`; `fam type` = 52 6 for the ALPHA2 GO | measured (flags per SCALA1 docs) |
|
|
19
|
+
| Flag bits | 0x10 button pressed (~4 s, pairing window), 0x01 client connected, 0x02 alarm, 0x04 warning, 0x08 LED blinking | 0x10 measured, rest documented |
|
|
20
|
+
| GATT characteristic | `859cffd1-036e-432a-aa28-1a0085b87ba9`, write-without-response + notify | measured |
|
|
21
|
+
| Write size | ≤ 20 bytes per write, whatever the MTU | captured, documented |
|
|
22
|
+
| Reply size | ≤ 20 bytes per notification; reassemble to LEN + 4 | measured |
|
|
23
|
+
| Encryption | mandatory; unencrypted link dropped ~2 s after connect; SMP accepted ~3 s after the button; at least two bonds coexist (proxy and phone) | measured |
|
|
24
|
+
| While a client is connected | the pump stops advertising; the last advert seen had flag 0x08 | measured |
|
|
25
|
+
| Advertising interval | ~8 s, continuous | measured |
|
|
26
|
+
|
|
27
|
+
## Telegram
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
SD LEN DA SA APDU… CRC-hi CRC-lo
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- SD: 0x27 request, 0x26 message (no reply), 0x24 reply, 0x25 error reply.
|
|
34
|
+
- LEN: bytes from DA to the last APDU byte.
|
|
35
|
+
- DA/SA: pump 0xE7, client 0xF8 (requests `27 LL E7 F8`, replies `24 LL F8 E7`); broadcast 0xFF.
|
|
36
|
+
- CRC-16/CCITT, poly 0x1021, init 0xFFFF, result inverted, big-endian, over LEN..last APDU byte.
|
|
37
|
+
- APDU: `class`, `(op << 6) | len`, data (≤ 63 bytes). Request op: 0 GET, 2 SET, 3 INFO.
|
|
38
|
+
Reply ack: 0 OK, 1 class unknown, 2 ID unknown, 3 illegal operation.
|
|
39
|
+
|
|
40
|
+
Source: GENIbus Protocol Specification (Grundfos, 2005) for everything in this section;
|
|
41
|
+
measured on the ALPHA2 GO.
|
|
42
|
+
|
|
43
|
+
## Classic classes
|
|
44
|
+
|
|
45
|
+
| Class | Use here | Status |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| 2 measured data | IDs 148/149/150 = unit family/type/version (`34 06 00`) | captured; advert bytes agree (measured) |
|
|
48
|
+
| 3 commands | 2 reset alarm (SCALA1), 5 stop, 6 start, 7 remote, 8 local, 21 constant flow, 22 constant speed, 23 proportional pressure, 24 constant pressure, 25 min, 26 max | captured (5, 6 tested by others), rest documented |
|
|
49
|
+
| 5 reference values | ID 1 `ref_rem` answers 255 = unsupported | captured |
|
|
50
|
+
| 7 ASCII strings | 1 product name, 8 product number, 9 serial, 10 production code, 17 unit name, 19 model, 21 GSC file, 50 software, 58 BLE software, 100 ECU id; reply = NUL-terminated string | captured |
|
|
51
|
+
| INFO | head byte VI/BO/SIF, UNIT/ZERO/RANGE, Grundfos unit table | spec |
|
|
52
|
+
|
|
53
|
+
## Class-10 data objects
|
|
54
|
+
|
|
55
|
+
GET: `0A 03 id sub-hi sub-lo` (several refs may be concatenated). Reply data per object:
|
|
56
|
+
`status type(2) version size(3) data(size)`. Replies name the **type**, not the id asked, so
|
|
57
|
+
replies are matched by order. SET: `0A (0x80|len) id sub type(2) version size(3) data`;
|
|
58
|
+
reply `0A 01 status` (0 OK, 2 busy, 4 failed). Floats are big-endian IEEE-754;
|
|
59
|
+
`7F FF FF FF` = not available (and "keep the stored value" in SETs), `FF C0 00 00` = NaN.
|
|
60
|
+
|
|
61
|
+
| Object | Type | Layout | Status |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| 93/290 process values | 565 v2, 36 B | flow m³/s @0, Δp Pa @4, N/A @8 @12, media °C @16, ΔT @20, flow m³/h @24, head m @28, "kv" @32 | measured (temps N/A on a heating pump) |
|
|
64
|
+
| 87/69 motor state | 256 v3, 41 B | grid V @0, DC-link V @4, A @8, W @12, W @16 (shaft?), rpm @20, NaN @24 @28, electronics °C @32 | measured; @16/@32 meaning unconfirmed |
|
|
65
|
+
| 86/8 realised operation | 303, 7 B | source u8, operation mode u8, control mode u8, setpoint f32 (Pa / rpm / m³/s) | captured; answered on the ALPHA2 GO |
|
|
66
|
+
| 86/6 local request, 86/10 control-mode request (RW) | 303 | same; SET 86/10 as `00 06 mode 7F FF FF FF` | captured |
|
|
67
|
+
| 86/13,15,17,19,21,39 factory limits | 301, 28 B | default, min, max, resulting min, Kp, Ti, Td | captured |
|
|
68
|
+
| 86/14,16,18,20,22,40 user setpoint (RW) | 302, 18 B | feedback id u16, setpoint f32, Kp, Ti, Td; write = read-modify-write | captured (20/22 unverified) |
|
|
69
|
+
| 86/350 panel setting | 868, 1 B | enum 0–20 | captured; answered on the ALPHA2 GO |
|
|
70
|
+
| 86/351–359 panel presets | 869, 5 B | mode u8, setpoint f32 | captured |
|
|
71
|
+
| 86/600, 86/601 flow limiters (RW) | 895, 18 B | id, enabled, limit m³/s, Kp, Ti, Td | captured; answered on the ALPHA2 GO |
|
|
72
|
+
| 86/620, 86/621 limiter ranges | 897, 9 B | id, lower, upper (m³/s) | captured |
|
|
73
|
+
| 90/8 AutoNight (RW) | 333, 1 B | bool | captured; answered on the ALPHA2 GO |
|
|
74
|
+
| 90/4 PP curve | 286, 25 B | quadratic u8, foot point, ?, c0, c1, c2, foot point min | captured |
|
|
75
|
+
| 93/180–184, 93/200 QH coefficients | 1114 / 563 | floats | captured |
|
|
76
|
+
| 88/0 alarms, 88/11 warnings | 570 | uint16 codes, 0-terminated | answered on the ALPHA2 GO (no fault active yet) |
|
|
77
|
+
| 93/1 statistics | 248 v2, 28 B | start count u32, starts 1 h u16, starts 24 h u16, operating time s u32, running time 24 h s u32, last cycle s u32, power-ups u32, power-on time s u32 | operating time and last cycle measured; rest answered. Cumulative since commissioning; survives power loss |
|
|
78
|
+
| 93/300 temperatures | 534 v2, 13 B | media, PCB, ambient °C (floats), then one byte | answered on the ALPHA2 GO |
|
|
79
|
+
| 87/1 energy | 232 v1, 26 B | float64 joules @0; f32 @8, u16 @12, f32 @14, repeated float64 @18 unconfirmed | answered on the ALPHA2 GO |
|
|
80
|
+
| 94/101 clock | 322, 12 B | local wall clock | documented (HWR), not implemented |
|
|
81
|
+
|
|
82
|
+
Error codes follow the GENI ErrorCode enum (`registry/alpha2go.py::ErrorCode`); the ALPHA2 GO
|
|
83
|
+
manual's table maps panel codes E1 → 51, E2 → 40, E3 → 4/72/76/85/132, E4 → 57, E9 → 25.
|
|
84
|
+
|
|
85
|
+
## Measured values (one ALPHA2 GO)
|
|
86
|
+
|
|
87
|
+
| Date | Flow | Head | Grid | DC-link | Current | Power | Speed | Temp |
|
|
88
|
+
|---|---|---|---|---|---|---|---|---|
|
|
89
|
+
| 2026-11-07 | 1.427 m³/h | 0.148 m | 235.2 V | 332.7 V | 0.038 A | 4.01 W | 1640 rpm | 44 °C |
|
|
90
|
+
| 2026-11-09 | 0.995 m³/h | 0.434 m | 230.6 V | 324.0 V | 0.038 A | 4.01 W | 1658 rpm | 39 °C |
|
|
91
|
+
|
|
92
|
+
The panel showed 1.4 m³/h, 0.1 m, 4 W and 1640 rpm on 2026-11-07. Frames are in
|
|
93
|
+
`src/grundfos_ble/testdata/alpha2go/`.
|
|
94
|
+
|
|
95
|
+
## Open questions
|
|
96
|
+
|
|
97
|
+
- Meaning of 93/290 @8, @12 and @32, and of 87/69 @16, @24, @28 on the ALPHA2 GO.
|
|
98
|
+
- Which sensor 87/69 @32 reads: it differs from the PCB temperature in 93/300 (42 °C against 37 °C).
|
|
99
|
+
- How the alarm objects look with a fault active.
|
|
100
|
+
- Whether the alarm/warning/connected advert flag bits behave on the ALPHA2 GO as on the SCALA1.
|
|
101
|
+
- How a pump behaves after losing a bond, which has never been observed.
|
|
102
|
+
- SMP details (Just Works assumed; IO capability none works through ESPHome proxies).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "grundfos-ble"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async Python library for Grundfos circulator pumps over GENIbus-on-Bluetooth LE (ALPHA2 GO first)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.13"
|
|
13
|
+
authors = [{ name = "83noit" }]
|
|
14
|
+
keywords = ["grundfos", "alpha2", "genibus", "bluetooth", "ble", "pump", "home-assistant"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Framework :: AsyncIO",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Topic :: Home Automation",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"bleak>=3.0,<4",
|
|
28
|
+
"bleak-retry-connector>=4.0,<5",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/83noit/grundfos-ble"
|
|
33
|
+
Repository = "https://github.com/83noit/grundfos-ble"
|
|
34
|
+
Issues = "https://github.com/83noit/grundfos-ble/issues"
|
|
35
|
+
Changelog = "https://github.com/83noit/grundfos-ble/blob/main/CHANGELOG.md"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=9",
|
|
40
|
+
"pytest-asyncio>=1.0",
|
|
41
|
+
"pytest-cov>=7",
|
|
42
|
+
"ruff>=0.14",
|
|
43
|
+
"mypy>=1.19",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/grundfos_ble"]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.sdist]
|
|
50
|
+
include = ["/src", "/tests", "/docs", "/README.md", "/CHANGELOG.md", "/LICENSE"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py313"
|
|
55
|
+
src = ["src", "tests"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = [
|
|
59
|
+
"E", "W", "F", "I", "B", "UP", "N", "SIM", "RUF", "ASYNC", "PT", "T20", "D",
|
|
60
|
+
"ANN", "ARG", "C4", "DTZ", "ERA", "ICN", "ISC", "PIE", "RET", "RSE", "SLF", "TID", "TRY",
|
|
61
|
+
]
|
|
62
|
+
ignore = [
|
|
63
|
+
"D203", # one-blank-line-before-class (conflicts with D211)
|
|
64
|
+
"D213", # multi-line-summary-second-line (conflicts with D212)
|
|
65
|
+
"TRY003", # long messages in exceptions are fine here
|
|
66
|
+
"ISC001", # conflicts with the formatter
|
|
67
|
+
"D105", # magic methods need no docstring
|
|
68
|
+
"D107", # __init__ is documented by the class
|
|
69
|
+
"N818", # exception names read better without an Error suffix here
|
|
70
|
+
"RUF001", # × and – are used deliberately in notes and docstrings
|
|
71
|
+
"RUF002",
|
|
72
|
+
"RUF003",
|
|
73
|
+
"ASYNC109", # explicit timeout parameters are part of the transport API
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint.per-file-ignores]
|
|
77
|
+
"tests/**" = ["D", "ANN", "SLF001", "PLR2004", "ARG001"]
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint.pydocstyle]
|
|
80
|
+
convention = "pep257"
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.isort]
|
|
83
|
+
known-first-party = ["grundfos_ble"]
|
|
84
|
+
|
|
85
|
+
[tool.mypy]
|
|
86
|
+
python_version = "3.13"
|
|
87
|
+
strict = true
|
|
88
|
+
warn_unreachable = true
|
|
89
|
+
show_error_codes = true
|
|
90
|
+
packages = ["grundfos_ble"]
|
|
91
|
+
mypy_path = "src"
|
|
92
|
+
|
|
93
|
+
[[tool.mypy.overrides]]
|
|
94
|
+
module = ["tests.*"]
|
|
95
|
+
disallow_untyped_defs = false
|
|
96
|
+
|
|
97
|
+
[tool.pytest.ini_options]
|
|
98
|
+
asyncio_mode = "auto"
|
|
99
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
100
|
+
testpaths = ["tests"]
|
|
101
|
+
addopts = "-q --strict-markers"
|
|
102
|
+
|
|
103
|
+
[tool.coverage.run]
|
|
104
|
+
source = ["grundfos_ble"]
|
|
105
|
+
branch = true
|
|
106
|
+
|
|
107
|
+
[tool.coverage.report]
|
|
108
|
+
show_missing = true
|
|
109
|
+
skip_covered = false
|