Investigate if its possible to get Uponor Clean 1 into Home Assistant
- From the modem port connect an ESPHOME (e.g ESP32 S3) and insert values into Home Assistant
- It's an 868.2 MHz (channel 117) radio between inner unit and outer unit, and make decoding of this device in rtl_433
Confirmed: serial/GSM telemetry (CYCLE COUNTER/PLANT STATUS/ALARM STATUS over the modem port, no SIM needed); the 868.2 MHz radio frame
(address, 32-byte payload, CRC-16) and its messaging protocol (node IDs,
heartbeat, all 5 alarm types); the E-code/S-code/radio-type numbering; the
7-channel actuator output mux (MV1–MV5/Compressor/Spare) and its
self-test → alarm-code path; the High water sensor input, traced end to
end from silkscreen to alarm code.
Open — highest value first: the RF service/handheld command protocol
(needs a live capture, static analysis can't get further); the physical
Test-button GPIO/timer; the other 3 sensor inputs (Startup level,
Chemical pressure, Spare have no traced firmware consumer yet); an
ESP32+nRF905 TX proof-of-concept (radio config is fully known, but nothing
has transmitted yet — the actual open question is whether the real panel
accepts it).
Where to look:
| Doc | Covers |
|---|---|
| docs/u2-serial-protocol.md | Serial/GSM telemetry — the working Home Assistant route |
| docs/rtl433-decoder.md | Radio frame spec, capture recipes, rtl_433 decoder |
| docs/ghidra/README.md | Payload byte map, who writes/sends it |
| docs/eeprom-map.md | EEPROM layout, alarm/E-code table, actuator + sensor firmware traces |
| docs/ghidra/codes.md | The 3 separate numbering spaces (E-code/S-code/radio type), kept apart |
| docs/ghidra-firmware-analysis.md | How the MC9S08 Ghidra analysis was run, driver map |
| docs/nrf9e5-firmware.md | U1's embedded 8051/radio firmware |
| docs/ghidra/register-map.md | Peripheral registers, cross-checked against datasheets |
This project uses two physical Clean 1 boards, and it matters which artifact came from which:
- Board A — the original, retired after OC13 (a MOC3063 output-driver
optotriac, one of the OC8–OC14 channels) failed. All firmware and memory
dumps in
dumps/were read from board A (u2-mc9s08gt-flash.*,u3-m24128-eeprom.bin). OC13 is only an output stage, so its CPU / serial / EEPROM / radio are intact — board A is the safe bench mule. - Board B — the new active replacement, currently installed. All
868 MHz
rtl_433radio captures come from board B, as does the display's cycle counter (satsräknare).
So firmware/EEPROM analysis describes board A and on-air behaviour describes
board B. They are the same model but not guaranteed byte-identical — the
EEPROM config (code + phone numbers) and the counter differ per board, and the
radios may be individually paired. Confirm SW Ver: / Proc Ver: match on both
before using board-A firmware to explain board-B radio.
This is now the validated primary telemetry route. The U2 CPU runs a full GSM/SMS controller: it drives a Telit modem over SCI2 at 9600 8N1 and answers a text command interface that reports the plant state in plain ASCII.
Impersonating that modem on the bench (no SIM) reads it directly — send the unit's 4-character access code and it replies:
CYCLE COUNTER:2129
PLANT STATUS:S102 (S102 = Aeration; S1xx clean / S2xx wait / S3xx maint / S4xx test)
ALARM STATUS:NO
The J5 "Modem" header is RS-232 via U10/SP3232 (not TTL), which is why the
first attempt — a bare USB-serial cable sweeping baud rates — saw nothing: it was
reading inverted RS-232 levels, and with no modem attached U2 mostly stays quiet.
Tap the SP3232 TTL side (or use an RS-232 adapter), impersonate the modem, and the
telemetry falls out. Full write-up, wiring, command grammar, and the
tools/fake_telit.py bench tool:
docs/u2-serial-protocol.md.
| No | Description | IC |
|---|---|---|
| U1 | Transceiver | nRF9E5 |
| U2 | CPU | MC9S08GT32ACFBE |
| U10 | Serial | sipex 3232 |
| U3 | I2C Mem | 4128BWP 8424K |
| OC8–OC14 | Optoisolator | MOC3063 1512 |
| D6–D12 | Mosfet | VNE46 AC 4DLMG |
| OC1–OC7 | Mosfet | 1435 814 |
The board carries a row of seven identical output-driver channels (the repeated stages that switch the magnetventiler MV1–MV5, the compressor and the pumps), so the designators fall into three matched series — one part per channel:
- D6–D12 are all the same VNE46 mosfet as D11.
- OC1–OC7 are all the same 1435 814 mosfet as OC6.
- OC8–OC14 are all the same MOC3063 optoisolator as OC13.
The two OC series interleave across the row as two descending runs (…OC14, OC7,
OC13, OC6, OC12, OC5… reading left to right), read off the high-resolution
silkscreen in docs/uclean1-pcb.jpg.
Confirmed by silkscreen (a second board photo, terminal-block edge): the
output row's screw terminals are labelled, left to right, MV1 Compressor,
MV2, MV3, MV4, MV5, Spare, Alarm — matching
docs/eeprom-map.md's EEPROM-derived fault table exactly
(E040 compressor, E041–E045 = MV1–MV5) and confirming Spare as a genuine
unused 7th channel, not a numbering gap. Firmware-confirmed (FUN_a138/
FUN_c9e1, docs/eeprom-map.md):
MV1 Compressor is actually two separate adjacent channels (Compressor =
index 0, MV1 = index 1) that just read as one combined label on the
silkscreen — the self-test loop addresses all 7 channels through one shared
PTBD mux bus, not individual GPIOs, and its per-channel fault codes
(channel_index + 0x28) match E040–E045 exactly. Alarm is a separate
2-pin terminal (not part of the output row) — likely a dry-contact/relay
output for an external alarm siren or building-management input, not yet
traced in firmware.
Same photo also shows, near U2: a 4-digit 7-segment display, a physical
push-button (S1) silkscreened Test directly above the display, and a
2-pin screw terminal (J16) silkscreened Test btn directly below
it — plus a TEST 464 PASSED QC sticker elsewhere on the board (unrelated;
just a factory QC-pass label).
Correction from an earlier read of this photo: S1 and J16 are not
separate factory-vs-field interfaces — they're adjacent, on the same small
sub-circuit next to the display, and confirmed by a continuity check
(multimeter, S1's pads to J16's terminals) to be the same net: S1
is the onboard equivalent of the customer's external green Test button (the
manual's 5s/10s/14s hold behavior, eeprom-map.md), not
a separate factory-only path. This also fits the firmware: codes.md found
only one flag (DAT_014a) for the physical button's long-press
behavior, no evidence of two independent button inputs. The QC sticker was
unrelated electrically and shouldn't have been cited as evidence either way.
Distinct from the still-open "handheld service device" question in
docs/ghidra/codes.md.
Also visible: a gold SMA-style antenna connector next to U1, and the Modem
header from docs/u2-serial-protocol.md.
Sensor inputs, confirmed by silkscreen (J4, top-left, 8-pin screw
terminal): the board has exactly 4 sensor inputs, left to right:
Startup level, High water, Chemical pressure, Spare. This resolves the
"High water?" uncertainty in docs/eeprom-map.md — it's
the confirmed silkscreen name, not a guess — and shows the chemical/flocculant
side uses a pressure sensor, not a level float. Spare here is a second
genuine unused channel (distinct from the output row's Spare). Two 14-pin
SOICs (U4/U5, part numbers not legible in the photo) sit directly behind
the header, likely conditioning the 4 raw signals before U2 reads them.
Firmware-traced for the High water channel specifically
(docs/eeprom-map.md):
sensor → mux → FUN_a0da poll → FUN_c03c debounce → alarm code 0x1F,
exactly EEPROM E031 "High water; tank". The other 3 channels don't have an
equivalent traced handler yet.
U3 is an M24128 EEPROM on the I2C bus at address 0x50, read with a Raspberry Pi
and i2c-tools. Wiring the Pi to the chip, reading the full 16 KB (the i2cdump
above only reaches the first 256 bytes), and the dump analysis now live on their
own page: docs/eeprom-map.md.
To read out the flash content from the CPU using reader like USBDM will get you and srecord dumps/u2-mc9s08gt-flash.s19 file. This file one need to convert to binary file dumps/u2-mc9s08gt-flash.bin with e.g objcopy.
objcopy --input-target=srec --output-target=binary dumps/u2-mc9s08gt-flash.s19 dumps/u2-mc9s08gt-flash.bin
Reading the flash is a debug operation, not a programming one. A plain flash programmer only exposes the program flow (blank-check → erase → program → verify) and has no "read". To dump memory you use the BDM debug channel and a tool that issues the BDM memory-read command:
-
USBDM + GDB server — start the HCS08 GDB server that ships with USBDM, then in the HCS08
gdb:target remote localhost:1234 dump srec memory flash.s19 0x8000 0x10000 # 32 KB flash of the GT32 dump srec memory ram.s19 0x0080 0x1080 # RAM, optionaldump srec memory FILE START ENDwalks the range over BDM and writes an S-record (usedump ihex memory …for Intel-hex). MC9S08GT32 map: 32 KB flash at 0x8000–0xFFFF, RAM at 0x0080–0x107F. -
CodeWarrior / P&E Multilink — connect in the debugger and use Import/Export Memory → Export (or the
savecommand) over the flash range.
Flash security caveat. If read-back is blocked, the part is secured: the FSEC
byte at 0xFFBF (SEC01:SEC00) makes a programmer able only to mass-erase, never
read. When secured, BDM reads of flash and RAM are blocked and the only legal
operation is a mass-erase (which unsecures by wiping everything — useless for
dumping). The one non-destructive escape is the 8-byte backdoor key at
0xFFB0–0xFFB7, if the firmware enabled KEYEN and you know it. The existing
dumps/u2-mc9s08gt-flash.s19 came out cleanly, so U2 on this board was not
secured — a straight BDM dump is enough.
The U2 CPU flash (dumps/u2-mc9s08gt-flash.s19) was disassembled in Ghidra. How the headless analysis was run, the gotchas, and the recovered driver map (I2C EEPROM access + the SPI-slave finding) now live on their own page: docs/ghidra-firmware-analysis.md.
U1's radio firmware (8051, on-air Manchester/frame/nRF905 config) has no external boot chip — it's embedded in the U2 flash we already dumped, and has been extracted + disassembled. See docs/nrf9e5-firmware.md.
The 868 MHz frame (address, 32-byte payload, CRC-16) is confirmed and the decoder is enabled and CRC-gated in the fork: andy778/rtl_433, branch add-uponor-clean1. Spec, byte mapping, capture recipes, and what's still open: docs/rtl433-decoder.md.
