Hereās the YAML. Hopefully itās readable to somebody else. Feel free to ask questions, Iām sure there are things I havenāt explained well ā but everything has a purpose and a reason
Iām using the āsimple thermostat cardā.
First the actual climate entity and the backing input entities:
climate:
- platform: mqtt
name: recroom
current_temperature_topic: stat/NodeMCU04/STATUS8
current_temperature_template: '{{ states.sensor.recroom_data.attributes.StatusSNS.DHT11.Temperature | float }}'
action_topic: climate/recroom/stat
action_template: '{{ value_json.action }}'
mode_command_topic: climate/recroom/MODE
mode_state_topic: climate/recroom/stat
mode_state_template: '{{ value_json.mode }}'
modes: ['off', 'cool', 'dry', 'heat']
temperature_command_topic: climate/recroom/TEMP
temperature_state_topic: climate/recroom/stat
temperature_state_template: '{{ value_json.temp }}'
fan_mode_command_topic: climate/recroom/FAN
fan_mode_state_topic: climate/recroom/stat
fan_mode_state_template: '{{ value_json.fan }}'
fan_modes: ['auto', 'low', 'medium', 'high']
min_temp: 17
max_temp: 30
temp_step: 0.1
input_text:
# this stores the parameter for the command line that I use to send the IR command to the heat pump
livingroom_heatpump_command:
name: livingroom_heatpump_command
input_number:
# How far the temp can dip below the set point before the heating/cooling turns on.
livingroom_temperature_range_low:
min: 0
max: 5
mode: box
unit_of_measurement: 'Ā°C'
# How far the temp can rise above the set point before the heating/cooling turns off
livingroom_temperature_range_high:
min: 0
max: 5
mode: box
unit_of_measurement: 'Ā°C'
# The temperature set point for the thermostat
livingroom_heatpump_temp:
min: 17
max: 30
step: 0.1
mode: slider
icon: mdi:thermometer
unit_of_measurement: 'Ā°C'
input_select:
# The 'mode' of the thermostat
livingroom_heatpump_mode:
icon: mdi:air-conditioner
options:
- 'off'
- 'heat'
- 'cool'
- 'dry'
- 'fan'
# The fan speed for the heat pump
livingroom_heatpump_fanspeed:
icon: mdi:fan
options:
- 'auto'
- 'low'
- 'medium'
- 'high'
Next, I have a few template sensors to detect how far off the temperature is from the set point: The first one is the difference. Normally, if the temp is hotter than the set point, itās positive, but I have a multiplier in there so that if weāre cooling the āhighā value (which will actually be negative) comes out to be positive as well. If the mode is set to āoffā, the multiplier is set to 0, so the āhighā and ālowā sensors will always be 0, and wonāt try to activate the heating/cooling.
sensor:
- platform: template
sensors:
recroom_temperature_diff:
entity_id:
- input_select.recroom_heatpump_mode
- input_number.recroom_heatpump_temp
- sensor.recroom_temperature_statistics
unit_of_measurement: 'Ā°C'
value_template: >
{%- set area = 'recroom' -%}
{%- set diff = states('sensor.' + area + '_temperature_statistics')|float - states('input_number.' + area + '_heatpump_temp')|float -%}
{%- set multiplier = 1 -%}
{%- if is_state('input_select.' + area + '_heatpump_mode', 'cool' ) -%}
{%- set multiplier = -1 -%}
{%- elif is_state('input_select.' + area + '_heatpump_mode', 'off') -%}
{%- set multiplier = 0 -%}
{%- endif -%}
{{ (diff * multiplier) | round(1) }}
binary_sensor:
- platform: template
sensors:
livingroom_temperature_diff_high:
entity_id:
- sensor.livingroom_temperature_diff
- input_number.livingroom_temperature_range_high
value_template: >
{{ (states('sensor.livingroom_temperature_diff') | float)
> (states('input_number.livingroom_temperature_range_high') | float) }}
livingroom_temperature_diff_low:
entity_id:
- sensor.livingroom_temperature_diff
- input_number.livingroom_temperature_range_low
value_template: >
{{ (states('sensor.livingroom_temperature_diff') | float)
< (states('input_number.livingroom_temperature_range_low') | float * -1) }}
Now we have the MQTT automations. The climate entity will send MQTT messages whenever you call one of the climate services (or you change one of the settings on the lovelace card). These automations, just listen for those messages, and store the payload in the input entities we defined earlier.
automation:
- alias: livingroom_heatpump_mode
trigger:
- platform: mqtt
topic: 'climate/livingroom/MODE'
action:
- service: input_select.select_option
data_template:
entity_id: input_select.livingroom_heatpump_mode
option: >
{{ trigger.payload }}
- alias: livingroom_heatpump_fanspeed
trigger:
- platform: mqtt
topic: 'climate/livingroom/FAN'
action:
- service: input_select.select_option
data_template:
entity_id: input_select.livingroom_heatpump_fanspeed
option: >
{{ trigger.payload }}
- alias: livingroom_heatpump_temp
trigger:
- platform: mqtt
topic: 'climate/livingroom/TEMP'
action:
- service: input_number.set_value
data_template:
entity_id: input_number.livingroom_heatpump_temp
value: >
{{ trigger.payload }}
Hereās where it gets a little crazy. I use the sensors above to to maintain an input_text of the command line parameter I need to use to control the actual heat pump, but because everything affects everything else, I found that I ran into a race condition, where if I called a service to set both the temperature and the mode together, the climate component would sent two separate MQTT messages. Then because various sensors and automations are triggered by both the mode and temp changes, I wasnāt getting the correct status in the end, because some automations were triggering before all the changes had taken effect.
I introduced a 1 second timer so I could effectively wait for the value of input_text.livingroom_heatpump_command
to āstabilizeā, i.e., remain unchanged for at least 1 second. Unfortunately, you canāt use the for
attribute of the state trigger unless you also specify the from
or to
attribute, neither of which I would know.
timer:
livingroom_heatpump_command:
duration: '00:00:01'
automation:
- alias: livingroom_heatpump_status_changed
trigger:
- platform: state
entity_id: input_text.livingroom_heatpump_command
condition:
- condition: template
value_template: >
{{ trigger.from_state.state
and (trigger.from_state.state not in ['', 'unknown', 'undefined'])
and (trigger.from_state.state != trigger.to_state.state) }}
action:
- service: script.livingroom_climate_stat
data_template:
area: 'livingroom'
send_command: false
- service: timer.start
data:
entity_id: timer.livingroom_heatpump_command
- alias: livingroom_heatpump_status_stable
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.livingroom_heatpump_command
condition:
- condition: template
value_template: >
{{ not is_state('input_text.livingroom_heatpump_command', '') }}
action:
- service: script.livingroom_climate_stat
data_template:
area: 'livingroom'
- service: mqtt.publish
data_template:
topic: climate/livingroom/COMMAND
payload: "/home/hass/config/ircommand.sh --command LivingroomHeatPump,{{ states('input_text.livingroom_heatpump_command').split('_')[0] }}"
- service: shell_command.livingroom_heatpump_command
Hereās the script that publishes the MQTT status message back to the climate entity, simulating the device talking back to HA:
script:
livingroom_climate_stat:
sequence:
- service: mqtt.publish
data_template:
topic: >
climate/{{ area | lower }}/stat
retain: true
payload: >
{%- set area = area | lower -%}
{%- set mode = states('input_select.' + area + '_heatpump_mode') -%}
{%- set action = 'idle' -%}
{%- if (mode == 'off') -%}
{%- set action = 'off' -%}
{%- elif (states('input_text.' + area + '_heatpump_command') | lower).startswith(mode) -%}
{%- set action = mode + 'ing' -%}
{%- endif -%}
{{
{
'mode': mode,
'action': action,
'temp': states('input_number.' + area + '_heatpump_temp'),
'fan': states('input_select.' + area + '_heatpump_fanspeed'),
} | to_json
}}
Lastly, there is the automation used to fill out the command input_text
, and the shell_command
itself. This is where your implementation will almost certain differ from mine:
automation:
- alias: livingroom_heatpump_command
trigger:
- platform: state
entity_id:
- input_number.livingroom_heatpump_temp
- input_select.livingroom_heatpump_mode
- input_select.livingroom_heatpump_fanspeed
- binary_sensor.livingroom_temperature_diff_high
- binary_sensor.livingroom_temperature_diff_low
action:
- service: input_text.set_value
data_template:
entity_id: input_text.livingroom_heatpump_command
value: >
{%- set area = 'livingroom' -%}
{%- set entity = 'climate.' + (area | lower) -%}
{%- set mode = states('input_select.' + area + '_heatpump_mode') | lower -%}
{%- set temp = states('input_number.' + area + '_heatpump_temp') | float | round(1) -%}
{%- set fanspeed = states('input_select.' + area + '_heatpump_fanspeed') | lower -%}
{%- set sensorName = 'binary_sensor.' + (area | lower) + '_temperature_diff_' -%}
{%- if (mode == 'off') or is_state(sensorName + 'high', 'on') -%}
PowerOff_{{ temp }}
{%- elif mode in ['heat', 'cool'] and is_state(sensorName + 'low', 'on') -%}
{%- set heatpumpTemp = temp -%}
{%- if is_state('input_select.' + area + '_heatpump_mode', 'heat') -%}
{%- set heatpumpTemp = temp + 2.5 -%}
{%- elif is_state('input_select.' + area + '_heatpump_mode', 'cool') -%}
{%- set heatpumpTemp = temp - 2.5 -%}
{%- endif -%}
{%- if heatpumpTemp > state_attr(entity, 'max_temp') | float -%}
{%- set heatpumpTemp = state_attr(entity, 'max_temp') | float -%}
{%- elif heatpumpTemp < state_attr(entity, 'min_temp') | float -%}
{%- set heatpumpTemp = state_attr(entity, 'min_temp') | float -%}
{%- endif -%}
{{ mode }}{{ heatpumpTemp | round(0) | int }}_{{ temp }}
{%- else -%}
{# If we don't have cause to change, then keep the sensor value the same as it was. #}
{{ states('input_text.' + area + '_heatpump_command') }}
{%- endif -%}
shell_command:
livingroom_heatpump_command: /home/hass/config/ircommand.sh --command "LivingroomHeatPump,{{ states('input_text.livingroom_heatpump_command').split('_')[0] }}"
Edit: fixed typos.