Created
December 24, 2025 10:43
-
-
Save Komzpa/f2da05c0a0ba1706cbd1deaab68c98ff to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {# ============================ | |
| SENSOR INPUTS (edit entity_ids if needed) | |
| ============================ #} | |
| {% set s_occ_air = 'sensor.living_room_occupied_temperature_10_min' %} | |
| {% set s_work_air = 'sensor.workplace_temperature' %} | |
| {% set s_floor_min = 'sensor.living_room_floor_min_temperature' %} | |
| {% set s_floor_surface = 'sensor.living_room_floor_temperature' %} | |
| {% set s_floor_max = 'sensor.living_room_floor_max_temperature' %} | |
| {% set s_bookshelf = 'sensor.bookshelf_temperature' %} | |
| {% set s_work_floor = 'sensor.workplace_floor_temperature' %} | |
| {# Optional trend sensors (°C per hour). If you do not have them, leave as-is (they will behave as 0). #} | |
| {% set s_floor_surface_slope = 'sensor.living_room_floor_temperature_slope_c_per_h' %} | |
| {% set s_work_floor_slope = 'sensor.workplace_floor_temperature_slope_c_per_h' %} | |
| {% set t_occ_air = states(s_occ_air) | float(none) %} | |
| {% set t_work_air = states(s_work_air) | float(none) %} | |
| {% set t_floor_min = states(s_floor_min) | float(none) %} | |
| {% set t_floor_surface = states(s_floor_surface) | float(none) %} | |
| {% set t_floor_max = states(s_floor_max) | float(none) %} | |
| {% set t_bookshelf = states(s_bookshelf) | float(none) %} | |
| {% set t_work_floor = states(s_work_floor) | float(none) %} | |
| {% set slope_floor_surface = states(s_floor_surface_slope) | float(0) %} | |
| {% set slope_work_floor = states(s_work_floor_slope) | float(0) %} | |
| {# If we have no useful base sensors, return unknown #} | |
| {% if (t_occ_air is none) and (t_work_air is none) and (t_floor_min is none) %} | |
| unknown | |
| {% else %} | |
| {# ============================ | |
| TUNABLES (all knobs are here) | |
| ============================ #} | |
| {# Base behavior: | |
| This is your "normal comfort temp" when nothing violates boundaries. | |
| We compute it as a weighted average of two air sensors. #} | |
| {% set base_w_occ_air = 1.0 %} {# weight of occupied air sensor in normal mode #} | |
| {% set base_w_work_air = 1.0 %} {# weight of workplace air sensor in normal mode #} | |
| {# Cold corridor: | |
| low_target is the boundary you care about (corner should not go below it). | |
| low_band is softness: smaller reacts more aggressively around the boundary. #} | |
| {% set low_target = 19.0 %} {# °C, cold boundary you want to protect (anti-freeze) #} | |
| {% set low_band = 2.0 %} {# °C, how wide the "ramp" is around low_target #} | |
| {# Cold weighting shape: | |
| cold_priority controls how hard it pulls exactly at the boundary (when cold == low_target). | |
| alpha_cold controls how sharp the pull grows when going below the boundary. #} | |
| {% set cold_priority = 8.0 %} {# multiplier at boundary; 5..20 typical. Higher = more jump to cold #} | |
| {% set alpha_cold = 2.0 %} {# exponent slope; higher = more aggressive once below boundary #} | |
| {# Which sensors participate in the cold sentinel (min()): | |
| include_floor_min = true means floor_min participates as "cold spot". | |
| If floor_min causes unwanted behavior, set it to false. #} | |
| {% set include_floor_min = true %} | |
| {# Hot corridor thresholds (failsafe-like): | |
| These are the temperatures where you want the system to back off. | |
| Each has its own band (softness) so they can ramp in smoothly. #} | |
| {% set high_floor_max = 28.0 %} {# °C, floor max threshold (general "too hot" indicator) #} | |
| {% set high_floor_max_band = 2.0 %} {# °C, softness around high_floor_max #} | |
| {% set high_floor_surface = 26.0 %} {# °C, floor surface threshold (feet comfort limit) #} | |
| {% set high_floor_surface_band = 1.0 %} {# °C, softness around high_floor_surface #} | |
| {% set high_bookshelf = 30.0 %} {# °C, air hotspot threshold (bookshelf corner overheat) #} | |
| {% set high_bookshelf_band = 2.0 %} {# °C, softness around high_bookshelf #} | |
| {% set high_work_floor = 31.0 %} {# °C, workplace in-floor threshold (feet melting sensor) #} | |
| {% set high_work_floor_band = 0.7 %} {# °C, small band = very strict near this threshold #} | |
| {# Hot weighting shape: | |
| hot_priority controls pull when exactly at threshold (score = 0). | |
| alpha_hot controls how fast it dominates once above threshold. #} | |
| {% set hot_priority = 1.0 %} {# multiplier at threshold; raise if you want earlier domination #} | |
| {% set alpha_hot = 4.0 %} {# exponent slope; higher = stronger safety clamp on overshoot #} | |
| {# Trend anticipation: | |
| lead_time_hours predicts future hot temps: T_pred = T_now + max(0, slope)*lead_time_hours | |
| If you do not have slope sensors, this still works but behaves as lead_time=0. #} | |
| {% set lead_time_hours = 0.5 %} {# hours, 0.5 means anticipate 30 minutes ahead #} | |
| {# General softmax stability: | |
| base_priority is the "always on" weight of the base temperature. | |
| clamp_score limits exponent blowups (bigger = more extreme weights possible). #} | |
| {% set base_priority = 1.0 %} {# baseline weight; smaller = boundaries win more often #} | |
| {% set clamp_score = 6.0 %} {# clamp for scores before exponent; 4..8 typical #} | |
| {# Math constant for exp approximation: exp(x) = e**x #} | |
| {% set e = 2.718281828 %} | |
| {# ============================ | |
| BASE TEMPERATURE (normal regime) | |
| ============================ #} | |
| {% set base_sum = 0.0 %} | |
| {% set base_wsum = 0.0 %} | |
| {% if t_occ_air is not none %} | |
| {% set base_sum = base_sum + base_w_occ_air * t_occ_air %} | |
| {% set base_wsum = base_wsum + base_w_occ_air %} | |
| {% endif %} | |
| {% if t_work_air is not none %} | |
| {% set base_sum = base_sum + base_w_work_air * t_work_air %} | |
| {% set base_wsum = base_wsum + base_w_work_air %} | |
| {% endif %} | |
| {% if base_wsum > 0 %} | |
| {% set t_base = base_sum / base_wsum %} | |
| {% else %} | |
| {# fallback to something available #} | |
| {% set t_base = (t_floor_min if t_floor_min is not none else 0) %} | |
| {% endif %} | |
| {# ============================ | |
| COLD SENTINEL (min temperature you care about) | |
| ============================ #} | |
| {% set cold_list = [t_occ_air, t_work_air] %} | |
| {% if include_floor_min %} | |
| {% set cold_list = cold_list + [t_floor_min] %} | |
| {% endif %} | |
| {% set cold_list = cold_list | reject('eq', none) | list %} | |
| {% set t_cold = (cold_list | min) if (cold_list | length > 0) else t_base %} | |
| {# cold score: | |
| positive when t_cold is below low_target | |
| 0 at boundary | |
| negative when safely above #} | |
| {% set s_cold = (low_target - t_cold) / low_band %} | |
| {# clamp s_cold to avoid huge exponents #} | |
| {% set s_cold_c = [[s_cold, clamp_score] | min, -clamp_score] | max %} | |
| {# ============================ | |
| HOT SENTINEL (worst offender) | |
| ============================ #} | |
| {# Predict future hot temps using positive slope only #} | |
| {% set slope_floor_surface_pos = (slope_floor_surface if slope_floor_surface > 0 else 0) %} | |
| {% set slope_work_floor_pos = (slope_work_floor if slope_work_floor > 0 else 0) %} | |
| {% set t_floor_surface_pred = (t_floor_surface if t_floor_surface is not none else t_base) + slope_floor_surface_pos * lead_time_hours %} | |
| {% set t_work_floor_pred = (t_work_floor if t_work_floor is not none else t_base) + slope_work_floor_pos * lead_time_hours %} | |
| {# Hot score per sensor: | |
| positive when above its threshold | |
| 0 at threshold | |
| negative when below threshold #} | |
| {% set s_hot_floor_max = ((t_floor_max if t_floor_max is not none else t_base) - high_floor_max) / high_floor_max_band %} | |
| {% set s_hot_floor_surface = (t_floor_surface_pred - high_floor_surface) / high_floor_surface_band %} | |
| {% set s_hot_bookshelf = ((t_bookshelf if t_bookshelf is not none else t_base) - high_bookshelf) / high_bookshelf_band %} | |
| {% set s_hot_work_floor = (t_work_floor_pred - high_work_floor) / high_work_floor_band %} | |
| {# One hot score to rule them all: take the maximum normalized overshoot #} | |
| {% set s_hot = [s_hot_floor_max, s_hot_floor_surface, s_hot_bookshelf, s_hot_work_floor] | max %} | |
| {# clamp s_hot #} | |
| {% set s_hot_c = [[s_hot, clamp_score] | min, -clamp_score] | max %} | |
| {# Hot sentinel temperature (used for blending, not only scoring) #} | |
| {% set hot_list = [t_floor_max, t_floor_surface_pred, t_bookshelf, t_work_floor_pred] | reject('eq', none) | list %} | |
| {% set t_hot = (hot_list | max) if (hot_list | length > 0) else t_base %} | |
| {# ============================ | |
| SOFTMAX-LIKE BLEND | |
| ============================ #} | |
| {# Weights: | |
| At boundary (score=0): | |
| w_base = base_priority | |
| w_cold = cold_priority | |
| w_hot = hot_priority | |
| So cold_priority directly controls how close we jump toward cold at low_target. #} | |
| {% set w_base = base_priority %} | |
| {% set w_cold = cold_priority * (e ** (alpha_cold * s_cold_c)) %} | |
| {% set w_hot = hot_priority * (e ** (alpha_hot * s_hot_c)) %} | |
| {% set denom = w_base + w_cold + w_hot %} | |
| {% set t_virtual = (w_base*t_base + w_cold*t_cold + w_hot*t_hot) / denom %} | |
| {{ t_virtual | round(2) }} | |
| {% endif %} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment