poolctl.

A DIY pool and spa controller replacing a Pentair IntelliConnect — and the record of everything that had to be measured rather than assumed.

Built on nodejs-poolController running on a Raspberry Pi, with a separate supervisor process holding the interlocks that njsPC does not have. One specific pool, documented in full: thirteen architecture decisions with their rejected alternatives, the panel layout down to the cable runs, and the measurements that contradicted the datasheets.

Stack
on the Pi
njsPC
v10.0.1
Tests
686
Equipment on bus
none yet

Why the measurements are the point

A rule this project runs on: every number needs a source. If a figure cannot say where it came from, it is treated as fiction. That rule keeps earning its place.

Relay card

Its product page names the 8relay-rpi driver.

The routing is 8relind-rpi's. That driver's channels close relays 1, 2, 8, 7, 3, 4, 5, 6 — so writing channel 8, the nominal spare, starts the blower. Mapped by hand on the bench.

Watchdog

The same page advertises a hardware watchdog.

The I²C bus holds a single port expander at 0x27 and nothing else — no microcontroller, no watchdog. It had to be built in software instead. It buys recovery, not safety: the expander latches, so a wedged Pi leaves every relay where it was. Measured — relays held through all 51 s of a deliberate wedge.

Requirements

Two figures sat in the requirements document justifying design decisions.

Both were invented outright — a pool-heating duration and the exchanger pressure drop that justified automating a valve. Found by auditing every number for a citation.

Architecture

Several decisions rested on what njsPC was assumed to do.

Four were reversed once someone read its source. It creates the bodies, circuits and valves unprompted, drives them from its own timers, and cannot be made passive — so this project supervises it rather than replacing it.

Test suites

Unit tests passed on interlocks that were wrong in the same way three times.

Integration suites — a relay card faked at the process boundary, an njsPC answering real HTTP — found that the bypass, the pool heat call and the blower were each enforced only when an intent was what moved. njsPC takes the body without asking. Every one had the right end state and the wrong route through it.

The afternoon it was real

Everything above is a discovery that something was wrong. Here is the other half. On 30 August 2026 a temporary rig — lever connectors, pluggable blocks, no gland and no permanent cable — put the heat pump on two relay channels for one afternoon.

11:36:35relays -> 0x10 REL4pool heat, tapped on a phone 11:39:58relays -> 0x00 (all off)released — flow held through the exchanger 11:43:01purge elapsed3 m 03 s, against 180 s configured 11:43:01relays -> 0x40 REL3bypass isolates — at rest

The interesting line is the first one. Resting state was 0x40 — the bypass valve held around the heater. The call is 0x10. So the bypass released and the heat call closed in a single write to a single byte: there is no instant at which the heater is firing into an exchanger that has no water going through it. That interlock is not enforced by getting the order of two writes right. The bad combination cannot be represented.

The third line is the other half of the same idea. Ending a call does not isolate the exchanger — it keeps water moving through it for three minutes first, and only then lets the valve swing. A spa call ran the same way on the other channel.

Then the wires came off and the heater went back to its own thermostat. It is not installed. What survives is measurements: a flow floor that had been invented, a purge that had never run, and a three-wire control model that had been read from a manual and was wrong about which terminals it used.

Where it has got to

PartStateNotes
Supervisor and UI on the Pirunning Both systemd services, verified across a reboot, watchdog restarts a wedged process
Relay drivingdone Channel map measured, not inherited; written to the expander directly
Interlocksdone Bypass policy, heat-conditional pump floor, purge, targets as cutoffs
Commissioning checksdone Ten checks comparing njsPC's settings against what this repo believes. It reports; it never corrects
Equipment on the busnot yet No serial port, so every reading is null — which is the state the tests are written against
Valve travel modellingblocked Needs a stopwatch on a real actuator. Until then njsPC's own delay clock is displayed, and said to be its
Scheduled preheatblocked Wants a real water temperature. Refuses with a reason rather than doing nothing
TLSdeferred Deliberately. The reasoning is in the requirements document

⚠︎ Safety

This documents a system switching 240 V pool equipment — a heat pump, a variable-speed pump and a 1.5 HP blower — in a wet environment.

  • The 240 V side, including NEC 680 equipotential bonding, is work for a licensed electrician and in most places needs a permit and an inspection. This is not a wiring guide.
  • The heater keeps its own thermostat and its own hard caps, in its own firmware. That is deliberate, and it is why no software bug here can produce a scalding spa.
  • Everything is specific to one site. Valve travels, relay wiring and flow thresholds do not transfer. Survey your own system.