A set of ESPHome external components for talking to Pentair pool equipment over RS-485 and exposing it to Home Assistant — without a Pentair automation panel (EasyTouch / IntelliCenter).
| Component | What it does |
|---|---|
intellichlor |
Read & control a Pentair IntelliChlor / iChlor salt chlorine generator (SWG): salt level, water temp, faults, and the chlorine output %. |
pentair_pump |
Master/controller for a Pentair-protocol variable-speed pump: read RPM/watts/GPM and set speed, flow, and run state. |
uart_splitter |
Fan a single hardware UART out to multiple read-only channels so several components can share one RS-485 bus. |
pentair_sniffer |
Passive bus monitor — decodes pump frames and counts traffic for debugging/verification. Writes nothing. |
Each component is a standalone "hub" that you drop into your own ESPHome YAML; every entity is optional, so you expose only what you want. The ESP can run as a passive monitor of an existing controller, or take over and drive the equipment directly.
- An ESP32 (the examples use an
esp32-s3-devkitc-1, ESP-IDF framework). - An RS-485 transceiver (e.g. MAX485 / a 3.3 V-safe automatic-direction module)
wired to the equipment's RS-485 / COM bus:
- ESP
tx_pin→ transceiverDI - ESP
rx_pin← transceiverRO - Optional
flow_control_pin→DE+!REtied together (for non-auto-direction modules in half-duplex).
- ESP
- The IntelliChlor / Pentair bus is 9600 8N1 (parity
NONE, 1 stop bit). Theintellichlorcomponent enforces this at validation time.
⚠️ Mains & pool safety. This involves wiring into pool equipment carrying mains voltage. Do the work with power off, and only if you're comfortable with it. This is unofficial, reverse-engineered software with no affiliation with Pentair. Use at your own risk.
Point external_components at this repo and pull in the components you need:
external_components:
- source: github://wolfson292/intellichlor
components: [intellichlor] # add pentair_pump, uart_splitter, pentair_sniffer as needed
refresh: 1dA complete, copy-paste-friendly single-device config lives in
example.yaml. The sections below summarize each component.
ESPHome caches external components. If a rebuild doesn't pick up changes, lower
refreshor clear the.esphome/directory so it re-pulls.
Talks to the IntelliChlor cell (a slave at bus address 0x50). With takeover off
the ESP passively reads the cell as the existing controller polls it; with takeover
on the ESP impersonates the controller and asserts the output % itself.
uart:
id: swg_uart
tx_pin: GPIO17
rx_pin: GPIO18
baud_rate: 9600
parity: NONE # required
stop_bits: 1 # required
# Optional: a time source lets "boost" survive a reboot/OTA (stored as an
# absolute timestamp in flash).
time:
- platform: sntp
id: sntp_time
intellichlor:
id: swg
uart_id: swg_uart
# flow_control_pin: GPIO8 # optional DE/!RE pin for half-duplex transceivers
time_id: sntp_time # optional, enables boost persistence
update_interval: 60sSensors
| Key | Description |
|---|---|
salt_ppm |
Salt level, ppm |
water_temp |
Water temperature (reported in °F) |
output_percent |
Cell-reported actual generation % |
set_percent |
Last commanded output % |
boost_remaining |
Minutes left in an active boost |
status |
Status byte from the cell |
error |
Raw alarm/fault bitmask (decode manually if needed) |
Binary sensors (fault bits): no_flow, low_salt, very_low_salt,
high_current, clean, low_volts, low_temp, check_pcb.
Text sensors: version (model string, e.g. Intellichlor--40),
firmware_version (cell firmware, e.g. 1.8).
Controls
| Platform | Key | Description |
|---|---|---|
number |
swg_percent |
Output setpoint, 0–100% (persisted across reboot/OTA) |
switch |
takeover_mode |
ON = ESP drives the cell; OFF = passive monitor |
select |
swg_boost |
Run at 100% for a fixed time: Off / 6h / 12h / 24h / 48h |
button |
end_boost |
Cancel an active boost early |
Boost only takes effect while
takeover_modeis ON. With takeover off the cell is driven by the real controller; boost will log a warning and do nothing.
Fault bit caveat: the
high_current/cleanbits follow the protocol spec, which diverges from one hardware capture. If a real "clean cell" condition lights up "High Current" (or vice versa), seeAGENTS.md— the rawerrorsensor always carries the unmodified value.
See the example for the full entity block.
Master/controller for a Pentair-protocol variable-speed pump (e.g. a LINGXIAO VS pump
in ECOM mode) at bus address 0x60. Polls the pump, can hold it in remote control, and
sets speed/flow + run state.
pentair_pump:
id: pump
uart_id: pump_uart # its own bus, or a uart_splitter channel
address: 0x60 # pump address (default 0x60)
source_address: 0x10 # controller/source address (default 0x10)
update_interval: 2sSensors: rpm, watts, gpm, mode, drive_state, run_state, status_code.
Binary sensor: running.
Text sensor: last_status (full hex of the last status reply).
Controls
| Platform | Key | Description |
|---|---|---|
switch |
takeover |
Master enable: actively control vs monitor-only |
switch |
run |
Start/stop the pump |
select |
mode |
Setpoint mode: Speed (RPM) or Flow (GPM) |
number |
target_rpm |
Target speed, 600–3450 RPM (step 10) |
number |
target_gpm |
Target flow, 15–130 GPM (step 1) |
The IntelliChlor and the pump can share the same physical RS-485 bus. ESPHome's uart
hub is single-owner, so this component takes over one hardware UART and fans it out to
several channels that each look like a UART to downstream components.
uart:
id: mod_uart
tx_pin: GPIO17
rx_pin: GPIO18
baud_rate: 9600
uart_splitter:
uart_id: mod_uart
channels:
- id: uart_chlor # -> intellichlor
- id: uart_pump # -> pentair_pump
- id: uart_pump_sniff # -> pentair_sniffer
intellichlor:
uart_id: uart_chlor
# ...
pentair_pump:
uart_id: uart_pump
# ...Channels default to 9600 8N1; override baud_rate / data_bits / stop_bits /
parity per channel if needed. Components on shared channels queue and gate their
transmits (≈one frame per 100 ms) to stay polite on the shared bus.
A passive, write-nothing monitor of the pump protocol — useful for verifying what the pump masters are actually putting on the wire. Decodes frames and exposes counters.
pentair_sniffer:
id: sniffer
uart_id: uart_pump_sniff
pump_address: 0x60
log_all_frames: true
# all sensors below are optional:
pump_rpm: { name: "Sniffer Pump RPM" }
pump_watts: { name: "Sniffer Pump Watts" }
pump_gpm: { name: "Sniffer Pump GPM" }
valid_frames: { name: "Sniffer Valid Frames" }
pump_frames: { name: "Sniffer Pump Frames" }
chlorinator_frames: { name: "Sniffer Chlorinator Frames" }
checksum_errors: { name: "Sniffer Checksum Errors" }
last_frame: { name: "Sniffer Last Frame" }There's no standalone firmware here — these are components compiled by ESPHome inside a consuming project. To compile-check the whole set against this clone:
esphome config compile-test.yaml
esphome compile compile-test.yamlcompile-test.yaml wires all four components together exactly as
on the live board (single bus → uart_splitter → chlorinator + pump + sniffer) and is
a good reference for a combined setup.
The reverse-engineered RS-485 wire format (framing, DLE byte-stuffing, checksum, command
set, and status decoding) is documented in
docs/IntelliChlor_Protocol.md. Implementation notes
and history for component authors live in AGENTS.md.
GNU General Public License v3.0.
Not affiliated with or endorsed by Pentair. "IntelliChlor", "IntelliCenter", and "EasyTouch" are trademarks of their respective owners.