Optimizing heat pump w. accumulator tank run times to heat home and produce hot water for lowest cost, using nordpool hourly price

With the recent high energy prices, I wanted to optimize my energy usage costs.
The biggest consumer is the house heating and hot water production, all lights and other appliances pale in comparison on how much energy they draw.

My system is:

  • CTC geothermal heat pump (onoff)
  • 500 liter accumulator tank
  • radiators using heat from the accumulator tank to heat the house
  • hot water production from coils in the accumulator tank (produced on demand, minimal risks for bacteria)

The accumulator tank works as a battery, so why not charge it up when prices are low, and use the stored energy while the prices are high. Given enough price variance, this should give good savings!

Early on I tried a schedule with an on/off automation for the pump, where I manually scheduled the run times. It is hard though to estimate energy used for different days and times, and much work to check prices and set up the schedule every day.

Additionally, the higher the water temperature, the lower the efficiency of the heat pump, and higher losses from the accumulator tank.

So I wanted a “smart” system that could predict the energy consumption hour by hour, and produce and store just enough heat to get through the expensive hours.

Since high water temperature is a big cost, I decided to use a homeassistant local calendar to schedule when we want hot water for showers. Otherwise the minimum temperature is what the radiators need given the outdoor temperature, which is musch lower.

So I created a model with the following inputs:

  • heat pump production capacity (W) and power draw (W)
  • desired indoor temperature
  • current indoor temperature
  • current outdoor temperature
  • outdoor temperature hourly forecast the coming 24 hours
  • schedule with times for showers, wanted minimum temp and approximate power usage
  • minimum wanted hot water temperature
  • hourly electricity prices (end prices with tax and all, or the optimization will make bad decisions)

Using these parameters it is rather easy to estimate heat consumption in W, and minimum required temperature in the tank hour by hour

Given the the consumption, energy prices and pump capacity, one can create a linear optimization model for when to run the pump.

However, I don’t know how to make that run as a script in HA, so I created a REST web API inside a docker image running on the same server as the HA docker, which I could call using the HA Rest API and sensors.

Said and done, wiring it all together, here is the generated plan for the upcoming 24 hours, visualized by HA:

First is the graph showing predicted heat water consumption in Watts, by hot water, radiators, heat loss and showers.

Second is the planned pump production (output) in W period by period, plotted against the electricity price for each period.

Lastly is a graph showing the prediced accumulator tank temperature, radiator temperature, pump thermostat setpoint, and outside temp.

On the very top is shown the predicted cost for running the pump according to this plan the coming 24 hours. It is quite fun, as the min hot water temp and indoor temp sliders are interactive, and reflects “immediately” on the estimated cost.

There is also a hidden slider, where changing the accumulator tank volume shows immediately how cost could be affected.

Configuration in HA is a bit special, as REST sensors cannot send a template payload as body.
To get around that I first defined a rest_command which can use a template body, and the defined the rest sensors to get the last state from the rest api (I can do this since I control the webserver).

An automation is triggered every 20 minutes to recalculate the optimization, and update the sensors

The entire configuration looks like this:

    name: Använd Optimerare
    icon: mdi:graph
    name: Min varmvatten temp
    icon: mdi:thermometer
    min: 20
    max: 40
    step: 1
    unit_of_measurement: "°C"
    mode: slider
    name: Acc volym
    icon: mdi:thermometer
    min: 1
    max: 1001
    step: 100
    unit_of_measurement: "liter"
    mode: slider

    url: ""
    method: POST
    content_type:  'application/json'
    payload: >
                {% set indoor_target_temp = states('number.indoor_target_temp') | float(18) %}
                {% set current_outdoor_temp = states('sensor.torild_air_temperature') | float(18) %}
                {% set forecast = state_attr('weather.forecast_home_hourly', 'forecast') %}
                {% set idle_min_temp = states('input_number.warm_water_min_temp') | float(20) %}

                {% set data = namespace(outdoor = [{"time": now().isoformat(), "temp": current_outdoor_temp}]) %}
                {% for f in forecast  %}
                  {% set data.outdoor = data.outdoor + [{ "time": f.datetime, "temp": f.temperature}]  %}  
                {% endfor %}

                {% set body = {
                      "time": now().isoformat(),
                      "split_hours_into": 3,
                      "plan_hours": 24,
                      "acc_spec": {
                                "liters": states('input_number.acc_tank_volume') | float(500),
                                "max_temp": 55,
                                "min_temp": 20,
                                "current_temp": states('sensor.acc_tank_avg_temp'),
                                "temp_loss_per_hour_degrees": 0.02189
                      "pump_spec": {
                          "start_delay_minutes": 2,
                          "max_temp": 55,
                          "power": [{
                                  "below_degrees": 35,
                                  "heating_power_watt": 8200,
                                  "consumed_power_watt": 1980
                              }, {
                                  "below_degrees": 45,
                                  "heating_power_watt": 8000,
                                  "consumed_power_watt": 2200
                              }, {
                                  "below_degrees": 50,
                                  "heating_power_watt": 7900,
                                  "consumed_power_watt": 3000
                              }, {
                                  "heating_power_watt": 7800,
                                  "consumed_power_watt": 3100
                      "indoor": {
                        "target_temp": indoor_target_temp,
                        "passive_heating_degrees": states('number.passive_heating_degrees') | float(3),
                        "current_temp": states('sensor.house_hall_temp') | float(18),
                      "outdoor": data.outdoor,
                      "radiator": {
                          "c_flow": 0.45,
                          "power_per_temp_delta": 400
                        "today":state_attr('sensor.nordpool_kwh_se3_sek_3_095_025', 'today'),
                        "tomorrow":state_attr('sensor.nordpool_kwh_se3_sek_3_095_025', 'tomorrow'),      
                      "showers": [{
                        "start": state_attr('calendar.shower', 'start_time'),
                        "end": state_attr('calendar.shower', 'end_time'),
                        "temp": 35,
                        "energy": 1000
                      "hot_water": {
                          "min_temp": states('input_number.warm_water_min_temp') | float(20),
                          "average_power": 100

                {{ body | tojson(2) }}

  - alias: calculate_optimal_heating_2
    id: 9cad5a69-4a9e-4d31-83c3-c2530776ee63
    mode: restart
    - platform: time_pattern
      minutes: /20
    - platform: state
      - calendar.shower
      - input_boolean.use_optimizer
      - number.indoor_target_temp
      - input_number.warm_water_min_temp
      - number.passive_heating_degrees
      - input_number.acc_tank_volume
      - service: rest_command.calculate_optimization_result2
      - service: homeassistant.update_entity
        entity_id: sensor.optimized_target_pump_thermostat2   

  - resource:
    method: GET
    scan_interval: 120
    timeout: 120
      - name: optimized_target_pump_thermostat2
        unique_id: 'c8f646df-9792-43e2-aab4-a0b5360af52a'
        unit_of_measurement: "°C"
        device_class: "temperature"
        value_template: >
            {{ value_json.plan[0].pump.target_temp }}
          - ok
          - cost
          - plan
          - params
          - result
      - name: optimized_pump_production2
        unit_of_measurement: "W"
        device_class: "power"
        value_template: >        
            {{ value_json.plan[0].pump.production }}
      - name: optimized_target_flow_temp2
        unit_of_measurement: "°C"
        device_class: "temperature"
        value_template: >        
            {{ value_json.plan[0].rad_flow_temp }}
      - name: optimized_heat_consumption2
        unit_of_measurement: "W"
        device_class: "power"
        value_template: >
            {{ -value_json.plan[0].consumption.total }}

      - name: optimized_predicted_24h_cost2
        unique_id: '32154361-7a3f-43de-9ddd-a50e240af4cf'
        unit_of_measurement: "SEK"
        device_class: "monetary"
        value_template: >
            {{ value_json.cost | round(2) }}

This graph shows the optimization in action on past dates, rather then the future plan. It shows the avg temperature of the accumulator tank, compared to the hourly electricity prices.

You can clearly see that it “charges” up the tank just before prices go up, and idles throughout the high price periods.

This was from an earlier optimization model though, that didn’t account for the lower efficiency of the pump at higher temeratures. That’s why it always goes up to ~55 degrees, when that might not be optimal.
A later model currently under test takes that into account though


Very interesting. My implementation does something similar, but I’m using an RC thermal model of the house.

Very impressive! Are you planning to release the code? I’ve been recently trying out EMHASS to optimize my power use between some loads, grid and PV plant, but the lack of heating model makes it impractical for the biggest load I have: the heatpump.

1 Like

I am also very interested!

Can you post a link to your code?

1 Like