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.
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
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
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.
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' }}"
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.
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.