Daikin Altherma Heating Controls via Home Assistant and Onecta Integration

Ok so likely not setting the world on fire here for revolutionary controls but it may help some, especially here in the UK, where Octopus are installing a lot of these Dainkin units.

so they have 2 modes, LWT which runs and balances temps of a heat curve and ignores internal temps. It’s very efficient but doesn’t balance internal comfort without a lot of tinkering.
Then Madoka mode that uses the internal thermostat and constantly messes about with the heat pump, is not great at efficiency but does do comfort well.

So, behold. LWT with some internal reference for the best of both.

alias: Heat Pump - Adjust Heat Curve Offset based on Room Temp
triggers:

- minutes: /30
  trigger: time_pattern
  conditions:

- condition: numeric_state
  entity_id: sensor.altherma_heat_pump_climatecontrol_room_temperature
  above: 0
  actions:

- variables:
  room_temp: >
  {{ states(‘sensor.altherma_heat_pump_climatecontrol_room_temperature’) |
  float(21) }}
  current_counter: |
  {{ states(‘input_number.heat_curve_offset_counter’) | float(0) }}

- choose:

  - conditions:

    - condition: numeric_state
      entity_id: sensor.altherma_heat_pump_climatecontrol_room_temperature
      above: 21.5

    - condition: numeric_state
      entity_id: sensor.altherma_heat_pump_climatecontrol_leaving_water_temperature
      above: 30
      sequence:

    - variables:
      new_value: |
      {% set next = current_counter - 1 %} {% if next < -5 %}
      -5
      {% else %}
      {{ next | int }}
      {% endif %}

    - target:
      entity_id: input_number.heat_curve_offset_counter
      data:
      value: “{{ new_value }}”
      action: input_number.set_value

  - conditions:

    - condition: numeric_state
      entity_id: sensor.altherma_heat_pump_climatecontrol_room_temperature
      below: 20.5
      sequence:

    - variables:
      new_value: |
      {% set next = current_counter + 1 %} {% if next > 5 %}
      5
      {% else %}
      {{ next | int }}
      {% endif %}

    - target:
      entity_id: input_number.heat_curve_offset_counter
      data:
      value: “{{ new_value }}”
      action: input_number.set_value

  - conditions:

    - condition: numeric_state
      entity_id: sensor.altherma_heat_pump_climatecontrol_room_temperature
      above: 20.5

    - condition: numeric_state
      entity_id: sensor.altherma_heat_pump_climatecontrol_room_temperature
      below: 21.5
      sequence:

    - target:
      entity_id: input_number.heat_curve_offset_counter
      data:
      value: 0
      action: input_number.set_value

- target:
  entity_id: climate.heating_leaving_water_offset
  data:
  temperature: “{{ states(‘input_number.heat_curve_offset_counter’) | int }}”
  action: climate.set_temperature
  mode: single

This automation will reference the internal thermostat every 30 minutes and then tweak the flow temperature by 1 up or down to help keep the room temp within the ideal mark of 20.5 and 21.5 degrees C.
Its max adjustment is +/- 5 to the flow temperature.

It also needs a helper to track that adjustment value which you can drop into your configuration.yaml

input_number:
heat_curve_offset_counter:
name: Heat Curve Offset Counter
min: -5
max: 5
step: 1
mode: box
initial: 0

This has been keeping my house warm now for a good few weeks and stopping the house over heating, whilst keeping the heat pump running slow and steady. Give it a look if you’re interested.

Welcome @belly120 - thanks for the contribution. I’ve slightly edited your post for formatting (you can use ``` to start and end a code block). I’m not at all familiar with the HA YAML, so please check I’ve not messed anything up :slight_smile:

Here’s what I normally write (thanks to Brian Orpin)

For future reference, when posting code or output, please put 3 ‘backticks’ (normally found at the top left of the keyboard) on a line of their own before the code, and 3 more backticks also on a line of their own after the code:

```
code
```

If it is something like php you can add a language identifier after the first 3 backticks: ```php or even ```text if you don’t want any language markup applied.

1 Like

Interesting.

Do you have any cop figures from before / after implementing this code?

How much of a difference to unit efficiency did it make and was the weather curve correctly configured before implementing.

Quite a few of us here have had Daikin installs and balancing the radiators and ensuring the ashp is not oversized has been the main issue.

Is your unit on heatpumpmonitor.org so we can see the impact at all. For reference mine is Farnborough, Hampshire.

Ok so big update on this one. I’m now using a PID style controller that factors in my solar PV, weather forecasting and internal/external temps to refine and run the system. My system is running at its most optimal level and can now take advantage of solar gain as well as automatically turning on and off when long periods make it wasteful to run. Its a think of beauty.

code below…

###############################################################################
# PACKAGE: Forecast-Aware PID + Seasonal Supervisor (Tuned for Overshoot)
###############################################################################

input_number:
  heat_curve_integral:
    name: "Heat Curve Integral"
    min: -20
    max: 20
    step: 0.1
    mode: box
  heating_room_setpoint:
    name: "Heating Room Setpoint"
    min: 18
    max: 23
    step: 0.1
    unit_of_measurement: "°C"

sensor:
  - platform: filter
    entity_id: sensor.altherma_heat_pump_climatecontrol_room_temperature
    name: "Room Temp Smoothed"
    filters:
      - filter: lowpass
        time_constant: 10
        precision: 2

template:
  - trigger:
      - trigger: time_pattern
        hours: "/1"
      - trigger: homeassistant
        event: start
    action:
      - action: weather.get_forecasts
        data:
          type: hourly
        target:
          entity_id: weather.forecast_home
        response_variable: hourly_data
    sensor:
      - name: "Outdoor Temperature 1h Forecast"
        unique_id: outdoor_temperature_1h_forecast
        unit_of_measurement: "°C"
        device_class: temperature
        state: >
          {% set target_entity = 'weather.forecast_home' %}
          {% if hourly_data[target_entity] is defined and hourly_data[target_entity].forecast | length > 0 %}
            {{ hourly_data[target_entity].forecast[0].temperature | float }}
          {% else %}
            {{ states('sensor.altherma_heat_pump_climatecontrol_outdoor_temperature') | float(10) }}
          {% endif %}

      - name: "Outdoor Temperature 2h Forecast"
        unique_id: outdoor_temperature_2h_forecast
        unit_of_measurement: "°C"
        device_class: temperature
        state: >
          {% set target_entity = 'weather.forecast_home' %}
          {# Index [1] is typically 2 hours from now in the hourly array #}
          {% if hourly_data[target_entity] is defined and hourly_data[target_entity].forecast | length > 1 %}
            {{ hourly_data[target_entity].forecast[1].temperature | float }}
          {% else %}
            {{ states('sensor.outdoor_temperature_1h_forecast') | float(10) }}
          {% endif %}

  # 2. TUNED PID LOGIC SENSORS
  - sensor:
      - name: "HP PID Forecast Bias"
        unique_id: hp_pid_forecast_bias
        state: >
          {% set pv = states('sensor.solcast_pv_forecast_forecast_next_hour') | float(0) %}
          {% set pv_capacity = 5000 %} 
          {% set room = states('sensor.room_temp_smoothed') | float(20) %}
          {% set target = states('input_number.heating_room_setpoint') | float(20) %}
          {% set room_deadband = 0.2 %}

          {% set temp_now = states('sensor.altherma_heat_pump_climatecontrol_outdoor_temperature') | float(0) %}
          {% set temp_1h = states('sensor.outdoor_temperature_1h_forecast') | float(temp_now) %}
          {% set temp_2h = states('sensor.outdoor_temperature_2h_forecast') | float(temp_1h) %}

          {# Weighting: 1.2 is the max solar bias, 0.5 is the max weather bias #}
          {% set Smax, Wmax, Rmax, Bmax = 1.2, 0.8, 1.5, 2.5 %}

          {# New Weather Trend: Average the delta over 2 hours #}
          {% set delta_1h = temp_1h - temp_now %}
          {% set delta_2h = temp_2h - temp_now %}
          {% set avg_trend = (delta_1h + delta_2h) / 2 %}

          {% set solar_bias = ([0, [pv / pv_capacity, 1] | min] | max) * Smax %}

          {# Weather bias now reacts to the 2-hour trend. 
             If avg_trend is +3°C (warming up), it subtracts from heat demand #}
          {% set weather_bias = ([0, [avg_trend / 3, 1] | min] | max) * Wmax %}

          {% set room_bias = ([0, [(room - target - room_deadband) / 2, 1] | min] | max) * Rmax %}

          {{ [0, [solar_bias + weather_bias + room_bias, Bmax] | min] | max | round(3) }}
      - name: "HP PID Output"
        unique_id: hp_pid_output
        state: >
          {% set t = states('sensor.room_temp_smoothed') | float(20) %}
          {% set target = states('input_number.heating_room_setpoint') | float(20) %}
          {% set error = target - t %}

          {# TUNING: Weight negative error 1.5x to drop heat faster when over setpoint #}
          {% set error_weighted = error * 1.5 if error < 0 else error %}

          {% set integral = states('input_number.heat_curve_integral') | float(0) %}
          {% set bias = states('sensor.hp_pid_forecast_bias') | float(0) %}

          {# Kp increased to 1.5 for sharper reaction #}
          {% set Kp, Ki = 1.5, 0.05 %}
          {{ ((Kp * error_weighted) + (Ki * integral) - bias) | round(1) }}

  # 3. SEASONAL BINARY SENSOR
  - binary_sensor:
      - name: "Heating Season Active"
        unique_id: heating_season_active
        device_class: running
        # We can reduce these delays slightly because the 2h forecast
        # is now doing the "heavy lifting" of preventing rapid toggling.
        delay_on: "01:00:00"
        delay_off: "01:00:00"
        state: >
          {% set t_now = states('sensor.altherma_heat_pump_climatecontrol_outdoor_temperature') | float(10) %}
          {% set t_1h  = states('sensor.outdoor_temperature_1h_forecast') | float(t_now) %}
          {% set t_2h  = states('sensor.outdoor_temperature_2h_forecast') | float(t_1h) %}

          {# Create a list of the window temperatures #}
          {% set window = [t_now, t_1h, t_2h] %}
          {% set window_max = window | max %}
          {% set window_min = window | min %}

          {# LOGIC: 
             1. Only turn ON if the WARMREST point in the next 2h is still below 12°C.
                (Prevents turning on if a warm spell is imminent)
             2. Only turn OFF if the COLDEST point in the next 2h is above 14°C.
                (Prevents turning off if a cold snap is imminent) #}

          {% if window_max < 12 %}
            true
          {% elif window_min > 14 %}
            false
          {% else %}
            {# Maintain current state if in the 'deadzone' between 12 and 14 #}
            {{ is_state('binary_sensor.heating_season_active', 'on') }}
          {% endif %}

automation:
  - alias: "Heat Pump - PID Loop"
    id: hp_pid_loop
    trigger:
      - trigger: time_pattern
        minutes: "/30"
    actions:
      - variables:
          target_temp: "{{ states('input_number.heating_room_setpoint') | float(20) }}"
          current_temp: "{{ states('sensor.room_temp_smoothed') | float(20) }}"
          error: "{{ target_temp - current_temp }}"
          pid_raw: "{{ states('sensor.hp_pid_output') | float(0) }}"
          pid_clamped: "{{ [ [-5, (pid_raw | round(0))] | max, 5] | min }}"

      # 1. Update the Integral with faster downward accumulation
      - action: input_number.set_value
        target:
          entity_id: input_number.heat_curve_integral
        data:
          value: >
            {% set weight = 0.08 if error < 0 else 0.04 %}
            {{ [ [-20, (states('input_number.heat_curve_integral')|float(0) + (error * weight))] | max, 20] | min | round(2) }}

      # 2. Update Visual Counter
      - action: input_number.set_value
        target:
          entity_id: input_number.heat_curve_offset_counter
        data:
          value: "{{ pid_clamped }}"

      # 3. Send command to Altherma
      - action: climate.set_temperature
        target:
          entity_id: climate.heating_leaving_water_offset
        data:
          temperature: "{{ pid_clamped }}"

  - alias: "Heat Pump - Seasonal Supervisor"
    id: hp_seasonal_supervisor
    trigger:
      - trigger: state
        entity_id: binary_sensor.heating_season_active
    actions:
      - action: climate.set_hvac_mode
        target:
          entity_id: climate.heating_leaving_water_offset
        data:
          hvac_mode: "{{ 'heat' if is_state('binary_sensor.heating_season_active', 'on') else 'off' }}"

1 Like

for numbers, today has been a steady 9 degrees, system is in Leaving Water mode not madoka mode. But the above PID controller, targeting internal 20 degree comfort temp has been running my curve 4 points lower then my 40 @-3 curve and still keeping the house around 20.8 degrees.
Since midnight last night, 6kw of energy used as 16:35.

My house needed the 40 at -3 when we had those prolonged periods of cold but now these shoulder months are a breeze.

1 Like

further update, today is another good example of a coldish but very sunny day,

Thats my general readings, so on the leaving water temps the 2 spikes are hot water cycles.
Today, the PID controller has kept it sat 3 degrees lower for a nice long steady period due to solar gain etc. Seems to be running very well, suspect today will likely 8 to 9kw of energy used across a 24h period which for an Octopus installed Daikin 8kw is probably the best i can hope for.