Smarter Swimming Pool 1: Pool Pump

When I bought my house I also inherited a nice outdoor swimming pool. Little did I know about pool maintenance back then and so during the first couple of months everything that could go wrong did go wrong. Anyway, I am now in a position to go beyond the static pool maintenance and am smartening up the whole thing. This post is the first one in a short series describing various small improvements I have made – starting with making the pool pump a bit smarter.

Pool Pump – Before

The pool pump needs to run several hours per day – a little less in winter and a bit more in summer during the swimming season. Mine was hooked up to a simple timer switch which turned on the pool pump and the liquid chlorine pump at the same time. And over the course of the year I had to fiddle around with the timer switch to increase and decrease the duration as necessary.

Pool timer switch

The timer switch is a simple approach, but comes with a few disadvantages:

  • To make most of my self-produced solar electricity, I had to continuously modify the switch’s settings.
  • During a power outage the timer just stops, and continuous afterwards wherever it left, i.e. I had to realign the timer after each outage.
  • There is no feedback, i.e. if for example the fuse blows for whatever reason the pump stops working and it just takes a few days until the water turns green and a lot of chemicals are required to fix this.

Pool Pump – After

Remote control the pump

To be able to control the pool pump through Home Assistant, I first of all had to have the ability to remote control the electricity circuit. My first choice was a Z-Wave switch, but unfortunately the power outlet is in a very inconvenient position: behind and under the pool, quite far away from the house and far from any location where a repeater would have helped. The outlet and the switch are not water-proof, so both had to remain inside the pool pump cupboard. And I could not find a Z-Wave switch with an external antenna.

Modified WeMo Insight switch

Next, I looked at Wifi switches, and settled on a WeMo Insight. There are a few tear-down reports for that switch which taught me how to replace the built-in antenna with a pigtail cable, so that I could connect a 3 metre antenna cable (SMA male to female jack, coax RG58) with the external antenna mounted on the pool fence, well above the water level so that it can reach my Wifi.

WeMo Insight’s external antenna

The integration of the WeMo Insight switch into Home Assistant was working as expected – well at least after a small fix to the underlying library, since WeMo now apparently uses more network ports where the switches can be listening on.

Pool wifi switch distribution: The WeMo Insight switches the power board at the top where pool pump and liquid chlorine feeder are connected.

Once that was working I could focus on the actual automation.

Of course you can use other types of switches depending your individual circumstances or preferences. In the following configuration snippets you will have to replace the entity id switch.wemo_insight_pool with your switch’s entity id; it just needs to support states on and off as well as services turn_on and turn_off.

Automating the pump

With a pool pump switched by Home Assistant I still wanted some level of control and introduced several levers and switches, and also implemented a level of monitoring and reporting.

Pool pump control panel

Controls – Auto, On and Off

Anything that you would normally just turn on or off and that should be automated in the future would probably require a 3-way-switch with the options: Auto, On and Off. While On and Off simply turn the attached Wifi switch on or off and the automation is ignored, in Auto mode the automation component is controlling the switch state.

input_select:
  pool_pump:
    name: Pool Pump mode
    options:
      - 'Auto'
      - 'On'
      - 'Off'
    initial: 'Auto'
    icon: mdi:engine

Controls – Swimming Season or not

I am distinguishing between Swimming Season and Off-peak Season. With a simple switch (input_boolean) I can change the current mode.

input_boolean:
  swimming_season:
    name: Swimming Season
    icon: mdi:swim

Controls – Duration

For each mode – swimming or off-peak – a separate duration can be configured defining how many hours per day the pool pump should run. For now I kept it simple only allowing to select a full number of hours with reasonable minimums and maximums.

input_number:
  run_pool_pump_hours_swimming_season:
    name: Run Pool Pump in Swimming Season
    min: 1
    max: 8
    step: 1
    mode: slider
  run_pool_pump_hours_off_season:
    name: Run Pool Pump in Off Season
    min: 1
    max: 6
    step: 1
    mode: slider

Automating the Wifi switch

The actual automation of the pool pump is triggered every 5 minutes with a check that compares the current state of the system with how it should be. Since the pool pump should run at daytime to make use of my solar electricity, and cannot run during the night due to local council restrictions, I added a condition to only run the check from one hour before sunrise to one hour after sunset. The actual check is happening inside a custom component. I ruled out the other options – a script would not be powerful enough, and an app based on AppDaemon would introduce another level of complexity without a real benefit compared to Home Assistant’s custom component.
The reason for the regular checks instead of just calculating static on/off times is that this approach can survive outages, and will just pick up when Home Assistant is restarted.

automation:
  - alias: 'Pool Pump On'
    trigger:
      - platform: state
        entity_id: input_select.pool_pump
        to: 'On'
    action:
      service: homeassistant.turn_on
      entity_id: switch.wemo_insight_pool
  - alias: 'Pool Pump Off'
    trigger:
      - platform: state
        entity_id: input_select.pool_pump
        to: 'Off'
    action:
      service: homeassistant.turn_off
      entity_id: switch.wemo_insight_pool
  - alias: 'Check Pool Pump'
    trigger:
      - platform: time
        minutes: '/5'
        seconds: 00
    condition:
      condition: and
      conditions:
        - condition: sun
          after: sunrise
          after_offset: '-1:00:00'
        - condition: sun
          before: sunset
          before_offset: '1:00:00'
    action:
      service: pool_pump_service.check
      data:
        switch_entity_id: switch.wemo_insight_pool

Custom component

The code below follows this high-level structure:

  • If the pool pump mode is set to ‘Auto’, continue with the check.
  • If the sun is above the horizon, continue with the check, otherwise switch the pump off.
  • If set to swimming season, split the total time into 3 runs (1st and 3rd for 25% of the configured duration, and 2nd run for 50%), if in off season split the total time into 2 runs (1st and 2nd run for 50% of the configured duration each).
    Start the first run at the hard-coded number of minutes after sunrise and leave on for the calculated amount of time. Start the second run after the first one-hour break. And in swimming season schedule a third run, again after a break after the second run.

All the selected and hard-coded timings can be fine-tuned to match the individual circumstances. In my case, I am observing a steep increase in solar electricity production early in the morning and hence selected the start of the first run accordingly. In off season the second run is then happening around noon when the solar electricity production is at its peak. During swimming season the first run starts even earlier and a longer break happens in the afternoon before the third run finishes shortly before sunset – and shortly before solar electricity production decreases.

What is currently missing in this setup is displaying the next start or stop time.

# Check if pool pump is supposed to run, and turn it on or off accordingly

import logging

from datetime import timedelta

from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.util import dt as dt_util

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'pool_pump_service'
ATTRIBUTE_SWITCH_ENTITY_ID = 'switch_entity_id'

OFF_SEASON_MAX_DURATION = 6.0
OFF_SEASON_RUN_1_AFTER_SUNRISE_OFFSET_MINS = 120
OFF_SEASON_1ST_BREAK_MINUTES = 60

SWIMMING_SEASON_MAX_DURATION = 8.25
SWIMMING_SEASON_RUN_1_AFTER_SUNRISE_OFFSET_MINS = 75
SWIMMING_SEASON_BREAK_1_MINUTES = 60
SWIMMING_SEASON_BREAK_2_MINUTES = 165


def setup(hass, config):
    """Set up is called when Home Assistant is loading our component."""

    def switch_pool_pump(switch_entity_id, target_state):
        switch = hass.states.get(switch_entity_id)
        if switch:
            if switch.state == target_state:
                # Already in the correct state
                _LOGGER.info("Switch is in correct state: %s", target_state)
            else:
                # Not in the correct state
                data = { "entity_id": switch_entity_id }
                if target_state == 'on':
                    hass.services.call('homeassistant', 'turn_on', data)
                else:
                    hass.services.call('homeassistant', 'turn_off', data)
                _LOGGER.info("Switched from '%s' to '%s'", switch.state,
                             target_state)
        else:
            _LOGGER.warning("Switch unavailable: %s", switch_entity_id)

    def handle_check(call):
        _LOGGER.info("Starting pool pump check")
        switch_entity_id = call.data.get(ATTRIBUTE_SWITCH_ENTITY_ID)

        # Read the pool pump configuration
        mode = hass.states.get('input_select.pool_pump')
        swimming_season = hass.states.get('input_boolean.swimming_season')
        run_hours_swimming_season = min(SWIMMING_SEASON_MAX_DURATION,
                                        float(hass.states.get(
                                            'input_number.run_pool_pump_hours_swimming_season').state))
        run_hours_off_season = min(OFF_SEASON_MAX_DURATION,
                                   float(hass.states.get(
                                       'input_number.run_pool_pump_hours_off_season').state))

        _LOGGER.info("* Pool pump mode: %s", mode.state)
        _LOGGER.info("* Swimming season: %s", swimming_season.state)
        _LOGGER.info("* Pool pump run hours swimming season: %s",
                     run_hours_swimming_season)
        _LOGGER.info("* Pool pump run hours off season: %s",
                     run_hours_off_season)

        # Only check if pool pump is set to 'Auto'
        if mode.state == 'Auto':
            _LOGGER.info("Pool pump set to 'Auto'")
            # Get sun details for today
            now = dt_util.now()
            _LOGGER.info("* Time is now %s", now)
            sun = hass.states.get('sun.sun')
            if sun.state == 'above_horizon':
                _LOGGER.info("* Sun above horizon")
                date = now.date()
                sunrise = get_astral_event_date(hass, 'sunrise', date)
                sunset = get_astral_event_date(hass, 'sunset', date)
                _LOGGER.info("* Sunrise: %s",
                             sunrise.astimezone(hass.config.time_zone))
                _LOGGER.info("* Sunset: %s",
                             sunset.astimezone(hass.config.time_zone))
                if swimming_season.state == 'on':
                    # Swimming Season (Summer)
                    _LOGGER.info("* Swimming season")
                    duration1 = run_hours_swimming_season * 60.0 * 0.25
                    duration2 = run_hours_swimming_season * 60.0 * 0.5
                    duration3 = run_hours_swimming_season * 60.0 * 0.25
                    _LOGGER.info(
                        "* Run pool pump 3 times for %s/%s/%s minutes",
                        duration1, duration2, duration3)
                    # Check for 1st run
                    run_1_start = sunrise + timedelta(
                        minutes=SWIMMING_SEASON_RUN_1_AFTER_SUNRISE_OFFSET_MINS)
                    run_1_stop = run_1_start + timedelta(minutes=duration1)
                    _LOGGER.info("* Run 1/3: %s - %s",
                                 run_1_start.astimezone(hass.config.time_zone),
                                 run_1_stop.astimezone(hass.config.time_zone))
                    if run_1_start <= now <= run_1_stop:
                        # Turn on pool pump
                        _LOGGER.info("* Pool pump should be on (Run 1/3)")
                        switch_pool_pump(switch_entity_id, 'on')
                    else:
                        # Check for 2nd run
                        run_2_start = run_1_stop + timedelta(
                            minutes=SWIMMING_SEASON_BREAK_1_MINUTES)
                        run_2_stop = run_2_start + timedelta(minutes=duration2)
                        _LOGGER.info("* Run 2/3: %s - %s",
                                     run_2_start.astimezone(
                                         hass.config.time_zone),
                                     run_2_stop.astimezone(
                                         hass.config.time_zone))
                        if run_2_start <= now <= run_2_stop:
                            # Turn on pool pump
                            _LOGGER.info("* Pool pump should be on (Run 2/3)")
                            switch_pool_pump(switch_entity_id, 'on')
                        else:
                            # Check for 3rd run
                            run_3_start = run_2_stop + timedelta(
                                minutes=SWIMMING_SEASON_BREAK_2_MINUTES)
                            run_3_stop = run_3_start + timedelta(
                                minutes=duration3)
                            _LOGGER.info("* Run 3/3: %s - %s",
                                         run_3_start.astimezone(
                                             hass.config.time_zone),
                                         run_3_stop.astimezone(
                                             hass.config.time_zone))
                            if run_3_start <= now <= run_3_stop:
                                # Turn on pool pump
                                _LOGGER.info(
                                    "* Pool pump should be on (Run 3/3)")
                                switch_pool_pump(switch_entity_id, 'on')
                            else:
                                # Turn off pool pump
                                _LOGGER.info("* Pool pump should be off")
                                switch_pool_pump(switch_entity_id, 'off')
                else:
                    # Off Season (Winter)
                    _LOGGER.info("* Off season")
                    duration = run_hours_off_season * 60.0 * 0.5
                    _LOGGER.info("* Run pool pump 2 times for %s/%s minutes",
                                 duration, duration)
                    # Check for 1st run
                    run_1_start = sunrise + timedelta(
                        minutes=OFF_SEASON_RUN_1_AFTER_SUNRISE_OFFSET_MINS)
                    run_1_stop = run_1_start + timedelta(minutes=duration)
                    _LOGGER.info("* Run 1/2: %s - %s",
                                 run_1_start.astimezone(hass.config.time_zone),
                                 run_1_stop.astimezone(hass.config.time_zone))
                    if run_1_start <= now <= run_1_stop:
                        # Turn on pool pump
                        _LOGGER.info("* Pool pump should be on (Run 1/2)")
                        switch_pool_pump(switch_entity_id, 'on')
                    else:
                        # Check for 2nd run
                        run_2_start = run_1_stop + timedelta(
                            minutes=OFF_SEASON_1ST_BREAK_MINUTES)
                        run_2_stop = run_2_start + timedelta(minutes=duration)
                        _LOGGER.info("* Run 2/2: %s - %s",
                                     run_2_start.astimezone(
                                         hass.config.time_zone),
                                     run_2_stop.astimezone(
                                         hass.config.time_zone))
                        if run_2_start <= now <= run_2_stop:
                            # Turn on pool pump
                            _LOGGER.info("* Pool pump should be on (Run 2/2)")
                            switch_pool_pump(switch_entity_id, 'on')
                        else:
                            # Turn off pool pump
                            _LOGGER.info("* Pool pump should be off")
                            switch_pool_pump(switch_entity_id, 'off')
            else:
                _LOGGER.info("* Sun below horizon")
                # Turn pool pump if it's still running
                _LOGGER.info("* Pool pump should be off")
                switch_pool_pump(switch_entity_id, 'off')
        else:
            _LOGGER.info("Pool pump set to '%s'", mode.state)

    hass.services.register(DOMAIN, 'check', handle_check)

    # Return boolean to indicate that initialization was successfully.
    return True

The code is registered as a service so that it can easily be used in an automation action.

The custom component is stored in a file [config_dir]/custom_components/pool_pump_service.py. And to make the custom component known to Home Assistant a simple configuration entry is required:

# Enable custom component
pool_pump_service:

Future improvements of the automation could include actual current power production and the weather forecast, so that for example the pool pump could be deferred if it’s cloudy in the morning while sunshine is predicted for the afternoon. Another improvement could be a better coordination of the pool pump with other appliances running during the day.

Monitoring – How much energy is used?

Knowing how much energy is used is not just a nice byproduct that the WeMo Insight switch provides. The current power usage is a very useful indicator if the pool pump is running – as opposed to just knowing if the switch is turned on. I also had a couple of occasions where Home Assistant claimed to have turned on the switch and showed it as on, but had actually lost the connection and neither switch nor pump were actually on.

sensor:
  - platform: template
    sensors:
      wemo_insight_pool_energy_today:
        value_template: '{% if (states.switch.wemo_insight_pool.attributes.today_energy_kwh) %}{{ states.switch.wemo_insight_pool.attributes.today_energy_kwh | float(2) }}{% else %}0.0{% endif %}'
        friendly_name: "WeMo Insight Pool Energy Today"
        unit_of_measurement: "kWh"
      wemo_insight_pool_current_power:
        value_template: '{% if is_state("switch.wemo_insight_pool", "on") %}{{ states.switch.wemo_insight_pool.attributes.current_power_w | float(0) }}{% else %}0{% endif %}'
        friendly_name: "WeMo Insight Pool Current Power"
        unit_of_measurement: "W"

The reason for the if-statements in the above configuration is that the WeMo Insight switch does not always report a value or does not report the attribute at all.

Monitoring – Pump running or not?

To know if the pool pump is actually running or not I am using the previous sensor that reports the current power usage of the switch. In the following binary sensor I am assuming that the pool pump is on if the switch reports more than 8W of power usage.

Pool pump running badge
binary_sensor:
  - platform: template
    sensors:
      pool_pump_running:
        value_template: "{{ states.sensor.wemo_insight_pool_current_power.state | float > 8 }}"
        friendly_name: "Pool Pump running"
        device_class: moving
Pool pump running dashboard widget

Monitoring – How long is the pump running?

Based on the binary template sensor that determines whether or not the pump is running, I can now use the history_stats sensor to tell me how long the pump has been running each day.

sensor:
  - platform: history_stats
    name: Pool Pump running today
    entity_id: binary_sensor.pool_pump_running
    state: 'on'
    type: time
    start: '{{ now().replace(hour=0).replace(minute=0).replace(second=0) }}'
    end: '{{ now() }}'

Reporting – The daily message

Showing the details on the Home Assistant dashboard is one thing, but I thought it would be useful to send myself a daily message confirming how many hours the pool pump had been running, compared to how long it was supposed to run. Since I already integrated Pushover, it was just a matter of formatting the message and send it out each day, 30 minutes after sunset.

automation:
  - alias: 'Report Pool Pump'
    trigger:
      - platform: sun
        event: sunset
        offset: '00:30:00'
    action:
      service: notify.pushover
      data_template:
        message: "Pool Pump was on for <font color='#0000ff'><b>{{states.sensor.pool_pump_running_today.attributes.value}}</b></font> today. <br/>
({%- if is_state('input_boolean.swimming_season', 'on') -%}Swimming Season / {{ states.input_number.run_pool_pump_hours_swimming_season.state | round(0) }}h{%- else -%}Off Season / {{ states.input_number.run_pool_pump_hours_off_season.state | round(0) }}h{%- endif -%})."
        title: "Pool Pump Report"
        data:
          html: 1
Pool pump report via Pushover

Outlook

Now that the pool pump is fixed up, I will look at monitoring the water temperature in the next post.

Update 18 Apr 2019

Home Assistant 0.86 introduced time_pattern for triggering an automation on a regular basis. If you are using version 0.86 or later, please change the above automation for checking the pool pump to:

automation:
  - alias: 'Check Pool Pump'
    trigger:
      - platform: time_pattern
        minutes: '/5'
        seconds: 00
    condition:
      condition: and
      conditions:
        - condition: sun
          after: sunrise
          after_offset: '-1:00:00'
        - condition: sun
          before: sunset
          before_offset: '1:00:00'
    action:
      service: pool_pump_service.check
      data:
        switch_entity_id: switch.wemo_insight_pool

Smarter Swimming Pool Series

  1. Pool Pump
  2. Water Temperature
  3. Water Level
  4. Liquid Chlorine Level
  5. Improvements Under the Surface

13 Replies to “Smarter Swimming Pool 1: Pool Pump”

  1. Love your work. Thanks for your detailed instructions. I want to replicate but I’m not strong on coding and doubting my ability to mod your custom service to have my pump run 1hr after sunrise for the programmed input select time. I run 8hrs in season and 4hrs off season. Is that something you could share without taking up too much of your time?

    1. Thanks for your feedback.
      The existing input sliders already allow you to control the duration in season and off season.
      And you can change the start of the pump by modifying the constants at the top of the custom component code. The following two control the offset after sunrise in minutes when the pump should run for off season and swimming season respectively. To start 60 minutes after sunrise, just change the values to 60.
      OFF_SEASON_RUN_1_AFTER_SUNRISE_OFFSET_MINS = 60
      SWIMMING_SEASON_RUN_1_AFTER_SUNRISE_OFFSET_MINS = 60

  2. Hi,

    Just what I’m looking for. Great job.

    Question:

    Max duration is defined in the ‘input_number’ definition and in the custom component code. Aren’t they the same? Or is there a reason for this?

    I’ve got the code running with a Sonoff Basic switch. Seems to be fine except that I can’t use the ‘Pump Running’ bit as there is no sense of power usage in the Sonoff. May I could us a Sonoff POW.

    Anyway thank you for that. I learned a lot just trying to get your setup to work on mine.

    1. Glad to hear that it’s working for you.

      Yes, in principle you are right about the max duration. The reason why I separated the two is that the input slider allows to select the desired duration in hours, and it requires its min and max configuration. In the custom component though I am just double-checking that the selected duration value is reasonable for this algorithm which tries to run the pump while the sun shines to use of solar electricity.
      For example, if you wanted going beyond the 8.25 hours during swimming season, you would probably need to adjust the breaks between the runs.

  3. Thank you for the feedback.

    Can you tell me where I can find information about the ‘Logging’ aspect of your code?

    1. I am using the standard logger, and if you haven’t changed the log configuration in HA, you should see entries under ‘custom_components.pool_pump_service’ at ‘INFO’ level in your log file. If that is too much information in the log file for you, you could change the log level, e.g.
      logger:
        default: info
        logs:
          custom_components.pool_pump_service: warning

  4. Great write-up! I will be looking closer at this as we reach spring. The pool is still under 50cm of snow here in Sweden. In season, my pool pump runs more or less 24×7 in order for the heat pump to work. An issue that I will be looking to automate is that some weeks, there is soo much pollen and debris from surrounding trees that the filter gets clogged, the flow is reduced and the heat pump stops. I have ordered a flow sensor that will be connected to an esp8266 sending mqtt messages to Home Assistant. Whenever the flow drops benath certain threshold, I will get a notification and hopefully be able to correct before heat pump stops. If heat pump stops, pool pump should be stopped as well.

    1. Thanks. Good idea to monitor the water flow. I have a similar challenge here and was already thinking about various approaches.

  5. Hi

    Lately (Hassio 0.93.2) the pool_pump_service has not been running and giving this error:

    Sun Jun 02 2019 18:46:01 GMT+0200 (CEST)

    ‘NoneType’ object has no attribute ‘lower’
    Traceback (most recent call last):
    File “/usr/local/lib/python3.7/site-packages/homeassistant/components/websocket_api/commands.py”, line 121, in handle_call_service
    connection.context(msg))
    File “/usr/local/lib/python3.7/site-packages/homeassistant/core.py”, line 1141, in async_call
    self._execute_service(handler, service_call))
    File “/usr/local/lib/python3.7/site-packages/homeassistant/core.py”, line 1165, in _execute_service
    await self._hass.async_add_executor_job(handler.func, service_call)
    File “/usr/local/lib/python3.7/concurrent/futures/thread.py”, line 57, in run
    result = self.fn(*self.args, **self.kwargs)
    File “/config/custom_components/pool_pump_service.py”, line 168, in handle_check
    switch_pool_pump(switch_entity_id, ‘off’)
    File “/config/custom_components/pool_pump_service.py”, line 29, in switch_pool_pump
    switch = hass.states.get(switch_entity_id)
    File “/usr/local/lib/python3.7/site-packages/homeassistant/core.py”, line 829, in get
    return self._states.get(entity_id.lower())
    AttributeError: ‘NoneType’ object has no attribute ‘lower’

    1. Hi Pierre,

      the only way I could reproduce this is if the service call is missing the switch_entity_id altogether.
      Could you please double-check that your automation that calls the pool_pump_service.check actually does define this parameter?

      The action part should look something like shown above, for example in the “Update 18 Apr 2019” section (sorry, the comments section here does not support indentation, so yaml configuration doesn’t really work in here).

  6. Dear Malte,

    Many thanks for your great work that inspire me!
    I test some automation now with template condition instead of custom_component, it seems to work fine. I store some calculated timestamp in sensor (cycle1_start, cycle1_stop etc) and I make 2 automations “Auto On” and “Auto Off” like that :

    - alias: 'Pool Pump Auto Off'
    trigger:
    - platform: time_pattern
    minutes: '/5'
    condition:
    - condition: state
    entity_id: input_boolean.pool_wintering
    state: 'off'
    - condition: state
    entity_id: input_select.pool_pump
    state: 'Auto'
    - condition: state
    entity_id: switch.pool_pump
    state: 'on'
    - condition: template
    value_template: >-
    {{ not ((states('sensor.now_timestamp') >= states('sensor.pool_pump_cycle1_start') and states('sensor.now_timestamp') = states('sensor.pool_pump_cycle2_start') and states('sensor.now_timestamp') = states('sensor.pool_pump_cycle2_start') and states('sensor.now_timestamp') <= states('sensor.pool_pump_cycle3_stop'))) }}
    action:
    service: switch.turn_off
    entity_id: switch.pool_pump

    Have a nice day
    Gaetan

Leave a Reply

Your email address will not be published. Required fields are marked *