Try our Chrome extension
Easily add the current web-page from your browser directly into your changedetection.io tool, more great features coming soon!Changedetection.io needs your support!
You can help us by supporting changedetection.io on these platforms;
- Rate us at AlternativeTo.net
- Star us on GitHub
- Follow us at Twitter/X
- G2 Software reviews
- Check us out on LinkedIn
- And tell your friends and colleagues :)
The more popular changedetection.io is, the more time we can dedicate to adding amazing features!
Many thanks :)
changedetection.io team
Not yet seconds ago
False
2,732,432 seconds ago
1 month ago
| blueprint: name: π₯ Advanced Heating Control V5 author: panhans homeassistant: min_version: "2024.10.0" description: > π₯ room based heating / β based on > π₯ people presence ποΈ multiple schedulers πΆ presence sensor βοΈ proximity aka geo fencing π₯Ά frost protection π‘ adjustable aggressive mode π€οΈ activation based on weather, temperature or boolean entities ποΈ granular schedule adjustments πͺ multiple window open detection π party mode π€ guest mode βοΈ liming protection π dynamic valve positioning π§ thermostat calibration for the most common devices (Tado, Aqara, Popp / Danfoss / Hive, Tuya) βοΈ several tweaks for fixing your thermostat issues π¬ custom action π€« calm & πͺ reliable **Version**: 5.3.4 **Help & FAQ**: [Advanced Heating Control](https://community.home-assistant.io/t/advanced-heating-control/469873) **Documentation:** [panhans.github.io/HomeAssistant/](https://panhans.github.io/HomeAssistant/) [](https://ko-fi.com/Q5Q3QEH52) source_url: https://github.com/panhans/HomeAssistant/blob/main/blueprints/automation/panhans/advanced_heating_control.yaml domain: automation input: thermostat_section: name: Thermostats & Sensors icon: mdi:thermostat input: input_trvs: name: π₯ Thermostats / Climates description: > `thermostats` `climates` [Thermostats / Climates](https://www.home-assistant.io/integrations/climate/) to be controlled. selector: entity: filter: - domain: - climate multiple: true input_hvac_mode: name: ποΈ Operation / HVAC Mode description: > `hvac` Select the hvac mode for your [thermostats](https://www.home-assistant.io/integrations/climate/). Be sure your selected thermostats support the hvac mode you've chosen. AHC will log a warning if there is a miss match. For radiator [thermostats]((https://www.home-assistant.io/integrations/climate/)) the default is mostly *heat*. If you own an air conditioner it will support *auto* or *cool*, too. default: "heat" selector: select: options: - heat - cool - auto - heat_cool input_temperature_sensor: name: π‘οΈ Room Temperature Sensor description: > `calibration` `aggressive mode` `optional` For some features an external temperature sensor is reqired, e.g. calibration. Temperature calibration for your [thermostats](https://www.home-assistant.io/integrations/climate/). The following is supported: * Tado, Aqara, Popp, Danfoss, Hive, Tuya * generic calibration Note: This is an additional sensor inside your room usually next to your favourite spot. [Thermostats](https://www.home-assistant.io/integrations/climate/) or its integration (e.g. Z2M or ZHA) except Tado should provide a seperate calibration entity. default: [] selector: entity: filter: - domain: - sensor device_class: - temperature multiple: false temperature_section: name: Temperatures icon: mdi:thermometer collapsed: true input: input_temperature_comfort_static: name: ποΈ Static Comfort Temperature description: > `comfort temperature` You can set a static comfort temperature here. default: 22 selector: number: min: 12.0 max: 86.0 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_temperature_eco_static: name: π± Static Eco Temperature description: > `eco temperature` The temperature that is set when your heating schedule is not active. default: 19 selector: number: min: 4.0 max: 75.0 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_temperature_comfort: name: ποΈ Comfort Temperature description: > `comfort temperature` `optional` To control your comfort temperature via automations or the UI, you can specify an *[input_number](https://www.home-assistant.io/integrations/input_number/)* entity here. Create your helper [here](https://my.home-assistant.io/redirect/helpers/). default: [] selector: entity: filter: - domain: - input_number multiple: false input_temperature_eco: name: π± Eco Temperature description: > `eco temperature` `optional` To control your eco temperature via automations or the UI, you can specify an *[input_number](https://www.home-assistant.io/integrations/input_number/)* entity here. Create your helper [here](https://my.home-assistant.io/redirect/helpers/). default: [] selector: entity: filter: - domain: - input_number multiple: false adjustment_section: name: Adjustments / Heating Plan icon: mdi:sun-clock collapsed: true input: input_adjustments: name: ποΈ Heating Schedule Adjustments description: > `optional` Here you can setup some adjustments to your heating schedule.<br/><br/> *Note*: Here you can set values for eco or comfort temperature. The switch between those target temperatures is controled by schedules, presence sensors, proximity, ect. <br/> <details> <summary><code><strong>CLICK HERE:</strong> Modifiers</code></summary> <br/> > π **time** > Timestamp when the adjustment should kick in. (required) > π **days** > Select days where this setting shall be enabled. > ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] > ποΈ **scheduler** > Only enable this entry if this string is part of the name of your active scheduler. > ποΈ **comfort** > Adjust comfort temperature > π± **eco** > Adjust eco temperature > π§ **calibration** > Toggle calibration > on/off > π **mode** > Overwrite the operation mode > comfort/eco/off/auto (auto means no overwrite) </details> <br/> <details> <summary><code><strong>CLICK HERE:</strong> Example</code></summary> <br/> ```yaml - time: "08:00" comfort: "20" calibration: "off" - time: "16:00" eco: "19" calibration: "on" - time: "20:00" days: ['Sat','Sun'] scheduler: 'Holidays' comfort: "24" eco: "17" ``` </details> selector: object: default: "[]" # modes mode_section: name: Force Comfort/Eco Mode icon: mdi:fire collapsed: true input: input_mode_party: name: π Party mode description: > `optional` If on, all settings are ignored and heating takes place. You can define multiple [timers](https://www.home-assistant.io/integrations/timer/) or boolean entities. If you put a number at the end of the friendly name like *Party Timer 20* this number will be taken as the desired comfort temperature for this [timer](https://www.home-assistant.io/integrations/timer/). Create your timer [here](https://my.home-assistant.io/redirect/helpers/). default: [] selector: entity: filter: - domain: - input_boolean - binary_sensor - timer multiple: true input_force_max_temperature: name: π₯΅ Force Max Temperature description: > `optional` Set the maximum temperature of all [thermostats](https://www.home-assistant.io/integrations/climate/) regardless of any other settings. **HINT:** Implemented by developer for maintenance reasons. Create your helper [here](https://my.home-assistant.io/redirect/helpers/). default: [] selector: entity: filter: - domain: - input_boolean - binary_sensor multiple: false input_force_eco_temperature: name: π± Force Eco Temperature description: > `optional` If enabled *eco* temperature will be forced. default: [] selector: entity: filter: - domain: - input_boolean - binary_sensor multiple: false input_party_legacy_restore: name: π Legacy Restore description: > `party` Enable this if the temperatures after airing (closing windows) or party won't restore properly default: false selector: boolean: temperature_tweak_section: name: Temperature Tweaks icon: mdi:knob collapsed: true input: input_off_instead_of_eco: name: π Off Instead Of Eco description: > `optional` `temperature tweak` Turn off your [thermostats](https://www.home-assistant.io/integrations/climate/) instead of lower the target temperature to eco temperature. default: false selector: boolean: input_min_instead_of_off: name: β¬οΈ Min Instead Of Off description: > `optional` `temperature tweak` Lower the temperature instead of turning them *OFF*, e.g. during airing. default: false selector: boolean: input_fahrenheit: name: π« Fahrenheit description: > `optional` `temperature tweak` Enable this if your unit of measurement is Fahrenheit (untested). default: false selector: boolean: input_reset_temperature: name: β©οΈ Reset Temperature description: > `optional` `temperature tweak` Reset your temperature entities to the values of the static temperatures after [schedule](https://www.home-assistant.io/integrations/schedule/), [proximity](https://www.home-assistant.io/integrations/proximity/), [people](https://www.home-assistant.io/integrations/person/), presence ends. The comfort entity is reset when eco takes place and vice versa. default: false selector: boolean: input_off_if_above_room_temperature: name: βοΈ Off If Above/Below Room Temperature description: > Turns your [climate](https://www.home-assistant.io/integrations/climate/) entity *off* if the target temperature is below(cooling) / above(heating) the room temperature. default: false selector: boolean: input_off_if_nobody_home: name: π πΆββ‘οΈ Off If Nobody Home description: > Turns your [climate](https://www.home-assistant.io/integrations/climate/) entity *off* if persons are set and nobody is home. default: false selector: boolean: input_physical_change: name: π§ͺ Physical Temperature Change / Sync (experimental) description: > `optional` `temperature tweak` Enable this if your want to adjust the temperature using your thermostat or thermostat card. Make sure aggressive mode and generic calibration is disabled for this feature. (experimental). You also need to set entities for eco and comfort temperature for the moment. default: false selector: boolean: # persons person_section: name: Persons icon: mdi:account-multiple collapsed: true input: input_persons: name: π₯ Persons description: > `person` `optional` You can specify [persons](https://www.home-assistant.io/integrations/person/) to make your heating plan more dynamic. If you do not use [schedulers](https://www.home-assistant.io/integrations/schedule/) or presence sensors, heating is activated as soon as someone is at home.<br/> With [schedulers](https://www.home-assistant.io/integrations/schedule/) or presence sensors, these are only active when someone is at home. default: [] selector: entity: filter: - domain: - person multiple: true input_people_entering_home_duration: name: π Enter Home Duration description: > `person` Duration for which someone must be at home for heating to be activated. default: hours: 0 minutes: 0 seconds: 2 selector: duration: input_people_leaving_home_duration: name: π¨ Leaving Home Duration description: > `person` Duration for which someone must be out of the house for heating to be deactivated. default: hours: 0 minutes: 0 seconds: 2 selector: duration: input_mode_guest: name: π€ Guest Mode description: > `person` `optional` If an entity is specified here, it is treated like a [person](https://www.home-assistant.io/integrations/person/). It's usefull when you're leaving your guests alone in your home and you are not using presence detection. * entity defined -> [person](https://www.home-assistant.io/integrations/person/) defined * enitity is *on* -> simulates [person](https://www.home-assistant.io/integrations/person/) is home * enitity is *off* -> simulates [person](https://www.home-assistant.io/integrations/person/) is away default: selector: entity: filter: - domain: - input_boolean - binary_sensor - timer multiple: false # scheduler scheduling_section: name: Scheduling icon: mdi:clock-outline collapsed: true input: input_schedulers: name: β²οΈ Schedules description: > `schedules` `optional` A [schedule](https://www.home-assistant.io/integrations/schedule/) specifies when heating to comfort temperature should take place. You can create it in the helper section of Home Assistant.<br/> If you have also specified [people](https://www.home-assistant.io/integrations/person/), someone must also be at home for heating. This is the same behaviour with a [proximity](https://www.home-assistant.io/integrations/proximity/) entity.<br/> You can create as many [schedules](https://www.home-assistant.io/integrations/schedule/) as you like. Make sure the names are clear. default: [] selector: entity: filter: - domain: - schedule multiple: true input_scheduler_selector: name: βπ» Scheduler Selector description: > `schedule` `optional` Define an entity to choose from your schedules. If you use one schedule only you can ignore this. If you use more than one schedule you have multiple possibilities to setup your selection.<br/> <details> <summary><code><strong>CLICK HERE:</strong> More information</code></summary> <br/> * toggle [input_boolean](https://www.home-assistant.io/integrations/input_boolean/) or [binary_sensor](https://www.home-assistant.io/integrations/binary_sensor/): If *off* the first defined [schedule](https://www.home-assistant.io/integrations/schedule/) is enabled. If *on* the second [schedule](https://www.home-assistant.io/integrations/schedule/) is active. More than two [schedules](https://www.home-assistant.io/integrations/schedule/) cannot be selected with binary inputs. * text [input text](https://www.home-assistant.io/integrations/input_text/), drop down [input text](https://www.home-assistant.io/integrations/input_select/) or [sensor](https://www.home-assistant.io/integrations/sensor/): * The value has to match the friendly name of the selected [schedule](https://www.home-assistant.io/integrations/schedule/) at least partially. Example: If you provide three [schedules](https://www.home-assistant.io/integrations/schedule/) called *work*, *holiday/sick*, *guest* you can select the holiday [schedule](https://www.home-assistant.io/integrations/schedule/) while setting the selection entity to *sick*, *holiday* or *holiday/sick*. This option is case insensitive. * You also can go with numbers: if you want to choose the first [schedule](https://www.home-assistant.io/integrations/schedule/) the selector entity must return the number *1*. For the 2nd number *2* and so on. </details> default: selector: entity: filter: - domain: - input_boolean - binary_sensor - input_text - input_number - input_select multiple: false # presence presence_section: name: Presence Detection icon: mdi:location-enter collapsed: true input: input_presence_sensor: name: πΆ Presence Sensor / On/Off-Entity description: > `presence detection` `optional` If you specify a presence sensor, heating will take place if it detects presence.<br/> If you have specified [persons](https://www.home-assistant.io/integrations/person/), at least one must also be at home. You also can select an [input boolean](https://www.home-assistant.io/integrations/input_boolean/) entity to realise a simple On/Off-Logic. default: selector: entity: filter: - domain: - binary_sensor - input_boolean multiple: false input_scheduler_presence: name: β²οΈ Presence Sensor Scheduler description: > `presence detection` `optional` The presence [schedule](https://www.home-assistant.io/integrations/schedule/) specifies exactly when the presence sensor should be used during the day. default: selector: entity: filter: - domain: - schedule multiple: false input_presence_reaction_on_time: name: β³ Presence Reaction On Time description: > `presence detection` Specify the duration for which the presence sensor must detect any presence so that the comfort temperature is set. default: hours: 0 minutes: 5 seconds: 0 selector: duration: input_presence_reaction_off_time: name: β Presence Reaction Off Time description: > `presence detection` Specify the duration for which the presence sensor must not detect any presence so that the eco temperature is set. default: hours: 0 minutes: 5 seconds: 0 selector: duration: # proximity proximity_section: name: Proximity icon: mdi:leak collapsed: true input: input_proximity: name: βοΈ Proximity description: > `proximity` `optional` You can preheat your rooms with help of home assistant's [proximity integration](https://www.home-assistant.io/integrations/proximity/).<br/> Just select your proxmity zone and take your adjustments to distance and duration.<br/> If you're in range of your distance and towards to your home heating kicks in.<br/> **Note**: The proximity entity is handles like a person. Comfort heating takes place when coming or beeing home. Combinations with [schedules](https://www.home-assistant.io/integrations/schedule/) are also possible. default: selector: device: filter: integration: proximity multiple: false input_proximity_duration: name: β° Proximity Duration description: > `proximity` Duration for which someone must be on way home before heating occurs. default: hours: 0 minutes: 2 seconds: 0 selector: duration: input_proximity_distance: name: βοΈ Proximity Distance description: > `proximity` The distance when [proximity](https://www.home-assistant.io/integrations/proximity/) sensor gets impact for this automation. Hint: Unit depends on the setup of your integration. default: 500 selector: number: min: 0 max: 999999999 step: 1 mode: box # away mode away_section: name: Away Mode icon: mdi:walk collapsed: true input: # AWAY OFFSET input_away_offset: name: π Away Temperature Offset description: > `scheduler` `persons` `presence` `away mode` First: This feature only works for [schedule](https://www.home-assistant.io/integrations/schedule/) and/or presence based heating combined with [persons](https://www.home-assistant.io/integrations/person/). You can define an offset for your comfort temperature that will be subtracted (heating) from or added (cooling) to your comfort temperature. If you enable this option for [schedules](https://www.home-assistant.io/integrations/schedule/) the away offset will be substracted from the comfort temperature if your schedule is *on* but nobody is at home. For presence detection this is the case if you are at home but no presence is detected. For presence detection you can also ignoring [persons](https://www.home-assistant.io/integrations/person/). So the away temperature is set when no presence is detected but the presence [schedule](https://www.home-assistant.io/integrations/schedule/) is *on*. default: 0 selector: number: min: 0 max: 10 step: 0.5 mode: slider unit_of_measurement: Β°C / Β°F input_away_scheduler_mode: name: β²οΈ Scheduler Away Mode description: > `scheduler` `away mode` Enable/Disable the Away Offset for [schedules](https://www.home-assistant.io/integrations/schedule/) based heating/cooling. default: false selector: boolean: input_away_presence_mode: name: πΆ Presence Away Mode description: > `presence` `away mode` Enable/Disable the Away Offset for presence based heating/cooling. default: false selector: boolean: input_away_presence_ignor_people: name: πΆ Ignore People For Presence Away Mode description: > `presence` `away mode` If you want to make away happen if your presence [schedule](https://www.home-assistant.io/integrations/schedule/) is on but no motion is detected regardless if somebody is at home enable this option. default: false selector: boolean: # windows window_section: name: Window & Door Detection icon: mdi:door collapsed: true input: input_windows: name: πͺ Windows & Doors description: > `airing` `optional` If open during airing your [thermostats](https://www.home-assistant.io/integrations/climate/) will be set to *off* at least to their minimum temperature if they don't support hvac mode *OFF* except you set a custom window open temperature. default: [] selector: entity: filter: - domain: - binary_sensor - sensor multiple: true input_windows_reaction_time_open: name: β³ Window & Door Reaction Time Open description: > `airing` Duration for which a window or door must be open for the [thermostats](https://www.home-assistant.io/integrations/climate/) to close. default: hours: 0 minutes: 0 seconds: 30 selector: duration: input_windows_reaction_time_close: name: β Window & Door Reaction Time Close description: > `airing` Duration for which a window or door must be closed for the [thermostats](https://www.home-assistant.io/integrations/climate/) to open. default: hours: 0 minutes: 0 seconds: 30 selector: duration: input_window_open_temperature: name: Window Open Temperature description: > `airing` If 0Β° your thermostat turns *off* or if not supported it turns to the minimum temperature of your thermostat. default: 0 selector: number: min: 0 max: 15 step: 1 mode: slider unit_of_measurement: Β°C / Β°F input_window_legacy_restore: name: ποΈ Legacy Restore description: > `airing` Enable this if the temperatures after airing (closing windows) won't restore properly. default: false selector: boolean: # calibration calibration_section: name: Calibration icon: mdi:compass description: "" collapsed: true input: input_calibration_timeout: name: β³ Calibration Timeout description: > `calibration` Define a timeout if you want to decrease the amount of calibration calls if temperature changes too much. At least the temperature of the external sensor or [thermostat](https://www.home-assistant.io/integrations/climate/) must stay for that duration before calibration gets triggered. **HINT:** A minimum timeout of 2s is recommended. default: hours: 0 minutes: 1 seconds: 0 selector: duration: input_calibration_delta: name: βοΈ Calibration Delta description: > `calibration` If the difference between the [thermostat](https://www.home-assistant.io/integrations/climate/) temperature and the external sensor temperature is greater or less than the calibration delta the [thermostat](https://www.home-assistant.io/integrations/climate/) calibration will be triggered.<br/> The lower the delta the often calibration gets triggered. default: 0.5 selector: number: min: 0 max: 5 step: 0.1 mode: slider unit_of_measurement: Β°C / Β°F input_calibration_key_word: name: ποΈ Calibration Entity Key Word description: > `calibration` Keyword for finding the calibration entity. This word must be part of the entity id. As a rule, the entities with the word *offset*, *calibration* or *external* are marked by the integrations. Just have a look into your device overview, select your thermostat and check the naming of the *entity_ids* for the calibration. default: "calibration" selector: text: input_calibration_step_size: name: π¦Ά Step Size description: > `calibration` Usually the step size is determined automatically. You can overwrite the step size by selecting another option if you know your thermostat handles the calibration not like the entities are exposed. default: auto selector: select: mode: dropdown options: - label: Auto value: auto - label: "0.1" value: "0.1" - label: "0.5" value: "0.5" - label: "Full Values" value: "full" input_calibration_generic: name: π§ Generic Calibration description: > `generic` `calibration` Adds the difference between room and [thermostat](https://www.home-assistant.io/integrations/climate/) temperature to the target temperature. This is useful if your thermostat integration doesn't provide a special entity for calibration. Keep in mind the set temperatures for your thermostats will differ to the target temperature. default: false selector: boolean: input_generic_calibration_offset: name: βοΈ Generic Calibration Offset description: > `generic` `calibration` If the temperature difference between the thermostat and the temperature sensor is very high, the offset, i.e. the correction temperature, can be limited to this value. <details> <summary><code><strong>CLICK HERE:</strong> Example</code></summary> Generic Calibration Offset = 5Β°</br> Thermostat Temperature = 28Β°</br> Room Temperature = 18Β°</br> </br> Difference = Thermostat Temperature - Room Temperature = 10Β°</br> Difference > Generic Calibration Offset -> Corrected Difference = 5Β°</br> New Target Temperature = Thermostat Temperature + Corrected Difference = 33Β° </details> default: 5 selector: number: min: 0 max: 20 step: 1 mode: slider unit_of_measurement: Β°C / Β°F # aggressive mode aggressive_mode_section: name: Aggressive Mode icon: mdi:emoticon-angry collapsed: true input: input_aggressive_mode_range: name: π‘ Aggressive Range description: > `aggressive mode` `tweak` Activate this option if your [thermostats](https://www.home-assistant.io/integrations/climate/) react slowly or only start to react at a large temperature difference between actual and set temperature. Define a range when your real target temperature shall be set. <details> <summary><code><strong>CLICK HERE:</strong> More information</code></summary> <br/> E.g. you target temperature is 20Β°C and your room temperature is 19.5Β°C. If your range is set to 0.5Β°C the real target temperature (20Β°C) will be set when room temperature is between 19.5Β°C and 20.5Β°C. If the room temperature is above or lower than range, it gets some offset in order to force your [thermostat](https://www.home-assistant.io/integrations/climate/) to react. (see Aggressive Mode - Offset) </details> default: 0 selector: number: min: 0 max: 5 step: 0.1 mode: slider unit_of_measurement: Β°C / Β°F input_aggressive_mode_offset: name: β Aggressive Offset description: > `aggressive mode` `tweak` Here you can define the offset that will be added to your target temperature if the room temperature is not in range of your target temperature. If your room temperature is not in the defined range, e.g. 19.5Β°C - 20.5Β°C this offset will be added to your target temperature. If range is 0, then offset is always added. default: 0 selector: number: min: 0 max: 5 step: 0.5 mode: slider unit_of_measurement: Β°C / Β°F input_aggressive_mode_calibration: name: π‘οΈ Aggressive Calibration description: > `aggressive mode` `tweak` `experimental` If you'd setup an temperature sensor and your thermostats allow calibration, you can enable this feature. If enabled the aggressive offset will be add to the calibration value and not the target temperature. *Note*: This feature is marked as experimental since not every calibration method could be tested. If you notice any problems simple open an issue or post a message in the [AHC-Thread](https://community.home-assistant.io/t/advanced-heating-control/469873). Enable this only if native calibration does NOT work when using generic calibration. default: false selector: boolean: # frost protection frostprotection_section: name: Frost Protection icon: mdi:snowflake collapsed: true input: input_frost_protection_temp: name: βοΈ Frost Protection Temperature description: > `frost protection` You can set the frost protection temperature here. default: 5 selector: number: min: 5.0 max: 62.0 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_frost_protection_duration: name: βοΈ Frost Protection Fallback Duration description: > `frost protection` If the defined [persons](https://www.home-assistant.io/integrations/person/) are not at home for a longer period of time or the presence sensor has no longer detected any presence, the frost protection temperature can be lowered after a this duration. Note: If set to zero frost protection temperature never will be set. default: days: 0 hours: 0 minutes: 0 seconds: 0 selector: duration: enable_day: true # liming protection liming_protection_section: name: Liming Protection icon: mdi:pipe-valve collapsed: true input: input_liming_protection: name: ποΈ Liming Protection description: > `liming protection` Most smart thermostats come with that feature out of the box. If your thermostat doesn't support this or you're using the generic thermostat integration this feature is maybe handy for you in order to prevent your valve against limescale. The automation will set the thermostat to its max and open the valve for one minute. default: off selector: boolean: input_liming_protection_day: name: ποΈ Day description: > `liming protection` Select the day of the week for the execution. default: "Mon" selector: select: options: - label: Monday value: Mon - label: Tuesday value: Tue - label: Wednesday value: Wed - label: Thursday value: Thu - label: Friday value: Fri - label: Saturday value: Sat - label: Sunday value: Sun input_liming_protection_time: name: π Time description: > `liming protection` Select the time for the execution. default: "12:00:00" selector: time: input_liming_protection_duration: name: π Liming Protection Duration description: > `liming protection` Duration of liming protection before the thermostat is reset to its initial state. default: 1 selector: number: min: 1 max: 30 step: 1 mode: slider unit_of_measurement: min input_liming_in_winter: name: π¨οΈ Liming In Winter / Liming If Automation is Disabled description: > `liming protection` Enable this if you want liming protection even if the automation is active. default: false selector: boolean: # winter mode toggle_section: name: "On/Off Automation Options" icon: mdi:light-switch collapsed: true input: input_mode_winter: name: β Winter Mode / Automation Toggle description: > `activation` `optional` If *on* the automation is active. If *off* your valves will set to *off* and the automation is going to sleep. You can set this up with: * [input boolean](https://www.home-assistant.io/integrations/input_boolean/) * [binary sensor](https://www.home-assistant.io/integrations/binary_sensor/) Create your helper [here](https://my.home-assistant.io/redirect/helpers/). default: selector: entity: filter: - domain: - input_boolean - binary_sensor multiple: false input_invert_winter_mode_value: name: π Invert Winter Mode Value description: > `activation` If enabled the the value of the binary winter mode entity will be inverted: * off -> activates the automation * on -> disables the automation default: off selector: boolean: input_mode_outside_temperature: name: π€οΈ Outside Temperature Sensor description: > `activation` `optional` You can control the switching on and off of your thermostats via the outside temperature. To do this, select a temperature sensor or a weather entity and adjust the threshold below. * [weather entity](https://www.home-assistant.io/integrations/weather/) * [temperature sensor entity](https://www.home-assistant.io/integrations/sensor/) default: selector: entity: filter: - domain: - weather - domain: - sensor device_class: temperature multiple: false input_mode_outside_temperature_threshold: name: ποΈ Outside Temperature Threshold description: > `activation` If you'd select a temperature [sensor](https://www.home-assistant.io/integrations/sensor/) or a [weather entity](https://www.home-assistant.io/integrations/weather/) for controlling heating you can adjust the temperature threshold here. If the outside temperature falls below the threshold value, heating is activated. default: 15 selector: number: min: 5 max: 68 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_mode_room_temperature: name: π Enable Room Temperature Threshold description: > `activation` `optional` If you enable this option the value of the defined room temperature sensor and the value of the outside temperautre must be below / above its threshold. That makes sense if you go with an A/C and the room is still heated up but it has already cooled down outside. **Not recommendend for heating** default: false selector: boolean: input_mode_room_temperature_threshold: name: ποΈ Room Temperature Threshold description: > `activation` Threshold for your room temperature sensor. default: 18 selector: number: min: 5 max: 68 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F # valve positioning valve_positioning_section: name: "Dynamic Valve Positioning" icon: mdi:valve collapsed: true input: input_valve_positioning_mode: name: π¦Ά Valve Positioning Mode description: > `valve positioning` If your thermostat supports valve positioning you can enable this here. Everytime the autmation gets triggered the code checks if there is an adjustment needed. π **regular**: means linear. The valve will open close proportional to the difference of the target and room temperature. π **optimistic**: The valve opening is reduced earlier, as it is assumed that the radiator still has enough residual heat to heat the room. π **pessimistic**: The valve opening is initially left relatively open and only closes rapidly when the target temperature is almost reached. default: "off" selector: select: mode: dropdown options: - label: "off" value: "off" - label: "regular" value: "regular" - label: "optimistic" value: "optimistic" - label: "pessimistic" value: "pessimistic" input_fully_open_difference: name: βοΈ Positioning Temperature Difference description: > `valve positioning` The difference between target and set temperature when dynamic valve positioninig should happen. <details> <summary><code><strong>CLICK HERE:</strong> Example</code></summary> <br/> > Positioning Temperature Difference: 1Β° > Target Temperature: 21Β°<br/> > Positioning takes place in a range between 21Β° and 20Β° (21Β°-1Β°) > If the local temperature is 21.5Β° the valve positioning is calculated and set. > If the local temperture is below this range the valve is fully open. </details> default: 1 selector: number: min: 0.5 max: 20 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_valve_positioning_step_size: name: π¦Ά Valve Positioning Step Size description: > `valve positioning` The step size of for opening/closing the valve. default: "10" selector: select: mode: dropdown options: - label: "5%" value: "5" - label: "10%" value: "10" - label: "20%" value: "20" input_valve_positioning_max_opening: name: ποΈ Max Opening Valve Position description: > `valve positioning` The maximal opening of the valve. Some thermostats have a maximum valve position of 80-90%. You can adjust the value here. *Force Max Temperature* still sets the value to 100%. default: 100 selector: number: min: 1 max: 100 step: 1 mode: slider unit_of_measurement: "%" input_valve_positioning_timeout: name: β±οΈ Valve Positioning Timeout description: > `valve positioning` Timeout that must lie between two adjustments before the second is executed. default: hours: 0 minutes: 20 seconds: 0 selector: duration: input_valve_opening_keyword: name: ποΈ Positioning Entity Keyword description: > `valve positioning` The key word for selecting the opening entity of your thermostats. default: "valve_opening_degree" selector: text: # tweaks tweak_section: name: Custom Settings icon: mdi:cog-box collapsed: true input: input_action_call_delay: name: βοΈ Action Call Delay description: > `tweak` Some [thermostats](https://www.home-assistant.io/integrations/climate/) have problems with setting mode and temperature. You can try to increase the delay between the action calls. This could fix your problems. default: hours: 0 minutes: 0 seconds: 2 selector: duration: input_startup_delay: name: β² Startup Delay description: > `tweak` If your AHC automation is triggered directly after a Home Assistant restart, but the required integrations have not yet been loaded or certain sensors have not yet been initialized, you can set an automation delay here. *Note:* Make sure that you have set up the uptime integration for this purpose. default: hours: 0 minutes: 0 seconds: 0 selector: duration: # custom action input_custom_action: name: π¬ Custom Action description: > `optional` This custom action gets executed with every temperature / mode change except calibration. If you want to control other devices just check states before doing an action call. Use the variable *is_heating* in your conditions. *True* means heating is active. default: selector: action: input_custom_condition: name: βοΈ Temperature Change Custom Condition description: > `optional` Define a custom condition that prevents / allows temperature changes to your thermostats. This has no impact to the rest of logic like calibration. default: selector: condition: input_custom_condition_calibration: name: βοΈ Calibration Custom Condition description: > `optional` Define a custom condition that prevents / allows calibration. default: selector: condition: input_log_level: name: βοΈ Log Level description: "" default: debug selector: select: mode: dropdown options: - info - warning - error - debug trigger_variables: # thermostats / sensors input_trvs: !input input_trvs input_temperature_sensor: !input input_temperature_sensor is_temperature_sensor_defined: "{{ input_temperature_sensor != [] }}" # people input_persons: !input input_persons input_mode_guest: !input input_mode_guest input_people_entering_home_duration: !input input_people_entering_home_duration input_people_leaving_home_duration: !input input_people_leaving_home_duration input_person_count: "{{ input_persons | count }}" is_person_defined: "{{ input_person_count > 0 }}" is_guest_mode_defined: "{{ input_mode_guest != none }}" # scheduler input_schedulers: !input input_schedulers input_scheduler_selector: !input input_scheduler_selector input_scheduler_presence: !input input_scheduler_presence is_scheduler_presence_defined: "{{ input_scheduler_presence != none }}" # temperatures input_temperature_comfort: !input input_temperature_comfort input_temperature_eco: !input input_temperature_eco input_hvac_mode: !input input_hvac_mode factor: "{{ iif(input_hvac_mode == 'cool', -1, 1) | int }}" is_heat_only_if_below_real_temp: !input input_off_if_above_room_temperature # on/ff input_mode_winter: !input input_mode_winter input_mode_outside_temperature: !input input_mode_outside_temperature input_mode_outside_temperature_threshold: !input input_mode_outside_temperature_threshold input_mode_room_temperature_threshold: !input input_mode_room_temperature_threshold input_mode_room_temperature: !input input_mode_room_temperature input_invert_winter_mode_value: !input input_invert_winter_mode_value # party / force max input_mode_party: !input input_mode_party # adjustments / heating plan input_adjustments: !input input_adjustments # calibration input_calibration_timeout: !input input_calibration_timeout # windows input_windows: !input input_windows #presence input_presence_sensor: !input input_presence_sensor is_presence_sensor_defined: "{{ input_presence_sensor != none }}" input_presence_reaction_on_time: !input input_presence_reaction_on_time input_presence_reaction_off_time: !input input_presence_reaction_off_time # proximity input_proximity: !input input_proximity input_proximity_duration: !input input_proximity_duration input_proximity_distance: !input input_proximity_distance # frost protection input_frost_protection_duration: !input input_frost_protection_duration # liming protection input_liming_protection: !input input_liming_protection input_liming_protection_day: !input input_liming_protection_day input_liming_protection_time: !input input_liming_protection_time input_liming_in_winter: !input input_liming_in_winter input_liming_protection_duration: !input input_liming_protection_duration trigger: # system - trigger: homeassistant event: start id: temperature_change_hastart - trigger: event event_type: automation_reloaded id: temperature_change_reload - trigger: event event_type: ahc_delay_event id: delayed_call_temperature_change event_data: automation: "{{ this.entity_id }}" - trigger: event event_type: ahc_positioning_event id: positioning_event event_data: automation: "{{ this.entity_id }}" # thermostats become available - trigger: state entity_id: !input input_trvs from: - unknown - unavailable for: seconds: 5 id: temperature_change_available # physical change - trigger: state entity_id: !input input_trvs attribute: temperature for: seconds: 5 id: temperature_change_valve_target # eco/comfort change - trigger: state entity_id: !input input_temperature_eco for: !input input_action_call_delay id: temperature_change_eco - trigger: state entity_id: !input input_temperature_comfort for: !input input_action_call_delay id: temperature_change_comfort # persons - trigger: template value_template: > {{ input_persons | expand | selectattr('state', 'eq', 'home') | list | count > 0 or (is_guest_mode_defined and states(input_mode_guest) in ['on','active'] ) }} id: temperature_change_person_on for: !input input_people_entering_home_duration - trigger: template value_template: > {{ input_persons | expand | selectattr('state', 'eq', 'home') | list | count == 0 and (not is_guest_mode_defined or (is_guest_mode_defined and states(input_mode_guest) not in ['on','active'])) }} id: temperature_change_person_off for: !input input_people_leaving_home_duration # scheduler - trigger: template id: temperature_change_scheduler_on value_template: > {% set selected_scheduler = none %} {% set schedules_count = input_schedulers | count %} {% if schedules_count == 0 %} {% set selected_scheduler = none %} {% elif schedules_count == 1 or input_scheduler_selector == none %} {% set selected_scheduler = input_schedulers | first %} {% elif schedules_count > 1 %} {% set selector_value = states(input_scheduler_selector) %} {% if is_number(selector_value) %} {% set selector_value = iif(selector_value | int > schedules_count, schedules_count, selector_value) %} {% set selector_value = iif(selector_value | int <= 0, 1, selector_value) %} {% set selected_scheduler = input_schedulers[selector_value | int - 1] %} {% elif selector_value in ['on','off'] %} {% set selected_scheduler = iif(selector_value == 'off', input_schedulers[0], input_schedulers[1]) %} {% else %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'eq', selector_value) | map(attribute='entity_id') | first | default(none) %} {% if (selected_scheduler == none) %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'search', '(?i)' + selector_value) | map(attribute='entity_id') | first | default(none) %} {% endif %} {% endif %} {% endif %} {% if selected_scheduler == none %} {{ false }} {% else %} {{ is_state(selected_scheduler, 'on') }} {% endif %} - trigger: template id: temperature_change_scheduler_off value_template: > {% set selected_scheduler = none %} {% set schedules_count = input_schedulers | count %} {% if schedules_count == 0 %} {% set selected_scheduler = none %} {% elif schedules_count == 1 or input_scheduler_selector == none %} {% set selected_scheduler = input_schedulers | first %} {% elif schedules_count > 1 %} {% set selector_value = states(input_scheduler_selector) %} {% if is_number(selector_value) %} {% set selector_value = iif(selector_value | int > schedules_count, schedules_count, selector_value) %} {% set selector_value = iif(selector_value | int <= 0, 1, selector_value) %} {% set selected_scheduler = input_schedulers[selector_value | int - 1] %} {% elif selector_value in ['on','off'] %} {% set selected_scheduler = iif(selector_value == 'off', input_schedulers[0], input_schedulers[1]) %} {% else %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'eq', selector_value) | map(attribute='entity_id') | first | default(none) %} {% if (selected_scheduler == none) %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'search', '(?i)' + selector_value) | map(attribute='entity_id') | first | default(none) %} {% endif %} {% endif %} {% endif %} {% if selected_scheduler == none %} {{ false }} {% else %} {{ is_state(selected_scheduler, 'off') }} {% endif %} # presence sensor - trigger: template id: temperature_change_presence_on value_template: "{{ input_presence_sensor != none and is_state(input_presence_sensor, 'on') }}" for: !input input_presence_reaction_on_time - trigger: template id: temperature_change_presence_off value_template: "{{ input_presence_sensor != none and is_state(input_presence_sensor, 'off') }}" for: !input input_presence_reaction_off_time # presence scheduler - trigger: template id: temperature_change_presence_scheduler_on value_template: "{{ input_scheduler_presence != none and is_state(input_scheduler_presence, 'on') }}" for: !input input_action_call_delay - trigger: template id: temperature_change_presence_scheduler_off value_template: "{{ input_scheduler_presence != none and is_state(input_scheduler_presence, 'off') }}" for: !input input_action_call_delay # proximity - trigger: template id: temperature_change_person_proximity_on value_template: > {% set proximity_entities = device_entities(input_proximity) %} {% set is_arrived = proximity_entities | select('is_state','arrived') | expand | selectattr('attributes.device_class', 'eq', 'enum') | list | count > 0 %} {% set entities_towards = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'enum') | map(attribute='entity_id') | select('is_state','towards') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | map(attribute='state') | reject('eq', 'unknown') | map('int') | select('<=', input_proximity_distance | int) | map('string') | list %} {% set entities_distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | selectattr('state', 'in', distances) | map(attribute='entity_id') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set entites_towards_and_in_distance = entities_towards | select('in', entities_distances) | list | count > 0 %} {{ entites_towards_and_in_distance or is_arrived }} for: !input input_proximity_duration - trigger: template id: temperature_change_person_proximity_off value_template: > {% set proximity_entities = device_entities(input_proximity) %} {% set is_arrived = proximity_entities | select('is_state','arrived') | expand | selectattr('attributes.device_class', 'eq', 'enum') | list | count > 0 %} {% set entities_towards = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'enum') | map(attribute='entity_id') | select('is_state','towards') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | map(attribute='state') | reject('eq', 'unknown') | map('int') | select('<=', input_proximity_distance | int) | map('string') | list %} {% set entities_distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | selectattr('state', 'in', distances) | map(attribute='entity_id') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set entites_towards_and_in_distance = entities_towards | select('in', entities_distances) | list | count > 0 %} {{ entites_towards_and_in_distance == false and is_arrived == false }} for: !input input_proximity_duration # window - trigger: template value_template: "{{ expand(input_windows) | selectattr('state', 'in', ['on','open','tilted']) | list | count > 0 }}" for: !input input_windows_reaction_time_open id: temperature_change_window_on - trigger: template value_template: "{{ expand(input_windows) | selectattr('state', 'in', ['on','open','tilted']) | list | count == 0 }}" for: !input input_windows_reaction_time_close id: temperature_change_window_off # on/off winter mode - trigger: template id: temperature_change_winter_mode_on value_template: > {% if input_mode_winter != none %} {% set activation_state = iif(input_invert_winter_mode_value, 'off', 'on') %} {{ is_state(input_mode_winter, activation_state) }} {% endif %} for: !input input_action_call_delay - trigger: template id: temperature_change_winter_mode_off value_template: > {% if input_mode_winter != none %} {% set activation_state = iif(input_invert_winter_mode_value, 'off', 'on') %} {{ not is_state(input_mode_winter, activation_state) }} {% endif %} for: !input input_action_call_delay # on/off temperature - trigger: template id: temperature_change_outside_on value_template: > {% if input_mode_outside_temperature == none %} {{ false }} {% else %} {% set outside_state = false %} {% set use_room_temp = input_mode_room_temperature and is_temperature_sensor_defined %} {% set room_state = iif(use_room_temp, false, true) %} {% set state = states(input_mode_outside_temperature) %} {% set state = iif(is_number(state) == true, state, state_attr(input_mode_outside_temperature,'temperature'))%} {% if is_number(state) %} {% set outside_state = (state | float - input_mode_outside_temperature_threshold | float) * factor < 0 %} {% endif %} {% if use_room_temp %} {% set state = states(input_temperature_sensor) %} {% if is_number(state) %} {% set room_state = (state | float - input_mode_room_temperature_threshold | float) * factor < 0 %} {% endif %} {% endif %} {{ room_state and outside_state }} {% endif %} for: !input input_action_call_delay - trigger: template id: temperature_change_outside_off value_template: > {% if input_mode_outside_temperature == none %} {{ false }} {% else %} {% set outside_state = false %} {% set use_room_temp = input_mode_room_temperature and is_temperature_sensor_defined %} {% set room_state = iif(use_room_temp, false, true) %} {% set state = states(input_mode_outside_temperature) %} {% set state = iif(is_number(state) == true, state, state_attr(input_mode_outside_temperature,'temperature'))%} {% if is_number(state) %} {% set outside_state = (state | float - input_mode_outside_temperature_threshold | float) * factor < 0 %} {% endif %} {% if use_room_temp %} {% set state = states(input_temperature_sensor) %} {% if is_number(state) %} {% set room_state = (state | float - input_mode_room_temperature_threshold | float) * factor < 0 %} {% endif %} {% endif %} {{ not (room_state and outside_state) }} {% endif %} for: !input input_action_call_delay # force max temp - trigger: state id: temperature_change_force_max_temperature_on entity_id: !input input_force_max_temperature for: !input input_action_call_delay # force eco temp - trigger: state id: temperature_change_force_eco_temperature_ds entity_id: !input input_force_eco_temperature for: !input input_action_call_delay # party - trigger: template id: temperature_change_party_on value_template: "{{ input_mode_party | expand | selectattr('state', 'in', ['active','on']) | list | count > 0 }}" for: !input input_action_call_delay - trigger: template value_template: "{{ input_mode_party | expand | selectattr('state', 'in', ['active','on']) | list | count == 0 }}" id: temperature_change_party_off for: !input input_action_call_delay # aggressive mode / heating above/below temp - trigger: state id: calibration_aggressive_mode_above_temp_thermostat_current_temp_change entity_id: !input input_trvs attribute: current_temperature for: !input input_calibration_timeout - trigger: state id: calibration_aggressive_mode_thermostat_temp_change entity_id: !input input_trvs attribute: temperature for: seconds: 30 - trigger: state id: aggressive_mode_above_temp_sensor_change entity_id: !input input_temperature_sensor for: seconds: 30 # calibration trigger - trigger: state id: calibration_sensor_change entity_id: !input input_temperature_sensor for: !input input_calibration_timeout - trigger: state id: calibration_popp_change entity_id: !input input_temperature_sensor for: seconds: 2 - trigger: template id: calibration_popp_ping value_template: > {% set has_valves_danfoss = input_trvs | select('is_device_attr', 'manufacturer', 'Danfoss') | list %} {% set has_valves_popp = input_trvs | select('is_device_attr', 'manufacturer', 'Popp') | list %} {% set valves_hive = input_trvs | select('is_device_attr', 'manufacturer', 'Hive') | list %} {% set has_valves = (has_valves_danfoss + has_valves_popp + valves_hive) | count > 0 %} {{ has_valves and is_temperature_sensor_defined and now().strftime('%M') | int % 10 == 0 }} # heating adjustments - trigger: template id: temperature_change_heating_adjustment value_template: > {% set timestamp = now() %} {% set current_day = timestamp.strftime('%a') %} {% set current_time = timestamp.strftime('%H:%M') %} {% set plan = input_adjustments | rejectattr('time', 'undefined') | selectattr('time','eq', current_time | string) | sort(attribute='time', reverse = true) | list %} {{ plan | count > 0 and now() < now().replace(second=2) }} # liming protection - trigger: template value_template: > {% if not input_liming_protection%} {{ false }} {% else %} {% set enable_liming = true %} {% if input_mode_winter != none %} {% set enable_liming = is_state(input_mode_winter,'on') or input_liming_in_winter %} {% endif %} {% set current_timestamp = now() %} {% set is_liming_day = input_liming_protection_day == as_datetime(current_timestamp).strftime('%a') %} {% set start_hour = input_liming_protection_time.split(':')[0] | int %} {% set start_minute = input_liming_protection_time.split(':')[1] | int %} {% set today_start = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) %} {% set today_end = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) + timedelta(minutes=input_liming_protection_duration | int) %} {% set is_liming_time = as_datetime(current_timestamp) >= today_start and as_datetime(current_timestamp) <= today_end %} {{ enable_liming and is_liming_day and is_liming_time }} {% endif %} id: temperature_change_liming_protection_on - trigger: template value_template: > {% if not input_liming_protection%} {{ false }} {% else %} {% set enable_liming = true %} {% if input_mode_winter != none %} {% set enable_liming = is_state(input_mode_winter,'on') or input_liming_in_winter %} {% endif %} {% set current_timestamp = now() %} {% set current_timestamp = now() %} {% set is_liming_day = input_liming_protection_day == as_datetime(current_timestamp).strftime('%a') %} {% set start_hour = input_liming_protection_time.split(':')[0] | int %} {% set start_minute = input_liming_protection_time.split(':')[1] | int %} {% set today_start = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) %} {% set today_end = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) + timedelta(minutes=input_liming_protection_duration | int) %} {% set is_liming_time = as_datetime(current_timestamp) >= today_start and as_datetime(current_timestamp) <= today_end %} {{ not (enable_liming and is_liming_day and is_liming_time) }} {% endif %} id: temperature_change_liming_protection_off # frost protection - trigger: template id: temperature_change_frost_protection_on for: !input input_frost_protection_duration value_template: > {% set now_ts = now() %} {% set frost_protection_timestamp = as_datetime(now_ts) - timedelta(**input_frost_protection_duration) %} {% if frost_protection_timestamp == now_ts %} {{ false }} {% else %} {% set relevant_entities = [input_presence_sensor] + [input_mode_guest] + input_persons %} {% set relevant_entities_count = relevant_entities | reject('eq',none) | list | count %} {% if relevant_entities_count > 0 %} {% set presence_count = [input_presence_sensor] | reject('eq',none) | reject('is_state','on') | list | count %} {% set guest_mode_count = [input_mode_guest] | reject('eq',none) | reject('is_state','on') | list | count %} {% set person_count = input_persons | reject('is_state','home') | list | count %} {{ presence_count + guest_mode_count + person_count == relevant_entities_count }} {% else %} {{ false }} {% endif %} {% endif %} variables: ##################################################################################### ###################################### INPUTS ####################################### ##################################################################################### # thermostats / sensors input_trvs: !input input_trvs input_hvac_mode: !input input_hvac_mode input_temperature_sensor: !input input_temperature_sensor # temperatures input_temperature_comfort: !input input_temperature_comfort input_temperature_comfort_entity: "{{ iif(input_temperature_comfort == [], none, input_temperature_comfort) }}" input_temperature_comfort_static: !input input_temperature_comfort_static input_temperature_eco: !input input_temperature_eco input_temperature_eco_entity: "{{ iif(input_temperature_eco == [], none, input_temperature_eco) }}" input_temperature_eco_static: !input input_temperature_eco_static #frost protection input_frost_protection_temp: !input input_frost_protection_temp input_frost_protection_duration: !input input_frost_protection_duration #liming protection input_liming_protection: !input input_liming_protection input_liming_protection_day: !input input_liming_protection_day input_liming_protection_time: !input input_liming_protection_time input_liming_in_winter: !input input_liming_in_winter input_liming_protection_duration: !input input_liming_protection_duration # heating scheduler input_schedulers: !input input_schedulers input_scheduler_selector: !input input_scheduler_selector # presence input_presence_sensor: !input input_presence_sensor input_scheduler_presence: !input input_scheduler_presence input_presence_reaction_off_time: !input input_presence_reaction_off_time input_presence_reaction_on_time: !input input_presence_reaction_on_time # window detection input_windows: !input input_windows input_windows_reaction_time_open: !input input_windows_reaction_time_open input_windows_reaction_time_close: !input input_windows_reaction_time_close input_window_open_temperature: !input input_window_open_temperature input_party_legacy_restore: !input input_party_legacy_restore input_window_legacy_restore: !input input_window_legacy_restore is_legacy_restore: "{{ input_party_legacy_restore or input_window_legacy_restore }}" # wintermode / on/off input_mode_winter: !input input_mode_winter input_invert_winter_mode_value: !input input_invert_winter_mode_value input_mode_outside_temperature: !input input_mode_outside_temperature input_mode_outside_temperature_threshold: !input input_mode_outside_temperature_threshold input_mode_room_temperature: !input input_mode_room_temperature input_mode_room_temperature_threshold: !input input_mode_room_temperature_threshold # proximity input_proximity: !input input_proximity # people input_persons: !input input_persons input_mode_guest: !input input_mode_guest input_people_entering_home_duration: !input input_people_entering_home_duration input_people_leaving_home_duration: !input input_people_leaving_home_duration # force comfort input_mode_party: !input input_mode_party input_force_max_temperature: !input input_force_max_temperature input_force_eco_temperature: !input input_force_eco_temperature # calibration input_calibration_delta: !input input_calibration_delta input_calibration_generic: !input input_calibration_generic input_calibration_step_size: !input input_calibration_step_size input_calibration_key_word: !input input_calibration_key_word input_generic_calibration_offset: !input input_generic_calibration_offset # Aggressive Mode input_aggressive_mode_offset: !input input_aggressive_mode_offset input_aggressive_mode_range: !input input_aggressive_mode_range input_aggressive_mode_calibration: !input input_aggressive_mode_calibration # away mode input_away_offset: !input input_away_offset is_scheduler_away_mode: !input input_away_scheduler_mode is_presence_away_mode: !input input_away_presence_mode presence_ignor_people: !input input_away_presence_ignor_people # heating adjustments input_adjustments: !input input_adjustments # temperature tweaks is_reset_temperature: !input input_reset_temperature is_off_instead_min: !input input_off_instead_of_eco is_not_off_but_min: !input input_min_instead_of_off is_fahrenheit: !input input_fahrenheit is_heat_only_if_below_real_temp: !input input_off_if_above_room_temperature is_physical_change_enabled: !input input_physical_change is_off_if_nobody_home: !input input_off_if_nobody_home # custom tweaks input_action_call_delay: !input input_action_call_delay input_custom_action: !input input_custom_action input_startup_delay: !input input_startup_delay # valve positioning input_fully_open_difference: !input input_fully_open_difference input_valve_opening_keyword: !input input_valve_opening_keyword input_valve_positioning_step_size: !input input_valve_positioning_step_size input_valve_positioning_mode: !input input_valve_positioning_mode input_valve_positioning_timeout: !input input_valve_positioning_timeout input_valve_positioning_max_opening: !input input_valve_positioning_max_opening ##################################################################################### #################################### EVALUATION ##################################### ##################################################################################### # global is_temperature_sensor_defined: "{{ input_temperature_sensor != [] }}" invalid_states: > {{ ['unknown', 'unavailable'] }} value_temperature_sensor: > {% if is_temperature_sensor_defined %} {{ states(input_temperature_sensor) }} {% else %} {{ 'unknown' }} {% endif %} valid_temperature_sensor: > {{ value_temperature_sensor not in invalid_states }} factor: "{{ iif(input_hvac_mode == 'cool', -1, 1) | int }}" current_time_stamp: "{{ now() }}" is_metric: "{{ not is_temperature_sensor_defined or (is_temperature_sensor_defined and state_attr(input_temperature_sensor,VAR_UNIT_OF_MEASUREMENT) == 'Β°C') }}" # uptime up_time_sensor: "{{ integration_entities('uptime') | first | default(none) }}" is_uptime_defined: "{{ up_time_sensor != none }}" uptime: > {% if is_uptime_defined %} {{ states(up_time_sensor) | as_datetime }} {% else %} {{ current_time_stamp | as_datetime }} {% endif %} #startup delay startup_delay: > {% set start_delay_seconds = timedelta(**input_startup_delay).total_seconds() %} {% if not is_uptime_defined or start_delay_seconds == 0 %} {{ none }} {% else %} {% set difference = (current_time_stamp | as_datetime - uptime | as_datetime).total_seconds() %} {% set real_start_delay = (start_delay_seconds - difference) %} {{ iif(real_start_delay > 0, real_start_delay, none) }} {% endif %} # on/off state_outside_temp: > {% if input_mode_outside_temperature == none %} {{ none }} {% else %} {% set outside_state = false %} {% set use_room_temp = input_mode_room_temperature and valid_temperature_sensor %} {% set room_state = iif(use_room_temp, false, true) %} {% set state = states(input_mode_outside_temperature) %} {% set state = iif(is_number(state) == true, state, state_attr(input_mode_outside_temperature,'temperature'))%} {% if is_number(state) %} {% set outside_state = (state | float - input_mode_outside_temperature_threshold | float) * factor < 0 %} {% endif %} {% if use_room_temp %} {% set state = states(input_temperature_sensor) %} {% if is_number(state) %} {% set room_state = (state | float - input_mode_room_temperature_threshold | float) * factor < 0 %} {% endif %} {% endif %} {{ room_state and outside_state }} {% endif %} state_ahc: > {% set result = true %} {% if input_mode_winter != none %} {% set activation_state = iif(input_invert_winter_mode_value, 'off', 'on') %} {% set result = is_state(input_mode_winter, activation_state) %} {% endif %} {{ iif(state_outside_temp == none, result, result and state_outside_temp) }} # proximity is_proximity_defined: "{{ input_proximity != none }}" state_proximity_arrived: > {% set proximity_entities = device_entities(input_proximity) %} {% set is_arrived = proximity_entities | select('is_state','arrived') | expand | selectattr('attributes.device_class', 'eq', 'enum') | list | count > 0 %} {{ is_arrived }} state_proximity_way_home: > {% set proximity_entities = device_entities(input_proximity) %} {% set earliest_timestamp = current_time_stamp | as_datetime - timedelta(**input_proximity_duration) %} {% set uptime_duration = as_datetime(uptime) + timedelta(**input_proximity_duration) %} {% if uptime_duration > earliest_timestamp %} {% set earliest_timestamp = uptime_duration%} {% endif %} {% set entities_towards = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'enum') | selectattr('last_changed', '<=', earliest_timestamp) | map(attribute='entity_id') | select('is_state','towards') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | map(attribute='state') | reject('eq', 'unknown') | map('int') | select('<=', input_proximity_distance | int) | map('string') | list %} {% set entities_distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | selectattr('state', 'in', distances) | map(attribute='entity_id') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set towards_and_in_distance = entities_towards | select('in', entities_distances) | list | count > 0 %} {{ towards_and_in_distance }} # persons is_person_defined: "{{ input_persons | count > 0 or input_mode_guest != none }}" is_guest_mode: "{{ input_mode_guest != none and is_state(input_mode_guest, 'on') }}" is_anybody_home: > {% if is_guest_mode %} {{ true }} {% elif not is_person_defined %} {{ false }} {% else %} {% set on_time_delta = current_time_stamp | as_datetime - timedelta(**input_people_entering_home_duration) %} {% set off_time_delta = current_time_stamp | as_datetime - timedelta(**input_people_leaving_home_duration) %} {% set uptime_on = as_datetime(uptime) + timedelta(**input_people_entering_home_duration) %} {% set uptime_off = as_datetime(uptime) + timedelta(**input_people_leaving_home_duration) %} {% set result = false %} {% if uptime_on > on_time_delta or uptime_off > off_time_delta %} {{ input_persons | expand | selectattr('state', 'eq', 'home') | list | count > 0 }} {% else %} {% set persons_home = state_attr('zone.home','persons') | select('in', input_persons) | list %} {% set somebody_is_home = persons_home | expand | selectattr('last_changed', '<=', on_time_delta) | list | count > 0 %} {% set somebody_is_leaving = persons_home | count == 0 and ['zone.home'] | expand | map(attribute='last_changed') | first | default(off_time_delta) > off_time_delta %} {{ somebody_is_home or somebody_is_leaving }} {% endif %} {% endif %} is_anybody_home_or_proximity: "{{ is_anybody_home or state_proximity_way_home or state_proximity_arrived}}" # schedules active_scheduler: > {% set selected_scheduler = none %} {% set schedules_count = input_schedulers | count %} {% if schedules_count == 0 %} {% set selected_scheduler = none %} {% elif schedules_count == 1 or input_scheduler_selector == none %} {% set selected_scheduler = input_schedulers | first %} {% elif schedules_count > 1 %} {% set selector_value = states(input_scheduler_selector) %} {% if is_number(selector_value) %} {% set selector_value = iif(selector_value | int > schedules_count, schedules_count, selector_value) %} {% set selector_value = iif(selector_value | int <= 0, 1, selector_value) %} {% set selected_scheduler = input_schedulers[selector_value | int - 1] %} {% elif selector_value in ['on','off'] %} {% set selected_scheduler = iif(selector_value == 'off', input_schedulers[0], input_schedulers[1]) %} {% else %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'eq', selector_value) | map(attribute='entity_id') | first | default(none) %} {% if (selected_scheduler == none) %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'search', '(?i)' + selector_value) | map(attribute='entity_id') | first | default(none) %} {% endif %} {% endif %} {% endif %} {{ selected_scheduler }} is_scheduler_defined: "{{ active_scheduler != none }}" state_scheduler: "{{ active_scheduler != none and is_state(active_scheduler,'on') }}" # presence is_presence_sensor_defined: "{{ input_presence_sensor != none }}" is_presence_scheduler_defined: "{{ input_scheduler_presence != none }}" state_presence_scheduler: "{{ is_presence_scheduler_defined and is_state(input_scheduler_presence, 'on') }}" state_presence_sensor: > {% if not is_presence_sensor_defined %} {{ false }} {% else %} {% set last_changed = [input_presence_sensor] | expand | map(attribute='last_changed') | first %} {% set sensor_state = is_state(input_presence_sensor, 'on') %} {% set reaction_time = iif(sensor_state, input_presence_reaction_on_time, input_presence_reaction_off_time) %} {% set min_timestamp = last_changed + timedelta(**reaction_time) %} {% set current_ts = current_time_stamp | as_datetime%} {% if is_uptime_defined and as_datetime(uptime) + timedelta(**reaction_time) > current_ts - timedelta(**reaction_time) %} {{ sensor_state }} {% else %} {% set is_limit = min_timestamp <= current_ts %} {{ (sensor_state == true and is_limit) or (sensor_state == false and not is_limit) }} {% endif %} {% endif %} state_presence: > {{ iif(is_presence_scheduler_defined, state_presence_scheduler and state_presence_sensor, state_presence_sensor) }} # force max temperature is_force_max_temperature: "{{ input_force_max_temperature != [] and is_state(input_force_max_temperature, 'on') }}" is_force_eco_temperature: "{{ input_force_eco_temperature != [] and is_state(input_force_eco_temperature, 'on') }}" # party active_party_entity: "{{ input_mode_party | expand | selectattr('state', 'in', ['active','on']) | map(attribute='entity_id') | first | default(none) }}" state_party: "{{ active_party_entity != none }}" party_temp: > {% set pos_party_temp = none %} {% if state_party == true %} {% set name = state_attr(active_party_entity,'friendly_name') %} {% set pos_temp = name.split(' ') | last %} {% if is_number(pos_temp) %} {% set pos_party_temp = pos_temp | float %} {% endif %} {% endif %} {{ pos_party_temp }} # away is_away: > {% if is_person_defined and not is_anybody_home_or_proximity %} {{ (is_scheduler_away_mode and state_scheduler) or (is_presence_away_mode and state_presence_scheduler and not state_presence) }} {% elif presence_ignor_people and is_presence_away_mode %} {{ state_presence_scheduler and not state_presence }} {% elif is_presence_away_mode and is_person_defined and is_anybody_home_or_proximity and not presence_ignor_people %} {{ not state_presence }} {% else %} {{ false }} {% endif %} # windows & doors state_window: > {% set current_ts = current_time_stamp | as_datetime %} {% set on_time_delta = current_ts - timedelta(**input_windows_reaction_time_open) %} {% set off_time_delta = current_ts - timedelta(**input_windows_reaction_time_close) %} {% set has_open_windows = input_windows | expand | selectattr('state', 'in', ['on','open','tilted']) | selectattr('last_changed', '<=', on_time_delta) | list | count > 0 %} {% set closed_but_not_in_duration = input_windows | expand | selectattr('state', 'in', ['off','closed']) | selectattr('last_changed', '>=', off_time_delta) | list | count > 0 %} {{ has_open_windows or closed_but_not_in_duration }} # aggressive mode is_aggressive_mode: "{{ input_aggressive_mode_offset > 0 }}" is_aggressive_mode_calibration: "{{ is_aggressive_mode and input_aggressive_mode_calibration and valid_temperature_sensor }}" # frost protection is_frost_protection: > {% set frost_protection_timestamp = as_datetime(current_time_stamp) - timedelta(**input_frost_protection_duration) %} {% if frost_protection_timestamp == as_datetime(current_time_stamp) %} {{ false }} {% else %} {% set relevant_entities = [input_presence_sensor] + [input_mode_guest] + input_persons %} {% set relevant_entities_count = relevant_entities | reject('eq',none) | list | count %} {% if relevant_entities_count > 0 %} {% set presence_count = [input_presence_sensor] | reject('eq',none) | reject('is_state','on') | expand | selectattr('last_changed', '<=', frost_protection_timestamp) | list | count %} {% set persons_count = input_persons | reject('eq',none) | reject('is_state','home') | expand | selectattr('last_changed', '<=', frost_protection_timestamp) | list | count %} {% set guest_mode_count = [input_mode_guest] | reject('eq',none) | reject('is_state','on') | expand | selectattr('last_changed', '<=', frost_protection_timestamp) | list | count %} {{ presence_count + guest_mode_count + persons_count == relevant_entities_count }} {% else %} {{ false }} {% endif %} {% endif %} # liming protection is_liming_protection: > {% if not input_liming_protection%} {{ false }} {% else %} {% set enable_liming = true %} {% if input_mode_winter != none %} {% set enable_liming = is_state(input_mode_winter,'on') or input_liming_in_winter %} {% endif %} {% set current_timestamp = now() %} {% set is_liming_day = input_liming_protection_day == as_datetime(current_timestamp).strftime('%a') %} {% set start_hour = input_liming_protection_time.split(':')[0] | int %} {% set start_minute = input_liming_protection_time.split(':')[1] | int %} {% set today_start = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) %} {% set today_end = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) + timedelta(minutes=input_liming_protection_duration | int) %} {% set is_liming_time = as_datetime(current_timestamp) >= today_start and as_datetime(current_timestamp) <= today_end %} {{ enable_liming and is_liming_day and is_liming_time }} {% endif %} # thermostat groups valves: > {{ input_trvs | expand | selectattr('attributes.hvac_modes','search','(?i)'+input_hvac_mode) | map(attribute='entity_id') | list }} valves_unsupported: > {{ input_trvs | reject('in',valves) | list }} valves_off_mode: > {{ valves | expand | selectattr('attributes.hvac_modes','search','(?i)off') | map(attribute='entity_id') | list }} valves_without_off_mode: > {{ valves | reject('in',valves_off_mode) | list }} # tado valves_tado: "{{ valves | select('is_device_attr', 'manufacturer', 'Tado') | list }}" # xiaomi / aqara valves_xiaomi_xiaomi: "{{ valves | select('is_device_attr', 'manufacturer', 'Xiaomi') | list }}" valves_xiaomi_aqara: "{{ valves | select('is_device_attr', 'manufacturer', 'Aqara') | list }}" valves_xiaomi: "{{ valves_xiaomi_xiaomi + valves_xiaomi_aqara }}" # danfoss / popp / hive valves_danfoss: "{{ valves | select('is_device_attr', 'manufacturer', 'Danfoss') | list }}" valves_popp: "{{ valves | select('is_device_attr', 'manufacturer', 'Popp') | list }}" valves_hive: "{{ valves | select('is_device_attr', 'manufacturer', 'Hive') | list }}" valves_dph: "{{ valves_danfoss + valves_popp + valves_hive }}" valves_calibration_common: "{{ valves | reject('in', valves_tado + valves_dph + valves_xiaomi) | list }}" # global last_comfort_entity_change: "{{ [input_temperature_comfort_entity] | expand | map(attribute='last_changed') | list | first | default(none) }}" last_eco_entity_change: "{{ [input_temperature_eco_entity] | expand | map(attribute='last_changed') | list | first | default(none) }}" ##################################################################################### ################################## ADJUSTMENTS ###################################### ##################################################################################### latest_entry_today: > {% set scheduler_name = none %} {% if active_scheduler != none %} {% set scheduler_name = state_attr(active_scheduler,'friendly_name') %} {% endif %} {% set current_ts = current_time_stamp | as_datetime %} {% set current_day = current_ts.strftime('%a') %} {% set current_time = current_ts.strftime('%H:%M') %} {% set plan = input_adjustments | rejectattr('time', 'undefined') | selectattr('time','<=', current_time| string) | list %} {% set selected_entries_days_and_schedule = plan | rejectattr('days','==',Undefined) | selectattr('days','search',current_day) | rejectattr('scheduler','==',Undefined) | selectattr('scheduler','in',scheduler_name) | list %} {% set selected_entries_days = plan | rejectattr('days','==',Undefined) | selectattr('days','search',current_day) | selectattr('scheduler','in',[Undefined]) | list %} {% set selected_entries_schedule = plan | rejectattr('scheduler','==',Undefined) | selectattr('scheduler','in',scheduler_name) | selectattr('days','in',[Undefined]) | list %} {% set selected_entries_time_only = plan | selectattr('days','in',[Undefined]) | selectattr('scheduler','in',[Undefined]) | list %} {% set selected_entries = selected_entries_days_and_schedule + selected_entries_days + selected_entries_schedule + selected_entries_time_only %} {% if selected_entries | count > 0%} {{ selected_entries | sort(attribute='time', reverse = true) | first }} {% else %} {{ none }} {% endif %} latest_entry_day_before: > {% set timestamp = as_datetime(current_time_stamp).replace(hour=23,minute=59) + timedelta(days=-1) %} {% set scheduler_name = none %} {% if active_scheduler != none %} {% set scheduler_name = state_attr(active_scheduler,'friendly_name') %} {% endif %} {% set current_day = timestamp.strftime('%a') %} {% set current_time = timestamp.strftime('%H:%M') %} {% set plan = input_adjustments | rejectattr('time', 'undefined') | selectattr('time','<=', current_time| string) | list %} {% set selected_entries_days_and_schedule = plan | rejectattr('days','==',Undefined) | selectattr('days','search',current_day) | rejectattr('scheduler','==',Undefined) | selectattr('scheduler','in',scheduler_name) | list %} {% set selected_entries_days = plan | rejectattr('days','==',Undefined) | selectattr('days','search',current_day) | selectattr('scheduler','in',[Undefined]) | list %} {% set selected_entries_schedule = plan | rejectattr('scheduler','==',Undefined) | selectattr('scheduler','in',scheduler_name) | selectattr('days','in',[Undefined]) | list %} {% set selected_entries_time_only = plan | selectattr('days','in',[Undefined]) | selectattr('scheduler','in',[Undefined]) | list %} {% set selected_entries = selected_entries_days_and_schedule + selected_entries_days + selected_entries_schedule + selected_entries_time_only %} {% if selected_entries | count > 0%} {{ selected_entries | sort(attribute='time', reverse = true) | first }} {% else %} {{ none }} {% endif %} entry: "{{ iif(latest_entry_today != none, latest_entry_today, latest_entry_day_before) }}" entry_time: > {% if entry != none %} {% set entry_hour = entry['time'].split(':')[0] | int %} {% set entry_minute = entry['time'].split(':')[1] | int %} {{ as_datetime(current_time_stamp).replace(hour=entry_hour, minute=entry_minute, second=0, microsecond=0) + timedelta(days=iif(latest_entry_today == none,-1,0)) }} {% endif %} entry_comfort_temp: > {% if entry != none and 'comfort' in entry.keys() and (last_comfort_entity_change == none or as_datetime(entry_time) > as_datetime(last_comfort_entity_change)) %} {% set entry_temp = entry['comfort']%} {% if is_number(entry_temp) %} {{ entry_temp }} {% elif states[entry_temp] != none %} {{ states(entry_temp) }} {% endif %} {% else %} {{ none }} {% endif %} entry_eco_temp: > {% if entry != none and 'eco' in entry.keys() and (last_eco_entity_change == none or as_datetime(entry_time) > as_datetime(last_eco_entity_change)) %} {% set entry_temp = entry['eco']%} {% if is_number(entry_temp) %} {{ entry_temp }} {% elif states[entry_temp] != none %} {{ states(entry_temp) }} {% endif %} {% else %} {{ none }} {% endif %} entry_calibration: > {% if entry != none and 'calibration' in entry.keys() %} {{ entry['calibration'] == 'on' }} {% else %} {{ true }} {% endif %} entry_mode: > {% if entry != none and 'mode' in entry.keys() %} {{ entry['mode'] }} {% else %} {{ 'auto' }} {% endif %} ##################################################################################### ############################### TRIGGER EVALUATION ################################## ##################################################################################### trigger_id_defined: "{{ trigger.id is defined }}" # calibration is_calibration_trigger: > {% if valves_dph | count > 0 and trigger_id_defined and trigger.id in ['calibration_popp_ping','calibration_popp_change'] %} {{ true }} {% elif is_aggressive_mode_calibration and trigger_id_defined and 'aggressive_mode' in trigger.id %} {{ true }} {% else %} {{ trigger_id_defined and 'calibration' in trigger.id and not trigger.id == 'calibration_aggressive_mode_thermostat_temp_change' }} {% endif %} # changes is_generic_calibration_trigger: "{{ is_calibration_trigger and input_calibration_generic }}" is_generic_calibration: "{{ is_generic_calibration_trigger and entry_calibration and valid_temperature_sensor }}" is_aggressive_mode_trigger: "{{ is_aggressive_mode and trigger_id_defined and 'aggressive_mode' in trigger.id }}" is_change_trigger: > {{ trigger_id_defined and 'temperature_change' in trigger.id and ('presence' in trigger.id or 'scheduler' in trigger.id or 'proximity' in trigger.id or 'person' in trigger.id or '_ds' in trigger.id) and not trigger.id == 'temperature_change_valve_target' }} set_max_temperature: "{{ is_force_max_temperature or is_liming_protection }}" is_pysical_change: > {{ trigger_id_defined and trigger.id == 'temperature_change_valve_target' and is_physical_change_enabled and not state_window and not set_max_temperature and not is_away }} is_adjustment_trigger: "{{ trigger_id_defined and trigger.id == 'temperature_change_heating_adjustment' and (entry_comfort_temp != none or entry_eco_temp != none) }}" is_reset: > {{ (is_reset_temperature and is_change_trigger) or is_pysical_change or is_adjustment_trigger }} is_changes_trigger: > {% if state_window %} {% if trigger_id_defined and 'temperature_change_window_on' in trigger.id %} {{ true }} {% elif trigger_id_defined and 'temperature_change_window_off' not in trigger.id %} {{ false }} {% endif %} {% elif trigger.platform == none %} {{ true }} {% elif trigger_id_defined and trigger.id == 'temperature_change_valve_target' %} {{ false }} {% elif is_heat_only_if_below_real_temp and trigger_id_defined and 'above_temp' in trigger.id %} {{ true }} {% elif is_aggressive_mode_calibration and is_aggressive_mode_trigger %} {{ false }} {% elif is_aggressive_mode_trigger %} {{ true }} {% elif is_generic_calibration %} {{ true }} {% else %} {{ trigger_id_defined and 'temperature_change' in trigger.id}} {% endif %} is_scene_create_trigger: > {{ trigger_id_defined and (("window_on" in trigger.id and not state_party) or ("party_on" in trigger.id and not state_window)) }} is_scene_apply_trigger: > {{ trigger_id_defined and ("window_off" in trigger.id or "party_off" in trigger.id) and not is_legacy_restore and not (state_window or state_party) }} is_scene_destroy_trigger: > {{ (is_change_trigger or trigger.id == 'temperature_change_heating_adjustment') and (state_window or state_party) }} # scene management scene_entities: "{{ valves }}" scene_window_id: "{{ 'scene.' + this.entity_id | replace('automation.','') | replace('.','_') + '_window' }}" scene_party_id: "{{ 'scene.' + this.entity_id | replace('automation.','') | replace('.','_') + '_party' }}" scenes_all: "{{ [scene_window_id, scene_party_id] }}" scene_to_apply: > {% if is_scene_apply_trigger and "window_off" in trigger.id %} {{ scene_window_id }} {% elif is_scene_apply_trigger and "party_off" in trigger.id %} {{ scene_party_id }} {% else %} {{ none }} {% endif %} scenes_to_destroy: > {% set scenes = [] %} {% if is_scene_destroy_trigger %} {% set scenes = iif(state_window, scenes + [scene_window_id], scenes) %} {% set scenes = iif(state_party, scenes + [scene_party_id], scenes) %} {% endif %} {{ scenes }} scene_to_create: > {{ iif(is_scene_create_trigger and "window_on" in trigger.id, scene_window_id, scene_party_id) }} ##################################################################################### #################################### CHANGES ######################################## ##################################################################################### set_comfort: > {% if is_force_max_temperature %} {{ true }} {% elif entry_mode == 'eco' %} {{ false }} {% elif entry_mode == 'comfort' %} {{ true }} {% elif state_party %} {{ true }} {% elif is_force_eco_temperature %} {{ false }} {% elif is_away %} {{ true }} {% elif not is_scheduler_defined and not is_presence_sensor_defined %} {{ is_anybody_home_or_proximity }} {% else %} {% set comfort_state = state_scheduler or state_presence %} {% if is_person_defined or is_proximity_defined %} {{ is_anybody_home_or_proximity and comfort_state }} {% else %} {{ comfort_state }} {% endif %} {% endif %} mode: > {% if not state_ahc %} {{ 'off' }} {% elif state_window and input_window_open_temperature | int == 0 and not set_max_temperature %} {{ 'off' }} {% elif entry_mode == 'off' %} {{ 'off' }} {% elif is_off_instead_min and not set_comfort %} {{ 'off' }} {% elif is_off_if_nobody_home and is_person_defined and not is_anybody_home_or_proximity and not set_comfort %} {{ 'off' }} {% else %} {{ input_hvac_mode }} {% endif %} temperature_comfort_of_entity: > {% if(input_temperature_comfort_entity != none) %} {{ states(input_temperature_comfort_entity) | float }} {% else %} {{ none }} {% endif %} temperature_eco_of_entity: > {% if(input_temperature_eco_entity != none) %} {{ states(input_temperature_eco_entity) | float }} {% else %} {{ none }} {% endif %} temperature_comfort: "{{ [entry_comfort_temp, temperature_comfort_of_entity, input_temperature_comfort_static] | reject('==', none) | first }}" temperature_away: "{{ temperature_comfort | float - input_away_offset }}" temperature_eco: "{{ [entry_eco_temp, temperature_eco_of_entity, input_temperature_eco_static] | reject('==', none) | first }}" target_temperature: > {% if state_window and input_window_open_temperature > 0 %} {{ input_window_open_temperature }} {% elif state_party %} {{ iif(party_temp != none, party_temp, temperature_comfort) }} {% elif is_frost_protection %} {{ input_frost_protection_temp }} {% else %} {{ iif(set_comfort, iif(is_away, temperature_away, temperature_comfort), temperature_eco) }} {% endif %} changes: > {% set n = namespace(dict=[]) %} {% set original_mode = mode %} {% if not is_changes_trigger %} {{ n.dict }} {% else %} {% for valve in input_trvs %} {% set current_valve_temp = state_attr(valve, 'current_temperature') | float(20) %} {% set current_valve_target_temp = state_attr(valve, 'temperature') | float(temperature) %} {% set current_valve_mode = states(valve) %} {% set min_temp = state_attr(valve, 'min_temp') | float(5) %} {% set max_temp = state_attr(valve, 'max_temp') | float(30) %} {% set valve_temp = target_temperature %} {% set dont_turn_off = valve in valves_without_off_mode or is_not_off_but_min or (state_window and input_window_open_temperature > 0) or set_max_temperature %} {% set ref_temp = current_valve_temp %} {% if valid_temperature_sensor %} {% set ref_temp = value_temperature_sensor | float(current_valve_temp) %} {% endif %} {% if is_heat_only_if_below_real_temp and iif(factor == 1, target_temperature <= ref_temp, target_temperature >= ref_temp) %} {% set mode = 'off' %} {% endif %} {% set valve_mode = iif(mode == 'off' and dont_turn_off, current_valve_mode, mode) %} {% if mode != 'off' %} {% if is_aggressive_mode and not is_aggressive_mode_calibration %} {% set temp_diff = valve_temp - ref_temp %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set valve_temp = valve_temp - input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set valve_temp = valve_temp + input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% if input_calibration_generic %} {% if current_valve_temp != ref_temp %} {% set offset = current_valve_temp - ref_temp %} {% set offset = iif(offset > float(input_generic_calibration_offset), input_generic_calibration_offset, offset) %} {% set offset = iif(offset < float(input_generic_calibration_offset) * -1, input_generic_calibration_offset * -1, offset) %} {% set temp_with_offset = float(valve_temp) + float(offset) %} {% set step = state_attr(valve, 'target_temp_step') | float(0.5) %} {% set temp_with_offset = (temp_with_offset | float(0) / float(step)) | round(0) * float(step) %} {% set valve_temp = iif(input_calibration_step_size == 'full', float(temp_with_offset) | round(), temp_with_offset | round(1)) %} {% endif %} {% endif %} {% endif %} {% if mode == 'off' and dont_turn_off %} {% set valve_temp = min_temp %} {% endif %} {% set valve_temp = iif(set_max_temperature, max_temp, valve_temp) %} {% set valve_temp = iif(valve_temp > max_temp, max_temp, valve_temp) %} {% set valve_temp = iif(valve_temp < min_temp, min_temp, valve_temp) %} {% if current_valve_mode != valve_mode or current_valve_target_temp != valve_temp %} {% set n.dict = n.dict + [(valve, [{'mode': valve_mode , 'temp': valve_temp}])] %} {% endif %} {% endfor %} {% set mode = original_mode %} {{ dict.from_keys(n.dict) }} {% endif %} positioning: > {% set n = namespace(dict=[]) %} {% if input_valve_positioning_mode == 'off' %} {{ n.dict }} {% else %} {% for valve in input_trvs %} {% set current_temp = state_attr(valve, 'current_temperature') | float(none) %} {% if valid_temperature_sensor %} {% set current_temp = value_temperature_sensor | float(none) %} {% endif %} {% set target_temp = state_attr(valve, 'temperature') | float(none) %} {% set open_valve_entity = device_entities(device_id(valve)) | expand | selectattr('domain','in','number') | selectattr('entity_id', 'search', input_valve_opening_keyword) | map(attribute='entity_id') | list | first | default(none) %} {% if open_valve_entity != none and current_temp != none and target_temp != none and ( (trigger_id_defined and trigger.id == 'positioning_event') or ([open_valve_entity] | expand | map(attribute='last_changed') | first) + timedelta(**input_valve_positioning_timeout) <= now() ) %} {% set opening = 100 %} {% set difference = target_temp - current_temp %} {% set step_size = input_valve_positioning_step_size | int %} {% if input_fully_open_difference > 0 and not is_force_max_temperature %} {% set opening_regular = (100 / input_fully_open_difference) * difference %} {% set opening_pessimistic = sqrt(((100 / input_fully_open_difference) * difference) | abs) * 10 %} {% set opening_optimistic = ((100 / input_fully_open_difference) * difference)**2 / 100 %} {% set opening = opening_regular %} {% set opening = iif(input_valve_positioning_mode == 'pessimistic', opening_pessimistic, opening) %} {% set opening = iif(input_valve_positioning_mode == 'optimistic', opening_optimistic, opening) %} {% set opening = iif(difference >= input_fully_open_difference, 100, opening) %} {% set opening = iif(difference < 0, 0, opening) %} {% set opening = opening / 100 * input_valve_positioning_max_opening %} {% set opening = ((opening + step_size / 2) // step_size * step_size) | int %} {% endif %} {% set open_valve_entity_value = states(open_valve_entity) | int %} {% if open_valve_entity_value != opening %} {% set n.dict = n.dict + [(valve, [{'entity': open_valve_entity , 'value': opening, 'current_temp': current_temp, 'target_temp': target_temp, 'difference': difference}])] %} {% endif %} {% endif %} {% endfor %} {{ dict.from_keys(n.dict) }} {% endif %} # reset temperature reset_data: > {% set result = [] %} {% if is_adjustment_trigger %} {% if entry_comfort_temp != none and input_temperature_comfort_entity != none %} {% set result = result + [{'entity': input_temperature_comfort_entity, 'temp': entry_comfort_temp}] %} {% endif %} {% if entry_eco_temp != none and input_temperature_eco_entity != none %} {% set result = result + [{'entity': input_temperature_eco_entity, 'temp': entry_eco_temp}] %} {% endif %} {% else %} {% set entity = none %} {% if is_reset and set_comfort %} {% set entity = iif(is_pysical_change, input_temperature_comfort_entity, input_temperature_eco_entity) %} {% elif is_reset and not set_comfort %} {% set entity = iif(is_pysical_change, input_temperature_eco_entity, input_temperature_comfort_entity) %} {% endif %} {% set temp_r = none %} {% if is_pysical_change %} {% set temp_r = state_attr(trigger.to_state.entity_id,'temperature') %} {% else %} {% set temp_r = iif(is_reset and entity == input_temperature_eco_entity, input_temperature_eco_static, input_temperature_comfort_static) %} {% endif %} {% if entity != none and temp_r != none %} {% set result = result + [{'entity': entity, 'temp': temp_r}] %} {% endif %} {% endif %} {{ result }} is_reset_trigger: "{{ is_reset and reset_data | count > 0 }}" ##################################################################################### ################################## CALIBRATION ###################################### ##################################################################################### is_native_calibration: "{{ not input_calibration_generic and entry_calibration and valid_temperature_sensor }}" is_native_calibration_trigger: "{{ is_calibration_trigger and is_native_calibration }}" rounding_mode: > {% if is_number(input_calibration_step_size) or input_calibration_step_size == 'full' %} {{ 'manual' }} {% else %} {{ 'auto' }} {% endif %} # TADO calibration_tado: > {% set n = namespace(dict=[]) %} {% if is_native_calibration_trigger %} {% for valve in valves_tado %} {% set offset_old = state_attr(valve, 'offset_celsius') | float(0) %} {% set local_temperature = state_attr(valve, 'current_temperature') | float %} {% set calibration_sensor_temperature = value_temperature_sensor | float %} {% set offset_new = (-(local_temperature - calibration_sensor_temperature) + offset_old) %} {% if is_aggressive_mode_calibration %} {% set temp_diff = state_attr(valve,'temperature') | float(target_temperature) - calibration_sensor_temperature %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set offset_new = offset_new + input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set offset_new = offset_new - input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% set t_min = -10.9 %} {% set t_max = 10.9 %} {% set offset_new = iif(offset_new > t_max, t_max, offset_new) %} {% set offset_new = iif(offset_new < t_min, t_min, offset_new) %} {% set offset_new = offset_new | round(1) %} {% if (float(offset_old) - float(offset_new)) | abs >= float(input_calibration_delta) %} {% set n.dict = n.dict + [(valve, [{'value': offset_new }])] %} {% endif %} {% endfor %} {% endif %} {{ dict.from_keys(n.dict) }} # XIAOMI / AQARA calibration_xiaomi: > {% set n = namespace(dict=[]) %} {% if is_native_calibration_trigger %} {% for valve in valves_xiaomi %} {% set calibration_entities = device_entities(device_id(valve)) | expand | selectattr('domain','in','number') | selectattr('entity_id', 'search', input_calibration_key_word) | map(attribute='entity_id') | list %} {% if calibration_entities | count > 0 %} {% set calibration_entity = calibration_entities | first %} {% set offset_old = states(calibration_entity) | float(0) %} {% set offset_new = value_temperature_sensor | float %} {% set step = state_attr(calibration_entity, 'step') | float(1) %} {% if rounding_mode == 'manual' %} {% set step = input_calibration_step_size | float(1) %} {% endif %} {% set min_val = state_attr(calibration_entity,'min') | float(0) %} {% set max_val = state_attr(calibration_entity,'max') | float(55) %} {% if is_aggressive_mode_calibration %} {% set temp_diff = state_attr(valve,'temperature') | float(target_temperature) - value_temperature_sensor | float %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set offset_new = offset_new + input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set offset_new = offset_new - input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% set round_size = iif('.' in (step | string), (step | string).split('.')[1] | length, 0) %} {% set offset_new = ((offset_new | float(0) / step) | round(0) * step) | round(round_size) | float %} {% set offset_new = iif(offset_new > max_val, max_val, offset_new) %} {% set offset_new = iif(offset_new < min_val, min_val, offset_new) %} {% if (float(offset_old) - float(offset_new)) | abs >= float(input_calibration_delta) %} {% set n.dict = n.dict + [(calibration_entity, [{'value': offset_new, 'valve': valve}])] %} {% endif %} {% endif %} {% endfor %} {% endif %} {{ dict.from_keys(n.dict) }} # DANFOSS, POPP, HIVE calibration_dph: > {% set n = namespace(dict=[]) %} {% if is_native_calibration_trigger %} {% for valve in valves_dph %} {% set calibration_entities = device_entities(device_id(valve)) | expand | selectattr('domain','in','number') | selectattr('entity_id', 'search', input_calibration_key_word) | map(attribute='entity_id') | list %} {% if calibration_entity != calibration_entities | count > 0 %} {% set calibration_entity = calibration_entities | first %} {% set min_val = state_attr(calibration_entity,'min')%} {% set max_val = state_attr(calibration_entity,'max')%} {% set step = state_attr(calibration_entity, 'step') | float(1) %} {% if rounding_mode == 'manual' %} {% set step = input_calibration_step_size | float(1) %} {% endif %} {% set current_temp = state_attr(valve,'current_temperature') | float(20) %} {% set new_state = value_temperature_sensor | float(current_temp) %} {% set old_state = states(calibration_entity) | float %} {% if is_aggressive_mode_calibration %} {% set temp_diff = state_attr(valve,'temperature') | float(target_temperature) - value_temperature_sensor | float %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set new_state = new_state + input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set new_state = new_state - input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% if step <= 1 and max_val | string | count < 4 %} {% set round_size = iif('.' in (step | string), (step | string).split('.')[1] | length, 0) %} {% set new_state = ((new_state | float(0) / step) | round(0) * step) | round(round_size) | float %} {% else %} {% set new_state = new_state * 100 | int %} {% set old_state = old_state | int %} {% endif %} {% set update_calibration = old_state != new_state %} {% if is_calibration_trigger and not update_calibration %} {% set last_updated = [calibration_entity] | expand | map(attribute='last_updated') | first %} {% set update_calibration = as_datetime(current_time_stamp) - timedelta(minutes=20) >= last_updated %} {% endif %} {% if update_calibration %} {% set n.dict = n.dict + [(calibration_entity, [{'value': new_state, 'valve': valve}])] %} {% endif%} {% endif %} {% endfor %} {% endif %} {{ dict.from_keys(n.dict) }} # COMMON CALIBRATION e.g. TUYA calibration_common: > {% set n = namespace(dict=[]) %} {% if is_native_calibration_trigger %} {% for valve in valves_calibration_common %} {% set calibration_entities = device_entities(device_id(valve)) | expand | selectattr('domain','in','number') | selectattr('entity_id', 'search', input_calibration_key_word) | map(attribute='entity_id') | list %} {% if calibration_entities | count > 0%} {% set calibration_entity = calibration_entities | first %} {% set step = state_attr(calibration_entity, 'step') | float(1) %} {% if rounding_mode == 'manual' %} {% set step = input_calibration_step_size | float(1) %} {% endif %} {% set min_calibration_value = state_attr(calibration_entity,'min') | float %} {% set max_calibration_value = state_attr(calibration_entity,'max') | float %} {% set thermostat_temperature = state_attr(valve, 'current_temperature') | float %} {% set offset_old = states(calibration_entity) | float(0) %} {% set new_calibration_value = (-(thermostat_temperature - value_temperature_sensor) + offset_old) %} {% if is_aggressive_mode_calibration %} {% set temp_diff = state_attr(valve,'temperature') | float(target_temperature) - value_temperature_sensor %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set new_calibration_value = new_calibration_value + input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set new_calibration_value = new_calibration_value - input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% set new_calibration_value = iif(new_calibration_value > max_calibration_value, max_calibration_value, new_calibration_value) %} {% set new_calibration_value = iif(new_calibration_value < min_calibration_value, min_calibration_value, new_calibration_value) %} {% set round_size = iif('.' in (step | string), (step | string).split('.')[1] | length, 0) %} {% set offset_new = ((new_calibration_value | float(0) / step) | round(0) * step) | round(round_size) | float %} {% if (float(offset_old) - float(offset_new)) | abs >= float(input_calibration_delta) %} {% set n.dict = n.dict + [(calibration_entity, [{'value': offset_new, 'valve': valve}])] %} {% endif %} {% endif %} {% endfor %} {% endif %} {{ dict.from_keys(n.dict) }} calibration_value_set: "{{ dict(dict(calibration_xiaomi, **calibration_dph),**calibration_common) }}" ############################################################################################## ################################## CONDITIONS / BLOCKER ###################################### ############################################################################################## no_changes: > {{ (input_persons | count == 0 and input_mode_guest == none and input_schedulers | count == 0 and input_presence_sensor == none and input_proximity == none) or (is_temperature_sensor_defined and not valid_temperature_sensor) }} # conditions scene_trigger: "{{ is_scene_create_trigger or is_scene_apply_trigger or is_scene_destroy_trigger }}" change_trigger: "{{ is_changes_trigger and not scene_trigger and changes | count > 0 and not no_changes}}" reset_trigger: "{{ is_reset_trigger and not no_changes }}" calibration_trigger: "{{ is_calibration_trigger and not input_calibration_generic and (calibration_value_set | count > 0 or calibration_tado | count > 0) }}" positioning_trigger: "{{ positioning | count > 0 }}" # warnings automation_name: "{{ state_attr(this.entity_id,'friendly_name') }}" warnings: > {% set messages = [] %} {% if not is_uptime_defined %} {% set messages = messages + ['To make Advance Heating Control work properly just setup the uptime integration (https://www.home-assistant.io/integrations/uptime/)'] %} {% elif is_aggressive_mode and not input_aggressive_mode_calibration and is_physical_change_enabled %} {% set messages = messages + ['Aggressive Mode in combination with physical change / sync feature is not recommended. Expect unwanted side effects.'] %} {% elif is_generic_calibration and is_physical_change_enabled %} {% set messages = messages + ['Generic Calibration in combination with physical change / sync feature is not recommended. Expect unwanted side effects.'] %} {% elif valves_unsupported | count > 0 %} {% set messages = messages + ['Unsupported climate entities: ' + valves_unsupported | join(',') | string ] %} {% elif is_temperature_sensor_defined and not valid_temperature_sensor %} {% set messages = messages + ['The temperature sensor' + input_temperature_sensor + ' has an invalid state: ' + states(input_temperature_sensor) ] %} {% endif %} {{ messages }} climates_information: > {% set n = namespace(dict=[]) %} {% for valve in input_trvs %} {% set temperature = state_attr(valve,'temperature') %} {% set current_temperature = state_attr(valve,'current_temperature') %} {% set state = states(valve) %} {% set n.dict = n.dict + [{'entity_id': valve, 'state': state, 'temperature': temperature, 'current_temperature': current_temperature}] %} {% endfor %} {{ n.dict }} conditions: - condition: or conditions: - condition: template value_template: "{{ calibration_trigger }}" - condition: template value_template: "{{ scene_trigger }}" - condition: template value_template: "{{ change_trigger }}" - condition: template value_template: "{{ reset_trigger }}" - condition: template value_template: "{{ positioning_trigger }}" actions: - variables: is_delayed: "{{ not (not is_uptime_defined or (now() | as_datetime - states(up_time_sensor) | as_datetime) > timedelta(**input_startup_delay)) }}" - action: system_log.write data: message: > {{ 'AHC - ' + automation_name | string + ' \n ' + 'automation delayed: ' + is_delayed | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - wait_template: > {{ not is_uptime_defined or (now() | as_datetime - states(up_time_sensor) | as_datetime) > timedelta(**input_startup_delay) }} - choose: - conditions: "{{ is_delayed }}" sequence: - event: ahc_delay_event event_data: automation: "{{ this.entity_id }}" default: - if: - condition: template value_template: "{{ warnings | count > 0 }}" then: - action: system_log.write data: level: warning logger: blueprints.panhans.heatingcontrol message: > {{ 'AHC-Warnings - ' + automation_name + ':\n' + warnings | join('\n') }} - event: ahc_event event_data: state: "{{ state_ahc }}" mode: "{{ iif(set_comfort == true, 'comfort', 'eco') }}" automation: "{{ this.entity_id }}" is_person_defined: "{{ is_person_defined }}" is_anybody_home: "{{ is_anybody_home }}" is_proximity_defined: "{{ is_proximity_defined }}" is_anybody_home_or_proximity: "{{ is_anybody_home_or_proximity }}" is_guest_mode: "{{ is_guest_mode }}" active_scheduler: "{{ active_scheduler }}" state_scheduler: "{{ state_scheduler }}" state_presence_sensor: "{{ state_presence_sensor }}" state_presence_scheduler: "{{ state_presence_scheduler }}" state_presence: "{{ state_presence }}" state_proximity_arrived: "{{ state_proximity_arrived }}" state_proximity_way_home: "{{ state_proximity_way_home }}" is_force_max_temperature: "{{ is_force_max_temperature }}" is_force_eco_temperature: "{{ is_force_eco_temperature }}" active_party_entity: "{{ active_party_entity }}" party_temp: "{{ party_temp }}" is_away: "{{ is_away }}" state_window: "{{ state_window }}" is_aggressive_mode: "{{ is_aggressive_mode }}" is_frost_protection: "{{ is_frost_protection }}" is_liming_protection: "{{ is_liming_protection }}" state_outside_temp: "{{ state_outside_temp }}" entry_time: "{{ entry_time }}" thermostats: "{{ input_trvs }}" hvac_mode: "{{ mode }}" temperature_comfort: "{{ temperature_comfort }}" temperature_eco: "{{ temperature_eco }}" target_temperature: "{{ target_temperature }}" set_max_temperature: "{{ set_max_temperature }}" last_trigger_id: "{{ iif(trigger_id_defined, trigger.id, '') }}" calibration_trigger: "{{ is_generic_calibration_trigger or calibration_trigger }}" change_trigger: "{{ change_trigger }}" warnings: "{{ warnings | count > 0 }}" # calibration - if: - condition: template value_template: "{{ calibration_trigger }}" - condition: and conditions: !input input_custom_condition_calibration then: - action: system_log.write data: message: > {{ 'AHC - Calibration - ' + automation_name | string + ' \n ' + 'calibration data set: ' + calibration_value_set | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - repeat: count: "{{ calibration_value_set | count | int }}" sequence: - variables: index: "{{ repeat.index-1 }}" calibration_entity: "{{ (calibration_value_set.keys() | list) [index] }}" thermostat: "{{ (((calibration_value_set.values() | list) [index]) | first) ['valve'] }}" offset: "{{ (((calibration_value_set.values() | list) [index]) | first) ['value'] }}" select_entity: "{{ device_entities(device_id(thermostat)) | expand | selectattr('domain','in','select') | selectattr('attributes.options', 'contains', 'external') | map(attribute='entity_id') | list | first | default(none) }}" - action: system_log.write data: message: > {{ 'AHC - Calibration - ' + automation_name | string + ' \n ' + 'calibration entity: ' + calibration_entity | string + ' \n ' + 'offset: ' + offset | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - if: - condition: template value_template: "{{ thermostat in valves_xiaomi }}" - condition: template value_template: "{{ select_entity != none and not is_state(select_entity, 'external') }}" then: - action: select.select_option target: entity_id: "{{ select_entity }}" data: option: external - delay: !input input_action_call_delay - action: number.set_value data: value: "{{ float(offset) }}" target: entity_id: "{{ calibration_entity }}" - delay: !input input_action_call_delay # TADO CALIBRATION - repeat: count: "{{ calibration_tado | count | int }}" sequence: - variables: index: "{{ repeat.index-1 }}" thermostat: "{{ (calibration_tado.keys() | list) [index] }}" offset: "{{ (((calibration_tado.values() | list) [index]) | first) ['value'] }}" - action: "{{ 'tado.set_climate_temperature_offset' }}" data: offset: "{{ offset }}" entity_id: "{{ thermostat }}" - delay: !input input_action_call_delay # valve opening - if: - condition: template value_template: "{{ positioning_trigger }}" then: - repeat: count: "{{ positioning | count | int }}" sequence: - variables: index: "{{ repeat.index-1 }}" thermostat: "{{ (positioning.keys() | list) [index] }}" positioning_value: "{{ (((positioning.values() | list) [index]) | first) ['value'] }}" positioning_entity: "{{ (((positioning.values() | list) [index]) | first) ['entity'] }}" - action: system_log.write data: message: > {{ 'AHC - Positioning - ' + automation_name | string + ' \n ' + 'entity: ' + positioning_entity | string + ' \n ' + 'value: ' + positioning_value | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - action: number.set_value data: value: "{{ positioning_value | int }}" target: entity_id: "{{ positioning_entity }}" - delay: !input input_action_call_delay # scenes # scene create - if: - condition: template value_template: "{{ is_scene_create_trigger }}" - condition: template value_template: "{{ states[scene_to_create] == none }}" then: - action: scene.create data: snapshot_entities: "{{ scene_entities }}" scene_id: "{{ scene_to_create.split('.')[1] }}" # scene destroy - if: - condition: template value_template: "{{ is_scene_destroy_trigger }}" - condition: template value_template: "{{ scenes_to_destroy | count > 0 }}" then: - repeat: count: "{{ scenes_to_destroy | count | int }}" sequence: - variables: scene_to_destroy: "{{ scenes_to_destroy[repeat.index-1] }}" - if: - condition: template value_template: "{{ states[scene_to_destroy] != none }}" then: - action: scene.delete target: entity_id: "{{ scene_to_destroy }}" # scene apply - variables: scene_to_apply_tmp: > {% if scene_to_apply != none and states[scene_to_apply] != none %} {{ scene_to_apply }} {% else %} {{ scenes_all | expand | reject('==',none) | map(attribute="entity_id") | list | first | default(none) }} {% endif %} - if: - condition: template value_template: "{{ is_scene_apply_trigger }}" - condition: template value_template: "{{ scene_to_apply_tmp != none and states[scene_to_apply_tmp] != none }}" then: - action: system_log.write data: message: > {{ 'AHC - Calibration - ' + automation_name | string + ' \n ' + 'apply scene: ' + scene_to_apply_tmp | string + ' state: ' + states[scene_to_apply_tmp] | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - action: scene.turn_on target: entity_id: "{{ scene_to_apply_tmp }}" - action: scene.delete target: entity_id: "{{ scene_to_apply_tmp }}" - condition: template value_template: "{{ false }}" else: # reset - if: - condition: template value_template: "{{ is_reset_trigger }}" then: - repeat: count: "{{ reset_data | count | int }}" sequence: - action: system_log.write data: message: > {{ 'AHC - Calibration - ' + automation_name | string + ' \n ' + 'reset data: ' + reset_data | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - variables: index: "{{ repeat.index-1 }}" reset_entity: "{{ reset_data[index]['entity'] }}" reset_temp: > {% set temp_r = reset_data[index]['temp'] %} {% set t_min = state_attr(reset_entity,'min') %} {% set t_max = state_attr(reset_entity,'max') %} {% set step = state_attr(reset_entity,'step') %} {% set temp_r = ((temp_r | float(0) / step) | round(0) * step) | float %} {% set temp_r = iif(temp_r > t_max, t_max, temp_r) %} {% set temp_r = iif(temp_r < t_min, t_min, temp_r) %} {{ temp_r }} - action: input_number.set_value data: value: "{{ reset_temp }}" target: entity_id: "{{ reset_entity }}" - if: - condition: and conditions: !input input_custom_condition - condition: template value_template: "{{ changes | count | int > 0 and (not no_changes or (no_changes and state_window)) }}" then: - repeat: count: "{{ changes | count | int }}" sequence: - variables: index: "{{ repeat.index-1 }}" thermostat: "{{ (changes.keys() | list) [index] }}" mode: "{{ (((changes.values() | list) [index]) | first) ['mode'] }}" temp_target: "{{ (((changes.values() | list) [index]) | first) ['temp'] }}" - action: system_log.write data: message: > AHC - Change - {{ automation_name }} {{" \n "}} Trigger ID: {{ iif(trigger_id_defined, trigger.id, '') }} Thermostat: {{ thermostat }} {{" \n "}} Mode: {{ mode }} {{" \n "}} New Target Temp: {{ temp_target }} {{" \n "}} Current Target Temp: {{ state_attr(thermostat,'temperature') }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - if: - condition: template value_template: "{{ states(thermostat) | lower != mode | lower }}" then: - action: climate.set_hvac_mode data: entity_id: "{{ thermostat }}" hvac_mode: "{{ mode }}" - delay: !input input_action_call_delay - if: - condition: template value_template: "{{ state_attr(thermostat, 'temperature') != temp_target and mode != 'off' }}" then: - action: climate.set_temperature data: entity_id: "{{ thermostat }}" temperature: "{{ temp_target | float }}" - delay: !input input_action_call_delay - if: - condition: template value_template: "{{ input_valve_positioning_mode != 'off' }}" - condition: template value_template: "{{ changes | count | int > 0 or is_scene_apply_trigger }}" then: - delay: seconds: 10 - event: ahc_positioning_event event_data: automation: "{{ this.entity_id }}" - delay: !input input_action_call_delay # custom action - if: - condition: template value_template: "{{ input_custom_action != none }}" then: !input "input_custom_action" mode: queued 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 | blueprint: name: π₯ Advanced Heating Control V5 author: panhans homeassistant: min_version: "2024.10.0" description: > π₯ room based heating / β based on > π₯ people presence ποΈ multiple schedulers πΆ presence sensor βοΈ proximity aka geo fencing π₯Ά frost protection π‘ adjustable aggressive mode π€οΈ activation based on weather, temperature or boolean entities ποΈ granular schedule adjustments πͺ multiple window open detection π party mode π€ guest mode βοΈ liming protection π dynamic valve positioning π§ thermostat calibration for the most common devices (Tado, Aqara, Popp / Danfoss / Hive, Tuya) βοΈ several tweaks for fixing your thermostat issues π¬ custom action π€« calm & πͺ reliable **Ensure that you have installed the uptime integration:** [](https://my.home-assistant.io/redirect/integration/?domain=uptime) **Version**: 5.4.3 **Help & FAQ**: [Advanced Heating Control](https://community.home-assistant.io/t/advanced-heating-control/469873) **Documentation:** [panhans.github.io/HomeAssistant/](https://panhans.github.io/HomeAssistant/) [](https://ko-fi.com/Q5Q3QEH52) source_url: https://github.com/panhans/HomeAssistant/blob/main/blueprints/automation/panhans/advanced_heating_control.yaml domain: automation input: thermostat_section: name: Thermostats & Sensors icon: mdi:thermostat input: input_trvs: name: π₯ Thermostats / Climates description: > `thermostats` `climates` [Thermostats / Climates](https://www.home-assistant.io/integrations/climate/) to be controlled. selector: entity: filter: - domain: - climate multiple: true input_hvac_mode: name: ποΈ Operation / HVAC Mode description: > `hvac` Select the hvac mode for your [thermostats](https://www.home-assistant.io/integrations/climate/). Be sure your selected thermostats support the hvac mode you've chosen. AHC will log a warning if there is a miss match. For radiator [thermostats]((https://www.home-assistant.io/integrations/climate/)) the default is mostly *heat*. If you own an air conditioner it will support *auto* or *cool*, too. default: "heat" selector: select: options: - heat - cool - auto - heat_cool input_temperature_sensor: name: π‘οΈ Room Temperature Sensor description: > `calibration` `aggressive mode` `optional` For some features an external temperature sensor is reqired, e.g. calibration. Temperature calibration for your [thermostats](https://www.home-assistant.io/integrations/climate/). The following is supported: * Tado, Aqara, Popp, Danfoss, Hive, Bosch, SONOFF, Tuya * generic calibration Note: This is an additional sensor inside your room usually next to your favourite spot. [Thermostats](https://www.home-assistant.io/integrations/climate/) or its integration (e.g. Z2M or ZHA) except Tado should provide a seperate calibration entity. default: [] selector: entity: filter: - domain: - sensor device_class: - temperature multiple: false temperature_section: name: Temperatures icon: mdi:thermometer collapsed: true input: input_temperature_comfort_static: name: ποΈ Static Comfort Temperature description: > `comfort temperature` You can set a static comfort temperature here. default: 22 selector: number: min: 12.0 max: 86.0 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_temperature_eco_static: name: π± Static Eco Temperature description: > `eco temperature` The temperature that is set when your heating schedule is not active. default: 19 selector: number: min: 4.0 max: 75.0 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_temperature_comfort: name: ποΈ Comfort Temperature description: > `comfort temperature` `optional` To control your comfort temperature via automations or the UI, you can specify an *[input_number](https://www.home-assistant.io/integrations/input_number/)* entity here. Create your helper [here](https://my.home-assistant.io/redirect/helpers/). default: [] selector: entity: filter: - domain: - input_number multiple: false input_temperature_eco: name: π± Eco Temperature description: > `eco temperature` `optional` To control your eco temperature via automations or the UI, you can specify an *[input_number](https://www.home-assistant.io/integrations/input_number/)* entity here. Create your helper [here](https://my.home-assistant.io/redirect/helpers/). default: [] selector: entity: filter: - domain: - input_number multiple: false adjustment_section: name: Adjustments / Heating Plan icon: mdi:sun-clock collapsed: true input: input_adjustments: name: ποΈ Heating Schedule Adjustments description: > `optional` Here you can setup some adjustments to your heating schedule.<br/><br/> *Note*: Here you can set values for eco or comfort temperature. The switch between those target temperatures is controled by schedules, presence sensors, proximity, ect. <br/> <details> <summary><code><strong>CLICK HERE:</strong> Modifiers</code></summary> <br/> > π **time** > Timestamp when the adjustment should kick in. (required) > π **days** > Select days where this setting shall be enabled. > ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] > ποΈ **scheduler** > Only enable this entry if this string is part of the name of your active scheduler. > ποΈ **comfort** > Adjust comfort temperature > π± **eco** > Adjust eco temperature > π§ **calibration** > Toggle calibration > on/off > π **mode** > Overwrite the operation mode > comfort/eco/off/auto (auto means no overwrite) </details> <br/> <details> <summary><code><strong>CLICK HERE:</strong> Example</code></summary> <br/> ```yaml - time: "08:00" comfort: "20" calibration: "off" - time: "16:00" eco: "19" calibration: "on" - time: "20:00" days: ['Sat','Sun'] scheduler: 'Holidays' comfort: "24" eco: "17" ``` </details> selector: object: default: "[]" # modes mode_section: name: Force Comfort/Eco Mode icon: mdi:fire collapsed: true input: input_mode_party: name: π Party mode description: > `optional` If on, all settings are ignored and heating takes place. You can define multiple [timers](https://www.home-assistant.io/integrations/timer/) or boolean entities. If you put a number at the end of the friendly name like *Party Timer 20* this number will be taken as the desired comfort temperature for this [timer](https://www.home-assistant.io/integrations/timer/). Create your timer [here](https://my.home-assistant.io/redirect/helpers/). default: [] selector: entity: filter: - domain: - input_boolean - binary_sensor - timer multiple: true input_force_max_temperature: name: π₯΅ Force Max Temperature description: > `optional` Set the maximum temperature of all [thermostats](https://www.home-assistant.io/integrations/climate/) regardless of any other settings. **HINT:** Implemented by developer for maintenance reasons. Create your helper [here](https://my.home-assistant.io/redirect/helpers/). default: [] selector: entity: filter: - domain: - input_boolean - binary_sensor multiple: false input_force_eco_temperature: name: π± Force Eco Temperature description: > `optional` If enabled *eco* temperature will be forced. default: [] selector: entity: filter: - domain: - input_boolean - binary_sensor multiple: false input_party_legacy_restore: name: π Legacy Restore description: > `party` Enable this if the temperatures after airing (closing windows) or party won't restore properly default: false selector: boolean: temperature_tweak_section: name: Temperature Tweaks icon: mdi:knob collapsed: true input: input_off_instead_of_eco: name: π Off Instead Of Eco description: > `optional` `temperature tweak` Turn off your [thermostats](https://www.home-assistant.io/integrations/climate/) instead of lower the target temperature to eco temperature. default: false selector: boolean: input_min_instead_of_off: name: β¬οΈ Min Instead Of Off description: > `optional` `temperature tweak` Lower the temperature instead of turning them *OFF*, e.g. during airing. default: false selector: boolean: input_fahrenheit: name: π« Fahrenheit description: > `optional` `temperature tweak` Enable this if your unit of measurement is Fahrenheit (untested). default: false selector: boolean: input_reset_temperature: name: β©οΈ Reset Temperature description: > `optional` `temperature tweak` Reset your temperature entities to the values of the static temperatures after [schedule](https://www.home-assistant.io/integrations/schedule/), [proximity](https://www.home-assistant.io/integrations/proximity/), [people](https://www.home-assistant.io/integrations/person/), presence ends. The comfort entity is reset when eco takes place and vice versa. default: false selector: boolean: input_off_if_above_room_temperature: name: βοΈ Off If Above/Below Room Temperature description: > Turns your [climate](https://www.home-assistant.io/integrations/climate/) entity *off* if the target temperature is below(cooling) / above(heating) the room temperature. default: false selector: boolean: input_off_if_nobody_home: name: π πΆββ‘οΈ Off If Nobody Home description: > Turns your [climate](https://www.home-assistant.io/integrations/climate/) entity *off* if persons are set and nobody is home. default: false selector: boolean: input_physical_change: name: π§ͺ Physical Temperature Change / Sync (experimental) description: > `optional` `temperature tweak` Enable this if your want to adjust the temperature using your thermostat or thermostat card. Make sure aggressive mode and generic calibration is disabled for this feature. (experimental). You also need to set entities for eco and comfort temperature for the moment. default: false selector: boolean: # persons person_section: name: Persons icon: mdi:account-multiple collapsed: true input: input_persons: name: π₯ Persons description: > `person` `optional` You can specify [persons](https://www.home-assistant.io/integrations/person/) to make your heating plan more dynamic. If you do not use [schedulers](https://www.home-assistant.io/integrations/schedule/) or presence sensors, heating is activated as soon as someone is at home.<br/> With [schedulers](https://www.home-assistant.io/integrations/schedule/) or presence sensors, these are only active when someone is at home. default: [] selector: entity: filter: - domain: - person multiple: true input_people_entering_home_duration: name: π Enter Home Duration description: > `person` Duration for which someone must be at home for heating to be activated. default: hours: 0 minutes: 0 seconds: 2 selector: duration: input_people_leaving_home_duration: name: π¨ Leaving Home Duration description: > `person` Duration for which someone must be out of the house for heating to be deactivated. default: hours: 0 minutes: 0 seconds: 2 selector: duration: input_mode_guest: name: π€ Guest Mode description: > `person` `optional` If an entity is specified here, it is treated like a [person](https://www.home-assistant.io/integrations/person/). It's usefull when you're leaving your guests alone in your home and you are not using presence detection. * entity defined -> [person](https://www.home-assistant.io/integrations/person/) defined * enitity is *on* -> simulates [person](https://www.home-assistant.io/integrations/person/) is home * enitity is *off* -> simulates [person](https://www.home-assistant.io/integrations/person/) is away default: selector: entity: filter: - domain: - input_boolean - binary_sensor - timer multiple: false # scheduler scheduling_section: name: Scheduling icon: mdi:clock-outline collapsed: true input: input_schedulers: name: β²οΈ Schedules description: > `schedules` `optional` A [schedule](https://www.home-assistant.io/integrations/schedule/) specifies when heating to comfort temperature should take place. You can create it in the helper section of Home Assistant.<br/> If you have also specified [people](https://www.home-assistant.io/integrations/person/), someone must also be at home for heating. This is the same behaviour with a [proximity](https://www.home-assistant.io/integrations/proximity/) entity.<br/> You can create as many [schedules](https://www.home-assistant.io/integrations/schedule/) as you like. Make sure the names are clear. default: [] selector: entity: filter: - domain: - schedule multiple: true input_scheduler_selector: name: βπ» Scheduler Selector description: > `schedule` `optional` Define an entity to choose from your schedules. If you use one schedule only you can ignore this. If you use more than one schedule you have multiple possibilities to setup your selection.<br/> <details> <summary><code><strong>CLICK HERE:</strong> More information</code></summary> <br/> * toggle [input_boolean](https://www.home-assistant.io/integrations/input_boolean/) or [binary_sensor](https://www.home-assistant.io/integrations/binary_sensor/): If *off* the first defined [schedule](https://www.home-assistant.io/integrations/schedule/) is enabled. If *on* the second [schedule](https://www.home-assistant.io/integrations/schedule/) is active. More than two [schedules](https://www.home-assistant.io/integrations/schedule/) cannot be selected with binary inputs. * text [input text](https://www.home-assistant.io/integrations/input_text/), drop down [input text](https://www.home-assistant.io/integrations/input_select/) or [sensor](https://www.home-assistant.io/integrations/sensor/): * The value has to match the friendly name of the selected [schedule](https://www.home-assistant.io/integrations/schedule/) at least partially. Example: If you provide three [schedules](https://www.home-assistant.io/integrations/schedule/) called *work*, *holiday/sick*, *guest* you can select the holiday [schedule](https://www.home-assistant.io/integrations/schedule/) while setting the selection entity to *sick*, *holiday* or *holiday/sick*. This option is case insensitive. * You also can go with numbers: if you want to choose the first [schedule](https://www.home-assistant.io/integrations/schedule/) the selector entity must return the number *1*. For the 2nd number *2* and so on. </details> default: selector: entity: filter: - domain: - input_boolean - binary_sensor - input_text - input_number - input_select multiple: false # presence presence_section: name: Presence Detection icon: mdi:location-enter collapsed: true input: input_presence_sensor: name: πΆ Presence Sensor / On/Off-Entity description: > `presence detection` `optional` If you specify a presence sensor, heating will take place if it detects presence.<br/> If you have specified [persons](https://www.home-assistant.io/integrations/person/), at least one must also be at home. You also can select an [input boolean](https://www.home-assistant.io/integrations/input_boolean/) entity to realise a simple On/Off-Logic. default: selector: entity: filter: - domain: - binary_sensor - input_boolean multiple: false input_scheduler_presence: name: β²οΈ Presence Sensor Scheduler description: > `presence detection` `optional` The presence [schedule](https://www.home-assistant.io/integrations/schedule/) specifies exactly when the presence sensor should be used during the day. default: selector: entity: filter: - domain: - schedule multiple: false input_presence_reaction_on_time: name: β³ Presence Reaction On Time description: > `presence detection` Specify the duration for which the presence sensor must detect any presence so that the comfort temperature is set. default: hours: 0 minutes: 5 seconds: 0 selector: duration: input_presence_reaction_off_time: name: β Presence Reaction Off Time description: > `presence detection` Specify the duration for which the presence sensor must not detect any presence so that the eco temperature is set. default: hours: 0 minutes: 5 seconds: 0 selector: duration: # proximity proximity_section: name: Proximity icon: mdi:leak collapsed: true input: input_proximity: name: βοΈ Proximity description: > `proximity` `optional` You can preheat your rooms with help of home assistant's [proximity integration](https://www.home-assistant.io/integrations/proximity/).<br/> Just select your proxmity zone and take your adjustments to distance and duration.<br/> If you're in range of your distance and towards to your home heating kicks in.<br/> **Note**: The proximity entity is handles like a person. Comfort heating takes place when coming or beeing home. Combinations with [schedules](https://www.home-assistant.io/integrations/schedule/) are also possible. default: selector: device: filter: integration: proximity multiple: false input_proximity_duration: name: β° Proximity Duration description: > `proximity` Duration for which someone must be on way home before heating occurs. default: hours: 0 minutes: 2 seconds: 0 selector: duration: input_proximity_distance: name: βοΈ Proximity Distance description: > `proximity` The distance when [proximity](https://www.home-assistant.io/integrations/proximity/) sensor gets impact for this automation. Hint: Unit depends on the setup of your integration. default: 500 selector: number: min: 0 max: 999999999 step: 1 mode: box # away mode away_section: name: Away Mode icon: mdi:walk collapsed: true input: # AWAY OFFSET input_away_offset: name: π Away Temperature Offset description: > `scheduler` `persons` `presence` `away mode` First: This feature only works for [schedule](https://www.home-assistant.io/integrations/schedule/) and/or presence based heating combined with [persons](https://www.home-assistant.io/integrations/person/). You can define an offset for your comfort temperature that will be subtracted (heating) from or added (cooling) to your comfort temperature. If you enable this option for [schedules](https://www.home-assistant.io/integrations/schedule/) the away offset will be substracted from the comfort temperature if your schedule is *on* but nobody is at home. For presence detection this is the case if you are at home but no presence is detected. For presence detection you can also ignoring [persons](https://www.home-assistant.io/integrations/person/). So the away temperature is set when no presence is detected but the presence [schedule](https://www.home-assistant.io/integrations/schedule/) is *on*. default: 0 selector: number: min: 0 max: 10 step: 0.5 mode: slider unit_of_measurement: Β°C / Β°F input_away_scheduler_mode: name: β²οΈ Scheduler Away Mode description: > `scheduler` `away mode` Enable/Disable the Away Offset for [schedules](https://www.home-assistant.io/integrations/schedule/) based heating/cooling. default: false selector: boolean: input_away_presence_mode: name: πΆ Presence Away Mode description: > `presence` `away mode` Enable/Disable the Away Offset for presence based heating/cooling. default: false selector: boolean: input_away_presence_ignor_people: name: πΆ Ignore People For Presence Away Mode description: > `presence` `away mode` If you want to make away happen if your presence [schedule](https://www.home-assistant.io/integrations/schedule/) is on but no motion is detected regardless if somebody is at home enable this option. default: false selector: boolean: # windows window_section: name: Window & Door Detection icon: mdi:door collapsed: true input: input_windows: name: πͺ Windows & Doors description: > `airing` `optional` If open during airing your [thermostats](https://www.home-assistant.io/integrations/climate/) will be set to *off* at least to their minimum temperature if they don't support hvac mode *OFF* except you set a custom window open temperature. default: [] selector: entity: filter: - domain: - binary_sensor - sensor multiple: true input_windows_reaction_time_open: name: β³ Window & Door Reaction Time Open description: > `airing` Duration for which a window or door must be open for the [thermostats](https://www.home-assistant.io/integrations/climate/) to close. default: hours: 0 minutes: 0 seconds: 30 selector: duration: input_windows_reaction_time_close: name: β Window & Door Reaction Time Close description: > `airing` Duration for which a window or door must be closed for the [thermostats](https://www.home-assistant.io/integrations/climate/) to open. default: hours: 0 minutes: 0 seconds: 30 selector: duration: input_window_open_temperature: name: Window Open Temperature description: > `airing` If 0Β° your thermostat turns *off* or if not supported it turns to the minimum temperature of your thermostat. default: 0 selector: number: min: 0 max: 15 step: 1 mode: slider unit_of_measurement: Β°C / Β°F input_window_legacy_restore: name: ποΈ Legacy Restore description: > `airing` Enable this if the temperatures after airing (closing windows) won't restore properly. default: false selector: boolean: # calibration calibration_section: name: Calibration icon: mdi:compass description: "" collapsed: true input: input_calibration_timeout: name: β³ Calibration Timeout description: > `calibration` Define a timeout if you want to decrease the amount of calibration calls if temperature changes too much. At least the temperature of the external sensor or [thermostat](https://www.home-assistant.io/integrations/climate/) must stay for that duration before calibration gets triggered. **HINT:** A minimum timeout of 2s is recommended. default: hours: 0 minutes: 1 seconds: 0 selector: duration: input_calibration_delta: name: βοΈ Calibration Delta description: > `calibration` If the difference between the [thermostat](https://www.home-assistant.io/integrations/climate/) temperature and the external sensor temperature is greater or less than the calibration delta the [thermostat](https://www.home-assistant.io/integrations/climate/) calibration will be triggered.<br/> The lower the delta the often calibration gets triggered. **HINT:** If your thermostat supports external temperature sensor values it is recommended to set this to a lower value like 0 - 0.2. default: 0.5 selector: number: min: 0 max: 5 step: 0.1 mode: slider unit_of_measurement: Β°C / Β°F input_calibration_key_word: name: ποΈ Calibration Entity Key Word description: > `calibration` Keyword for finding the calibration entity. This word must be part of the entity id. As a rule, the entities with the word *offset*, *calibration* or *external* are marked by the integrations. Just have a look into your device overview, select your thermostat and check the naming of the *entity_ids* for the calibration. default: "calibration" selector: text: input_calibration_step_size: name: π¦Ά Step Size description: > `calibration` Usually the step size is determined automatically. You can overwrite the step size by selecting another option if you know your thermostat handles the calibration not like the entities are exposed. default: auto selector: select: mode: dropdown options: - label: Auto value: auto - label: "0.1" value: "0.1" - label: "0.5" value: "0.5" - label: "Full Values" value: "full" input_calibration_generic: name: π§ Generic Calibration description: > `generic` `calibration` Adds the difference between room and [thermostat](https://www.home-assistant.io/integrations/climate/) temperature to the target temperature. This is useful if your thermostat integration doesn't provide a special entity for calibration. Keep in mind the set temperatures for your thermostats will differ to the target temperature. default: false selector: boolean: input_generic_calibration_offset: name: βοΈ Generic Calibration Offset description: > `generic` `calibration` If the temperature difference between the thermostat and the temperature sensor is very high, the offset, i.e. the correction temperature, can be limited to this value. <details> <summary><code><strong>CLICK HERE:</strong> Example</code></summary> Generic Calibration Offset = 5Β°</br> Thermostat Temperature = 28Β°</br> Room Temperature = 18Β°</br> </br> Difference = Thermostat Temperature - Room Temperature = 10Β°</br> Difference > Generic Calibration Offset -> Corrected Difference = 5Β°</br> New Target Temperature = Thermostat Temperature + Corrected Difference = 33Β° </details> default: 5 selector: number: min: 0 max: 20 step: 1 mode: slider unit_of_measurement: Β°C / Β°F # aggressive mode aggressive_mode_section: name: Aggressive Mode icon: mdi:emoticon-angry collapsed: true input: input_aggressive_mode_range: name: π‘ Aggressive Range description: > `aggressive mode` `tweak` Activate this option if your [thermostats](https://www.home-assistant.io/integrations/climate/) react slowly or only start to react at a large temperature difference between actual and set temperature. Define a range when your real target temperature shall be set. <details> <summary><code><strong>CLICK HERE:</strong> More information</code></summary> <br/> E.g. you target temperature is 20Β°C and your room temperature is 19.5Β°C. If your range is set to 0.5Β°C the real target temperature (20Β°C) will be set when room temperature is between 19.5Β°C and 20.5Β°C. If the room temperature is above or lower than range, it gets some offset in order to force your [thermostat](https://www.home-assistant.io/integrations/climate/) to react. (see Aggressive Mode - Offset) </details> default: 0 selector: number: min: 0 max: 5 step: 0.1 mode: slider unit_of_measurement: Β°C / Β°F input_aggressive_mode_offset: name: β Aggressive Offset description: > `aggressive mode` `tweak` Here you can define the offset that will be added to your target temperature if the room temperature is not in range of your target temperature. If your room temperature is not in the defined range, e.g. 19.5Β°C - 20.5Β°C this offset will be added to your target temperature. If range is 0, then offset is always added. default: 0 selector: number: min: 0 max: 5 step: 0.5 mode: slider unit_of_measurement: Β°C / Β°F input_aggressive_mode_calibration: name: π‘οΈ Aggressive Calibration description: > `aggressive mode` `tweak` `experimental` If you'd setup an temperature sensor and your thermostats allow calibration, you can enable this feature. If enabled the aggressive offset will be add to the calibration value and not the target temperature. *Note*: This feature is marked as experimental since not every calibration method could be tested. If you notice any problems simple open an issue or post a message in the [AHC-Thread](https://community.home-assistant.io/t/advanced-heating-control/469873). Enable this only if native calibration does NOT work when using generic calibration. default: false selector: boolean: # frost protection frostprotection_section: name: Frost Protection icon: mdi:snowflake collapsed: true input: input_frost_protection_temp: name: βοΈ Frost Protection Temperature description: > `frost protection` You can set the frost protection temperature here. default: 5 selector: number: min: 5.0 max: 62.0 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_frost_protection_duration: name: βοΈ Frost Protection Fallback Duration description: > `frost protection` If the defined [persons](https://www.home-assistant.io/integrations/person/) are not at home for a longer period of time or the presence sensor has no longer detected any presence, the frost protection temperature can be lowered after a this duration. Note: If set to zero frost protection temperature never will be set. default: days: 0 hours: 0 minutes: 0 seconds: 0 selector: duration: enable_day: true # liming protection liming_protection_section: name: Liming Protection icon: mdi:pipe-valve collapsed: true input: input_liming_protection: name: ποΈ Liming Protection description: > `liming protection` Most smart thermostats come with that feature out of the box. If your thermostat doesn't support this or you're using the generic thermostat integration this feature is maybe handy for you in order to prevent your valve against limescale. The automation will set the thermostat to its max and open the valve for one minute. default: off selector: boolean: input_liming_protection_day: name: ποΈ Day description: > `liming protection` Select the day of the week for the execution. default: "Mon" selector: select: options: - label: Monday value: Mon - label: Tuesday value: Tue - label: Wednesday value: Wed - label: Thursday value: Thu - label: Friday value: Fri - label: Saturday value: Sat - label: Sunday value: Sun input_liming_protection_time: name: π Time description: > `liming protection` Select the time for the execution. default: "12:00:00" selector: time: input_liming_protection_duration: name: π Liming Protection Duration description: > `liming protection` Duration of liming protection before the thermostat is reset to its initial state. default: 1 selector: number: min: 1 max: 30 step: 1 mode: slider unit_of_measurement: min input_liming_in_winter: name: π¨οΈ Liming In Winter / Liming If Automation is Disabled description: > `liming protection` Enable this if you want liming protection even if the automation is active. default: false selector: boolean: # winter mode toggle_section: name: "On/Off Automation Options" icon: mdi:light-switch collapsed: true input: input_mode_winter: name: β Winter Mode / Automation Toggle description: > `activation` `optional` If *on* the automation is active. If *off* your valves will set to *off* and the automation is going to sleep. You can set this up with: * [input boolean](https://www.home-assistant.io/integrations/input_boolean/) * [binary sensor](https://www.home-assistant.io/integrations/binary_sensor/) Create your helper [here](https://my.home-assistant.io/redirect/helpers/). default: selector: entity: filter: - domain: - input_boolean - binary_sensor multiple: false input_invert_winter_mode_value: name: π Invert Winter Mode Value description: > `activation` If enabled the the value of the binary winter mode entity will be inverted: * off -> activates the automation * on -> disables the automation default: off selector: boolean: input_mode_outside_temperature: name: π€οΈ Outside Temperature Sensor description: > `activation` `optional` You can control the switching on and off of your thermostats via the outside temperature. To do this, select a temperature sensor or a weather entity and adjust the threshold below. * [weather entity](https://www.home-assistant.io/integrations/weather/) * [temperature sensor entity](https://www.home-assistant.io/integrations/sensor/) default: selector: entity: filter: - domain: - weather - domain: - sensor device_class: temperature multiple: false input_mode_outside_temperature_threshold: name: ποΈ Outside Temperature Threshold description: > `activation` If you'd select a temperature [sensor](https://www.home-assistant.io/integrations/sensor/) or a [weather entity](https://www.home-assistant.io/integrations/weather/) for controlling heating you can adjust the temperature threshold here. If the outside temperature falls below the threshold value, heating is activated. default: 15 selector: number: min: 5 max: 68 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_mode_room_temperature: name: π Enable Room Temperature Threshold description: > `activation` `optional` If you enable this option the value of the defined room temperature sensor and the value of the outside temperautre must be below / above its threshold. That makes sense if you go with an A/C and the room is still heated up but it has already cooled down outside. **Not recommendend for heating** default: false selector: boolean: input_mode_room_temperature_threshold: name: ποΈ Room Temperature Threshold description: > `activation` Threshold for your room temperature sensor. default: 18 selector: number: min: 5 max: 68 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F # valve positioning valve_positioning_section: name: "Dynamic Valve Positioning" icon: mdi:valve collapsed: true input: input_valve_positioning_mode: name: π¦Ά Valve Positioning Mode description: > `valve positioning` If your thermostat supports valve positioning you can enable this here. Everytime the autmation gets triggered the code checks if there is an adjustment needed. π **regular**: means linear. The valve will open close proportional to the difference of the target and room temperature. π **optimistic**: The valve opening is reduced earlier, as it is assumed that the radiator still has enough residual heat to heat the room. π **pessimistic**: The valve opening is initially left relatively open and only closes rapidly when the target temperature is almost reached. default: "off" selector: select: mode: dropdown options: - label: "off" value: "off" - label: "regular" value: "regular" - label: "optimistic" value: "optimistic" - label: "pessimistic" value: "pessimistic" input_fully_open_difference: name: βοΈ Positioning Temperature Difference description: > `valve positioning` The difference between target and set temperature when dynamic valve positioninig should happen. <details> <summary><code><strong>CLICK HERE:</strong> Example</code></summary> <br/> > Positioning Temperature Difference: 1Β° > Target Temperature: 21Β°<br/> > Positioning takes place in a range between 21Β° and 20Β° (21Β°-1Β°) > If the local temperature is 21.5Β° the valve positioning is calculated and set. > If the local temperture is below this range the valve is fully open. </details> default: 1 selector: number: min: 0.5 max: 20 step: 0.5 mode: box unit_of_measurement: Β°C / Β°F input_valve_positioning_step_size: name: π¦Ά Valve Positioning Step Size description: > `valve positioning` The step size of for opening/closing the valve. default: "10" selector: select: mode: dropdown options: - label: "5%" value: "5" - label: "10%" value: "10" - label: "20%" value: "20" input_valve_positioning_max_opening: name: ποΈ Max Opening Valve Position description: > `valve positioning` The maximal opening of the valve. Some thermostats have a maximum valve position of 80-90%. You can adjust the value here. *Force Max Temperature* still sets the value to 100%. default: 100 selector: number: min: 1 max: 100 step: 1 mode: slider unit_of_measurement: "%" input_valve_positioning_timeout: name: β±οΈ Valve Positioning Timeout description: > `valve positioning` Timeout that must lie between two adjustments before the second is executed. default: hours: 0 minutes: 20 seconds: 0 selector: duration: input_valve_opening_keyword: name: ποΈ Positioning Entity Keyword description: > `valve positioning` The key word for selecting the opening entity of your thermostats. default: "valve_opening_degree" selector: text: # tweaks tweak_section: name: Custom Settings icon: mdi:cog-box collapsed: true input: input_action_call_delay: name: βοΈ Action Call Delay description: > `tweak` Some [thermostats](https://www.home-assistant.io/integrations/climate/) have problems with setting mode and temperature. You can try to increase the delay between the action calls. This could fix your problems. default: hours: 0 minutes: 0 seconds: 2 selector: duration: input_startup_delay: name: β² Startup Delay description: > `tweak` If your AHC automation is triggered directly after a Home Assistant restart, but the required integrations have not yet been loaded or certain sensors have not yet been initialized, you can set an automation delay here. *Note:* Make sure that you have set up the uptime integration for this purpose. default: hours: 0 minutes: 0 seconds: 0 selector: duration: # custom action input_custom_action: name: π¬ Custom Action description: > `optional` This custom action gets executed with every temperature / mode change except calibration. If you want to control other devices just check states before doing an action call. Use the variable *is_heating* in your conditions. *True* means heating is active. default: selector: action: input_custom_condition: name: βοΈ Temperature Change Custom Condition description: > `optional` Define a custom condition that prevents / allows temperature changes to your thermostats. This has no impact to the rest of logic like calibration. default: selector: condition: input_custom_condition_calibration: name: βοΈ Calibration Custom Condition description: > `optional` Define a custom condition that prevents / allows calibration. default: selector: condition: input_log_level: name: βοΈ Log Level description: "" default: debug selector: select: mode: dropdown options: - info - warning - error - debug trigger_variables: # thermostats / sensors input_trvs: !input input_trvs input_temperature_sensor: !input input_temperature_sensor is_temperature_sensor_defined: "{{ input_temperature_sensor != [] }}" # people input_persons: !input input_persons input_mode_guest: !input input_mode_guest input_people_entering_home_duration: !input input_people_entering_home_duration input_people_leaving_home_duration: !input input_people_leaving_home_duration input_person_count: "{{ input_persons | count }}" is_person_defined: "{{ input_person_count > 0 }}" is_guest_mode_defined: "{{ input_mode_guest != none }}" # scheduler input_schedulers: !input input_schedulers input_scheduler_selector: !input input_scheduler_selector input_scheduler_presence: !input input_scheduler_presence is_scheduler_presence_defined: "{{ input_scheduler_presence != none }}" # temperatures input_temperature_comfort: !input input_temperature_comfort input_temperature_eco: !input input_temperature_eco input_hvac_mode: !input input_hvac_mode factor: "{{ iif(input_hvac_mode == 'cool', -1, 1) | int }}" is_heat_only_if_below_real_temp: !input input_off_if_above_room_temperature # on/ff input_mode_winter: !input input_mode_winter input_mode_outside_temperature: !input input_mode_outside_temperature input_mode_outside_temperature_threshold: !input input_mode_outside_temperature_threshold input_mode_room_temperature_threshold: !input input_mode_room_temperature_threshold input_mode_room_temperature: !input input_mode_room_temperature input_invert_winter_mode_value: !input input_invert_winter_mode_value # party / force max input_mode_party: !input input_mode_party # adjustments / heating plan input_adjustments: !input input_adjustments # calibration input_calibration_timeout: !input input_calibration_timeout # windows input_windows: !input input_windows #presence input_presence_sensor: !input input_presence_sensor is_presence_sensor_defined: "{{ input_presence_sensor != none }}" input_presence_reaction_on_time: !input input_presence_reaction_on_time input_presence_reaction_off_time: !input input_presence_reaction_off_time # proximity input_proximity: !input input_proximity input_proximity_duration: !input input_proximity_duration input_proximity_distance: !input input_proximity_distance # frost protection input_frost_protection_duration: !input input_frost_protection_duration # liming protection input_liming_protection: !input input_liming_protection input_liming_protection_day: !input input_liming_protection_day input_liming_protection_time: !input input_liming_protection_time input_liming_in_winter: !input input_liming_in_winter input_liming_protection_duration: !input input_liming_protection_duration trigger: # system - trigger: homeassistant event: start id: temperature_change_hastart - trigger: event event_type: automation_reloaded id: temperature_change_reload - trigger: event event_type: ahc_delay_event id: delayed_call_temperature_change event_data: automation: "{{ this.entity_id }}" - trigger: event event_type: ahc_positioning_event id: positioning_event event_data: automation: "{{ this.entity_id }}" # thermostats become available - trigger: state entity_id: !input input_trvs from: - unknown - unavailable for: seconds: 5 id: temperature_change_available # physical change - trigger: state entity_id: !input input_trvs attribute: temperature for: seconds: 5 id: temperature_change_valve_target # eco/comfort change - trigger: state entity_id: !input input_temperature_eco for: !input input_action_call_delay id: temperature_change_eco - trigger: state entity_id: !input input_temperature_comfort for: !input input_action_call_delay id: temperature_change_comfort # persons - trigger: template value_template: > {{ input_persons | expand | selectattr('state', 'eq', 'home') | list | count > 0 or (is_guest_mode_defined and states(input_mode_guest) in ['on','active'] ) }} id: temperature_change_person_on for: !input input_people_entering_home_duration - trigger: template value_template: > {{ input_persons | expand | selectattr('state', 'eq', 'home') | list | count == 0 and (not is_guest_mode_defined or (is_guest_mode_defined and states(input_mode_guest) not in ['on','active'])) }} id: temperature_change_person_off for: !input input_people_leaving_home_duration # scheduler - trigger: template id: temperature_change_scheduler_on value_template: > {% set selected_scheduler = none %} {% set schedules_count = input_schedulers | count %} {% if schedules_count == 0 %} {% set selected_scheduler = none %} {% elif schedules_count == 1 or input_scheduler_selector == none %} {% set selected_scheduler = input_schedulers | first %} {% elif schedules_count > 1 %} {% set selector_value = states(input_scheduler_selector) %} {% if is_number(selector_value) %} {% set selector_value = iif(selector_value | int > schedules_count, schedules_count, selector_value) %} {% set selector_value = iif(selector_value | int <= 0, 1, selector_value) %} {% set selected_scheduler = input_schedulers[selector_value | int - 1] %} {% elif selector_value in ['on','off'] %} {% set selected_scheduler = iif(selector_value == 'off', input_schedulers[0], input_schedulers[1]) %} {% else %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'eq', selector_value) | map(attribute='entity_id') | first | default(none) %} {% if (selected_scheduler == none) %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'search', '(?i)' + selector_value) | map(attribute='entity_id') | first | default(none) %} {% endif %} {% endif %} {% endif %} {% if selected_scheduler == none %} {{ false }} {% else %} {{ is_state(selected_scheduler, 'on') }} {% endif %} - trigger: template id: temperature_change_scheduler_off value_template: > {% set selected_scheduler = none %} {% set schedules_count = input_schedulers | count %} {% if schedules_count == 0 %} {% set selected_scheduler = none %} {% elif schedules_count == 1 or input_scheduler_selector == none %} {% set selected_scheduler = input_schedulers | first %} {% elif schedules_count > 1 %} {% set selector_value = states(input_scheduler_selector) %} {% if is_number(selector_value) %} {% set selector_value = iif(selector_value | int > schedules_count, schedules_count, selector_value) %} {% set selector_value = iif(selector_value | int <= 0, 1, selector_value) %} {% set selected_scheduler = input_schedulers[selector_value | int - 1] %} {% elif selector_value in ['on','off'] %} {% set selected_scheduler = iif(selector_value == 'off', input_schedulers[0], input_schedulers[1]) %} {% else %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'eq', selector_value) | map(attribute='entity_id') | first | default(none) %} {% if (selected_scheduler == none) %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'search', '(?i)' + selector_value) | map(attribute='entity_id') | first | default(none) %} {% endif %} {% endif %} {% endif %} {% if selected_scheduler == none %} {{ false }} {% else %} {{ is_state(selected_scheduler, 'off') }} {% endif %} # presence sensor - trigger: template id: temperature_change_presence_on value_template: "{{ input_presence_sensor != none and is_state(input_presence_sensor, 'on') }}" for: !input input_presence_reaction_on_time - trigger: template id: temperature_change_presence_off value_template: "{{ input_presence_sensor != none and is_state(input_presence_sensor, 'off') }}" for: !input input_presence_reaction_off_time # presence scheduler - trigger: template id: temperature_change_presence_scheduler_on value_template: "{{ input_scheduler_presence != none and is_state(input_scheduler_presence, 'on') }}" for: !input input_action_call_delay - trigger: template id: temperature_change_presence_scheduler_off value_template: "{{ input_scheduler_presence != none and is_state(input_scheduler_presence, 'off') }}" for: !input input_action_call_delay # proximity - trigger: template id: temperature_change_person_proximity_on value_template: > {% set proximity_entities = device_entities(input_proximity) %} {% set is_arrived = proximity_entities | select('is_state','arrived') | expand | selectattr('attributes.device_class', 'eq', 'enum') | list | count > 0 %} {% set entities_towards = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'enum') | map(attribute='entity_id') | select('is_state','towards') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | map(attribute='state') | reject('eq', 'unknown') | map('int') | select('<=', input_proximity_distance | int) | map('string') | list %} {% set entities_distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | selectattr('state', 'in', distances) | map(attribute='entity_id') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set entites_towards_and_in_distance = entities_towards | select('in', entities_distances) | list | count > 0 %} {{ entites_towards_and_in_distance or is_arrived }} for: !input input_proximity_duration - trigger: template id: temperature_change_person_proximity_off value_template: > {% set proximity_entities = device_entities(input_proximity) %} {% set is_arrived = proximity_entities | select('is_state','arrived') | expand | selectattr('attributes.device_class', 'eq', 'enum') | list | count > 0 %} {% set entities_towards = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'enum') | map(attribute='entity_id') | select('is_state','towards') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | map(attribute='state') | reject('eq', 'unknown') | map('int') | select('<=', input_proximity_distance | int) | map('string') | list %} {% set entities_distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | selectattr('state', 'in', distances) | map(attribute='entity_id') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set entites_towards_and_in_distance = entities_towards | select('in', entities_distances) | list | count > 0 %} {{ entites_towards_and_in_distance == false and is_arrived == false }} for: !input input_proximity_duration # window - trigger: template value_template: "{{ expand(input_windows) | selectattr('state', 'in', ['on','open','tilted']) | list | count > 0 }}" for: !input input_windows_reaction_time_open id: temperature_change_window_on - trigger: template value_template: "{{ expand(input_windows) | selectattr('state', 'in', ['on','open','tilted']) | list | count == 0 }}" for: !input input_windows_reaction_time_close id: temperature_change_window_off # on/off winter mode - trigger: template id: temperature_change_winter_mode_on value_template: > {% if input_mode_winter != none %} {% set activation_state = iif(input_invert_winter_mode_value, 'off', 'on') %} {{ is_state(input_mode_winter, activation_state) }} {% endif %} for: !input input_action_call_delay - trigger: template id: temperature_change_winter_mode_off value_template: > {% if input_mode_winter != none %} {% set activation_state = iif(input_invert_winter_mode_value, 'off', 'on') %} {{ not is_state(input_mode_winter, activation_state) }} {% endif %} for: !input input_action_call_delay # on/off temperature - trigger: template id: temperature_change_outside_on value_template: > {% if input_mode_outside_temperature == none %} {{ false }} {% else %} {% set outside_state = false %} {% set use_room_temp = input_mode_room_temperature and is_temperature_sensor_defined %} {% set room_state = iif(use_room_temp, false, true) %} {% set state = states(input_mode_outside_temperature) %} {% set state = iif(is_number(state) == true, state, state_attr(input_mode_outside_temperature,'temperature'))%} {% if is_number(state) %} {% set outside_state = (state | float - input_mode_outside_temperature_threshold | float) * factor < 0 %} {% endif %} {% if use_room_temp %} {% set state = states(input_temperature_sensor) %} {% if is_number(state) %} {% set room_state = (state | float - input_mode_room_temperature_threshold | float) * factor < 0 %} {% endif %} {% endif %} {{ room_state and outside_state }} {% endif %} for: !input input_action_call_delay - trigger: template id: temperature_change_outside_off value_template: > {% if input_mode_outside_temperature == none %} {{ false }} {% else %} {% set outside_state = false %} {% set use_room_temp = input_mode_room_temperature and is_temperature_sensor_defined %} {% set room_state = iif(use_room_temp, false, true) %} {% set state = states(input_mode_outside_temperature) %} {% set state = iif(is_number(state) == true, state, state_attr(input_mode_outside_temperature,'temperature'))%} {% if is_number(state) %} {% set outside_state = (state | float - input_mode_outside_temperature_threshold | float) * factor < 0 %} {% endif %} {% if use_room_temp %} {% set state = states(input_temperature_sensor) %} {% if is_number(state) %} {% set room_state = (state | float - input_mode_room_temperature_threshold | float) * factor < 0 %} {% endif %} {% endif %} {{ not (room_state and outside_state) }} {% endif %} for: !input input_action_call_delay # force max temp - trigger: state id: temperature_change_force_max_temperature_on entity_id: !input input_force_max_temperature for: !input input_action_call_delay # force eco temp - trigger: state id: temperature_change_force_eco_temperature_ds entity_id: !input input_force_eco_temperature for: !input input_action_call_delay # party - trigger: template id: temperature_change_party_on value_template: "{{ input_mode_party | expand | selectattr('state', 'in', ['active','on']) | list | count > 0 }}" for: !input input_action_call_delay - trigger: template value_template: "{{ input_mode_party | expand | selectattr('state', 'in', ['active','on']) | list | count == 0 }}" id: temperature_change_party_off for: !input input_action_call_delay # aggressive mode / heating above/below temp - trigger: state id: calibration_aggressive_mode_above_temp_thermostat_current_temp_change entity_id: !input input_trvs attribute: current_temperature for: !input input_calibration_timeout - trigger: state id: calibration_aggressive_mode_thermostat_temp_change entity_id: !input input_trvs attribute: temperature for: seconds: 30 - trigger: state id: aggressive_mode_above_temp_sensor_change entity_id: !input input_temperature_sensor for: seconds: 30 # calibration trigger - trigger: state id: calibration_sensor_change entity_id: !input input_temperature_sensor for: !input input_calibration_timeout - trigger: state id: calibration_popp_change entity_id: !input input_temperature_sensor for: seconds: 2 - trigger: template id: calibration_popp_ping value_template: > {% set has_valves_danfoss = input_trvs | select('is_device_attr', 'manufacturer', 'Danfoss') | list %} {% set has_valves_popp = input_trvs | select('is_device_attr', 'manufacturer', 'Popp') | list %} {% set valves_hive = input_trvs | select('is_device_attr', 'manufacturer', 'Hive') | list %} {% set valves_bosch = input_trvs | select('is_device_attr', 'manufacturer', 'Bosch') | list %} {% set has_valves = (has_valves_danfoss + has_valves_popp + valves_hive + valves_bosch) | count > 0 %} {{ has_valves and is_temperature_sensor_defined and now().strftime('%M') | int % 10 == 0 }} # heating adjustments - trigger: template id: temperature_change_heating_adjustment value_template: > {% set timestamp = now() %} {% set current_day = timestamp.strftime('%a') %} {% set current_time = timestamp.strftime('%H:%M') %} {% set plan = input_adjustments | rejectattr('time', 'undefined') | selectattr('time','eq', current_time | string) | sort(attribute='time', reverse = true) | list %} {{ plan | count > 0 and now() < now().replace(second=2) }} # liming protection - trigger: template value_template: > {% if not input_liming_protection%} {{ false }} {% else %} {% set enable_liming = true %} {% if input_mode_winter != none %} {% set enable_liming = is_state(input_mode_winter,'on') or input_liming_in_winter %} {% endif %} {% set current_timestamp = now() %} {% set is_liming_day = input_liming_protection_day == as_datetime(current_timestamp).strftime('%a') %} {% set start_hour = input_liming_protection_time.split(':')[0] | int %} {% set start_minute = input_liming_protection_time.split(':')[1] | int %} {% set today_start = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) %} {% set today_end = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) + timedelta(minutes=input_liming_protection_duration | int) %} {% set is_liming_time = as_datetime(current_timestamp) >= today_start and as_datetime(current_timestamp) <= today_end %} {{ enable_liming and is_liming_day and is_liming_time }} {% endif %} id: temperature_change_liming_protection_on - trigger: template value_template: > {% if not input_liming_protection%} {{ false }} {% else %} {% set enable_liming = true %} {% if input_mode_winter != none %} {% set enable_liming = is_state(input_mode_winter,'on') or input_liming_in_winter %} {% endif %} {% set current_timestamp = now() %} {% set current_timestamp = now() %} {% set is_liming_day = input_liming_protection_day == as_datetime(current_timestamp).strftime('%a') %} {% set start_hour = input_liming_protection_time.split(':')[0] | int %} {% set start_minute = input_liming_protection_time.split(':')[1] | int %} {% set today_start = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) %} {% set today_end = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) + timedelta(minutes=input_liming_protection_duration | int) %} {% set is_liming_time = as_datetime(current_timestamp) >= today_start and as_datetime(current_timestamp) <= today_end %} {{ not (enable_liming and is_liming_day and is_liming_time) }} {% endif %} id: temperature_change_liming_protection_off # frost protection - trigger: template id: temperature_change_frost_protection_on for: !input input_frost_protection_duration value_template: > {% set now_ts = now() %} {% set frost_protection_timestamp = as_datetime(now_ts) - timedelta(**input_frost_protection_duration) %} {% if frost_protection_timestamp == now_ts %} {{ false }} {% else %} {% set relevant_entities = [input_presence_sensor] + [input_mode_guest] + input_persons %} {% set relevant_entities_count = relevant_entities | reject('eq',none) | list | count %} {% if relevant_entities_count > 0 %} {% set presence_count = [input_presence_sensor] | reject('eq',none) | reject('is_state','on') | list | count %} {% set guest_mode_count = [input_mode_guest] | reject('eq',none) | reject('is_state','on') | list | count %} {% set person_count = input_persons | reject('is_state','home') | list | count %} {{ presence_count + guest_mode_count + person_count == relevant_entities_count }} {% else %} {{ false }} {% endif %} {% endif %} variables: ##################################################################################### ###################################### INPUTS ####################################### ##################################################################################### # thermostats / sensors input_trvs: !input input_trvs input_hvac_mode: !input input_hvac_mode input_temperature_sensor: !input input_temperature_sensor # temperatures input_temperature_comfort: !input input_temperature_comfort input_temperature_comfort_entity: "{{ iif(input_temperature_comfort == [], none, input_temperature_comfort) }}" input_temperature_comfort_static: !input input_temperature_comfort_static input_temperature_eco: !input input_temperature_eco input_temperature_eco_entity: "{{ iif(input_temperature_eco == [], none, input_temperature_eco) }}" input_temperature_eco_static: !input input_temperature_eco_static #frost protection input_frost_protection_temp: !input input_frost_protection_temp input_frost_protection_duration: !input input_frost_protection_duration #liming protection input_liming_protection: !input input_liming_protection input_liming_protection_day: !input input_liming_protection_day input_liming_protection_time: !input input_liming_protection_time input_liming_in_winter: !input input_liming_in_winter input_liming_protection_duration: !input input_liming_protection_duration # heating scheduler input_schedulers: !input input_schedulers input_scheduler_selector: !input input_scheduler_selector # presence input_presence_sensor: !input input_presence_sensor input_scheduler_presence: !input input_scheduler_presence input_presence_reaction_off_time: !input input_presence_reaction_off_time input_presence_reaction_on_time: !input input_presence_reaction_on_time # window detection input_windows: !input input_windows input_windows_reaction_time_open: !input input_windows_reaction_time_open input_windows_reaction_time_close: !input input_windows_reaction_time_close input_window_open_temperature: !input input_window_open_temperature input_party_legacy_restore: !input input_party_legacy_restore input_window_legacy_restore: !input input_window_legacy_restore is_legacy_restore: "{{ input_party_legacy_restore or input_window_legacy_restore }}" # wintermode / on/off input_mode_winter: !input input_mode_winter input_invert_winter_mode_value: !input input_invert_winter_mode_value input_mode_outside_temperature: !input input_mode_outside_temperature input_mode_outside_temperature_threshold: !input input_mode_outside_temperature_threshold input_mode_room_temperature: !input input_mode_room_temperature input_mode_room_temperature_threshold: !input input_mode_room_temperature_threshold # proximity input_proximity: !input input_proximity # people input_persons: !input input_persons input_mode_guest: !input input_mode_guest input_people_entering_home_duration: !input input_people_entering_home_duration input_people_leaving_home_duration: !input input_people_leaving_home_duration # force comfort input_mode_party: !input input_mode_party input_force_max_temperature: !input input_force_max_temperature input_force_eco_temperature: !input input_force_eco_temperature # calibration input_calibration_delta: !input input_calibration_delta input_calibration_generic: !input input_calibration_generic input_calibration_step_size: !input input_calibration_step_size input_calibration_key_word: !input input_calibration_key_word input_generic_calibration_offset: !input input_generic_calibration_offset # Aggressive Mode input_aggressive_mode_offset: !input input_aggressive_mode_offset input_aggressive_mode_range: !input input_aggressive_mode_range input_aggressive_mode_calibration: !input input_aggressive_mode_calibration # away mode input_away_offset: !input input_away_offset is_scheduler_away_mode: !input input_away_scheduler_mode is_presence_away_mode: !input input_away_presence_mode presence_ignor_people: !input input_away_presence_ignor_people # heating adjustments input_adjustments: !input input_adjustments # temperature tweaks is_reset_temperature: !input input_reset_temperature is_off_instead_min: !input input_off_instead_of_eco is_not_off_but_min: !input input_min_instead_of_off is_fahrenheit: !input input_fahrenheit is_heat_only_if_below_real_temp: !input input_off_if_above_room_temperature is_physical_change_enabled: !input input_physical_change is_off_if_nobody_home: !input input_off_if_nobody_home # custom tweaks input_action_call_delay: !input input_action_call_delay input_custom_action: !input input_custom_action input_startup_delay: !input input_startup_delay # valve positioning input_fully_open_difference: !input input_fully_open_difference input_valve_opening_keyword: !input input_valve_opening_keyword input_valve_positioning_step_size: !input input_valve_positioning_step_size input_valve_positioning_mode: !input input_valve_positioning_mode input_valve_positioning_timeout: !input input_valve_positioning_timeout input_valve_positioning_max_opening: !input input_valve_positioning_max_opening ##################################################################################### #################################### EVALUATION ##################################### ##################################################################################### # global is_temperature_sensor_defined: "{{ input_temperature_sensor != [] }}" invalid_states: > {{ ['unknown', 'unavailable'] }} value_temperature_sensor: > {% if is_temperature_sensor_defined %} {{ states(input_temperature_sensor) }} {% else %} {{ 'unknown' }} {% endif %} valid_temperature_sensor: > {{ value_temperature_sensor not in invalid_states }} factor: "{{ iif(input_hvac_mode == 'cool', -1, 1) | int }}" current_time_stamp: "{{ now() }}" is_metric: "{{ not is_temperature_sensor_defined or (is_temperature_sensor_defined and state_attr(input_temperature_sensor,VAR_UNIT_OF_MEASUREMENT) == 'Β°C') }}" # uptime up_time_sensor: "{{ integration_entities('uptime') | first | default(none) }}" is_uptime_defined: "{{ up_time_sensor != none }}" uptime: > {% if is_uptime_defined %} {{ states(up_time_sensor) | as_datetime }} {% else %} {{ current_time_stamp | as_datetime }} {% endif %} #startup delay startup_delay: > {% set start_delay_seconds = timedelta(**input_startup_delay).total_seconds() %} {% if not is_uptime_defined or start_delay_seconds == 0 %} {{ none }} {% else %} {% set difference = (current_time_stamp | as_datetime - uptime | as_datetime).total_seconds() %} {% set real_start_delay = (start_delay_seconds - difference) %} {{ iif(real_start_delay > 0, real_start_delay, none) }} {% endif %} # on/off state_outside_temp: > {% if input_mode_outside_temperature == none %} {{ none }} {% else %} {% set outside_state = false %} {% set use_room_temp = input_mode_room_temperature and valid_temperature_sensor %} {% set room_state = iif(use_room_temp, false, true) %} {% set state = states(input_mode_outside_temperature) %} {% set state = iif(is_number(state) == true, state, state_attr(input_mode_outside_temperature,'temperature'))%} {% if is_number(state) %} {% set outside_state = (state | float - input_mode_outside_temperature_threshold | float) * factor < 0 %} {% endif %} {% if use_room_temp %} {% set state = states(input_temperature_sensor) %} {% if is_number(state) %} {% set room_state = (state | float - input_mode_room_temperature_threshold | float) * factor < 0 %} {% endif %} {% endif %} {{ room_state and outside_state }} {% endif %} state_ahc: > {% set result = true %} {% if input_mode_winter != none %} {% set activation_state = iif(input_invert_winter_mode_value, 'off', 'on') %} {% set result = is_state(input_mode_winter, activation_state) %} {% endif %} {{ iif(state_outside_temp == none, result, result and state_outside_temp) }} # proximity is_proximity_defined: "{{ input_proximity != none }}" state_proximity_arrived: > {% set proximity_entities = device_entities(input_proximity) %} {% set is_arrived = proximity_entities | select('is_state','arrived') | expand | selectattr('attributes.device_class', 'eq', 'enum') | list | count > 0 %} {{ is_arrived }} state_proximity_way_home: > {% set proximity_entities = device_entities(input_proximity) %} {% set earliest_timestamp = current_time_stamp | as_datetime - timedelta(**input_proximity_duration) %} {% set uptime_duration = as_datetime(uptime) + timedelta(**input_proximity_duration) %} {% if uptime_duration > earliest_timestamp %} {% set earliest_timestamp = uptime_duration%} {% endif %} {% set entities_towards = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'enum') | selectattr('last_changed', '<=', earliest_timestamp) | map(attribute='entity_id') | select('is_state','towards') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | map(attribute='state') | reject('eq', 'unknown') | map('int') | select('<=', input_proximity_distance | int) | map('string') | list %} {% set entities_distances = proximity_entities | expand | selectattr('attributes.device_class', 'eq', 'distance') | selectattr('state', 'in', distances) | map(attribute='entity_id') | map('regex_replace','_(?=[^_]*$)(.*)', '') | list %} {% set towards_and_in_distance = entities_towards | select('in', entities_distances) | list | count > 0 %} {{ towards_and_in_distance }} # persons is_person_defined: "{{ input_persons | count > 0 or input_mode_guest != none }}" is_guest_mode: "{{ input_mode_guest != none and is_state(input_mode_guest, 'on') }}" is_anybody_home: > {% if is_guest_mode %} {{ true }} {% elif not is_person_defined %} {{ false }} {% else %} {% set on_time_delta = current_time_stamp | as_datetime - timedelta(**input_people_entering_home_duration) %} {% set off_time_delta = current_time_stamp | as_datetime - timedelta(**input_people_leaving_home_duration) %} {% set uptime_on = as_datetime(uptime) + timedelta(**input_people_entering_home_duration) %} {% set uptime_off = as_datetime(uptime) + timedelta(**input_people_leaving_home_duration) %} {% set result = false %} {% if uptime_on > on_time_delta or uptime_off > off_time_delta %} {{ input_persons | expand | selectattr('state', 'eq', 'home') | list | count > 0 }} {% else %} {% set persons_home = state_attr('zone.home','persons') | select('in', input_persons) | list %} {% set somebody_is_home = persons_home | expand | selectattr('last_changed', '<=', on_time_delta) | list | count > 0 %} {% set somebody_is_leaving = persons_home | count == 0 and ['zone.home'] | expand | map(attribute='last_changed') | first | default(off_time_delta) > off_time_delta %} {{ somebody_is_home or somebody_is_leaving }} {% endif %} {% endif %} is_anybody_home_or_proximity: "{{ is_anybody_home or state_proximity_way_home or state_proximity_arrived}}" # schedules active_scheduler: > {% set selected_scheduler = none %} {% set schedules_count = input_schedulers | count %} {% if schedules_count == 0 %} {% set selected_scheduler = none %} {% elif schedules_count == 1 or input_scheduler_selector == none %} {% set selected_scheduler = input_schedulers | first %} {% elif schedules_count > 1 %} {% set selector_value = states(input_scheduler_selector) %} {% if is_number(selector_value) %} {% set selector_value = iif(selector_value | int > schedules_count, schedules_count, selector_value) %} {% set selector_value = iif(selector_value | int <= 0, 1, selector_value) %} {% set selected_scheduler = input_schedulers[selector_value | int - 1] %} {% elif selector_value in ['on','off'] %} {% set selected_scheduler = iif(selector_value == 'off', input_schedulers[0], input_schedulers[1]) %} {% else %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'eq', selector_value) | map(attribute='entity_id') | first | default(none) %} {% if (selected_scheduler == none) %} {% set selected_scheduler = input_schedulers | expand | selectattr('attributes.friendly_name', 'search', '(?i)' + selector_value) | map(attribute='entity_id') | first | default(none) %} {% endif %} {% endif %} {% endif %} {{ selected_scheduler }} is_scheduler_defined: "{{ active_scheduler != none }}" state_scheduler: "{{ active_scheduler != none and is_state(active_scheduler,'on') }}" # presence is_presence_sensor_defined: "{{ input_presence_sensor != none }}" is_presence_scheduler_defined: "{{ input_scheduler_presence != none }}" state_presence_scheduler: "{{ is_presence_scheduler_defined and is_state(input_scheduler_presence, 'on') }}" state_presence_sensor: > {% if not is_presence_sensor_defined %} {{ false }} {% else %} {% set last_changed = [input_presence_sensor] | expand | map(attribute='last_changed') | first %} {% set sensor_state = is_state(input_presence_sensor, 'on') %} {% set reaction_time = iif(sensor_state, input_presence_reaction_on_time, input_presence_reaction_off_time) %} {% set min_timestamp = last_changed + timedelta(**reaction_time) %} {% set current_ts = current_time_stamp | as_datetime%} {% if is_uptime_defined and as_datetime(uptime) + timedelta(**reaction_time) > current_ts - timedelta(**reaction_time) %} {{ sensor_state }} {% else %} {% set is_limit = min_timestamp <= current_ts %} {{ (sensor_state == true and is_limit) or (sensor_state == false and not is_limit) }} {% endif %} {% endif %} state_presence: > {{ iif(is_presence_scheduler_defined, state_presence_scheduler and state_presence_sensor, state_presence_sensor) }} # force max temperature is_force_max_temperature: "{{ input_force_max_temperature != [] and is_state(input_force_max_temperature, 'on') }}" is_force_eco_temperature: "{{ input_force_eco_temperature != [] and is_state(input_force_eco_temperature, 'on') }}" # party active_party_entity: "{{ input_mode_party | expand | selectattr('state', 'in', ['active','on']) | map(attribute='entity_id') | first | default(none) }}" state_party: "{{ active_party_entity != none }}" party_temp: > {% set pos_party_temp = none %} {% if state_party == true %} {% set name = state_attr(active_party_entity,'friendly_name') %} {% set pos_temp = name.split(' ') | last %} {% if is_number(pos_temp) %} {% set pos_party_temp = pos_temp | float %} {% endif %} {% endif %} {{ pos_party_temp }} # away is_away: > {% if is_person_defined and not is_anybody_home_or_proximity %} {{ (is_scheduler_away_mode and state_scheduler) or (is_presence_away_mode and state_presence_scheduler and not state_presence) }} {% elif presence_ignor_people and is_presence_away_mode %} {{ state_presence_scheduler and not state_presence }} {% elif is_presence_away_mode and is_person_defined and is_anybody_home_or_proximity and not presence_ignor_people %} {{ not state_presence }} {% else %} {{ false }} {% endif %} # windows & doors state_window: > {% set current_ts = current_time_stamp | as_datetime %} {% set on_time_delta = current_ts - timedelta(**input_windows_reaction_time_open) %} {% set off_time_delta = current_ts - timedelta(**input_windows_reaction_time_close) %} {% set has_open_windows = input_windows | expand | selectattr('state', 'in', ['on','open','tilted']) | selectattr('last_changed', '<=', on_time_delta) | list | count > 0 %} {% set closed_but_not_in_duration = input_windows | expand | selectattr('state', 'in', ['off','closed']) | selectattr('last_changed', '>=', off_time_delta) | list | count > 0 %} {{ has_open_windows or closed_but_not_in_duration }} # aggressive mode is_aggressive_mode: "{{ input_aggressive_mode_offset > 0 }}" is_aggressive_mode_calibration: "{{ is_aggressive_mode and input_aggressive_mode_calibration and valid_temperature_sensor }}" # frost protection is_frost_protection: > {% set frost_protection_timestamp = as_datetime(current_time_stamp) - timedelta(**input_frost_protection_duration) %} {% if frost_protection_timestamp == as_datetime(current_time_stamp) %} {{ false }} {% else %} {% set relevant_entities = [input_presence_sensor] + [input_mode_guest] + input_persons %} {% set relevant_entities_count = relevant_entities | reject('eq',none) | list | count %} {% if relevant_entities_count > 0 %} {% set presence_count = [input_presence_sensor] | reject('eq',none) | reject('is_state','on') | expand | selectattr('last_changed', '<=', frost_protection_timestamp) | list | count %} {% set persons_count = input_persons | reject('eq',none) | reject('is_state','home') | expand | selectattr('last_changed', '<=', frost_protection_timestamp) | list | count %} {% set guest_mode_count = [input_mode_guest] | reject('eq',none) | reject('is_state','on') | expand | selectattr('last_changed', '<=', frost_protection_timestamp) | list | count %} {{ presence_count + guest_mode_count + persons_count == relevant_entities_count }} {% else %} {{ false }} {% endif %} {% endif %} # liming protection is_liming_protection: > {% if not input_liming_protection%} {{ false }} {% else %} {% set enable_liming = true %} {% if input_mode_winter != none %} {% set enable_liming = is_state(input_mode_winter,'on') or input_liming_in_winter %} {% endif %} {% set current_timestamp = now() %} {% set is_liming_day = input_liming_protection_day == as_datetime(current_timestamp).strftime('%a') %} {% set start_hour = input_liming_protection_time.split(':')[0] | int %} {% set start_minute = input_liming_protection_time.split(':')[1] | int %} {% set today_start = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) %} {% set today_end = as_datetime(current_timestamp).replace(second=0,microsecond=0,hour=start_hour,minute=start_minute) + timedelta(minutes=input_liming_protection_duration | int) %} {% set is_liming_time = as_datetime(current_timestamp) >= today_start and as_datetime(current_timestamp) <= today_end %} {{ enable_liming and is_liming_day and is_liming_time }} {% endif %} # thermostat groups valves: > {{ input_trvs | expand | selectattr('attributes.hvac_modes','search','(?i)'+input_hvac_mode) | map(attribute='entity_id') | list }} valves_unsupported: > {{ input_trvs | reject('in',valves) | list }} valves_off_mode: > {{ valves | expand | selectattr('attributes.hvac_modes','search','(?i)off') | map(attribute='entity_id') | list }} valves_without_off_mode: > {{ valves | reject('in',valves_off_mode) | list }} # tado valves_tado: "{{ valves | select('is_device_attr', 'manufacturer', 'Tado') | list }}" # valves external thermometer support valves_external: > {% set result = namespace(r=[]) %} {% for valve in valves %} {% set select = device_entities(device_id(valve)) | expand | selectattr('domain','in','select') | selectattr('attributes.options', 'contains', 'external') | map(attribute='entity_id') | list | first | default(none) %} {% if select != none %} {% set result.r = result.r + [valve] %} {% endif %} {% endfor %} {{ result.r }} # danfoss / popp / hive / Bosch valves_danfoss: "{{ valves | select('is_device_attr', 'manufacturer', 'Danfoss') | list }}" valves_popp: "{{ valves | select('is_device_attr', 'manufacturer', 'Popp') | list }}" valves_hive: "{{ valves | select('is_device_attr', 'manufacturer', 'Hive') | list }}" valves_bosch: "{{ valves | select('is_device_attr', 'manufacturer', 'Bosch') | list }}" valves_dph: "{{ valves_danfoss + valves_popp + valves_hive + valves_bosch }}" valves_calibration_common: "{{ valves | reject('in', valves_tado + valves_dph + valves_external) | list }}" # global last_comfort_entity_change: "{{ [input_temperature_comfort_entity] | expand | map(attribute='last_changed') | list | first | default(none) }}" last_eco_entity_change: "{{ [input_temperature_eco_entity] | expand | map(attribute='last_changed') | list | first | default(none) }}" ##################################################################################### ################################## ADJUSTMENTS ###################################### ##################################################################################### latest_entry_today: > {% set scheduler_name = none %} {% if active_scheduler != none %} {% set scheduler_name = state_attr(active_scheduler,'friendly_name') %} {% endif %} {% set current_ts = current_time_stamp | as_datetime %} {% set current_day = current_ts.strftime('%a') %} {% set current_time = current_ts.strftime('%H:%M') %} {% set plan = input_adjustments | rejectattr('time', 'undefined') | selectattr('time','<=', current_time| string) | list %} {% set selected_entries_days_and_schedule = plan | rejectattr('days','==',Undefined) | selectattr('days','search',current_day) | rejectattr('scheduler','==',Undefined) | selectattr('scheduler','in',scheduler_name) | list %} {% set selected_entries_days = plan | rejectattr('days','==',Undefined) | selectattr('days','search',current_day) | selectattr('scheduler','in',[Undefined]) | list %} {% set selected_entries_schedule = plan | rejectattr('scheduler','==',Undefined) | selectattr('scheduler','in',scheduler_name) | selectattr('days','in',[Undefined]) | list %} {% set selected_entries_time_only = plan | selectattr('days','in',[Undefined]) | selectattr('scheduler','in',[Undefined]) | list %} {% set selected_entries = selected_entries_days_and_schedule + selected_entries_days + selected_entries_schedule + selected_entries_time_only %} {% if selected_entries | count > 0%} {{ selected_entries | sort(attribute='time', reverse = true) | first }} {% else %} {{ none }} {% endif %} latest_entry_day_before: > {% set timestamp = as_datetime(current_time_stamp).replace(hour=23,minute=59) + timedelta(days=-1) %} {% set scheduler_name = none %} {% if active_scheduler != none %} {% set scheduler_name = state_attr(active_scheduler,'friendly_name') %} {% endif %} {% set current_day = timestamp.strftime('%a') %} {% set current_time = timestamp.strftime('%H:%M') %} {% set plan = input_adjustments | rejectattr('time', 'undefined') | selectattr('time','<=', current_time| string) | list %} {% set selected_entries_days_and_schedule = plan | rejectattr('days','==',Undefined) | selectattr('days','search',current_day) | rejectattr('scheduler','==',Undefined) | selectattr('scheduler','in',scheduler_name) | list %} {% set selected_entries_days = plan | rejectattr('days','==',Undefined) | selectattr('days','search',current_day) | selectattr('scheduler','in',[Undefined]) | list %} {% set selected_entries_schedule = plan | rejectattr('scheduler','==',Undefined) | selectattr('scheduler','in',scheduler_name) | selectattr('days','in',[Undefined]) | list %} {% set selected_entries_time_only = plan | selectattr('days','in',[Undefined]) | selectattr('scheduler','in',[Undefined]) | list %} {% set selected_entries = selected_entries_days_and_schedule + selected_entries_days + selected_entries_schedule + selected_entries_time_only %} {% if selected_entries | count > 0%} {{ selected_entries | sort(attribute='time', reverse = true) | first }} {% else %} {{ none }} {% endif %} entry: "{{ iif(latest_entry_today != none, latest_entry_today, latest_entry_day_before) }}" entry_time: > {% if entry != none %} {% set entry_hour = entry['time'].split(':')[0] | int %} {% set entry_minute = entry['time'].split(':')[1] | int %} {{ as_datetime(current_time_stamp).replace(hour=entry_hour, minute=entry_minute, second=0, microsecond=0) + timedelta(days=iif(latest_entry_today == none,-1,0)) }} {% endif %} entry_comfort_temp: > {% if entry != none and 'comfort' in entry.keys() and (last_comfort_entity_change == none or as_datetime(entry_time) > as_datetime(last_comfort_entity_change)) %} {% set entry_temp = entry['comfort']%} {% if is_number(entry_temp) %} {{ entry_temp }} {% elif states[entry_temp] != none %} {{ states(entry_temp) }} {% endif %} {% else %} {{ none }} {% endif %} entry_eco_temp: > {% if entry != none and 'eco' in entry.keys() and (last_eco_entity_change == none or as_datetime(entry_time) > as_datetime(last_eco_entity_change)) %} {% set entry_temp = entry['eco']%} {% if is_number(entry_temp) %} {{ entry_temp }} {% elif states[entry_temp] != none %} {{ states(entry_temp) }} {% endif %} {% else %} {{ none }} {% endif %} entry_calibration: > {% if entry != none and 'calibration' in entry.keys() %} {{ entry['calibration'] == 'on' }} {% else %} {{ true }} {% endif %} entry_mode: > {% if entry != none and 'mode' in entry.keys() %} {{ entry['mode'] }} {% else %} {{ 'auto' }} {% endif %} ##################################################################################### ############################### TRIGGER EVALUATION ################################## ##################################################################################### trigger_id_defined: "{{ trigger.id is defined }}" # calibration is_calibration_trigger: > {% if valves_dph | count > 0 and trigger_id_defined and trigger.id in ['calibration_popp_ping','calibration_popp_change'] %} {{ true }} {% elif is_aggressive_mode_calibration and trigger_id_defined and 'aggressive_mode' in trigger.id %} {{ true }} {% else %} {{ trigger_id_defined and 'calibration' in trigger.id and not trigger.id == 'calibration_aggressive_mode_thermostat_temp_change' }} {% endif %} # changes is_generic_calibration_trigger: "{{ is_calibration_trigger and input_calibration_generic }}" is_generic_calibration: "{{ is_generic_calibration_trigger and entry_calibration and valid_temperature_sensor }}" is_aggressive_mode_trigger: "{{ is_aggressive_mode and trigger_id_defined and 'aggressive_mode' in trigger.id }}" is_change_trigger: > {{ trigger_id_defined and 'temperature_change' in trigger.id and ('presence' in trigger.id or 'scheduler' in trigger.id or 'proximity' in trigger.id or 'person' in trigger.id or '_ds' in trigger.id) and not trigger.id == 'temperature_change_valve_target' }} set_max_temperature: "{{ is_force_max_temperature or is_liming_protection }}" is_pysical_change: > {{ trigger_id_defined and trigger.id == 'temperature_change_valve_target' and is_physical_change_enabled and not state_window and not set_max_temperature and not is_away }} is_adjustment_trigger: "{{ trigger_id_defined and trigger.id == 'temperature_change_heating_adjustment' and (entry_comfort_temp != none or entry_eco_temp != none) }}" is_reset: > {{ (is_reset_temperature and is_change_trigger) or is_pysical_change or is_adjustment_trigger }} is_changes_trigger: > {% if state_window %} {% if trigger_id_defined and 'temperature_change_window_on' in trigger.id %} {{ true }} {% elif trigger_id_defined and 'temperature_change_window_off' not in trigger.id %} {{ false }} {% endif %} {% elif trigger.platform == none %} {{ true }} {% elif trigger_id_defined and trigger.id == 'temperature_change_valve_target' %} {{ false }} {% elif is_heat_only_if_below_real_temp and trigger_id_defined and 'above_temp' in trigger.id %} {{ true }} {% elif is_aggressive_mode_calibration and is_aggressive_mode_trigger %} {{ false }} {% elif is_aggressive_mode_trigger %} {{ true }} {% elif is_generic_calibration %} {{ true }} {% else %} {{ trigger_id_defined and 'temperature_change' in trigger.id}} {% endif %} is_scene_create_trigger: > {{ trigger_id_defined and (("window_on" in trigger.id and not state_party) or ("party_on" in trigger.id and not state_window)) }} is_scene_apply_trigger: > {{ trigger_id_defined and ("window_off" in trigger.id or "party_off" in trigger.id) and not is_legacy_restore and not (state_window or state_party) }} is_scene_destroy_trigger: > {{ (is_change_trigger or trigger.id == 'temperature_change_heating_adjustment') and (state_window or state_party) }} # scene management scene_entities: "{{ valves }}" scene_window_id: "{{ 'scene.' + this.entity_id | replace('automation.','') | replace('.','_') + '_window' }}" scene_party_id: "{{ 'scene.' + this.entity_id | replace('automation.','') | replace('.','_') + '_party' }}" scenes_all: "{{ [scene_window_id, scene_party_id] }}" scene_to_apply: > {% if is_scene_apply_trigger and "window_off" in trigger.id %} {{ scene_window_id }} {% elif is_scene_apply_trigger and "party_off" in trigger.id %} {{ scene_party_id }} {% else %} {{ none }} {% endif %} scenes_to_destroy: > {% set scenes = [] %} {% if is_scene_destroy_trigger %} {% set scenes = iif(state_window, scenes + [scene_window_id], scenes) %} {% set scenes = iif(state_party, scenes + [scene_party_id], scenes) %} {% endif %} {{ scenes }} scene_to_create: > {{ iif(is_scene_create_trigger and "window_on" in trigger.id, scene_window_id, scene_party_id) }} ##################################################################################### #################################### CHANGES ######################################## ##################################################################################### set_comfort: > {% if is_force_max_temperature %} {{ true }} {% elif entry_mode == 'eco' %} {{ false }} {% elif entry_mode == 'comfort' %} {{ true }} {% elif state_party %} {{ true }} {% elif is_force_eco_temperature %} {{ false }} {% elif is_away %} {{ true }} {% elif not is_scheduler_defined and not is_presence_sensor_defined %} {{ is_anybody_home_or_proximity }} {% else %} {% set comfort_state = state_scheduler or state_presence %} {% if is_person_defined or is_proximity_defined %} {{ is_anybody_home_or_proximity and comfort_state }} {% else %} {{ comfort_state }} {% endif %} {% endif %} mode: > {% if not state_ahc %} {{ 'off' }} {% elif state_window and input_window_open_temperature | int == 0 and not set_max_temperature %} {{ 'off' }} {% elif entry_mode == 'off' %} {{ 'off' }} {% elif is_off_instead_min and not set_comfort %} {{ 'off' }} {% elif is_off_if_nobody_home and is_person_defined and not is_anybody_home_or_proximity and not set_comfort %} {{ 'off' }} {% else %} {{ input_hvac_mode }} {% endif %} temperature_comfort_of_entity: > {% if(input_temperature_comfort_entity != none) %} {{ states(input_temperature_comfort_entity) | float }} {% else %} {{ none }} {% endif %} temperature_eco_of_entity: > {% if(input_temperature_eco_entity != none) %} {{ states(input_temperature_eco_entity) | float }} {% else %} {{ none }} {% endif %} temperature_comfort: "{{ [entry_comfort_temp, temperature_comfort_of_entity, input_temperature_comfort_static] | reject('==', none) | first }}" temperature_away: "{{ temperature_comfort | float - input_away_offset }}" temperature_eco: "{{ [entry_eco_temp, temperature_eco_of_entity, input_temperature_eco_static] | reject('==', none) | first }}" target_temperature: > {% if state_window and input_window_open_temperature > 0 %} {{ input_window_open_temperature }} {% elif state_party %} {{ iif(party_temp != none, party_temp, temperature_comfort) }} {% elif is_frost_protection %} {{ input_frost_protection_temp }} {% else %} {{ iif(set_comfort, iif(is_away, temperature_away, temperature_comfort), temperature_eco) }} {% endif %} changes: > {% set n = namespace(dict=[]) %} {% set original_mode = mode %} {% if not is_changes_trigger %} {{ n.dict }} {% else %} {% for valve in input_trvs %} {% set current_valve_temp = state_attr(valve, 'current_temperature') | float(20) %} {% set current_valve_target_temp = state_attr(valve, 'temperature') | float(temperature) %} {% set current_valve_mode = states(valve) %} {% set min_temp = state_attr(valve, 'min_temp') | float(5) %} {% set max_temp = state_attr(valve, 'max_temp') | float(30) %} {% set valve_temp = target_temperature %} {% set dont_turn_off = valve in valves_without_off_mode or is_not_off_but_min or (state_window and input_window_open_temperature > 0) or set_max_temperature %} {% set ref_temp = current_valve_temp %} {% if valid_temperature_sensor %} {% set ref_temp = value_temperature_sensor | float(current_valve_temp) %} {% endif %} {% if is_heat_only_if_below_real_temp and iif(factor == 1, target_temperature <= ref_temp, target_temperature >= ref_temp) %} {% set mode = 'off' %} {% endif %} {% set valve_mode = iif(mode == 'off' and dont_turn_off, current_valve_mode, mode) %} {% if mode != 'off' %} {% if is_aggressive_mode and not is_aggressive_mode_calibration %} {% set temp_diff = valve_temp - ref_temp %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set valve_temp = valve_temp - input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set valve_temp = valve_temp + input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% if input_calibration_generic %} {% if current_valve_temp != ref_temp %} {% set offset = current_valve_temp - ref_temp %} {% set offset = iif(offset > float(input_generic_calibration_offset), input_generic_calibration_offset, offset) %} {% set offset = iif(offset < float(input_generic_calibration_offset) * -1, input_generic_calibration_offset * -1, offset) %} {% set temp_with_offset = float(valve_temp) + float(offset) %} {% set step = state_attr(valve, 'target_temp_step') | float(0.5) %} {% set temp_with_offset = (temp_with_offset | float(0) / float(step)) | round(0) * float(step) %} {% set valve_temp = iif(input_calibration_step_size == 'full', float(temp_with_offset) | round(), temp_with_offset | round(1)) %} {% endif %} {% endif %} {% endif %} {% if mode == 'off' and dont_turn_off %} {% set valve_temp = min_temp %} {% endif %} {% set valve_temp = iif(set_max_temperature, max_temp, valve_temp) %} {% set valve_temp = iif(valve_temp > max_temp, max_temp, valve_temp) %} {% set valve_temp = iif(valve_temp < min_temp, min_temp, valve_temp) %} {% if current_valve_mode != valve_mode or current_valve_target_temp != valve_temp %} {% set n.dict = n.dict + [(valve, [{'mode': valve_mode , 'temp': valve_temp}])] %} {% endif %} {% endfor %} {% set mode = original_mode %} {{ dict.from_keys(n.dict) }} {% endif %} positioning: > {% set n = namespace(dict=[]) %} {% if input_valve_positioning_mode == 'off' %} {{ n.dict }} {% else %} {% for valve in input_trvs %} {% set current_temp = state_attr(valve, 'current_temperature') | float(none) %} {% if valid_temperature_sensor %} {% set current_temp = value_temperature_sensor | float(none) %} {% endif %} {% set target_temp = state_attr(valve, 'temperature') | float(none) %} {% set open_valve_entity = device_entities(device_id(valve)) | expand | selectattr('domain','in','number') | selectattr('entity_id', 'search', input_valve_opening_keyword) | map(attribute='entity_id') | list | first | default(none) %} {% if open_valve_entity != none and current_temp != none and target_temp != none and ( (trigger_id_defined and trigger.id == 'positioning_event') or ([open_valve_entity] | expand | map(attribute='last_changed') | first) + timedelta(**input_valve_positioning_timeout) <= now() ) %} {% set opening = 100 %} {% set difference = target_temp - current_temp %} {% set step_size = input_valve_positioning_step_size | int %} {% if input_fully_open_difference > 0 and not is_force_max_temperature %} {% set opening_regular = (100 / input_fully_open_difference) * difference %} {% set opening_pessimistic = sqrt(((100 / input_fully_open_difference) * difference) | abs) * 10 %} {% set opening_optimistic = ((100 / input_fully_open_difference) * difference)**2 / 100 %} {% set opening = opening_regular %} {% set opening = iif(input_valve_positioning_mode == 'pessimistic', opening_pessimistic, opening) %} {% set opening = iif(input_valve_positioning_mode == 'optimistic', opening_optimistic, opening) %} {% set opening = iif(difference >= input_fully_open_difference, 100, opening) %} {% set opening = iif(difference < 0, 0, opening) %} {% set opening = opening / 100 * input_valve_positioning_max_opening %} {% set opening = ((opening + step_size / 2) // step_size * step_size) | int %} {% endif %} {% set open_valve_entity_value = states(open_valve_entity) | int %} {% if open_valve_entity_value != opening %} {% set n.dict = n.dict + [(valve, [{'entity': open_valve_entity , 'value': opening, 'current_temp': current_temp, 'target_temp': target_temp, 'difference': difference}])] %} {% endif %} {% endif %} {% endfor %} {{ dict.from_keys(n.dict) }} {% endif %} # reset temperature reset_data: > {% set result = [] %} {% if is_adjustment_trigger %} {% if entry_comfort_temp != none and input_temperature_comfort_entity != none %} {% set result = result + [{'entity': input_temperature_comfort_entity, 'temp': entry_comfort_temp}] %} {% endif %} {% if entry_eco_temp != none and input_temperature_eco_entity != none %} {% set result = result + [{'entity': input_temperature_eco_entity, 'temp': entry_eco_temp}] %} {% endif %} {% else %} {% set entity = none %} {% if is_reset and set_comfort %} {% set entity = iif(is_pysical_change, input_temperature_comfort_entity, input_temperature_eco_entity) %} {% elif is_reset and not set_comfort %} {% set entity = iif(is_pysical_change, input_temperature_eco_entity, input_temperature_comfort_entity) %} {% endif %} {% set temp_r = none %} {% if is_pysical_change %} {% set temp_r = state_attr(trigger.to_state.entity_id,'temperature') %} {% else %} {% set temp_r = iif(is_reset and entity == input_temperature_eco_entity, input_temperature_eco_static, input_temperature_comfort_static) %} {% endif %} {% if entity != none and temp_r != none %} {% set result = result + [{'entity': entity, 'temp': temp_r}] %} {% endif %} {% endif %} {{ result }} is_reset_trigger: "{{ is_reset and reset_data | count > 0 }}" ##################################################################################### ################################## CALIBRATION ###################################### ##################################################################################### is_native_calibration: "{{ not input_calibration_generic and entry_calibration and valid_temperature_sensor }}" is_native_calibration_trigger: "{{ is_calibration_trigger and is_native_calibration }}" rounding_mode: > {% if is_number(input_calibration_step_size) or input_calibration_step_size == 'full' %} {{ 'manual' }} {% else %} {{ 'auto' }} {% endif %} # TADO calibration_tado: > {% set n = namespace(dict=[]) %} {% if is_native_calibration_trigger %} {% for valve in valves_tado %} {% set offset_old = state_attr(valve, 'offset_celsius') | float(0) %} {% set local_temperature = state_attr(valve, 'current_temperature') | float %} {% set calibration_sensor_temperature = value_temperature_sensor | float %} {% set offset_new = (-(local_temperature - calibration_sensor_temperature) + offset_old) %} {% if is_aggressive_mode_calibration %} {% set temp_diff = state_attr(valve,'temperature') | float(target_temperature) - calibration_sensor_temperature %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set offset_new = offset_new + input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set offset_new = offset_new - input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% set t_min = -10.9 %} {% set t_max = 10.9 %} {% set offset_new = iif(offset_new > t_max, t_max, offset_new) %} {% set offset_new = iif(offset_new < t_min, t_min, offset_new) %} {% set offset_new = offset_new | round(1) %} {% if (float(offset_old) - float(offset_new)) | abs >= float(input_calibration_delta) %} {% set n.dict = n.dict + [(valve, [{'value': offset_new }])] %} {% endif %} {% endfor %} {% endif %} {{ dict.from_keys(n.dict) }} # XIAOMI / AQARA / SONOFF calibration_external: > {% set n = namespace(dict=[]) %} {% if is_native_calibration_trigger %} {% for valve in valves_external %} {% set calibration_entities = device_entities(device_id(valve)) | expand | selectattr('domain','in','number') | selectattr('entity_id', 'search', input_calibration_key_word) | map(attribute='entity_id') | list %} {% if calibration_entities | count > 0 %} {% set calibration_entity = calibration_entities | first %} {% set offset_old = states(calibration_entity) | float(0) %} {% set offset_new = value_temperature_sensor | float %} {% set step = state_attr(calibration_entity, 'step') | float(1) %} {% if rounding_mode == 'manual' %} {% set step = input_calibration_step_size | float(1) %} {% endif %} {% set min_val = state_attr(calibration_entity,'min') | float(0) %} {% set max_val = state_attr(calibration_entity,'max') | float(55) %} {% if is_aggressive_mode_calibration %} {% set temp_diff = state_attr(valve,'temperature') | float(target_temperature) - value_temperature_sensor | float %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set offset_new = offset_new + input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set offset_new = offset_new - input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% set round_size = iif('.' in (step | string), (step | string).split('.')[1] | length, 0) %} {% set offset_new = ((offset_new | float(0) / step) | round(0) * step) | round(round_size) | float %} {% set offset_new = iif(offset_new > max_val, max_val, offset_new) %} {% set offset_new = iif(offset_new < min_val, min_val, offset_new) %} {% if (float(offset_old) - float(offset_new)) | abs >= float(input_calibration_delta) %} {% set n.dict = n.dict + [(calibration_entity, [{'value': offset_new, 'valve': valve}])] %} {% endif %} {% endif %} {% endfor %} {% endif %} {{ dict.from_keys(n.dict) }} # DANFOSS, POPP, HIVE, BOSCH calibration_dph: > {% set n = namespace(dict=[]) %} {% if is_native_calibration_trigger %} {% for valve in valves_dph %} {% set calibration_entities = device_entities(device_id(valve)) | expand | selectattr('domain','in','number') | selectattr('entity_id', 'search', input_calibration_key_word) | map(attribute='entity_id') | list %} {% if calibration_entities | count > 0 %} {% set calibration_entity = calibration_entities | first %} {% set min_val = state_attr(calibration_entity,'min')%} {% set max_val = state_attr(calibration_entity,'max')%} {% set step = state_attr(calibration_entity, 'step') | float(1) %} {% if rounding_mode == 'manual' %} {% set step = input_calibration_step_size | float(1) %} {% endif %} {% set current_temp = state_attr(valve,'current_temperature') | float(20) %} {% set new_state = value_temperature_sensor | float(current_temp) %} {% set old_state = states(calibration_entity) | float %} {% if is_aggressive_mode_calibration %} {% set temp_diff = state_attr(valve,'temperature') | float(target_temperature) - value_temperature_sensor | float %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set new_state = new_state + input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set new_state = new_state - input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% if step <= 1 and max_val | float < 1000 %} {% set round_size = iif('.' in (step | string), (step | string).split('.')[1] | length, 0) %} {% set new_state = ((new_state | float(0) / step) | round(0) * step) | round(round_size) | float %} {% else %} {% set new_state = new_state * 100 | int %} {% set old_state = old_state | int %} {% endif %} {% set update_calibration = old_state != new_state %} {% if is_calibration_trigger and not update_calibration %} {% set last_updated = [calibration_entity] | expand | map(attribute='last_updated') | first %} {% set update_calibration = as_datetime(current_time_stamp) - timedelta(minutes=20) >= last_updated %} {% endif %} {% if update_calibration %} {% set n.dict = n.dict + [(calibration_entity, [{'value': new_state, 'valve': valve}])] %} {% endif%} {% endif %} {% endfor %} {% endif %} {{ dict.from_keys(n.dict) }} # COMMON CALIBRATION e.g. TUYA calibration_common: > {% set n = namespace(dict=[]) %} {% if is_native_calibration_trigger %} {% for valve in valves_calibration_common %} {% set calibration_entities = device_entities(device_id(valve)) | expand | selectattr('domain','in','number') | selectattr('entity_id', 'search', input_calibration_key_word) | map(attribute='entity_id') | list %} {% if calibration_entities | count > 0%} {% set calibration_entity = calibration_entities | first %} {% set step = state_attr(calibration_entity, 'step') | float(1) %} {% if rounding_mode == 'manual' %} {% set step = input_calibration_step_size | float(1) %} {% endif %} {% set min_calibration_value = state_attr(calibration_entity,'min') | float %} {% set max_calibration_value = state_attr(calibration_entity,'max') | float %} {% set thermostat_temperature = state_attr(valve, 'current_temperature') | float %} {% set offset_old = states(calibration_entity) | float(0) %} {% set new_calibration_value = (-(thermostat_temperature - value_temperature_sensor) + offset_old) %} {% if is_aggressive_mode_calibration %} {% set temp_diff = state_attr(valve,'temperature') | float(target_temperature) - value_temperature_sensor %} {% if temp_diff * factor < input_aggressive_mode_range * -1 %} {% set new_calibration_value = new_calibration_value + input_aggressive_mode_offset * factor %} {% elif temp_diff * factor > input_aggressive_mode_range %} {% set new_calibration_value = new_calibration_value - input_aggressive_mode_offset * factor %} {% endif %} {% endif %} {% set new_calibration_value = iif(new_calibration_value > max_calibration_value, max_calibration_value, new_calibration_value) %} {% set new_calibration_value = iif(new_calibration_value < min_calibration_value, min_calibration_value, new_calibration_value) %} {% set round_size = iif('.' in (step | string), (step | string).split('.')[1] | length, 0) %} {% set offset_new = ((new_calibration_value | float(0) / step) | round(0) * step) | round(round_size) | float %} {% if (float(offset_old) - float(offset_new)) | abs >= float(input_calibration_delta) %} {% set n.dict = n.dict + [(calibration_entity, [{'value': offset_new, 'valve': valve}])] %} {% endif %} {% endif %} {% endfor %} {% endif %} {{ dict.from_keys(n.dict) }} calibration_value_set: "{{ dict(dict(calibration_external, **calibration_dph),**calibration_common) }}" ############################################################################################## ################################## CONDITIONS / BLOCKER ###################################### ############################################################################################## no_changes: > {{ (input_persons | count == 0 and input_mode_guest == none and input_schedulers | count == 0 and input_presence_sensor == none and input_proximity == none) or (is_temperature_sensor_defined and not valid_temperature_sensor) }} # conditions scene_trigger: "{{ is_scene_create_trigger or is_scene_apply_trigger or is_scene_destroy_trigger }}" change_trigger: "{{ is_changes_trigger and not scene_trigger and changes | count > 0 and not no_changes}}" reset_trigger: "{{ is_reset_trigger and not no_changes }}" calibration_trigger: "{{ is_calibration_trigger and not input_calibration_generic and (calibration_value_set | count > 0 or calibration_tado | count > 0) }}" positioning_trigger: "{{ positioning | count > 0 }}" # warnings automation_name: "{{ state_attr(this.entity_id,'friendly_name') }}" warnings: > {% set messages = [] %} {% if not is_uptime_defined %} {% set messages = messages + ['To make Advance Heating Control work properly just setup the uptime integration (https://www.home-assistant.io/integrations/uptime/)'] %} {% elif is_aggressive_mode and not input_aggressive_mode_calibration and is_physical_change_enabled %} {% set messages = messages + ['Aggressive Mode in combination with physical change / sync feature is not recommended. Expect unwanted side effects.'] %} {% elif is_generic_calibration and is_physical_change_enabled %} {% set messages = messages + ['Generic Calibration in combination with physical change / sync feature is not recommended. Expect unwanted side effects.'] %} {% elif valves_unsupported | count > 0 %} {% set messages = messages + ['Unsupported climate entities: ' + valves_unsupported | join(',') | string ] %} {% elif is_temperature_sensor_defined and not valid_temperature_sensor %} {% set messages = messages + ['The temperature sensor' + input_temperature_sensor + ' has an invalid state: ' + states(input_temperature_sensor) ] %} {% endif %} {{ messages }} climates_information: > {% set n = namespace(dict=[]) %} {% for valve in input_trvs %} {% set temperature = state_attr(valve,'temperature') %} {% set current_temperature = state_attr(valve,'current_temperature') %} {% set state = states(valve) %} {% set n.dict = n.dict + [{'entity_id': valve, 'state': state, 'temperature': temperature, 'current_temperature': current_temperature}] %} {% endfor %} {{ n.dict }} conditions: - condition: or conditions: - condition: template value_template: "{{ calibration_trigger }}" - condition: template value_template: "{{ scene_trigger }}" - condition: template value_template: "{{ change_trigger }}" - condition: template value_template: "{{ reset_trigger }}" - condition: template value_template: "{{ positioning_trigger }}" actions: - variables: is_delayed: "{{ not (not is_uptime_defined or (now() | as_datetime - states(up_time_sensor) | as_datetime) > timedelta(**input_startup_delay)) }}" - action: system_log.write data: message: > {{ 'AHC - ' + automation_name | string + ' \n ' + 'automation delayed: ' + is_delayed | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - wait_template: > {{ not is_uptime_defined or (now() | as_datetime - states(up_time_sensor) | as_datetime) > timedelta(**input_startup_delay) }} - choose: - conditions: "{{ is_delayed }}" sequence: - event: ahc_delay_event event_data: automation: "{{ this.entity_id }}" default: - if: - condition: template value_template: "{{ warnings | count > 0 }}" then: - action: system_log.write data: level: warning logger: blueprints.panhans.heatingcontrol message: > {{ 'AHC-Warnings - ' + automation_name + ':\n' + warnings | join('\n') }} - event: ahc_event event_data: state: "{{ state_ahc }}" mode: "{{ iif(set_comfort == true, 'comfort', 'eco') }}" automation: "{{ this.entity_id }}" is_person_defined: "{{ is_person_defined }}" is_anybody_home: "{{ is_anybody_home }}" is_proximity_defined: "{{ is_proximity_defined }}" is_anybody_home_or_proximity: "{{ is_anybody_home_or_proximity }}" is_guest_mode: "{{ is_guest_mode }}" active_scheduler: "{{ active_scheduler }}" state_scheduler: "{{ state_scheduler }}" state_presence_sensor: "{{ state_presence_sensor }}" state_presence_scheduler: "{{ state_presence_scheduler }}" state_presence: "{{ state_presence }}" state_proximity_arrived: "{{ state_proximity_arrived }}" state_proximity_way_home: "{{ state_proximity_way_home }}" is_force_max_temperature: "{{ is_force_max_temperature }}" is_force_eco_temperature: "{{ is_force_eco_temperature }}" active_party_entity: "{{ active_party_entity }}" party_temp: "{{ party_temp }}" is_away: "{{ is_away }}" state_window: "{{ state_window }}" is_aggressive_mode: "{{ is_aggressive_mode }}" is_frost_protection: "{{ is_frost_protection }}" is_liming_protection: "{{ is_liming_protection }}" state_outside_temp: "{{ state_outside_temp }}" entry_time: "{{ entry_time }}" thermostats: "{{ input_trvs }}" hvac_mode: "{{ mode }}" temperature_comfort: "{{ temperature_comfort }}" temperature_eco: "{{ temperature_eco }}" target_temperature: "{{ target_temperature }}" set_max_temperature: "{{ set_max_temperature }}" last_trigger_id: "{{ iif(trigger_id_defined, trigger.id, '') }}" calibration_trigger: "{{ is_generic_calibration_trigger or calibration_trigger }}" change_trigger: "{{ change_trigger }}" warnings: "{{ warnings | count > 0 }}" # calibration - if: - condition: template value_template: "{{ calibration_trigger }}" - condition: and conditions: !input input_custom_condition_calibration then: - action: system_log.write data: message: > {{ 'AHC - Calibration - ' + automation_name | string + ' \n ' + 'calibration data set: ' + calibration_value_set | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - repeat: count: "{{ calibration_value_set | count | int }}" sequence: - variables: index: "{{ repeat.index-1 }}" calibration_entity: "{{ (calibration_value_set.keys() | list) [index] }}" thermostat: "{{ (((calibration_value_set.values() | list) [index]) | first) ['valve'] }}" offset: "{{ (((calibration_value_set.values() | list) [index]) | first) ['value'] }}" select_entity: "{{ device_entities(device_id(thermostat)) | expand | selectattr('domain','in','select') | selectattr('attributes.options', 'contains', 'external') | map(attribute='entity_id') | list | first | default(none) }}" is_external: "{{ select_entity != none and not is_state(select_entity, 'external') }}" - action: system_log.write data: message: > {{ 'AHC - Calibration - ' + automation_name | string + ' \n ' + 'calibration entity: ' + calibration_entity | string + ' \n ' + 'offset: ' + offset | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - if: - condition: template value_template: "{{ is_external }}" then: - action: select.select_option target: entity_id: "{{ select_entity }}" data: option: external - delay: !input input_action_call_delay - action: number.set_value data: value: "{{ float(offset) }}" target: entity_id: "{{ calibration_entity }}" - delay: !input input_action_call_delay # TADO CALIBRATION - repeat: count: "{{ calibration_tado | count | int }}" sequence: - variables: index: "{{ repeat.index-1 }}" thermostat: "{{ (calibration_tado.keys() | list) [index] }}" offset: "{{ (((calibration_tado.values() | list) [index]) | first) ['value'] }}" - action: "{{ 'tado.set_climate_temperature_offset' }}" data: offset: "{{ offset }}" entity_id: "{{ thermostat }}" - delay: !input input_action_call_delay # valve opening - if: - condition: template value_template: "{{ positioning_trigger }}" then: - repeat: count: "{{ positioning | count | int }}" sequence: - variables: index: "{{ repeat.index-1 }}" thermostat: "{{ (positioning.keys() | list) [index] }}" positioning_value: "{{ (((positioning.values() | list) [index]) | first) ['value'] }}" positioning_entity: "{{ (((positioning.values() | list) [index]) | first) ['entity'] }}" - action: system_log.write data: message: > {{ 'AHC - Positioning - ' + automation_name | string + ' \n ' + 'entity: ' + positioning_entity | string + ' \n ' + 'value: ' + positioning_value | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - action: number.set_value data: value: "{{ positioning_value | int }}" target: entity_id: "{{ positioning_entity }}" - delay: !input input_action_call_delay # scenes # scene create - if: - condition: template value_template: "{{ is_scene_create_trigger }}" - condition: template value_template: "{{ states[scene_to_create] == none }}" then: - action: scene.create data: snapshot_entities: "{{ scene_entities }}" scene_id: "{{ scene_to_create.split('.')[1] }}" # scene destroy - if: - condition: template value_template: "{{ is_scene_destroy_trigger }}" - condition: template value_template: "{{ scenes_to_destroy | count > 0 }}" then: - repeat: count: "{{ scenes_to_destroy | count | int }}" sequence: - variables: scene_to_destroy: "{{ scenes_to_destroy[repeat.index-1] }}" - if: - condition: template value_template: "{{ states[scene_to_destroy] != none }}" then: - action: scene.delete target: entity_id: "{{ scene_to_destroy }}" # scene apply - variables: scene_to_apply_tmp: > {% if scene_to_apply != none and states[scene_to_apply] != none %} {{ scene_to_apply }} {% else %} {{ scenes_all | expand | reject('==',none) | map(attribute="entity_id") | list | first | default(none) }} {% endif %} - if: - condition: template value_template: "{{ is_scene_apply_trigger }}" - condition: template value_template: "{{ scene_to_apply_tmp != none and states[scene_to_apply_tmp] != none }}" then: - action: system_log.write data: message: > {{ 'AHC - Calibration - ' + automation_name | string + ' \n ' + 'apply scene: ' + scene_to_apply_tmp | string + ' state: ' + states[scene_to_apply_tmp] | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - action: scene.turn_on target: entity_id: "{{ scene_to_apply_tmp }}" - action: scene.delete target: entity_id: "{{ scene_to_apply_tmp }}" - condition: template value_template: "{{ false }}" else: # reset - if: - condition: template value_template: "{{ is_reset_trigger }}" then: - repeat: count: "{{ reset_data | count | int }}" sequence: - action: system_log.write data: message: > {{ 'AHC - Calibration - ' + automation_name | string + ' \n ' + 'reset data: ' + reset_data | string }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - variables: index: "{{ repeat.index-1 }}" reset_entity: "{{ reset_data[index]['entity'] }}" reset_temp: > {% set temp_r = reset_data[index]['temp'] %} {% set t_min = state_attr(reset_entity,'min') %} {% set t_max = state_attr(reset_entity,'max') %} {% set step = state_attr(reset_entity,'step') %} {% set temp_r = ((temp_r | float(0) / step) | round(0) * step) | float %} {% set temp_r = iif(temp_r > t_max, t_max, temp_r) %} {% set temp_r = iif(temp_r < t_min, t_min, temp_r) %} {{ temp_r }} - action: input_number.set_value data: value: "{{ reset_temp }}" target: entity_id: "{{ reset_entity }}" - if: - condition: and conditions: !input input_custom_condition - condition: template value_template: "{{ changes | count | int > 0 and (not no_changes or (no_changes and state_window)) }}" then: - repeat: count: "{{ changes | count | int }}" sequence: - variables: index: "{{ repeat.index-1 }}" thermostat: "{{ (changes.keys() | list) [index] }}" mode: "{{ (((changes.values() | list) [index]) | first) ['mode'] }}" temp_target: "{{ (((changes.values() | list) [index]) | first) ['temp'] }}" - action: system_log.write data: message: > AHC - Change - {{ automation_name }} {{" \n "}} Trigger ID: {{ iif(trigger_id_defined, trigger.id, '') }} Thermostat: {{ thermostat }} {{" \n "}} Mode: {{ mode }} {{" \n "}} New Target Temp: {{ temp_target }} {{" \n "}} Current Target Temp: {{ state_attr(thermostat,'temperature') }} level: !input input_log_level logger: blueprints.panhans.heatingcontrol - if: - condition: template value_template: "{{ states(thermostat) | lower != mode | lower }}" then: - action: climate.set_hvac_mode data: entity_id: "{{ thermostat }}" hvac_mode: "{{ mode }}" - delay: !input input_action_call_delay - if: - condition: template value_template: "{{ state_attr(thermostat, 'temperature') != temp_target and mode != 'off' }}" then: - action: climate.set_temperature data: entity_id: "{{ thermostat }}" temperature: "{{ temp_target | float }}" - delay: !input input_action_call_delay - if: - condition: template value_template: "{{ input_valve_positioning_mode != 'off' }}" - condition: template value_template: "{{ changes | count | int > 0 or is_scene_apply_trigger }}" then: - delay: seconds: 10 - event: ahc_positioning_event event_data: automation: "{{ this.entity_id }}" - delay: !input input_action_call_delay # custom action - if: - condition: template value_template: "{{ input_custom_action != none }}" then: !input "input_custom_action" mode: queued 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 |
For now, Differences are performed on text, not graphically, only the latest screenshot is available.
2 hours ago