Pump Control
Controlling the ALPHA HWR (Start, Stop, Change Mode) involves sending specific Class 10 DataObject commands. Unlike simple registers, these commands require a strict sequence of operations, including packet fragmentation and a "Configuration Commit" step.
Three objects, not one
Control is not one object. Which one you address decides what actually changes, and getting it wrong is quiet rather than loud.
| What you want | Class | Sub | Obj | OpSpec |
|---|---|---|---|---|
| Start / stop | 3 | — | 0x06 START, 0x05 STOP |
0x81 |
| Control mode only | 10 | 0x5600 |
0x0A01 (Object 86 Sub 10) |
0x90 |
| Setpoint | 10 | 0x5600 |
0x0601 (Object 86 Sub 6) |
0x90 |
| Read state back | 10 | 0x5600 |
Object 86 Sub 7 | 0x03 |
0x0601 is fused: run state, control mode and setpoint travel in one
frame, so a write through it necessarily asserts all three. Routing
everything through it is why starting the pump used to also force a mode and
overwrite that mode's setpoint. Start and stop go through Class 3, which has
no room for either.
The Control Sequence (Critical)
Due to hardware limitations and firmware logic, sending a control command is not a simple atomic write. The following three rules must be observed:
1. MTU Fragmentation (Split-Writes)
The ALPHA HWR BLE interface has a hard Maximum Transmission Unit (MTU) of 20 bytes for write operations. * Action: Split the frame into as many 20-byte chunks as it needs, pacing each write. Do not assume two: a 24-byte control packet takes two, but a 59-byte schedule-layer write takes three (20 + 20 + 19). * Failure to split causes the device to silently drop the packet or return a CRC error.
A two-chunk implementation looks correct on control packets and silently truncates anything over 40 bytes. That shipped here, and the schedule write was the casualty — the second "chunk" still exceeded the MTU.
2. Transaction Locking
The pump streams telemetry at ~10 Hz, and its replies carry no reference to the request that caused them. If another write is interleaved between your fragments, the device's buffer is corrupted; if another read is in flight, its reply may be attributed to your command. * Action: Serialise writes, and match each reply against the command waiting for it rather than taking the next frame that arrives. See wire_format.md.
3. Configuration Commit
A setpoint write does not persist until it is committed. * Action: Send a Configuration Commit immediately after a setpoint write — built from the pump's current overview, never from a constant. See below. * A mode change needs no commit: it persists on its own, and the commit writes the schedule, which has nothing to do with the control mode.
4. The acknowledgement is not the verdict
The pump acks frames it is about to clamp. Requesting 600 RPM stores 1650 and requesting 4400 stores 3671 — the ends of its own limits block. Read Object 86 Sub 7 back to find out what happened.
Remote Control Mode — not supported, deliberately
The GENI protocol has a Remote Mode, and this library used to expose it. It is gone, because on the ALPHA it does the opposite of what its name suggests.
Measured on hardware:
- Engaging Remote stops the pump acting on commands from the BLE link for roughly 35-45 seconds, until it self-cancels. The pump appears to treat the BLE connection as its local control source, so claiming Remote priority deprioritises the only controller present.
- It does not persist. It reverts on its own after ~35 s unless re-asserted, and ordinary telemetry polling does not hold it.
- The pump accepts commands perfectly well in Local, and always has.
So there was demonstrated harm and no demonstrated benefit. control_source
is still readable — from Object 86 Sub 7, see below — and reads 1
(Local/Panel) in normal operation.
For the record, since older versions of this document had it wrong: the
commands were Class 3 IDs 7 (Remote) and 8 (Local), sent as SET
(0x81). The 0xC1 opcode this document used to specify is INFO, which the
pump answers with a descriptor rather than executing — so the frames it
described would not have done anything even if the feature were wanted.
Packet Structure
Control Command Payload
This object writes three things at once. Object 86 sub-id 6 (wire
Obj 0x0601) fuses the run state, the control mode and the setpoint into one frame, so anything sent through it necessarily asserts all three. It is used only for setpoint writes now — start/stop goes through Class 3 and mode changes through sub-id 10. See the Object 86 table below.
The payload for Sub 0x5600, Obj 0x0601 follows this structure:
2F 01 00 00 07 00 [RunState] [Mode] [Suffix...]
| Offset | Field | Value | Description |
|---|---|---|---|
| 0-1 | Header | 2F 01 |
Fixed header for this object. |
| 2-5 | Padding | 00 00 07 00 |
Fixed padding/flags. |
| 6 | operation_mode | 0x00, 0x01, 0x06 |
0x00 = AUTO, 0x01 = STOP, 0x06 = NoCmd (leave alone) |
| 7 | Mode | 0x00 - 0xFF |
Control Mode ID (see below). |
| 8-11 | set_point | float32 BE | The setpoint, or 7F FF FF FF (NaN) to keep the stored one. |
Never send
45 65 70 00here. That decodes to exactly 3671.0, which is this pump's maximum speed setpoint — it appears verbatim in the pump's own limits block at Object 86 Sub 13. It was long treated as an inert placeholder, so every start command wrote "run at full speed" over whatever setpoint the mode had. Where no setpoint is being asserted, send NaN (7F FF FF FF), which the pump reads as "keep what you have".
Configuration Commit Packet
This packet confirms the changes.
* Target: Object 84 Sub 1 (ClockProgramOverview), type 218 (0xDA01)
* OpSpec: 0x93
* Payload: the pump's current 10-byte overview, read back and rewritten.
Do not send a constant here. The commit carries the whole ClockProgramOverview, and byte 4 of that structure is the schedule's enabled flag. Earlier versions of this document published a fixed packet whose byte 4 was
0x00— and because a commit follows every setpoint write, sending it switched the user's weekly schedule off every time they changed a setpoint.Read Object 84 Sub 1, modify only what you mean to change, and write it back. If the overview cannot be read, send no commit at all: skipping a flush is recoverable, overwriting the schedule state is not.
Frame shape, with [overview] being those 10 bytes:
Supported Control Modes
The Mode byte determines the regulation behavior.
| ID | Name | Description |
|---|---|---|
0 |
Constant Pressure | Maintains constant differential pressure. |
1 |
Proportional Pressure | Adjusts pressure based on flow. |
2 |
Constant Speed | Runs at a fixed RPM (Default). |
5 |
AutoAdapt | Automatically analyzes system needs (generic). |
8 |
Constant Flow | Maintains a specific flow rate. |
13 |
AutoAdapt Radiator | AutoAdapt optimized for radiator systems. |
14 |
AutoAdapt Underfloor | AutoAdapt optimized for underfloor heating. |
15 |
AutoAdapt Combined | AutoAdapt for combined radiator + underfloor systems. |
25 |
DHW On/Off | Domestic Hot Water control. |
AutoAdapt Mode Variants
The three AutoAdapt variants (IDs 13, 14, 15) are system-specific optimizations:
- Radiator (13): Optimized for high-temperature radiator systems with steeper pump curves.
- Underfloor (14): Optimized for low-temperature underfloor heating with flatter curves.
- Combined (15): Balanced optimization for mixed radiator and underfloor systems.
Each AutoAdapt mode has dedicated factory configuration Sub-IDs for reading setpoint limits and defaults.
Example: Run at 1650 RPM in Constant Speed
Three separate frames, because the three things being asked for are separate concerns and the fused object cannot express one without asserting the others. Each is shown whole; each is sent in 20-byte chunks.
-
Set the mode — Object 86 Sub 10,
operation_mode = NoCmd (0x06), setpoint7F FF FF FF(keep):No commit follows. A mode change persists by itself, and the commit writes the schedule, which has nothing to do with the control mode.
-
Set the setpoint — Object 86 Sub 6 (fused),
operation_mode = AUTO (0x00), mode0x02, setpoint44 CE 40 00= 1650.0:Then send the configuration commit, built from the pump's current ClockProgramOverview as described above.
-
Start the pump — Class 3, and nothing else:
0x06= START,0x05= STOP. Nine bytes, with no room for a mode or a setpoint — which is the point.
Then read back. The acknowledgement means the frame was accepted, not that the value was stored. Request 600 RPM and the pump acknowledges, then stores 1650: its minimum. Read Object 86 Sub 7 to find out what actually happened.
Setpoint Configuration
Each control mode has factory-configured minimum, maximum, and default setpoint values stored in the pump's non-volatile memory.
Object 86: the control objects
Object 86 carries several sub-objects, and which one you address decides what a read or write actually does. Getting this wrong is quiet rather than loud, so the distinction is worth stating plainly:
| Sub | Name | Direction | What it does |
|---|---|---|---|
| 5 | overall_remote_operation_request |
write | Remote operation request |
| 6 | overall_operation_local_request |
write | Fused: run state + control mode + setpoint, all in one frame |
| 7 | overall_operation_prioritized_request |
read | The pump's state after it has weighed remote, local and alarm influence |
| 10 | overall_control_mode_local_request |
write | Control mode only |
| 13/15/39 | setpoint limits | read | Per-mode limits (see below) |
Read state from Sub 7, not Sub 6. Sub 6 is the request object: it
reports what was last written, and its control_source byte reads 0
whatever the pump is doing. Measured side by side on hardware, Sub 6
returned control_source = 0 while Sub 7 returned 1 (Local/Panel).
Reading remote/local state from Sub 6 can only ever say "undefined".
Response payload, after an optional 3-byte header:
control_source: 1 = Local/Panel, 2 = Remote/Digital.
operation_mode: 0 = AUTO, 1 = STOP, 6 = NoCmd (a write sentinel).
Change the mode through Sub 10, not Sub 6. Sub 6 writes the setpoint in
the same frame, so a mode change through it either forces the pump on or
overwrites the target mode's stored setpoint with whatever was supplied.
Sub 10 carries no-op sentinels in those fields - operation_mode = 0x06
(NoCmd) and set_point = 0x7FFFFFFF (NaN) - and applies only the mode.
Reading Sub 10 back returns exactly those sentinels, which is how the
format was confirmed.
Wire form: Sub 10 is addressed as Obj 0x0A01 / Sub 0x5600; Sub 6 is
Obj 0x0601 / Sub 0x5600.
Run state: Class 3, not Object 86
Starting and stopping the pump uses the Class 3 run commands, which carry no mode and no setpoint and so cannot disturb either:
| Command | Frame |
|---|---|
| START | 27 05 E7 F8 03 81 06 <CRC> |
| STOP | 27 05 E7 F8 03 81 05 <CRC> |
The operation specifier is 0x81 (SET). 0xC1 (INFO) is answered with a
descriptor rather than executed. The pump acknowledges with a bare frame -
[03 00] for a command it ran, [03 01 xx] for one it only described -
and sends no notification afterwards, so the resulting run state has to be
read back.
Factory Configuration Object
Setpoint limits are stored in:
- Class: 10 (
0x0A) - Object: 86 (
0x56) - Sub-IDs: Mode-specific (see table below)
Setpoint Limit Sub-IDs
Each control mode maps to a Sub-ID in Object 86 holding its limits.
| Control Mode | Sub-ID |
|---|---|
| Constant Speed | 13 |
| Constant Pressure | 15 |
| Proportional Pressure | 17 |
| AutoAdapt Radiator | 19 |
| AutoAdapt Underfloor | 21 |
| AutoAdapt Combined | 23 |
| Constant Flow | 39 |
All of them answer with the same identifier pair, 0x0001, 0x2D01 — the
identifiers name the object's type, not which sub-id you asked for.
Reading Setpoint Limits
The payload is float32, not uint16. Earlier revisions of this page said "3× uint16 (min, max, default)", which decodes real captures into nonsense.
Read request for Constant Speed (Sub-ID 13):
Measured reply, from an ALPHA HWR:
24 27 F8 E7 0A 23 00 01 2D 01 00 00 1C
45 2F 00 00 44 CE 40 00 45 65 70 00 C5 65 70 00
3F 80 00 00 3F 80 00 00 3F 80 00 00 [CRC]
After the 3-byte structure header (00 00 1C, the last byte being the
28-byte length that follows), seven big-endian float32:
| # | Bytes | Value | What it is |
|---|---|---|---|
| 0 | 45 2F 00 00 |
2800.0 | Nominal / default |
| 1 | 44 CE 40 00 |
1650.0 | Minimum |
| 2 | 45 65 70 00 |
3671.0 | Maximum |
| 3 | C5 65 70 00 |
−3671.0 | Maximum, negated |
| 4–6 | 3F 80 00 00 |
1.0 | Scaling factors |
Fields 1 and 2 are not inferred from position — they are confirmed by the pump's own behaviour. Ask for 600 RPM and it stores 1650; ask for 4400 and it stores 3671. The limits block and the clamp agree exactly.
This is also where 45 65 70 00 comes from. It appeared for years as an
inert-looking "suffix" in control payloads, and it is the pump's maximum
speed: sending it wrote run at full speed over whatever setpoint the mode
actually held.
Only Sub-ID 13 has been captured. The others are assumed to share the layout because they share the type code, but that is an inference, not a measurement — treat their field meanings as unverified.
Units
Fields carry the mode's native wire unit:
- Pressure modes: Pascals. Divide by 9806.65 for metres.
- Speed: RPM, no conversion.
- Flow: SI m³/s for setpoints. Telemetry flow is m³/h — both are correct and they differ. See units.md.
There is no client-side validation API
read_setpoint_limits() and validate_setpoint() do not exist, and never
did. The library validates against its own per-mode ranges before sending,
and otherwise lets the pump decide — which is the honest arrangement, because
the pump clamps rather than refusing, so no amount of pre-validation
guarantees that what you asked for is what you get.
Use the verified write path, which reads the value back and reports it:
result = await client.control.set_setpoint(ControlMode.CONSTANT_SPEED, 600.0)
result.status # CLAMPED
result.value # 1650.0
Cumulative Statistics
The pump stores cumulative operational statistics in non-volatile memory:
- Class: 10 (
0x0A) - Object: 93 (
0x5D) - Sub-ID: 1
Statistics Data Format
The reply carries a 3-byte structure header, then the data. Offsets below are from the start of the payload, i.e. after that header:
| Offset | Field | Type | Description |
|---|---|---|---|
| 0-3 | Start Count | uint32 | Number of motor starts |
| 4-5 | Starts, last 1 h | uint16 | |
| 6-7 | Starts, last 24 h | uint16 | |
| 8-11 | Operating Time | uint32 | Total seconds of operation |
All big-endian.
Read request — Class 10, OpSpec 0x03 (INFO), Object 93, Sub 1:
Note this is a read: nine bytes, opspec 0x03, and the object addressed
once as a single byte followed by a 16-bit sub-id. Earlier revisions of this
page showed 27 0C E7 F8 0A 90 5D 00 00 93 00 01, which uses the write
opspec 0x90 and encodes the object twice. It is not a frame the pump
answers.
Parsing: