This is a simple project to heat a swimming pool using excess solar power.
It’s my first attempt with Home Assistant and it was surprisingly easy to do once I’d figured out the basics of YAML syntax. It’s also my first forum post so apologies for any rookie mistakes.
I have rooftop PV solar and a swimming pool heated by a heat pump. A single water pump is used to both filter the pool and push water through the heat pump when it’s heating.
Here in Australia the power companies buy excess power from you at a fraction of the cost for which they sell it to you. So it’s costly to heat the pool unless you actually use the electricity at the time you’re producing it. Obviously a home battery would solve the problem but that’s beyond my budget.
I jotted down some basic requirements for my project:
-
The heat pump should only be turned on when there’s enough excess power being generated. I want to measure excess, not actual, production because if I’m already using power for the house there’s none spare for the pool.
-
The heat pump won’t work if there’s no water flow. So the water pump must be turned on whenever the heater is on.
-
However the water pump must run for a minimum 4 hours per day to filter the pool. The heating logic must not affect that. The pump must always run for 4 hours to avoid the pool becoming a hot green soup!
-
The water pump has no wifi control or timer. It’s just a dumb on-off switch from the mains so needs a wifi-enabled power switch.
-
I don’t want the pumps turning on and off every 2 minutes as the sun peeks out from behind a cloud. That’ll destroy the motors pretty quickly. I need to use some kind of rolling average to make a decision every X minutes, for example every 60 minutes.
Step 1 - Check my equipment will work with Home Assistant
The rooftop solar uses Enphase Envoy which is integrated with Home Assistant. So no set up required, Home Assistant detected it immediately. It reports real-time power production and consumption from which I can calculate excess power.
The heat pump is wifi enabled. Reading this post it’s apparent most heat pumps are made by one company and use Tuya for wifi control. That was indeed the case. Adding the heat pump to Home Assistant was simply a case of following the integration instructions. It’s a bit of a faff to get the necessary keys but not hard. Just follow the instructions closely.
The water pump has no built-in control but given it’s just on/off a wifi power switch is all that’s needed. Because the heat pump uses Tuya I figured I’d keep it the same and go with a Tuya-based switch too. This is perfect for the job and is waterproofed. Ideal for a pool area and saves messing about installing waterproof housings etc. Having already configured Tuya for the heat pump, Home Assistant detected the switch immediately, no problem.
Step 2 - Calculate excess power and smooth it into a 60 minute rolling average
Calculating excess power is simple. Just a template sensor to calculate production minus consumption:
template:
- sensor:
name: "Excess Power"
unit_of_measurement: "W"
state: "{{ (states('sensor.envoy_XXXXXXXXX_current_power_production')|float) - (states('sensor.envoy_XXXXXXXXX_current_power_consumption')|float) }}"
That’s an instantaneous reading, which I smoothed into a 60 minute rolling average using a statistics sensor:
sensor:
- platform: statistics
name: "Excess Power 60 minute average"
entity_id: sensor.excess_power
state_characteristic: mean
max_age:
minutes: 60
Step 3 - Trigger pump and heater based on average excess power
This is all done in automations.yaml, the full version of which is below.
As mentioned before I don’t want the pumps switching on/off too quickly. I decided a minimum one hour interval between switching was appropriate. So the automation begins with an hourly timer:
#01. AVERAGE POWER TRIGGER FOR POOL HEATING
- id: average_power_trigger_pool_heating
alias: average power trigger pool heating
trigger:
platform: time_pattern
minutes: "/59"
action:
- choose:
When the timer event fires, one of three situations needs to be handled:
- The system is off but there’s enough excess power (>1kw) to turn it on. Turn the water pump on and set the heater to heat:
#System is OFF. Generating sufficient excess power (>1kw) to turn it on.
- conditions:
alias: "Generating excess power and system currently off"
and:
- condition: numeric_state
entity_id: sensor.excess_power_60_minute_average
above: 1000
- condition: state
entity_id: switch.pool_pump_socket_1
state: "off"
sequence:
- service: switch.turn_on
target:
entity_id: switch.pool_pump_socket_1
- service: climate.set_hvac_mode
target:
entity_id: climate.pool_heater
data:
hvac_mode: 'heat'
- The system is already on and there’s still enough excess power to keep it on. This is no different to #1 except the excess power check is lower because the heater is active and using 1kw. Excess power just needs to be positive to tell me I’ve got enough to keep running the heater. I set it to 100w just to give a margin of error.
There will be a better way to combine this with the code for #1 but I this is simple enough and easy to follow:
#System is ON. Generating sufficient excess power (>100w) to keep it turned on
- conditions:
alias: "Generating excess power and system currently on"
and:
- condition: numeric_state
entity_id: sensor.excess_power_60_minute_average
above: 100
- condition: state
entity_id: switch.pool_pump_socket_1
state: "on"
sequence:
- service: switch.turn_on
target:
entity_id: switch.pool_pump_socket_1
- service: climate.set_hvac_mode
target:
entity_id: climate.pool_heater
data:
hvac_mode: 'heat'
- The system is on but there’s not enough power to keep it on. Turn it off.
#System is ON. NOT generating sufficient excess power (<100w). Turn it off.
- conditions:
alias: "Not generating excess power. Turn system off."
and:
- condition: numeric_state
entity_id: sensor.excess_power_60_minute_average
below: 100
- condition: state
entity_id: switch.pool_pump_socket_1
state: "on"
sequence:
- service_template: >
{%- if is_state("input_boolean.pool_pump_onoff_timer", "on") -%}
switch.turn_on
{%- else -%}
switch.turn_off
{% endif %}
target:
entity_id: switch.pool_pump_socket_1
- service: climate.set_hvac_mode
target:
entity_id: climate.pool_heater
data:
hvac_mode: 'off'
The one complication here is the 4 hour requirement for the water pump. The heater can always be turned off but, if the pump is on as part of the 4 hour filtration cycle, it needs to stay on.
My solution was to implement the 4 hour timer using an input_boolean.
input_boolean:
pool_pump_onoff_timer:
name: "Pool Pump OnOff Timer"
initial: "off"
A separate automation timer sets the boolean to true at 8am and false at 12pm and the water pump switch is linked to the boolean. Whenever the boolean changes the pump turns on/off accordingly.
That’s why #3 uses a service_template to control pool_pump_socket_1. The pool pump is not just blindly turned off. It’s turned on or off as appropriate for the 4 hour timer.
Performance
I’m sure there are more elegant ways to do this but it works perfectly. It’s added 15 degrees C to the pool in just over a week with no increase in power drawn from the grid.
Obviously you can tinker with the settings to make it more aggressive or conservative and it took a bit of time watching power readings to figure out appropriate excess power limits for my particular equipment.
But really the hardest part was figuring out the YAML syntax for what I wanted to do and I’ve implemented, with a $100 Raspberry Pi, something that even $1000 dedicated pool controllers cannot do.
I hope this is of use to others in keeping their pool costs down.
Full configuration.yaml
# Loads default set of integrations. Do not remove.
default_config:
# Text to speech
tts:
- platform: google_translate
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
# Example configuration.yaml entry
logger:
default: info
template:
- sensor:
name: "Excess Power"
unit_of_measurement: "W"
state: "{{ (states('sensor.envoy_122122028187_current_power_production')|float) - (states('sensor.envoy_122122028187_current_power_consumption')|float) }}"
sensor:
- platform: statistics
name: "Excess Power 60 minute average"
entity_id: sensor.excess_power
state_characteristic: mean
max_age:
minutes: 60
- platform: statistics
name: "Excess Power 30 minute average"
entity_id: sensor.excess_power
state_characteristic: mean
max_age:
minutes: 30
- platform: statistics
name: "Excess Power 02 minute average"
entity_id: sensor.excess_power
state_characteristic: mean
max_age:
minutes: 2
input_boolean:
pool_pump_onoff_timer:
name: "Pool Pump OnOff Timer"
initial: "off"
Full automations.yaml
#01. AVERAGE POWER TRIGGER FOR POOL HEATING
- id: average_power_trigger_pool_heating
alias: average power trigger pool heating
trigger:
platform: time_pattern
minutes: "/59"
action:
- choose:
#System is OFF. Generating sufficient excess power (>1kw) to turn it on.
- conditions:
alias: "Generating excess power and system currently off"
and:
- condition: numeric_state
entity_id: sensor.excess_power_60_minute_average
above: 1000
- condition: state
entity_id: switch.pool_pump_socket_1
state: "off"
sequence:
- service: switch.turn_on
target:
entity_id: switch.pool_pump_socket_1
- service: climate.set_hvac_mode
target:
entity_id: climate.pool_heater
data:
hvac_mode: 'heat'
#System is ON. Generating sufficient excess power (>100w) to keep it turned on
- conditions:
alias: "Generating excess power and system currently on"
and:
- condition: numeric_state
entity_id: sensor.excess_power_60_minute_average
above: 100
- condition: state
entity_id: switch.pool_pump_socket_1
state: "on"
sequence:
- service: switch.turn_on
target:
entity_id: switch.pool_pump_socket_1
- service: climate.set_hvac_mode
target:
entity_id: climate.pool_heater
data:
hvac_mode: 'heat'
#System is ON. NOT generating sufficient excess power (<100w). Turn it off.
- conditions:
alias: "Not generating excess power. Turn system off."
and:
- condition: numeric_state
entity_id: sensor.excess_power_60_minute_average
below: 100
- condition: state
entity_id: switch.pool_pump_socket_1
state: "on"
sequence:
- service_template: >
{%- if is_state("input_boolean.pool_pump_onoff_timer", "on") -%}
switch.turn_on
{%- else -%}
switch.turn_off
{% endif %}
target:
entity_id: switch.pool_pump_socket_1
- service: climate.set_hvac_mode
target:
entity_id: climate.pool_heater
data:
hvac_mode: 'off'
#02. POOL PUMP DAILY ON/OFF TIMER
- id: pool_pump_timer_on
alias: pool pump timer on
trigger:
platform: time
at: "08:30:00"
action:
service: input_boolean.turn_on
target:
entity_id: input_boolean.pool_pump_onoff_timer
- id: pool_pump_timer_off
alias: pool pump timer off
trigger:
platform: time
at: "10:30:00"
action:
service: input_boolean.turn_off
target:
entity_id: input_boolean.pool_pump_onoff_timer
#03. NIGHTLY HA RESTART
#- id: restart_ha
# alias: Restart HA
# trigger:
# platform: time
# at: "02:00:00"
# action:
# - service: homeassistant.restart
- id: reboot_pi
initial_state: true
alias: 'Reboot pi daily'
trigger:
- platform: time
at: '02:00:00'
action:
- service: hassio.host_reboot