Automating A Hot Tub With Home Assistant

Recently, I unexpectedly received some money - not a huge amount, but something that I wanted spent on something tangible and fun (rather that setting it aside or spending it on day-to-day expenses)

After some (though probably not enough) thought, I decided to spend it on a hot tub: we both live with chronic pain, so as well as being a fun purchase, it had the potential to perhaps help make life a little easier (it's also not something that I'd normally... errr... splash out on).

The funds that I had could stretch to a decent inflatable tub or an (extremely) entry level hard shell tub. The choice seemed obvious, especially as the inflatable carried the additional benefit of being easier to move around if we later decide that we aren't happy with the original placement.

The Spa that I chose (a Lay-z-spa Barbados) is also wifi enabled, opening up the possibility of using HomeAssistant to automate its operation - including creating automations to suck up energy during Plunge Pricing (or Powerup) events.

In this post, I want to talk a little about the various bits that I've set up to monitor and control the tub.


Integration With HomeAssistant

The manufacturer, of course, hasn't deliberately integrated control of the spa with HomeAssistant: The official way to control it is using either the Lay-Z-Spa or the BestWay app (BestWay are the parent company of Lay-Z-Spa).

A third party has created a HomeAssistant addon (installable via HACS) to integrate with BestWay's API, exposing control of the tub in HomeAssistant:

Screenshot of a list of entities in HomeAssistant. It shows whether the tub is connected, whether the bubbles are on, if the controls are locked as well as things like temperature and whether any errors are being reported

The thermostat appears as a climate entity, allowing control of the target temperature from within HomeAssistant.


Instrumentation

Before setting up any automation, I first wanted to make sure that I had visibility of the hot-tub's state.

My HomeAssistant instance was already configured to send states into InfluxDB, which made visualisation with Grafana quite easy:

Screenshot of a grafana dashboard showing the current state of the hottub including temperature and whether the bubbles are on

A copy of this dashboard can be found on Github.


Alerting - Errors

You may have noticed that both the HomeAssistant and Grafana screenshots above make reference to errors, this is because the tub can raise a range of errors.

Some of those errors are quite routine: For example E02 means that there's a waterflow issue into the pump, usually a sign that the filter needs cleaning or replacing.

We actually had an E02 error fire the day after setting the tub up (the yellow line in the grafana screenshot) - the water around here is incredibly hard and so the filter quickly got clogged by particulates. In that screenshot, you can see the recovery after I cleaned the filter, only for it to reclog and fire again later: a sign that we're probably looking at needing a new filter shortly after any full water change.

Obviously I don't want the pump to not be running for extended periods, so, I created an alert in Grafana to warn me if errors were reported:

Screenshot of the alert config

The field being queried is a count of errors: if the result is greater than 0 then at least 1 error has been reported and the alert should fire.

The alert is linked to the appropriate dashboard and panel:

Grafana alerting includes the ability to link the alert to a specific dashboard and panel, this screenshot shows that being configured on an alert

So, when it fires, the notification will include a link back to the dashboard cell which lists reported error messages.


Alerting - Data Throughput and Availability

The tub has got freeze prevention, so the intention is that we'll probably keep it out (and continue to use it) over winter.

However, for safety reasons, the tub is wired through a RCD. If that breaker were to trip for some reason, the tub could potentially then be damaged by an unnoticed drop in temperatures (the manual says that below 6 degrees is problematic). We really need to know that the tub is offline so that we can investigate and fix or (at worst), empty it.

So, I created a deadman alert - it's driven by a query which checks how many temperature readings the tub has submitted in a given time period

select 
    count("mean_current_temperature") 
FROM (
    SELECT 
        mean("current_temperature") AS "mean_current_temperature"
     FROM "home_assistant"."autogen"."climate.spa_thermostat" 
     WHERE 
         $timeFilter 
     GROUP BY time($interval) 
     FILL(null)
)

The alert configuration queries the last 2 hours and notifies if there is less than 1 report.

Crucially, the alert also needs to be configured to sound the alarm if the query results in no data at all (as it likely will if there are absolutely no entries)

Screenshot of alert configuration in Grafana showing that the alert should fire if it results in no data being returned

However, there is a minor issue with this logic: Homeassistant only writes values in to InfluxDB when something updates. If the tub is stable at the target temperature, we'll get a gap in readings and our alert will fire.

That's obviously undesirable, so, in HomeAssistant I created an automation to act as a liveness check. At 15 minutes past the hour, it reduces the target temperature by 1 degree, waits a second and then puts it back

alias: Tub Liveness Check
description: ""
trigger:
  - platform: time_pattern
    minutes: "15"
condition: []
action:
  - variables:
      tub_original_target_temperature: "{{ state_attr('climate.spa_thermostat','temperature') }}"
      new_target_temperature: "{{ state_attr('climate.spa_thermostat','temperature') - 1 }}"
  - service: climate.set_temperature
    metadata: {}
    data:
      temperature: "{{ new_target_temperature }}"     
    target:
      entity_id: climate.spa_thermostat
  - delay:
      hours: 0
      minutes: 0
      seconds: 1
      milliseconds: 0
  - service: climate.set_temperature
    metadata: {}
    data:
      temperature: "{{ tub_original_target_temperature }}" 
    target:
      entity_id: climate.spa_thermostat
mode: single

It subtracts rather than adds so that heater doesn't need to kick in and then immediately turn off. If the hot-tub is reachable/online, the currently reported temperature gets written into InfluxDB.


Other Alerts

I won't go into too much depth on these, but I also created alerts to fire

  • If the filter pump has been off for too long
  • If the temperature has dropped low enough that freeze-protection is likely to kick in soon

Screenshot of alerts in Grafana

I'm sure we'll probably end up with more over time.


Automation

With instrumentation and alerting in place, I moved onto setting up automation so that we could make the most of cheap or free electricity.

We're on Octopus Agile which sometimes sees what Octopus refer to as Plunge Pricing:

Octopus's explainer of Plunge Pricing. basically, when wholesale prices turn negative, so do ours - we literally get paid to use electricity

It's an absolute no-brainer that we should try and heat the tub when we're getting paid for every kWh that we consume.

I've got the Octopus Energy HomeAssistant Addon installed, so having an action fire when prices turn negative is pretty straightforward:

alias: Boost HotTub on Plunge
description: ""
trigger:
  - platform: numeric_state
    entity_id:
      - sensor.octopus_energy_electricity_blah_blahblah_current_rate
    below: 0
# Check that it's not already been boosted
# (because we don't want to turn it down afterwards if so)
condition:
  - condition: numeric_state
    entity_id: climate.spa_thermostat
    attribute: temperature
    below: 38
action:
  # Toggle a switch so we can see the automation activated
  - service: input_boolean.turn_on
    target:
      entity_id:
        - input_boolean.plunge_hottub_boost_active
    data: {}
  # Record the current target temperature so we can restore it
  # later
  - service: input_number.set_value
    data:
      value: "{{ state_attr('climate.spa_thermostat','temperature') }}"
    entity_id: input_number.hottub_target_temp
  # Increase the target temperature
  - service: climate.set_temperature
    metadata: {}
    data:
      temperature: 40
    target:
      entity_id: climate.spa_thermostat
  # If it's been set low for a while, the mode switches to off
  # so turn the heater back on
  - service: climate.set_hvac_mode
    metadata: {}
    data:
      hvac_mode: heat
    target:
      entity_id: climate.spa_thermostat      
  - service: notify.mobile_app_fp4
    data:
      message: Automatically detected Plunge Period - turning hottub up  
mode: single

This automation triggers when prices are below 0/kWh and then:

  • Checks whether the temperature is already at/above 38 (if so, abort)
  • Flip a boolean to show that we triggered
  • Store a copy of the current target temperature
  • Set the target temperature to 40
  • Make sure it's in heat mode
  • Notify my phone

A second automation then undoes all this when prices raise back above 0:

alias: Reduce Hottub at Plunge End
description: ""
trigger:
  - platform: numeric_state
    entity_id:
      - sensor.octopus_energy_electricity_blah_blahblah_current_rate
    above: 0
condition:
  - condition: state
    entity_id: input_boolean.plunge_hottub_boost_active
    state: "on"
action:
  # Restore the original target
  - service: climate.set_temperature
    data:
      temperature: "{{ float(states('input_number.hottub_target_temp')) }}"
    entity_id: climate.spa_thermostat
  # Turn the toggle off
  - service: input_boolean.turn_off
    metadata: {}
    data: {}
    target:
      entity_id: input_boolean.plunge_hottub_boost_active
  - service: notify.mobile_app_fp4
    data:
      message: >-
        Automatically detectd Plunge Period Ended - turning hottub back to {{
        float(states('input_number.hottub_target_temp')) }}
mode: single

I didn't have to wait too long for a plunge period to come along and, as expected, received a notification on my phone (typo and all):

The automation fired and I got a notification from the Homeassistant app on my phone

Checking the tub's setting in HomeAssistant also confirmed that it was now targeting 40.

The tub started heating

I've also updated my Octopus Powerup automation to do more or less exactly the same thing, the only difference with it is that I manually schedule it's start and end (primarily so that I can ensure that I've configured our battery appropriately at the same time).


Scheduling

As well as turning on for low price events, we obviously want the hot-tub to be available for use at other times.

Our exact requirements are quite likely to change over time, but as a starting point we agreed that the tub should be heated and ready at weekends. During the week, for now, we'll manually set it to heat ahead of time.

Historically, HomeAssistant's ability to handle calendar type scheduling has been a bit crap. I was recently complaining to someone that I might have to build something like this in order to provide visual scheduling:

Screenshot showing a hacked together timing mechanism

However, thankfully, I was corrected.

I hadn't noticed it'd snuck in (turns out it was about 18 months ago), but HomeAssistant now has a half-decent scheduling helper:

Screenshot of the heating schedule helper in HomeAssistant, I've got it configured to heat between 04:30 and 23:30

The downside, though, is that it doesn't appear to be very well supported in dashboards. You instead need to add it as an entity (which'll show current state) and then tap through to its settings if you want to change the schedule.

Screenshot of the entity on a dashboard, it's a simple text block showing the current state - On at the moment

That's not ideal, but is adequate for our needs - it's not something I really expect we'll be changing regularly.

The helper is essentially a clever front-end to a boolean: it can't be used to set different temperatures at different times, and simply express On or Off. But, that's all that's really needed:

alias: Hottub On (weekend)
description: ""
trigger:
  - platform: state
    entity_id:
      - schedule.hottub_heating_schedule
    to: "on"
    from: "off"
action:
  - service: climate.set_temperature
    metadata: {}
    data:
      temperature: 38
    target:
      entity_id: climate.spa_thermostat
- service: climate.set_hvac_mode
    metadata: {}
    data:
      hvac_mode: heat
    target:
      entity_id: climate.spa_thermostat            
mode: single

The off automation also includes a simple time based trigger to ensure that we don't accidentally leave the tub on overnight after manually turning it on during the week.

alias: Hot Tub Off
description: ""
trigger:
  - platform: time
    at: "23:45:00"
  - platform: state
    entity_id:
      - schedule.hottub_heating_schedule
    from: "on"
    to: "off"
condition: []
action:
  - service: climate.set_temperature
    metadata: {}
    data:
      temperature: 15
    target:
      entity_id: climate.spa_thermostat
mode: single

Creating Hot-Tub Playlists

Having the bubbles going is fun.

What isn't so fun, though, is deciding when they should be turned on and off. The airjets are pretty loud and make conversation quite difficult - conversely, though, it feels a bit mean turning them off just because there's something that you want to say.

When I was young, we used to go to a public swimming pool which had a Jacuzzi. The bubbles in that turned on and off on a fixed rotation, so you'd quite often end up sat in there eagerly awaiting the bubbles. They'd then run for a few minutes before switching back off and people would sit around chatting waiting for the next run.

It occurred to me that I could use HomeAssistant to create a similar experience (and hopefully the same excited anticipation) - allowing us to relax and enjoy the bubbles as and when they came.

The plan was that, before climbing in, we'd hit some button which would trigger a script:

Screenshot of the 1h playlist being edited in YAML. It waits 7 minutes then turns the jets on for 5 mins, then turns off for 10 before turning back on.

The screenshot is only part of the script, but (as the name suggests) the playlist runs for an hour - ending with 10 minutes of bubbles.

I've also created a second playlist which runs in an infinite loop and uses random to choose a time interval

alias: Hottub Random Playlist
sequence:
  - repeat:
      sequence:
        - delay:
            hours: 0
            minutes: "{{ range(1,10) | random }}"
            seconds: 0
            milliseconds: 0
        - service: switch.turn_on
          metadata: {}
          data: {}
          target:
            entity_id: switch.spa_bubbles
        - delay:
            hours: 0
            minutes: "{{ range(1,10) | random }}"
            seconds: 0
            milliseconds: 0
        - service: switch.turn_off
          metadata: {}
          data: {}
          target:
            entity_id: switch.spa_bubbles
      while:
        - condition: template
          value_template: "true"
mode: single

This gives fairly mixed results: it's perfectly possible to spend 10 minutes without bubbles and then only have them for 1 minute (or vice-versa).

Eventually, I'll probably put a zigbee button somewhere near the tub, but for now there are a couple of buttons on the relevant dashboard

Screenshot of the buttons in a HomeAssistant Dashboard

The playlists can be started by tapping the relevant button (or for a truly confusing time, both) before getting in. If we get out early (or are using the looped one), the script can be turned off by tapping the button again.

The pump will only allow the bubbles to run for up to 30 minutes at a time - after that it turns off to conserve energy (which is fairly welcome, given that the bubble pump has a 800w motor in it).


The Results

It all seems to work pretty well.

On Friday, though, we had a bit of an unusual scenario: the Powerup that I signed up for ended up slightly overlapping with a plunge pricing period - if I hadn't noticed, the tub would have turned itself off at the end of the plunge rather than remaining on until the Powerup had also finished.

To prevent this from happening, I used our normal schedule to configure the tub to switch on half an hour before the plunge started (the price was £0.00 anyway, so it didn't cost us anything). This meant that the automation's conditional resolved to False:

condition:
  - condition: numeric_state
    entity_id: climate.spa_thermostat
    attribute: temperature
    below: 38

This prevented it from executing any further (which in turn meant that input_boolean.plunge_hottub_boost_active was False, preventing the plunge end automation from running and turning the tub off).

Between them, the plunge and the power-up provided us with about 5.5 hours of free (or better than free) electricity, which meant that this weekend's hot-tub usage required very little paid heating time at all:

Graph showing temperature and heating state over a couple of days which started with a plunge period and a powerup

Over the course of the weekend, the heater has had to kick in briefly to maintain temperature, but the vast majority of heating happened on Friday with temperatures being (more or less) stable since.


Limitations

There's a slightly odd limitation to using the hot-tub to consume electricity during powerups/plunges: you can't really do it multiple days in a row.

The graphs above show that temperatures are quite easily maintained, but they don't really show half of just how well insulated the tub actually is.

The low rate of temperature decline can be better seen by looking at the few days last week where we didn't heat at all:

Graph showing temperature decline over 3 days. Details are explained below

The tub was last actively heated at 11:55 on 8 April. At that time, the water temperature was 38 degrees. We then didn't actively heat again until 23:30 on the 10th (when there was a short plunge period).

In that time, the temperature of the tub waned quite slowly, getting down to 25.5 degrees at the time the heater clicked back on.

In other words, over the course of 2.5 days, the temperature only declined by 12.5 degrees, despite the outside temperature being much lower than in the tub.

Graph showing outdoor temperatures. For the time that the heater was off we were lucky if outdoor temperatures reach 15 (though they did nearly get to 20 on the 11th)

From an energy-saving point of view, this is great and I'm certainly not going to complain about it.

It does mean though, if there are >1 consecutive days with plunge periods, the tub is probably only going to be a useful energy sink on the first because temperatures are unlikely to have declined much by the time that the next period rolls around.


Conclusion

Spending out on a hot tub purely to use as an energy store would, obviously, be an insane choice. It was primarily purchased for fun and possible pain relief (if you're wondering, sadly it's not as great at that as hoped - you get some small amount of relief whilst in there, but it ends once you get out).

As an energy sink, it can absorb quite a lot (600L of water will do that) - the main limitation really is the wattage of the heater, at 2kW there's a limit to how quickly you can pump heat in there.

It's also very good at retaining that heat, so you're unlikely to be able to pump much additional energy in on subsequent days.

I did toy with the idea of also having it kick in if we're exporting solar to the grid, but that doesn't currently make financial sense - so far we've been able to warm it for far less than £0.15/kWh so it makes more sense to continue to export.

What I may look at adding though, is an automation to impose a pricing cap on heating so that we defer (non emergency) heating if prices are above a certain level. To avoid temperatures in the tub getting too low, that'd probably need to be accompanied by a second automation to heat at the cheapest point in the day.

That plan, though, is complicated a little by the tub's heat retention capabilities - there may be a cheaper day coming, but there's no way to predict that ahead of time.

Either way, it's given me something new to automate around, as well as somewhere to sit and soak with a bottle or two.