@110hs I think that this is related to the inverter_ac_input_max parameter.
I’ve recently applied a fix to this.
It will be available in the upcoming release.
In the meantime you can try to increase it to match your battery’s maximum charging power and test.
The correct action is: .../action/naive-mpc-optim
And yes this should be a list: "nominal_power_of_deferrable_loads": [5000]
Glad @davidusb was able to identify the issue ‘_’ in place of ‘-’.
This does bring up an interesting supportability question.
I know my payload is getting way out of control and very difficult to manage, I’m wondering what others are doing to modularize their payloads?
alias: EMHASS Payload Script (Thermal)
description: ""
mode: single
fields: {}
sequence:
- if:
- condition: template
value_template: >
{{
state_attr('sensor.price_forecast_custom_model','scheduled_forecast')|count
== 0 }}
then:
- action: rest_command.price_forecast_model_fit
- action: rest_command.price_forecast_model_predict
- if:
- condition: template
value_template: >
{{
state_attr('sensor.cost_forecast_custom_model','scheduled_forecast')|count
== 0 }}
then:
- action: rest_command.cost_forecast_model_fit
- action: rest_command.cost_forecast_model_predict
- action: script.weather_forecasts
response_variable: action_response
- variables:
soc_init: |
{{
(
states('sensor.sigen_plant_battery_state_of_charge')|float(0) +
1*states('sensor.my_t_battery_level')|float(0) +
1*states('sensor.m3p_t_battery_level')|float(0)
) / 300
}}
amber_feed_in_list: |
{{
state_attr('sensor.amber_30min_forecasts_feed_in_price','Forecasts')
| selectattr('advanced_price_predicted','is_number')
| map(attribute='advanced_price_predicted')
| map('multiply', -1) | list
}}
amber_general_list: |
{{
state_attr('sensor.amber_30min_forecasts_general_price','Forecasts')
| selectattr('advanced_price_predicted','is_number')
| map(attribute='advanced_price_predicted') | list
}}
price_ml_list: |
{{
state_attr('sensor.price_forecast_custom_model','scheduled_forecast')
| map(attribute='price_forecast_custom_model') | map('float') | list
}}
cost_ml_list: |
{{
state_attr('sensor.cost_forecast_custom_model','scheduled_forecast')
| map(attribute='cost_forecast_custom_model') | map('float') | list
}}
count: "{{ amber_feed_in_list | count }}"
countML: "{{ price_ml_list | count }}"
horizon: "{{ [144, (countML|int(0))] | min }}"
sr_timesteps: "{{ states('sensor.emhass_sun_next_rising_timesteps')|int(0) }}"
- variables:
current_feed_in_price_adj: >
{{ states('sensor.amber_5min_current_feed_in_price')|float(0) +
states('input_number.emhass_spot_adjustment')|float(0) }}
current_general_price_adj: >
{{ states('sensor.amber_5min_current_general_price')|float(0) +
states('input_number.emhass_spot_adjustment')|float(0) }}
prod_price_forecast: |
{{
([ current_feed_in_price_adj ] + amber_feed_in_list[:(count|int)] + price_ml_list[(count|int)+1:])[:(horizon|int)]
}}
load_cost_forecast: |
{{
([ current_general_price_adj ] + amber_general_list[:(count|int)] + cost_ml_list[(count|int)+1:])[:(horizon|int)]
}}
- variables:
def1_desired_temps: >
{%- set set_ts = states('sensor.emhass_sun_next_setting_timesteps')|int
-%} {%- set total_blocks = horizon|int -%} {%- set max_heating_blocks =
6 -%} {%- set heating_today = [0, [max_heating_blocks, set_ts -
2]|min]|max -%} {%- set first_start = set_ts - 9 -%} {%- set
heating_tomorrow = max_heating_blocks -%} {%- set second_start =
first_start + 48 -%} {%- set before_first = [0, first_start]|max -%} {%-
set between = [0, second_start - (first_start + heating_today)]|max -%}
{%- set remaining = [0, total_blocks - (second_start +
heating_tomorrow)]|max -%} {%- set temps =
[15]*before_first +
[32]*heating_today +
[15]*between +
[32]*heating_tomorrow +
[15]*remaining +
[32]*heating_tomorrow +
[15]*remaining
-%} {{ temps[:(horizon|int)] }}
hws_desired_temps: >
{%- set set_ts = states('sensor.emhass_sun_next_setting_timesteps')|int
-%} {%- set total_blocks = horizon|int -%} {%- set max_heating_blocks =
10 -%} {%- set heating_today = [0, [max_heating_blocks, set_ts -
10]|min]|max -%} {%- set first_start = set_ts - 12 -%} {%- set
heating_tomorrow = max_heating_blocks -%} {%- set second_start =
first_start + 48 -%} {%- set before_first = [0, first_start]|max -%} {%-
set between = [0, second_start - (first_start + heating_today)]|max -%}
{%- set remaining = [0, total_blocks - (second_start +
heating_tomorrow)]|max -%} {%- set temps =
[40]*before_first +
[60]*heating_today +
[40]*between +
[60]*heating_tomorrow +
[40]*remaining +
[60]*heating_tomorrow +
[50]*remaining
-%} {{ temps[:(horizon|int)] }}
def7_desired_temps: >
{%- set now_dt = now() -%} {%- set sunrise =
as_timestamp(state_attr('sun.sun', 'next_rising')) -%} {%- set sunset =
as_timestamp(state_attr('sun.sun', 'next_setting')) -%} {%- set sr =
as_local(as_datetime(sunrise)) -%} {%- set ss =
as_local(as_datetime(sunset)) -%} {%- set sr_h = sr.hour + sr.minute/60
-%} {%- set ss_h = ss.hour + ss.minute/60 -%} {%- set vals =
namespace(list=[]) -%} {%- for i in range(146) -%}
{%- set dt = now_dt + timedelta(minutes=30*i) -%}
{%- set hm = dt.hour + dt.minute/60 -%}
{%- if sr_h <= hm < sr_h + 1 -%}
{%- set t = 18 -%}
{%- elif ss_h <= hm < ss_h + 2 -%}
{%- set t = 20 -%}
{%- elif hm < sr_h or hm >= ss_h + 4 -%}
{%- set t = 17 -%}
{%- else -%}
{%- set t = 16 -%}
{%- endif -%}
{%- set vals.list = vals.list + [t] -%}
{%- endfor -%} {{ vals.list[:(horizon|int)] }}
- variables:
def1_obj: |
{{
{
'thermal_config': {
'heating_rate': 1,
'sense': 'heat',
'cooling_constant': 0.02,
'overshoot_temperature': 33.0,
'start_temperature': states('sensor.ibs_th2_p01b_0529_temperature')|float(28),
'desired_temperatures': def1_desired_temps
}
} if is_state('automation.p_deferable1_automation','on') else {}
}}
def3_obj: |
{{
{
'thermal_config': {
'heating_rate': -6.0,
'sense': 'cool',
'cooling_constant': 0.2,
'overshoot_temperature': 20.0,
'start_temperature': state_attr('climate.climate_master', 'current_temperature')|float(0),
'desired_temperatures': [ states('input_number.emhass_deferrable3_set_point')|int(0) ] * (horizon|int)
}
} if is_state('automation.p_deferrable3_hvac_v2','on') else {}
}}
def4_obj: |
{{
{
'thermal_config': {
'heating_rate': 6.0,
'sense': 'heat',
'cooling_constant': 0.007,
'overshoot_temperature': 65.0,
'start_temperature': states('sensor.hws_power_based_sensor')|float(0),
'desired_temperatures': hws_desired_temps
}
}
}}
def7_obj: |
{{
{
'thermal_config': {
'heating_rate': states('input_number.emhass_heating_rate_7')|float(5),
'sense': 'heat',
'cooling_constant': states('input_number.emhass_cooling_constant_7')|float(0.1),
'overshoot_temperature': 22.0,
'start_temperature': state_attr('climate.climate_master', 'current_temperature')|float(0),
'desired_temperatures': def7_desired_temps
}
} if is_state('automation.p_deferrable_7_hvac_heat_automation','on') else {}
}}
def_load_config: |
{{
[
{},
def1_obj,
{},
def3_obj,
def4_obj,
{},
{},
def7_obj
]
}}
- variables:
def_total_hours: |
{{
[
states('sensor.def_total_hours_pool_filter')|int(0),
states('sensor.def_total_hours_pool_heatpump')|int(0),
states('sensor.def_total_hours_ev')|int(0),
states('sensor.def_total_hours_hvac')|int(0),
states('sensor.def_total_hours_hws')|int(0),
states('sensor.def_total_hours_ev2')|int(0),
((horizon|int)/2 - 5)|int(0),
0
]
}}
def_current_state: |
{{
[
iif(states('sensor.p_deferrable0')|int(0),1,0),
iif(states('sensor.p_deferrable1')|int(0),1,0),
iif(states('sensor.p_deferrable2')|int(0),1,0),
iif(states('sensor.p_deferrable3')|int(0),1,0),
iif(states('sensor.p_deferrable4')|int(0),1,0),
iif(states('sensor.p_deferrable5')|int(0),1,0),
iif(states('sensor.p_deferrable6')|int(0),1,0),
iif(states('sensor.p_deferrable7')|int(0),1,0)
]
}}
def_start_timestep: |
{{
[
0,
0,
[0, states('sensor.emhass_deferrable2_start_timesteps')|int(0)]|max,
0,
0,
[0, states('sensor.emhass_deferrable5_start_timesteps')|int(0)]|max,
0,
0
]
}}
def_end_timestep: |
{{
[
0,
0,
[0, states('sensor.emhass_deferrable2_end_timeslots')|int(0)]|max,
0,
0,
[0, states('sensor.emhass_p_deferable_5_end_timeslots')|int(0)]|max,
0,
0
]
}}
P_deferrable_nom: |
{{
[
440,
5500,
states('sensor.p_nom_ev')|int(0),
states('input_number.p_nom_hvac')|int(0),
600,
states('sensor.p_nom_ev2')|int(0),
770,
6000
]
}}
treat_def_as_semi_cont:
- 1
- 1
- 0
- 0
- 1
- 0
- 1
- 0
set_def_constant:
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
def_start_penalty:
- 1
- 0
- 1
- 1
- 1
- 1
- 0
- 0
- variables:
payload: |
{{
{
"prediction_horizon": horizon|int,
"prod_price_forecast": prod_price_forecast,
"load_cost_forecast": load_cost_forecast,
"num_def_loads": 8,
"def_load_config": def_load_config,
"outdoor_temperature_forecast": action_response.forecasts | map(attribute='temp') | list,
"alpha": 0,
"beta": 1,
"def_total_hours": def_total_hours,
"def_current_state": def_current_state,
"def_start_timestep": def_start_timestep,
"def_end_timestep": def_end_timestep,
"P_deferrable_nom": P_deferrable_nom,
"treat_def_as_semi_cont": treat_def_as_semi_cont,
"set_def_constant": set_def_constant,
"def_start_penalty": def_start_penalty,
"weight_battery_charge": states('input_number.weight_battery_charge')|float(0),
"weight_battery_discharge": states('input_number.weight_battery_discharge')|float(0),
"battery_nominal_energy_capacity": (states('input_number.battery_nominal_energy_capacity')|int(0))*1000,
"battery_discharge_power_max": (states('input_number.battery_discharge_power_max')|int(0))*1000,
"soc_final": (states('input_number.soc_final')|int(0))/100,
"battery_minimum_state_of_charge": (states('input_number.battery_minimum_state_of_charge')|int(0))/100,
"weather_forecast_cache_only": true,
"optimization_time_step": 30,
"entity_save": true,
"soc_init": soc_init
} | tojson
}}
- action: rest_command.mpc_servicecall_rest
enabled: true
data:
payload: "{{ payload }}"
- action: rest_command.publish_data_rest
enabled: true
data:
payload: |
{
"def_load_config": [
{}, {}, {}, {},
{ "thermal_config": {} },
{}, {},
{ "thermal_config": {} }
]
}
Unfortunately I have no answer to your question. I was also playing around with variables, but no passing the whole payload just the variables, which resulted in some issues that does variables are not correctly handled in the call.
How do you test your payload, when adjusting it?
Currently I work in the file editor and test my payload in the template editor.
One small remark, as far as I know, passed “def_total_hours” for thermal loads are ignored.
Ha,ha!! YES you are so right, and the most obvious is the hardest i guess. And you also did beat the ChatGPT even though prompted to “read the docs” and “look at commands and actions”… Thnx a million David!
I had to update the whole rest_command some more, because misalignments and aquardities that happened when the nordpoolsensor got updated in afternoon. For reference, this does work:
trigger_mpc_optim:
url: "http://192.xxx.xx.x:5000/action/naive-mpc-optim"
method: POST
timeout: 60
headers:
Content-Type: "application/json"
payload: >
{# Step 1: Compute next 15-min interval and remaining horizon #}
{% set next_interval = (now() + timedelta(minutes=(15 - now().minute % 15))).replace(second=0, microsecond=0) %}
{% set horizon = states('input_number.mpc_prediction_horizon') | int %}
{# Step 2: Load and filter forecasts from next_interval onwards #}
{% set load_raw = (state_attr('sensor.nordpool_se3_total_costs_import', 'forecast_merged') or [])
| selectattr('ts', '>=', next_interval.strftime('%Y-%m-%d %H:%M')) | list %}
{% set prod_raw = (state_attr('sensor.nordpool_se3_total_revenue_export', 'forecast_merged') or [])
| selectattr('ts', '>=', next_interval.strftime('%Y-%m-%d %H:%M')) | list %}
{% set pv_raw = (state_attr('sensor.solcast_pv_forecast_15min_intervals', 'forecast_merged') or [])
| selectattr('ts', '>=', next_interval.strftime('%Y-%m-%d %H:%M')) | list %}
{% set houseload_raw = state_attr('sensor.p_load_dayahead_forecast', 'forecast_merged') or [] %}
{# Step 3: Slice exactly horizon number of points #}
{% set load_slice = load_raw[:horizon] %}
{% set prod_slice = prod_raw[:horizon] %}
{% set pv_slice = pv_raw[:horizon] %}
{% set houseload_slice = houseload_raw[:horizon] %}
{# Step 4: Convert slices to ISO 8601 dicts with hardcoded timezone +01:00 #}
{% set ns_load = namespace(iso_dict={}) %}
{% for item in load_slice %}
{% set iso_ts = item.ts.split(' - ')[0] | replace(' ', 'T') ~ ':00+01:00' %}
{% set ns_load.iso_dict = ns_load.iso_dict | combine({ iso_ts: item.price }) %}
{% endfor %}
{% set ns_prod = namespace(iso_dict={}) %}
{% for item in prod_slice %}
{% set iso_ts = item.ts.split(' - ')[0] | replace(' ', 'T') ~ ':00+01:00' %}
{% set ns_prod.iso_dict = ns_prod.iso_dict | combine({ iso_ts: item.price }) %}
{% endfor %}
{% set ns_pv = namespace(iso_dict={}) %}
{% for item in pv_slice %}
{% set iso_ts = item.ts.split(' - ')[0] | replace(' ', 'T') ~ ':00+01:00' %}
{% set ns_pv.iso_dict = ns_pv.iso_dict | combine({ iso_ts: (item.pv_estimate * 1000) | round(0, 'floor') }) %}
{% endfor %}
{% set ns_houseload = namespace(iso_dict={}) %}
{% for item in houseload_slice %}
{% set iso_ts = item.ts.split(' - ')[0] | replace(' ', 'T') ~ ':00+01:00' %}
{% set ns_houseload.iso_dict = ns_houseload.iso_dict | combine({ iso_ts: item.load }) %}
{% endfor %}
{# Step 5: Output full MPC payload #}
{
"prediction_horizon": {{ horizon }},
"pv_power_forecast": {{ ns_pv.iso_dict | tojson }},
"load_power_forecast": {{ ns_houseload.iso_dict | tojson }},
"load_cost_forecast": {{ ns_load.iso_dict | tojson }},
"prod_price_forecast": {{ ns_prod.iso_dict | tojson }},
"soc_init": {{ (states('sensor.solaredge_b1_state_of_energy') | float) / 100 }},
"soc_final": 0.3,
"number_of_deferrable_loads": 1,
"nominal_power_of_deferrable_loads": [5000],
"operating_hours_of_each_deferrable_load": [{{ (horizon / 4) | round(0) | int }}],
"start_timesteps_of_each_deferrable_load": [0],
"end_timesteps_of_each_deferrable_load": [0],
"battery_minimum_state_of_charge": 0.2,
"battery_maximum_state_of_charge": 0.9,
"publish_prefix": "mpc_"
}
I am running Emhass Add-on 0.14.1 and I am now trying to make use of the Influxdb within Emhass. Influxdb Add-on is running fine and is being filled with all hass sensor states.
All settings for Influxdb are set in Emhass configuration:
"influxdb_host": "localhost",
"influxdb_port": 8086,
"influxdb_username": "homeassistant",
"influxdb_password": "mypassword",
"influxdb_database": "homeassistant",
"influxdb_measurement": "W",
"influxdb_retention_policy": "autogen",
"influxdb_use_ssl": false,
"influxdb_verify_ssl": false,
When calling a mpc optimization, as usual, I see in the logs that the connection to Influxdb is failing:
[ERROR] Failed to connect to InfluxDB: HTTPConnectionPool(host='localhost', port=8086): Max retries exceeded with url: /ping (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0xffff7c929490>: Failed to establish a new connection: [Errno 111] Connection refused'))
Maybe this has got something to do with influxdb_username/_password?
I read that these settings are considered to be secrets, but I don’t see any possibility to set that settings on the Configuration tabblad of the Emhass Add-on.
When setting them in the Web UI Configuration, I do not find them back in the config.json file.
When filled in directly in the config.json or leave them empty, none of this makes any difference. Connection to Influxdb is failing.
Also I tried setting SSL to true or false, in Emhass and Influxdb, but still the same.
Any good advice is welcome !
I struggled a bit before I got the connection to influxdb to work.
Have you tested to connect to the database from cli? I had influx installed on another linux computer and tested. Chatgtp gave me this proposal with curl:
curl -G http://localhost:8086/query \
-u username:password \
--data-urlencode "q=SHOW DATABASES"
You did not show this in your post:
"use_influxdb": true,
Thanks for your effort!
When I continue the conversion with chatgtp and also read more I realize that it was a bad choice to buy Growatt battery system. Growatt lack support or at least does not show documtntationfor how to easily change work mode using non EEPROM writes as described in the chatgtp proposal using SolaX Modbus integration.
I know I can set work mode using TOU (define a time span for a work mode) but involves writing to EEPROM which I wanted to skip).
For now I will se how I can translate the output from EMHASS to TOU definitions.
When I look at the published sensor p_bat the attributes looks like this:
device_class: power
unit_of_measurement: W
friendly_name: Battery Power Forecast
battery_scheduled_power:
- date: "2025-12-08T19:15:00+01:00"
p_batt_forecast: "4346.0"
- date: "2025-12-08T19:30:00+01:00"
p_batt_forecast: "4461.95"
- date: "2025-12-08T19:45:00+01:00"
p_batt_forecast: "5700.0"
- date: "2025-12-08T20:00:00+01:00"
p_batt_forecast: "1223.82"
- date: "2025-12-08T20:15:00+01:00"
p_batt_forecast: "5700.0"
- date: "2025-12-08T20:30:00+01:00"
p_batt_forecast: "-4749.42"
- date: "2025-12-08T20:45:00+01:00"
p_batt_forecast: "-4749.42"
- date: "2025-12-08T21:00:00+01:00"
p_batt_forecast: "-4749.42"
- date: "2025-12-08T21:15:00+01:00"
p_batt_forecast: "-4749.42"
- date: "2025-12-08T21:30:00+01:00"
p_batt_forecast: "-4749.42"
Is it possible get get a longer forecast? This is only a 2 hour forecast.
If I look at the table at emhassserver:5000 I get values that are longer after e.g. dayahead-optim. Can I have this entire forecast in a sensor published to HA?
When you use the mpc call you can specify a prediction horizon, which then gets published to Home Assistant. When you have enough forecasted load cost prices you can have a horizon of several days.
I run the dayahead-optim again and I got a 24 hour forecast in the p_batt_forecast sensor. I must have made a mistake last time.
When using dayahead your horizon shrinks until you run it again, so when you run it daily at 6am and publish every hour then your horizon ends on 6am the next day and therefor the attributes are not always 24 items long (or wahtever your optimization timestep is).
Many thanks!
The curl command showed me the way. Trying to connect to “localhost” gave me a “failed to connect”. Changing “localhost” to ‘internal IP adres’ did the trick and gives me a working connection from Emhass to Influxdb.
store all the variables in templates.yaml as sensors and then collate in a rest command. trigger rest command in automation that updates when the amber2mqtt 5 minute price updates.
storing in templates.yaml means the config for each variable is accessible in file editor / vscode which i find easier to read than the templating box in an automation
For the updated thermal model: which settings are obligatory now? I have this and runs into ‘not solved’ after a while:
def_load_config: >-
{% set horizon = 288 %} {% set mintemps = [40] * horizon %} {% set
maxtemps = [60] * horizon %} {% set start_temp =
states('sensor.boiler_dhw_current_extern_temperature') | float(0) %}
{%- set set = states('sensor.emhass_sun_next_setting_timesteps15min')|int -%}
{%- set total_blocks = horizon -%}
{%- set max_heating_blocks = 18 -%}
{# Calculate if heating is possible today #}
{%- set heating_today = max(0, min(max_heating_blocks, set - 18)) -%}
{%- set first_start = set - 24 -%}
{# For second day, always schedule 6 blocks #}
{%- set heating_tomorrow = max_heating_blocks -%}
{%- set second_start = first_start + 96 -%}
{# Calculate fill blocks #}
{%- set before_first = max(0, first_start) -%}
{%- set between = max(0, second_start - (first_start + heating_today)) -%}
{%- set remaining = max(0, total_blocks - (second_start + heating_tomorrow)) -%}
{%- set temps =
[45]*before_first +
[60]*heating_today +
[45]*between +
[60]*heating_tomorrow +
[45]*remaining +
[60]*heating_tomorrow +
[45]*remaining
-%}
{{ [
{},
{},
{},
{
"thermal_config": {
"heating_rate": 4.0,
"sense": "heat",
"cooling_constant": 0.007,
"overshoot_temperature": 65,
"start_temperature": start_temp,
"desired_temperatures": temps[:horizon],
"min_temperatures": mintemps,
"max_temperatures": maxtemps
}
}
] }}
No overshoot_temperature necessary anymore? It says ‘Legacy’ in the documentation. But desired_temperatures you would stil need?
I think the intention is to replace overshoot and desired with min and max.
However, I had the (cooling) case today when the starting temp (28 deg) was outside min/max (25 deg), which immediately resulted in Infeasible.
If I increase max to starting temp (28 deg), then my cooling doesn’t schedule for the rest of the day and is outside my desired comfort range.
Maybe I set min = 20, max = 28 (but still above start) and desired = 25?
'thermal_config': {'heating_rate': -1.0,
'sense': 'cool', 'cooling_constant': 0.2,
'start_temperature': 28.2,
'min_temperatures': [20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0],
'max_temperatures': [25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25]}
'outdoor_temperature_forecast': [27.5, 28.0, 28.0, 28.0, 28.0, 28.0, 27.5, 27.0, 27.0, 27.0, 26.5, 26.0, 25.5, 25.0, 24.5, 24.0, 23.5, 23.0, 23.0, 23.0, 23.0, 23.0, 22.5, 22.0, 22.0, 22.0, 22.0, 22.0, 22.0, 22.0, 21.5, 21.0, 21.0, 21.0, 21.0, 21.0, 22.0, 23.0, 24.0, 25.0, 25.5, 26.0, 26.0, 26.0, 26.5, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, 26.5, 26.0, 26.0, 26.0, 25.5, 25.0, 24.5, 24.0, 23.5, 23.0, 23.0, 23.0, 23.0, 23.0, 22.5, 22.0, 22.0, 22.0, 22.0, 22.0, 21.5, 21.0, 21.0, 21.0, 21.0, 21.0, 20.5, 20.0, 21.0, 22.0, 23.5, 25.0, 25.5, 26.0, 26.5, 27.0, 27.0, 27.0, 27.5, 28.0, 28.0, 28.0, 28.5, 29.0, 28.5, 28.0, 28.0, 28.0, 27.5, 27.0, 27.0, 27.0, 26.5, 26.0, 25.5, 25.0, 24.5, 24.0, 23.5, 23.0, 23.0, 23.0, 22.5, 22.0, 22.0, 22.0, 21.5, 21.0, 21.0, 21.0, 20.5, 20.0, 20.0, 20.0, 20.0, 20.0, 21.0, 22.0, 23.5, 25.0, 26.0, 27.0, 28.0, 29.0, 29.0, 29.0, 29.5, 30.0]
This gave me ‘optimal’
def_load_config: >-
{% set horizon = 288 %} {% set mintemps = [40] * horizon %} {% set
maxtemps = [65] * horizon %} {% set start_temp =
states('sensor.boiler_dhw_current_extern_temperature') | float(0) %}
{%- set set = states('sensor.emhass_sun_next_setting_timesteps15min')|int -%}
{%- set total_blocks = horizon -%}
{%- set max_heating_blocks = 18 -%}
{# Calculate if heating is possible today #}
{%- set heating_today = max(0, min(max_heating_blocks, set - 18)) -%}
{%- set first_start = set - 24 -%}
{# For second day, always schedule 6 blocks #}
{%- set heating_tomorrow = max_heating_blocks -%}
{%- set second_start = first_start + 96 -%}
{# Calculate fill blocks #}
{%- set before_first = max(0, first_start) -%}
{%- set between = max(0, second_start - (first_start + heating_today)) -%}
{%- set remaining = max(0, total_blocks - (second_start + heating_tomorrow)) -%}
{%- set temps =
[45]*before_first +
[60]*heating_today +
[45]*between +
[60]*heating_tomorrow +
[45]*remaining +
[60]*heating_tomorrow +
[45]*remaining
-%}
{{ [
{},
{},
{},
{
"thermal_config": {
"heating_rate": 4.0,
"sense": "heat",
"cooling_constant": 0.007,
"start_temperature": start_temp,
"min_temperatures": temps[:horizon],
"max_temperatures": maxtemps
}
}
] }}
The new code is an improvement of the previous model.
This might need some explanation and I will probably update the confusing “Legacy” term in the docs.
The new code adds min and max constraints but does not remove the old desired logic. They work differently:
min/max: These are new hard constraints. The solver must stay within these bounds. If the starting temperature (e.g., 28°C) is outside these bounds (e.g., Max 25°C), the solver correctly declares the problem “Infeasible” because it is mathematically impossible to satisfy the constraint at Time=0.desired: This acts as a soft target. It adds a “penalty” cost to the objective function
So desired_temperatures are definitely still needed.
Also you must also keep the same overshoot_temperature as in your previous config.
In the code the logic that applies the desired temperature penalty is currently wrapped inside a check for overshoot_temperature:
# In optimization.py
if desired_temperatures and overshoot_temperature is not None:
# ... logic that calculates penalty cost based on difference from desired temperature ...
But your point is right @markpurcell with your max values.
To handle cases where your home starts hotter than your target, set your max_temperature to a safety limit (e.g., 30°C) to avoid “Infeasible” errors. Then, keep desired_temperatures at your preferred setpoint (25°C) AND be sure that overshoot_temperature is defined in your config. This activates the penalty logic, forcing the solver to cool the house down from 30°C to 25°C as quickly as economically possible
Changed the config to:
def_load_config: >-
{% set horizon = 288 %} {% set mintemps = [40] * horizon %} {% set
maxtemps = [65] * horizon %} {% set start_temp =
states('sensor.boiler_dhw_current_extern_temperature') | float(0) %}
{%- set set = states('sensor.emhass_sun_next_setting_timesteps15min')|int -%}
{%- set total_blocks = horizon -%}
{%- set max_heating_blocks = 18 -%}
{# Calculate if heating is possible today #}
{%- set heating_today = max(0, min(max_heating_blocks, set - 18)) -%}
{%- set first_start = set - 24 -%}
{# For second day, always schedule 6 blocks #}
{%- set heating_tomorrow = max_heating_blocks -%}
{%- set second_start = first_start + 96 -%}
{# Calculate fill blocks #}
{%- set before_first = max(0, first_start) -%}
{%- set between = max(0, second_start - (first_start + heating_today)) -%}
{%- set remaining = max(0, total_blocks - (second_start + heating_tomorrow)) -%}
{%- set temps =
[45]*before_first +
[60]*heating_today +
[45]*between +
[60]*heating_tomorrow +
[45]*remaining +
[60]*heating_tomorrow +
[45]*remaining
-%}
{{ [
{},
{},
{},
{
"thermal_config": {
"heating_rate": 4.0,
"sense": "heat",
"cooling_constant": 0.007,
"overshoot_temperature": 65,
"desired_temperatures": temps[:horizon],
"start_temperature": start_temp,
"min_temperatures": mintemps,
"max_temperatures": maxtemps
}
}
] }}
Result: ‘Optimal’
Since I updated emhass yesterday I see this warning in the logs which wasn’t there before.
I waited a day to check but it’s still there.
[2025-12-11 14:50:00,241] WARNING in web_server: Unable to parse runtime parameters
[2025-12-11 14:50:00,241] INFO in web_server: >> Setting input data dict
[2025-12-11 14:50:00,242] INFO in command_line: Setting up needed data
[2025-12-11 14:50:00,256] INFO in retrieve_hass: get HA config from rest api.
[2025-12-11 14:50:00,283] INFO in web_server: >> Publishing data...
[2025-12-11 14:50:00,283] INFO in command_line: Publishing data to HASS instance
Does anyone else see this? Or is there something with my configuration that now gives a problem with this update?


