Smart immersion heating for domestic hot water

This article describes various automations I’ve implemented for a hot water tank immersion heating. Rather than focusing on control and monitoring (which this setup definitely helps with), the intent is to let the system run by itself, with minimal interventions. What’s covered:

  • Automatic on/off to consume excess solar energy from my solar panels
  • Scheduled night-time heating to levarage off-peak tariffs (e.g. Octopus Go)
  • Predictive target setting based on solar forecast and outside temparate (for night-time heating)
  • Evening top-up in case of low solar output

Why all this? Having had solar panels and battery installed in late 2022 (thank you FDG Group!), I found that on sunny days we had lots of surplus power that even the battery and any house load couldn’t suck up. And because E.On Energy comptelety messed up our initial solar install, we didn’t have our export sorted for months. This effectively meant we use it or loose it.

First, my plan was to purchase a solar immersion diverter (e.g. iBoost/Eddi), but due to high purchase cost I hesitated. After extensive research I decided to buy Sonoff POWR3 (£26), a smart switch with energy monitoring rated up to 25A, which is well above the 3kW immersion heater we have on our hot water cylinder. Thanks to our local electrician we had that installed in no time and it was ready for action! :slight_smile:

After the initial few days of testing the on/off solar automation, the immersion heater started leaking. With our preferred plumber not being available for the following 3 weeks, I went to buy an immersion heater spanner (£9) to try to tighten the immersion heater, hoping this would fix it. With about a quarter of a turn it finally seemed tight enough and… no more leaks!

With the leakage fixed and already being on an off-peak tariff (Octopus Go), I realised that in addition to just using it for solar excess, we could also replace the need for the gas boiler heating our hot water tank, with only using the immersion heater. The result (for our 210L tank and family of four):

  • hot water with zero gas burned at home
  • and after a whole month of February being off gas, the cost are looking over 50% cheaper!
    • For last July we used 368kWh of gas for just hot water, so approx. £37
    • Assuming in cold winter months it takes 20% more gas to heat colder water, that would be over £50 for mid-winter months
    • The result for February on just electricity (121kWh off-peak, 70kWh solar, 8kWh peak) came at £17 for the whole month with using the new setup (automations and solar+battery)
  • In total, the saving is somewhere between £20 and £35 for just February, so pretty amazing so far!

Clearly some of the lower cost can be attributed to using surpus solar (and some to bugs in automation scripts not heating enough :wink: ), but the night-time heating is so far twice the kWhs compared to day-time solar. With off-peak electricity being almost the same price per kWh as gas, this solution should be very cost effective, and more importantly, better for the planet! What’s more, you don’t need all the automations described here, although I do think they should give you a more complete setup where you can let go of gas for ever, even if you have no solar or battery!

Here is the screenshot of the dashboard:

This is night-time heating with evening top-up (no excess solar):

And limiting night-time heating when solar forecast is looking good (no evening top-up necessary):

Configuration files

Below are code snippets you can adopt to your needs - for all my setup please see my github repo.

automations.yaml

Setting night immersion target

Probably most complex automation, taking into account solar forecast and weather conditions. Double trigger time to ensure this runs every night.

- id: '12233445566789'
  alias: Sinks - Hot Water - Set immersion target level
  description: Set target immersion target heating level based on anticipated solar production
  trigger:
  - platform: time
    at: "23:45:00"
  - platform: time
    at: "23:55:00"  
  condition: []
  action:
  - service: input_number.set_value
    data_template:
      entity_id: input_number.night_immersion_target
      # Assumptions: 2.5kWh/h for 4hrs (10min pause), can use 2.5kWh for every 5kWh generated above 15kWh (to cater for weather fluctuation)
      #              total capacity of the 210L tank is about 15kWh (from 5 to 65C)
      #              at 5C outside, on average it could take 7.5kWh per night to heat to max, 
      #              also daily average (night+day) was 7.5kWh so target shouldn't be more than 8kWh, minimum to up of 3kWh
      #              end of night tariff 04:30am
      # Note: instead of using separate tomorrow/today's forecasts, use a single state (energy_production_upcoming) and then its average over a few hours (filtered)
      # Note: simple weather compensation adding kWh to target (+2 for below -3C, +1 for below 2C, -1 for above 12C, -2 for above 17C)
      # Note: if todays solar top up was low, ignore upcoming forecast and heat to max
      value: >
        {% set solar = states('sensor.filtered_upcoming_solar_forecast') %}
        {% set todaysSolarTopUp = states('sensor.daily_hot_water_peak')|float %}
        {% set currentTemperature = state_attr('weather.forecast_home', 'temperature') %}
        {% set min_immersion = states('input_number.night_min_immersion_target')|float %}
        {% set weatherAdjustment = 0 %} 
        {% if is_number(currentTemperature) %}
          {% set weatherAdjustment =  2 if (currentTemperature|float                                   <= -3) else weatherAdjustment %}
          {% set weatherAdjustment =  1 if (currentTemperature|float > -3 and currentTemperature|float <=  2) else weatherAdjustment %}
          {% set weatherAdjustment =  0 if (currentTemperature|float >  2 and currentTemperature|float <= 12) else weatherAdjustment %}
          {% set weatherAdjustment = -1 if (currentTemperature|float > 12 and currentTemperature|float <= 17) else weatherAdjustment %}
          {% set weatherAdjustment = -2 if (currentTemperature|float > 17)                                    else weatherAdjustment %}
        {% endif %} 
        {% set solarThreshold = 15 %} 
        {% set target = 8 + weatherAdjustment %} 
        {% if is_number(solar) %}
          {% set target = target - ((solar|float - solarThreshold)*2.5/5)|round(1) if (solar|float >  solarThreshold and todaysSolarTopUp > 2.5) else target %}          
        {% endif %} 
        {{ target if (target > min_immersion) else min_immersion }}
  - delay:
      hours: 0
      minutes: 0
      seconds: 5
      milliseconds: 0
  - service: input_number.set_value
    data_template:
      entity_id: input_number.max_temp_idle_seconds
      # Allow high idle time when target is high (super hot water), low in case high solar forecast (not so hot water)
      value: >
        {% set immersion = states('input_number.night_immersion_target') %}
        {% set target = 180 %} 
        {% if is_number(immersion) %}
          {% set target =  30 if (immersion|float <= 2) else target %}  
          {% set target =  60 if (immersion|float >  2 and immersion|float <= 3) else target %}
          {% set target =  90 if (immersion|float >  3 and immersion|float <= 4) else target %}
          {% set target = 120 if (immersion|float >  4 and immersion|float <= 5) else target %}
          {% set target = 150 if (immersion|float >  5 and immersion|float <= 6) else target %}
          {% set target = 180 if (immersion|float >  6 and immersion|float <= 7) else target %}
          {% set target = 240 if (immersion|float >  7 and immersion|float <  8) else target %}
          {% set target = 300 if (immersion|float >= 8) else target %}          
        {% endif %} 
        {{ target }}
  mode: single

Use excess solar energy

This automation uses a binary sensor calculated separately, and then wait for turning on/off.

- id: '1105783920393'
  alias: Sinks - Hot Water - Start immersion heater (excess solar)
  description: ''
  trigger:
  - platform: state
    entity_id: binary_sensor.request_3kw_immersion
    for:
      hours: 0
      minutes: 2
      seconds: 0
    to: "on"    
  action:
  - repeat:
      sequence:
      - type: turn_on
        device_id: 0e56664e1317e879ea8852efe217d98b
        entity_id: switch.sonoff_100168acb6
        domain: switch
      - delay:
          hours: 0
          minutes: 0
          seconds: 15
      until:
      - condition: template
        # Try up to 3 times if the updated setting doen't reflect the target
        value_template: >-
            {{ states('switch.sonoff_100168acb6') == 'on' or repeat.index == 3 }}
  mode: single

Evening top-up

This uses an input number that can be adjusted through the dashboard.

- id: '11001293920394'
  alias: Sinks - Hot Water - Run immersion heater (evening top-up in case of low solar)
  description: ''
  trigger:
  - platform: time
    at: "19:35:00" 
  condition:
  - condition: template
    # Only turn on immersion if usage so far is less than the target
    value_template: >-
      {{ states('sensor.daily_hot_water_peak')|float < states('input_number.evening_immersion_target')|float }}
  action:
  - repeat:
      sequence:
      - type: turn_on
        device_id: 0e56664e1317e879ea8852efe217d98b
        entity_id: switch.sonoff_100168acb6
        domain: switch
      - delay:
          hours: 0
          minutes: 0
          seconds: 15
      until:
      - condition: template
        # Try up to 3 times if the updated setting doen't reflect the target
        value_template: >-
            {{ states('switch.sonoff_100168acb6') == 'on' or repeat.index == 3 }}
  - repeat:
      sequence:
      - delay:
          hours: 0
          minutes: 1
          seconds: 0
      until:
      - condition: template
        # Keep it in on state until at least 1kWh 
        value_template: >-
            {{ states('sensor.daily_hot_water_peak')|float >= states('input_number.evening_immersion_target')|float }}
  - repeat:
      sequence:
      - type: turn_off
        device_id: 0e56664e1317e879ea8852efe217d98b
        entity_id: switch.sonoff_100168acb6
        domain: switch
      - delay:
          hours: 0
          minutes: 0
          seconds: 15
          milliseconds: 0
      until:
      - condition: template
        # Try up to 3 times if the updated setting doen't reflect the target
        value_template: >-
            {{ states('switch.sonoff_100168acb6') == 'off' or repeat.index == 3 }}
  mode: single

Night-time (off-peak) heating cycles

Rather than running a continuous X-hour heating, my intention was to let the immersion heater cool down for 10min (25min past finish, 35min past next start) to try to minimise any potential issues - might be pointless though :wink:

- id: '1105783920394'
  alias: Sinks - Hot Water - Start immersion heater (night start)
  description: ''
  trigger:
  - platform: time
    at: "00:35:00"
  - platform: time
    at: "01:35:00"      
  - platform: time
    at: "02:35:00"  
  - platform: time
    at: "03:35:00"   
  condition:
  - condition: template
    # Only turn on immersion if usage so far is less than the target
    value_template: >-
      {{ states('sensor.daily_hot_water_offpeak')|float < states('input_number.night_immersion_target')|float }}
  action:
  - repeat:
      sequence:
      - type: turn_on
        device_id: 0e56664e1317e879ea8852efe217d98b
        entity_id: switch.sonoff_100168acb6
        domain: switch
      - delay:
          hours: 0
          minutes: 0
          seconds: 15
      until:
      - condition: template
        # Try up to 3 times if the updated setting doen't reflect the target
        value_template: >-
            {{ states('switch.sonoff_100168acb6') == 'on' or repeat.index == 3 }}
  mode: single

When to stop...

Here are various triggers to stop heating.

- id: '1029486719232'
  alias: Sinks - Hot Water - Stop immersion heater (max temp idle time or night target reached)
  description: Turn off completely if thermostat starts turning off the heating (hot enough) or target achieved
  trigger:
  - platform: numeric_state
    entity_id: sensor.sonoff_100168acb6_power
    for:
      seconds: "{{ states('input_number.max_temp_idle_seconds')|int }}"
    below: 1000
  - platform: template
    value_template: >-
      {{ states('sensor.daily_hot_water_offpeak')|float >= states('input_number.night_immersion_target')|float }}
  condition: 
  - condition: time
    after: "00:30:00"
    before: "04:30:00"    
  action:
  - repeat:
      sequence:
      - type: turn_off
        device_id: 0e56664e1317e879ea8852efe217d98b
        entity_id: switch.sonoff_100168acb6
        domain: switch
      - delay:
          hours: 0
          minutes: 0
          seconds: 15
          milliseconds: 0
      until:
      - condition: template
        # Try up to 3 times if the updated setting doen't reflect the target
        value_template: >-
            {{ states('switch.sonoff_100168acb6') == 'off' or repeat.index == 3 }}
  mode: single

- id: '1283957837432'
  alias: Sinks - Hot Water - Stop immersion heater (low solar or night end)
  description: ''
  trigger:
  - platform: state
    entity_id: binary_sensor.request_3kw_immersion
    for:
      hours: 0
      minutes: 0
      seconds: 30
    to: "off"
  - platform: time
    at: "01:25:00"      
  - platform: time
    at: "02:25:00"         
  - platform: time
    at: "03:25:00"       
  - platform: time
    at: "04:25:00"    
  action:
  - repeat:
      sequence:
      - type: turn_off
        device_id: 0e56664e1317e879ea8852efe217d98b
        entity_id: switch.sonoff_100168acb6
        domain: switch
      - delay:
          hours: 0
          minutes: 0
          seconds: 15
      until:
      - condition: template
        # Try up to 3 times if the updated setting doen't reflect the target
        value_template: >-
            {{ states('switch.sonoff_100168acb6') == 'off' or repeat.index == 3 }}
  mode: single

configuration.yaml

Below are the relevant snippets require for the immersion automations. For complete configuration please see my git repo.

Calculations of the binary sensor used for excess solar energy are heavily reliant on my solar & battery setup described in Solax X1 Hybrid G4 (local & cloud API). The main idea is to let the battery charge first, unless there is so much power from solar that it makes sense to put it on earlier. The requirements for high solar output gradually decreases with battery getting fuller. There is also mention of a heater switch which is another sink (2kW electric heater), but with a lower priority than the immersion heater.

input_number:

  evening_immersion_target:
    name: evening_immersion_target
    unit_of_measurement: 'Wh'
    initial: 2.0
    min: 0.0
    max: 5.0

  night_min_immersion_target:
    name: night_min_immersion_target
    unit_of_measurement: 'kWh'
    initial: 3
    min: 0
    max: 8

  night_immersion_target:
    name: night_immersion_target
    unit_of_measurement: 'kWh'
    initial: 9
    min: 0
    max: 12

  max_temp_idle_seconds:
    name: max_temp_idle_seconds
    initial: 180
    min: 15
    max: 300

automation: !include automations.yaml

template:
  - sensor:
    # Logic divided into base conditions (bc) that apply regardless, and battery/solar specific
    - name: "Request 3kW Immersion"
      state: >
        {% set heaterOn = is_state('switch.heater', 'on') %}
        {% set alreadyOn = is_state('switch.sonoff_100168acb6', 'on')  %}
        {% set heatingPower = states('sensor.sonoff_100168acb6_power')|float(default=0) %}
        {% set batteryCharging = states('sensor.solax_local_battery_power_adjusted')|int %}
        {% set pv = states('sensor.solax_local_pv_output')|int %}
        {% set feedIn = states('sensor.solax_local_feedin_power')|int %}
        {% set load = states('sensor.solax_local_load_power')|int %}
        {% set batteryLevel = states('sensor.solax_local_battery_soc')|int %}
        {% set sparePower = feedIn + batteryCharging %}
        {% set bc = (feedIn > -100) and (
                 ((alreadyOn==false or heatingPower < 1000) and load < 2500) 
              or ((alreadyOn==false or heatingPower < 1000) and load < 4500 and heaterOn)
              or (alreadyOn==true and heatingPower > 1000 and load < 5500)) %}
        {{ bc and ((pv > 3400 and alreadyOn==false and sparePower > 3200) 
                or (pv > 3400 and alreadyOn==true  and sparePower > 100)
                or (pv > 2500 and alreadyOn==false and batteryLevel >  70) 
                or (pv > 2500 and alreadyOn==true  and batteryLevel >= 70)                 
                or (pv > 1500 and alreadyOn==false and batteryLevel >  80) 
                or (pv > 1500 and alreadyOn==true  and batteryLevel >= 80)                
                or (pv > 1000 and alreadyOn==false and batteryLevel >  85) 
                or (pv > 1000 and alreadyOn==true  and batteryLevel >= 85)
                or (pv >  400 and alreadyOn==false and batteryLevel >  90) 
                or (pv >  400 and alreadyOn==true  and batteryLevel >= 90)) }}

sensor powercalc_label: !include powercalc.yaml

utility_meter:

  # Need to use powercalc for more granular monitoring as the total gets aggregated too slowly by Sonoff
  daily_hot_water:
    #source: sensor.sonoff_100168acb6_energy
    source: sensor.immersion_heater_aggregated_energy
    name: Daily Hot Water
    cycle: daily
    tariffs:
      - peak
      - offpeak

  monthly_hot_water:
    source: sensor.sonoff_100168acb6_energy
    name: Hot Water
    cycle: monthly
    tariffs:
      - peak
      - offpeak

powercalc.yaml

This utility helps measuring the power usage throughout the day.

### Variable load sinks ###
- platform: powercalc
  entity_id: sensor.sonoff_100168acb6_power
  # Not reusing existing power sensor so that new energy sensor is created 
  #power_sensor_id: sensor.sonoff_100168acb6_power
  name: Immersion Heater Aggregated
  fixed:
    power: "{{states('sensor.sonoff_100168acb6_power')}}"

Summary

I’m sure this setup could (and will) be improved further, so any suggestions would be highly appreciated.

Any feedback, good or bad, is always welcome!

For more on my solar & battery setup please see Solax X1 Hybrid G4 (local & cloud API).

Happy heating!

10 Likes

Hello.

Can I just say, thank you for the above. I’m new to HA and about to be getting Solar+Battery and like you, reluctant to spend a load of cash on something like an eddi. So thank you again and I’ll be sure to see if I can make use of it.

Having said all that, at some point I’ll be switching out my existing “dumb” DHW cylinder and replacing for something like the Vaillant aroSTOR (air-source cylinder): https://www.vaillant.co.uk/downloads/aproducts/renewables-1/arostor-1/0020285063-00-arostor-200-270ltr-install-1456700.pdf. These seem to be ideal for my needs and are very efficient.

These cylinders have a connection to “Solar” (location 4 & 5 on the pdf) and I’ve not had any joy in getting details of exactly what that means even from Vaillant. But it seems they mean a solar controller (i.e. eddi). Now, for my money, that renders an eddi even more unsuitable as the eddi would only be used to send a signal for excess solar to the tank but not provide power as the cylinder controls the heat pump and immersion via a single supply (point 12 on page-28 of PDF) (not from the eddi!).

So something like your scripting above could do the trick. Something that is able to notify “stuff” that “there’s excess solar if anything is interested in taking it”. And also able to notify that same “stuff” on when the “cheap” rates are and they can use the grid if they want.

Now just need to figure out how the Vaillant needs to see that notification!?! Any ideas would be welcome.

Thanks.

2 Likes

Hi Kamil.

Is the Sonoff still working well? Also, are you in the UK? The POWR3 doesn’t appear to be the shape/size of either a single or double pattress box. How do you have it mounted?

Thanks.

Yes, all working well. So far more than 600kWh of hot water delivered, saving well over £100. On a few occasions the HA add on was showing wrong info but HA reboot fixes it. The switch itself working perfectly. Here are some pictures



That’s great, Kamil, thank you. I’m assuming any oversized JB can be used. Is heat an issue at all with the Sonoff enclosed like this?

Thank you for your post Kamil, I have been exploring the idea of adding a Shelly EM plus a contactor to my 16A immersion heater to implement a similar intelligent solar diverter and off peak water heater however the Sonoff switch sounds interesting.

My DNO limited my solar PV installation to 3.68kW (16A) so similarly the cost of an Eddi made limited sense financially however I still prefer the idea of using my solar generation in whatever way makes most sense at the time, e.g. trigger kitchen appliance to start, heat water, and absolute worst case, export.

1 Like

@Valiante, I haven’t noticed any heat issues, although I haven’t been particularly thorough with checking. It was never hot on touch though as far as I can recall, probably because it is well oversized for the load. In addition, to reduce the ambient temperature in the cylinder cabinet, and to reduce heat loss, I used a lot of wool to wrap any hot water pipes or contact points around the cylinder - this made a massive difference! I can check in the next few days when I get a chance…

After the Sonoff was on for 2hrs, it was warm-ish on touch. Put a bath thermometer in the enclosure and that didn’t even reach 30C after 15min or so, with ambient temp of 21C. Hope that helps.

This is great information, thank you. Appreciate you taking the time to test that and update.

1 Like

@kamilb woudl you mind advising what the

‘sensor.solax_local_feedin_power’

Actually refers to on your inverter?

sensor.solax_local_feedin_power is how much is being fed back to the grid. So positive means surplus, negative means using the grid.

1 Like

I found a project on GitHub that is using Shelly 2.5 or Shelly EM as it’s using the same power meter to divert excess solar to the hot water.

@kamilb this is great, using the Sonoff powr3 is exactly what I’m looking to do with my immersion. Do you know if it is mounted before or after the switch?

Sonoff sits between the original manual wall switch and the immersion heater.

1 Like

great, thanks!

This looks perfect solution and something I’m looking to do.

In your tank, is your immersion heater just in the top? Or do you have an economy 7 style tank with 2 elements?

Just one large-ish tank, with one immersion heater located in the bottom half of the tank.

great code @kamilb thanks for sharing it.
i have one question maybe you can help me out , i have a solar water heater with a TK-7 solar controller with bottom fixed sensor for non pressurized system , https://www.aliexpress.com/i/1005001734767504.html#nav-review and this is the water level water temperature sensor probe https://www.aliexpress.us/item/1005005332812635.html?gatewayAdapt=glo2usa , any idea what device i should use for the water temp and level sensor to integrate it into home assistant ?

No, sorry. Haven’t used additional sensors…

I do not have the solar panels, but do have control of hot water by both gas boiler central heating and by electric immersion heater.

I put a smart temperature sensor on the hot water cylinder, a smart switch on the electric immersion heater and another on the zone control valve for the hot water circuit. I then made Generic Thermostats for both hot water and immersion heater using the same temperature sensor. The idea is that heating is usually by the gas boiler, but the electric immersion heater acts as a top-up if the temperature falls below a certain threshold (e.g. after two people use the shower in quick succssion)

The thermostats are then scheduled by calendars using the blueprint Heating X2: Schedule Thermostats with Calendars