- ESP8266 - 2.32€: Controller
- ADS1115 - 1,92€: ADC board to have more analog input and better resolution.
- pH Sensor and circuit board:
- DFRobot ph sensor kit - 29,37€: Probe + sensor circuit board.
- DFRobot isolator board - 19,81€ + delivery: Required to get rid of parasite current and have stable readings.
- pH buffer 4 - 4,50€ - Much cheaper in growshops than pool shops.
- pH buffer 7 - 4,50€ - Much cheaper in growshops than pool shops.
- I power my ESP with a USB phone charger.
Optional:
- Collier de prise en charge - 2,30€: To mount on pool pipe (after the filter or in a bypass).
- Probe holder - 13,90€
- I2C OLED screen - 1,90€
- Plastic case - 5€
You can download the diagram below in PNG and open it in Excalidraw to edit it.
If you are just doing pH readings in HA, you can skip the screen, switch and pressure sensor.
substitutions:
device_name: esppool32-controller
base_id: "esppool32"
name: "Pool Controller"
esphome:
name: $device_name
comment: "Pool automation controller"
esp32:
board: nodemcu-32s
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
reboot_timeout: 0s
# Enable logging
logger:
# Enable Home Assistant API
api:
reboot_timeout: 0s
encryption:
key: !secret api_encryption_key
# Enable over-the-air updates
ota:
- platform: esphome
password: !secret ota_password
preferences:
flash_write_interval: 5s
captive_portal:
i2c:
sda: GPIO21
scl: GPIO22
scan: True
frequency: 800kHz
ads1115:
- address: 0x48
button:
- platform: restart
name: "Controller Restart"
font:
### Screen font
- file: "gfonts://Open+Sans"
id: arial_unicode
size: 18
display:
### Oled display screen
- platform: ssd1306_i2c
model: "SSD1306 128x64"
id: "${base_id}_display_screen"
# reset_pin: D0
address: 0x3C
lambda: |-
if (!id(${base_id}_display_switch).state) {
it.printf(0, 0, id(arial_unicode), "Filter: %.1f psi", id(${base_id}_filter_pressure).state);
it.printf(0, 18, id(arial_unicode), "Temp: %.1f °C", id(pool_temperature).state);
it.printf(0, 36, id(arial_unicode), "Ph: %.2f", id(${base_id}_ph).state);
}
binary_sensor:
### Toggle switch to turn on the screen.
- platform: gpio
pin:
number: GPIO18
mode:
input: true
pullup: true
id: "${base_id}_display_switch"
switch:
### VFD - Input for Frequency - input 1 and 3
### Relay #1
- platform: gpio
id: "${base_id}_relay_frequency_1"
pin: GPIO13
name: "Frequency input 1"
internal: true
inverted: true
restore_mode: RESTORE_DEFAULT_OFF
### Relay #2
- platform: gpio
id: "${base_id}_relay_frequency_3"
pin: GPIO25
name: "Frequency input 3"
internal: true
inverted: true
restore_mode: RESTORE_DEFAULT_OFF
### VFD - Input for Start
### Relay #3
- platform: gpio
icon: "mdi:pump"
id: "${base_id}_relay_run"
pin: GPIO26
name: "Start Pump"
inverted: true
restore_mode: RESTORE_DEFAULT_OFF
on_turn_on:
then:
- lambda: |-
id(${base_id}_last_vfd_speed) = id(${base_id}_pool_pump_speed).state.c_str();
- if:
condition:
lambda: 'return id(${base_id}_pool_pump_speed).state != "100%";'
then:
- select.set:
id: "${base_id}_pool_pump_speed"
option: "100%"
- delay: !lambda "return id(pump_delay) * 1000;" # Delay in milliseconds
- lambda: |-
id(${base_id}_pool_pump_speed).publish_state(id(${base_id}_last_vfd_speed).c_str());
### Controls contactor for chlorine pump
- platform: gpio
icon: "mdi:blood-bag"
id: "${base_id}_relay_chlorine_pump"
pin: GPIO27
name: "Chlorine pump"
interlock: ["${base_id}_relay_ph_pump"]
inverted: false
restore_mode: RESTORE_DEFAULT_OFF
### Controls contactor for pH pump
- platform: gpio
icon: "mdi:blood-bag"
id: "${base_id}_relay_ph_pump"
pin: GPIO32
name: "pH pump"
interlock: ["${base_id}_relay_chlorine_pump"]
inverted: false
restore_mode: RESTORE_DEFAULT_OFF
select:
### VFD - Speed options
- platform: template
name: "Pump Speed"
icon: "mdi:speedometer"
id: "${base_id}_pool_pump_speed"
optimistic: true
options:
- "100%"
- "90%"
- "60%"
- "50%"
initial_option: "100%"
restore_value: true
on_value:
then:
- lambda: |-
if (id(${base_id}_pool_pump_speed).state == "100%") {
id(${base_id}_relay_frequency_1).turn_off();
id(${base_id}_relay_frequency_3).turn_off();
} else if (id(${base_id}_pool_pump_speed).state == "90%") {
id(${base_id}_relay_frequency_1).turn_on();
id(${base_id}_relay_frequency_3).turn_off();
} else if (id(${base_id}_pool_pump_speed).state == "60%") {
id(${base_id}_relay_frequency_1).turn_off();
id(${base_id}_relay_frequency_3).turn_on();
} else if (id(${base_id}_pool_pump_speed).state == "50%") {
id(${base_id}_relay_frequency_1).turn_on();
id(${base_id}_relay_frequency_3).turn_on();
}
number:
### pH 7 Calibration voltage
- platform: template
name: "pH 7 calibration"
id: "${base_id}_ph7_calibration"
icon: "mdi:tune-vertical-variant"
unit_of_measurement: "V"
mode: box
min_value: 0
max_value: 4.5
step: 0.001
restore_value: true
optimistic: true
on_value:
then:
- lambda: |-
id(${base_id}_ph7_calibration_var) = float(x);
### pH 4 Calibration voltage
- platform: template
name: "pH 4 calibration"
id: "${base_id}_ph4_calibration"
icon: "mdi:tune-vertical-variant"
unit_of_measurement: "V"
mode: box
min_value: 0
max_value: 4.5
step: 0.001
restore_value: true
optimistic: true
on_value:
then:
- lambda: |-
id(${base_id}_ph4_calibration_var) = float(x);
### Time to run at full speed on startup
- platform: template
name: "Pump Full Speed Startup Delay"
id: "${base_id}_pump_full_start_delay"
icon: "mdi:timer"
unit_of_measurement: "s"
mode: box
min_value: 5
max_value: 300
step: 5
initial_value: 40
restore_value: true
optimistic: true
on_value:
then:
- lambda: |-
id(pump_delay) = (int)x;
globals:
### pH 7 Calibration number variable
- id: "${base_id}_ph7_calibration_var"
type: float
restore_value: no
initial_value: '0'
### pH 4 Calibration number variable
- id: "${base_id}_ph4_calibration_var"
type: float
restore_value: no
initial_value: '0'
### Store last VFD speed value
- id: "${base_id}_last_vfd_speed"
type: std::string
restore_value: no
### Time to run at full speed on startup
- id: pump_delay
type: int
restore_value: yes
initial_value: '40'
sensor:
### Filter pressure in PSI
- platform: ads1115
multiplexer: 'A0_GND'
gain: 6.144
name: "Filter Pressure"
id: "${base_id}_filter_pressure"
unit_of_measurement: "psi"
update_interval: 2s
icon: "mdi:propane-tank"
accuracy_decimals: 1
filters:
- calibrate_linear:
- 0.495 -> 0
- 4.5 -> 72
- median:
window_size: 5
send_every: 5
- clamp:
min_value: 0
### PH in generic units
- platform: ads1115
multiplexer: 'A1_GND'
gain: 6.144
name: "pH"
id: "${base_id}_ph"
unit_of_measurement: ""
update_interval: 5s
icon: "mdi:ph"
accuracy_decimals: 1
filters:
- lambda: |-
float m = (7.0 - 4.0)/(id(${base_id}_ph7_calibration_var) - id(${base_id}_ph4_calibration_var));
float c = 7.0 - (m * id(${base_id}_ph7_calibration_var));
return (m * x) + c;
- median:
window_size: 3
send_every: 3
### Filter pressure in Volts for calibration
- platform: ads1115
multiplexer: 'A0_GND'
gain: 6.144
name: "Filter Pressure Voltage"
id: "${base_id}_filter_pressure_volts"
disabled_by_default: true
unit_of_measurement: "V"
icon: "mdi:current-dc"
update_interval: 60s
### PH in Volts for calibration
- platform: ads1115
multiplexer: 'A1_GND'
gain: 6.144
name: "pH Voltage"
id: "${base_id}_ph_volts"
unit_of_measurement: ""
icon: "mdi:current-dc"
update_interval: 2s
accuracy_decimals: 3
### Water temperature taken from HA
- platform: homeassistant
id: pool_temperature
entity_id: sensor.pool_thermometer_temperatureThe color will change according to how suitable the pH is.
You can change the thresholds if you use bromine as it tolerates wider pH variations.
cards:
- type: gauge
entity: sensor.ph
min: 6.5
max: 8
needle: false
segments:
- from: 0
color: red
- from: 6.8
color: yellow
- from: 7
color: lightgreen
- from: 7.2
color: green
- from: 7.5
color: lightgreen
- from: 7.7
color: yellow
- from: 7.9
color: redred
- Open the
Pool-Controllerdevice in HA. - Dip the probe in deionized water until the voltage is stable.
- Take it out, shake the water off and dip it in buffer 7.
- When the voltage is stable, change the value of
pH 7 calibration. - Take it out, shake the water off and dip it in deionized water.
- Take it out, shake the water off and dip it in buffer 4.
- When the voltage is stable, change the value
pH 4 calibration.
