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.
Files changed (38) hide show
  1. grundfos_ble-0.1.0/.gitignore +31 -0
  2. grundfos_ble-0.1.0/CHANGELOG.md +29 -0
  3. grundfos_ble-0.1.0/LICENSE +21 -0
  4. grundfos_ble-0.1.0/PKG-INFO +181 -0
  5. grundfos_ble-0.1.0/README.md +155 -0
  6. grundfos_ble-0.1.0/docs/protocol.md +102 -0
  7. grundfos_ble-0.1.0/pyproject.toml +109 -0
  8. grundfos_ble-0.1.0/src/grundfos_ble/__init__.py +101 -0
  9. grundfos_ble-0.1.0/src/grundfos_ble/advertisement.py +84 -0
  10. grundfos_ble-0.1.0/src/grundfos_ble/exceptions.py +75 -0
  11. grundfos_ble-0.1.0/src/grundfos_ble/genibus/__init__.py +38 -0
  12. grundfos_ble-0.1.0/src/grundfos_ble/genibus/apdu.py +97 -0
  13. grundfos_ble-0.1.0/src/grundfos_ble/genibus/classic.py +249 -0
  14. grundfos_ble-0.1.0/src/grundfos_ble/genibus/crc.py +41 -0
  15. grundfos_ble-0.1.0/src/grundfos_ble/genibus/objects.py +225 -0
  16. grundfos_ble-0.1.0/src/grundfos_ble/genibus/telegram.py +188 -0
  17. grundfos_ble-0.1.0/src/grundfos_ble/pump.py +588 -0
  18. grundfos_ble-0.1.0/src/grundfos_ble/py.typed +0 -0
  19. grundfos_ble-0.1.0/src/grundfos_ble/registry/__init__.py +5 -0
  20. grundfos_ble-0.1.0/src/grundfos_ble/registry/alpha2go.py +698 -0
  21. grundfos_ble-0.1.0/src/grundfos_ble/registry/base.py +110 -0
  22. grundfos_ble-0.1.0/src/grundfos_ble/testdata/alpha2go/measured-2026-11-07.hex +15 -0
  23. grundfos_ble-0.1.0/src/grundfos_ble/testdata/alpha2go/measured-2026-11-09-full.hex +50 -0
  24. grundfos_ble-0.1.0/src/grundfos_ble/testdata/alpha2go/measured-2026-11-09.hex +14 -0
  25. grundfos_ble-0.1.0/src/grundfos_ble/testing.py +110 -0
  26. grundfos_ble-0.1.0/src/grundfos_ble/transport.py +279 -0
  27. grundfos_ble-0.1.0/tests/__init__.py +0 -0
  28. grundfos_ble-0.1.0/tests/conftest.py +53 -0
  29. grundfos_ble-0.1.0/tests/test_advertisement.py +40 -0
  30. grundfos_ble-0.1.0/tests/test_apdu_objects.py +146 -0
  31. grundfos_ble-0.1.0/tests/test_classic.py +104 -0
  32. grundfos_ble-0.1.0/tests/test_crc.py +60 -0
  33. grundfos_ble-0.1.0/tests/test_pump.py +378 -0
  34. grundfos_ble-0.1.0/tests/test_readme.py +58 -0
  35. grundfos_ble-0.1.0/tests/test_registry.py +63 -0
  36. grundfos_ble-0.1.0/tests/test_telegram.py +133 -0
  37. grundfos_ble-0.1.0/tests/test_testing.py +47 -0
  38. 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