Home Assistant Adaptive AC
| # ┌────────────────────────────────────────────────────────────────────┐ | |
| # │ Adaptive AC │ | |
| # │ │ | |
| # │ Raises and lowers the thermostat's target temperature based on how │ | |
| # │ long it has been actively heating or cooling over a configurable │ | |
| # │ lookback period. │ | |
| # └────────────────────────────────────────────────────────────────────┘ | |
| input_boolean: | |
| adaptive_ac_enabled: | |
| name: Adaptive AC Enabled | |
| icon: mdi:air-conditioner | |
| input_number: | |
| adaptive_ac_lookback_period: | |
| # How far back in time do the automations look. Two hours works best for my use case. | |
| name: Adaptive AC Lookback Period | |
| initial: 7200 | |
| min: 3600 | |
| max: 86400 | |
| adaptive_ac_active_time_floor: | |
| # When cooling time drops below this many seconds, the lower automation triggers. | |
| # When heating time drops below this many seconds, the raise automation triggers. | |
| name: Adaptive AC Active Time Floor | |
| initial: 3600 | |
| min: 0 | |
| max: 86400 | |
| adaptive_ac_active_time_ceiling: | |
| # When cooling time reaches this many seconds, the raise automation triggers. | |
| # When heating time reaches this many seconds, the lower automation triggers. | |
| name: Adaptive AC Active Time Ceiling | |
| initial: 6000 | |
| min: 0 | |
| max: 86400 | |
| adaptive_ac_raise_debounce_period: | |
| # A debouncing period that must elapse after the raise automation has triggered before it | |
| # can be triggered again. | |
| name: Adaptive AC Raise Debounce Period | |
| initial: 600 | |
| min: 0 | |
| max: 86400 | |
| adaptive_ac_lower_debounce_period: | |
| # A debouncing period that must elapse after the lower automation has triggered before it | |
| # can be triggered again. | |
| name: Adaptive AC Lower Debounce Period | |
| initial: 3600 | |
| min: 0 | |
| max: 86400 | |
| adaptive_ac_temp_ceiling: | |
| # The maximum temperature that the thermostat can be raised to. This also controls | |
| # whether the raise automation can trigger. If the thermostat is set to a temperature | |
| # greater than this, the raise automation will not trigger. | |
| name: Adaptive AC Temp Ceiling | |
| initial: 75 | |
| min: 60 | |
| max: 90 | |
| adaptive_ac_temp_floor: | |
| # The minimum temperature that the thermostat can be lowered to. This also controls | |
| # whether the lower automation can trigger. If the thermostat is set to a temperature | |
| # lower than this, the lower automation will not trigger. | |
| name: Adaptive AC Temp Floor | |
| initial: 70 | |
| min: 60 | |
| max: 90 | |
| input_text: | |
| adaptive_ac_time_window_start: | |
| # The start of the time window in which the automations can be triggered. | |
| name: Adaptive AC Time Window Start | |
| initial: "08:00" | |
| adaptive_ac_time_window_end: | |
| # The end of the time window in which the automations can be triggered. | |
| name: Adaptive AC Time Window End | |
| initial: "22:00" | |
| sensor: | |
| # These sensors record how long the thermostat has been in a given state during the | |
| # lookback window. | |
| - platform: history_stats | |
| name: Adaptive AC Cooling Hours | |
| unique_id: adaptive_ac_cooling_hours | |
| entity_id: sensor.hallway_hvac_action | |
| state: cooling | |
| type: time | |
| start: | | |
| {% set lookback_period = states('input_number.adaptive_ac_lookback_period') | int(0) %} | |
| {{ now() - timedelta(seconds=lookback_period) }} | |
| end: '{{ now() }}' | |
| - platform: history_stats | |
| name: Adaptive AC Heating Hours | |
| unique_id: adaptive_ac_heating_hours | |
| entity_id: sensor.hallway_hvac_action | |
| state: heating | |
| type: time | |
| start: | | |
| {% set lookback_period = states('input_number.adaptive_ac_lookback_period') | int(0) %} | |
| {{ now() - timedelta(seconds=lookback_period) }} | |
| end: '{{ now() }}' | |
| - platform: history_stats | |
| name: Adaptive AC Idle Hours | |
| unique_id: adaptive_ac_idle_hours | |
| entity_id: sensor.hallway_hvac_action | |
| state: idle | |
| type: time | |
| start: | | |
| {% set lookback_period = states('input_number.adaptive_ac_lookback_period') | int(0) %} | |
| {{ now() - timedelta(seconds=lookback_period) }} | |
| end: '{{ now() }}' | |
| - platform: history_stats | |
| name: Adaptive AC Off Hours | |
| unique_id: adaptive_ac_off_hours | |
| entity_id: sensor.hallway_hvac_action | |
| state: off | |
| type: time | |
| start: | | |
| {% set lookback_period = states('input_number.adaptive_ac_lookback_period') | int(0) %} | |
| {{ now() - timedelta(seconds=lookback_period) }} | |
| end: '{{ now() }}' | |
| template: | |
| - sensor: | |
| # These sensors convert HA's native float time data type to seconds. This is intended to make | |
| # configuration more accessible. | |
| - name: Adaptive AC Cooling Time | |
| unique_id: adaptive_ac_cooling_time | |
| unit_of_measurement: s | |
| device_class: duration | |
| state_class: measurement | |
| state: > | |
| {{ ((states('sensor.adaptive_ac_cooling_hours') | float(0)) * 3600) | round | int }} | |
| - name: Adaptive AC Heating Time | |
| unique_id: adaptive_ac_heating_time | |
| unit_of_measurement: s | |
| device_class: duration | |
| state_class: measurement | |
| state: > | |
| {{ ((states('sensor.adaptive_ac_heating_hours') | float(0)) * 3600) | round | int }} | |
| - name: Adaptive AC Idle Time | |
| unique_id: adaptive_ac_idle_time | |
| unit_of_measurement: s | |
| device_class: duration | |
| state_class: measurement | |
| state: > | |
| {{ ((states('sensor.adaptive_ac_idle_hours') | float(0)) * 3600) | round | int }} | |
| - name: Adaptive AC Off Time | |
| unique_id: adaptive_ac_off_time | |
| unit_of_measurement: s | |
| device_class: duration | |
| state_class: measurement | |
| state: > | |
| {{ ((states('sensor.adaptive_ac_off_hours') | float(0)) * 3600) | round | int }} | |
| - name: Adaptive AC Unaccounted Time | |
| unique_id: adaptive_ac_unaccounted_time | |
| unit_of_measurement: s | |
| device_class: duration | |
| state_class: measurement | |
| state: > | |
| {% set lookback_seconds = states('input_number.adaptive_ac_lookback_period') | float(0) %} | |
| {% set cooling = states('sensor.adaptive_ac_cooling_time') | float(0) %} | |
| {% set heating = states('sensor.adaptive_ac_heating_time') | float(0) %} | |
| {% set idle = states('sensor.adaptive_ac_idle_time') | float(0) %} | |
| {% set off = states('sensor.adaptive_ac_off_time') | float(0) %} | |
| {{ ([lookback_seconds - cooling - heating - idle - off, 0] | max) | round | int }} | |
| - binary_sensor: | |
| # Is the current time within the time window? | |
| - name: Adaptive AC In Window | |
| unique_id: adaptive_ac_in_window | |
| state: > | |
| {% set start = strptime(states('input_text.adaptive_ac_time_window_start'), '%H:%M').time() %} | |
| {% set end = strptime(states('input_text.adaptive_ac_time_window_end'), '%H:%M').time() %} | |
| {% set current = now().time() %} | |
| {{ (start <= current < end) if start <= end else (current >= start or current < end) }} | |
| automation: | |
| - id: adaptive_ac_raise_temp | |
| alias: Adaptive AC Raise Temp | |
| mode: single | |
| trigger: | |
| - platform: template | |
| value_template: | | |
| {% set cooling_time = states('sensor.adaptive_ac_cooling_time') | float(0) %} | |
| {% set heating_time = states('sensor.adaptive_ac_heating_time') | float(0) %} | |
| {% set ceiling = states('input_number.adaptive_ac_active_time_ceiling') | float(0) %} | |
| {% set floor = states('input_number.adaptive_ac_active_time_floor') | float(0) %} | |
| {{ cooling_time >= ceiling or heating_time < floor }} | |
| # Backstop in case the ceiling was crossed during the debounce period: cooling has | |
| # been active for the entire lookback window. | |
| - platform: template | |
| value_template: | | |
| {% set cooling_time = states('sensor.adaptive_ac_cooling_time') | float(0) %} | |
| {% set lookback_period = states('input_number.adaptive_ac_lookback_period') | float(0) %} | |
| {{ cooling_time == lookback_period }} | |
| # Backstop in case the floor was crossed during the debounce period: heating hasn't | |
| # been active at all during the lookback window. | |
| - platform: template | |
| value_template: | | |
| {% set heating_time = states('sensor.adaptive_ac_heating_time') | float(0) %} | |
| {{ heating_time == 0 }} | |
| condition: | |
| - condition: state | |
| entity_id: input_boolean.adaptive_ac_enabled | |
| state: "on" | |
| - condition: state | |
| entity_id: binary_sensor.adaptive_ac_in_window | |
| state: "on" | |
| - condition: numeric_state | |
| entity_id: climate.hallway | |
| attribute: temperature | |
| below: input_number.adaptive_ac_temp_ceiling | |
| - condition: template | |
| value_template: | | |
| {% set last_triggered = states.automation | selectattr('attributes.id', 'eq', 'adaptive_ac_raise_temp') | map(attribute='attributes.last_triggered') | first %} | |
| {% set debounce_period = states('input_number.adaptive_ac_raise_debounce_period') | int(0) %} | |
| {{ last_triggered is none or (now().timestamp() - last_triggered.timestamp()) >= debounce_period }} | |
| action: | |
| - service: climate.set_temperature | |
| data: | |
| temperature: "{{ state_attr('climate.hallway', 'temperature') + 1 }}" | |
| target: | |
| entity_id: climate.hallway | |
| - id: adaptive_ac_lower_temp | |
| alias: Adaptive AC Lower Temp | |
| trigger: | |
| - platform: template | |
| value_template: | | |
| {% set cooling_time = states('sensor.adaptive_ac_cooling_time') | float(0) %} | |
| {% set heating_time = states('sensor.adaptive_ac_heating_time') | float(0) %} | |
| {% set floor = states('input_number.adaptive_ac_active_time_floor') | float(0) %} | |
| {% set ceiling = states('input_number.adaptive_ac_active_time_ceiling') | float(0) %} | |
| {{ cooling_time < floor or heating_time >= ceiling }} | |
| # Backstop in case the floor was crossed during the debounce period: cooling hasn't | |
| # been active at all during the lookback window. | |
| - platform: template | |
| value_template: | | |
| {% set cooling_time = states('sensor.adaptive_ac_cooling_time') | float(0) %} | |
| {{ cooling_time == 0 }} | |
| # Backstop in case the ceiling was crossed during the debounce period: heating has | |
| # been active for the entire lookback window. | |
| - platform: template | |
| value_template: | | |
| {% set heating_time = states('sensor.adaptive_ac_heating_time') | float(0) %} | |
| {% set lookback_period = states('input_number.adaptive_ac_lookback_period') | float(0) %} | |
| {{ heating_time == lookback_period }} | |
| condition: | |
| - condition: state | |
| entity_id: input_boolean.adaptive_ac_enabled | |
| state: "on" | |
| - condition: state | |
| entity_id: binary_sensor.adaptive_ac_in_window | |
| state: "on" | |
| - condition: numeric_state | |
| entity_id: climate.hallway | |
| attribute: temperature | |
| above: input_number.adaptive_ac_temp_floor | |
| - condition: template | |
| value_template: | | |
| {% set last_triggered = states.automation | selectattr('attributes.id', 'eq', 'adaptive_ac_lower_temp') | map(attribute='attributes.last_triggered') | first %} | |
| {% set debounce_period = states('input_number.adaptive_ac_lower_debounce_period') | int(0) %} | |
| {{ last_triggered is none or (now().timestamp() - last_triggered.timestamp()) >= debounce_period }} | |
| action: | |
| - service: climate.set_temperature | |
| data: | |
| temperature: "{{ state_attr('climate.hallway', 'temperature') - 1 }}" | |
| target: | |
| entity_id: climate.hallway | |
| mode: single |
| # A little Apex Chart | |
| type: vertical-stack | |
| cards: | |
| - type: tile | |
| entity: input_boolean.adaptive_ac_enabled | |
| name: Adaptive AC | |
| icon: mdi:air-conditioner | |
| show_entity_picture: false | |
| features: | |
| - type: toggle | |
| - type: custom:apexcharts-card | |
| chart_type: donut | |
| header: | |
| show: true | |
| title: Adaptive AC Cooling Time | |
| show_states: true | |
| colorize_states: true | |
| stacked: true | |
| apex_config: | |
| dataLabels: | |
| formatter: | | |
| EVAL:function(val, opts) { | |
| const totalSeconds = opts.w.globals.series[opts.seriesIndex]; | |
| const totalMinutes = Math.round(totalSeconds / 60); | |
| const h = Math.floor(totalMinutes / 60); | |
| const m = totalMinutes % 60; | |
| let label = m + 'm' | |
| if (h > 0) { | |
| label = h + 'h ' + label; | |
| } | |
| return label; | |
| } | |
| legend: | |
| formatter: | | |
| EVAL:function(seriesName, opts) { | |
| const totalSeconds = opts.w.globals.series[opts.seriesIndex]; | |
| const totalMinutes = Math.round(totalSeconds / 60); | |
| const h = Math.floor(totalMinutes / 60); | |
| const m = totalMinutes % 60; | |
| let label = m + 'm' | |
| if (h > 0) { | |
| label = h + 'h ' + label; | |
| } | |
| return seriesName + ': ' + label; | |
| } | |
| series: | |
| - entity: sensor.adaptive_ac_cooling_time | |
| color: blue | |
| name: Cooling | |
| show: | |
| legend_value: true | |
| datalabels: percent | |
| in_header: false | |
| - entity: sensor.adaptive_ac_heating_time | |
| color: red | |
| name: Heating | |
| show: | |
| legend_value: true | |
| datalabels: percent | |
| in_header: false | |
| - entity: sensor.adaptive_ac_idle_time | |
| color: grey | |
| name: Idle | |
| show: | |
| legend_value: true | |
| datalabels: percent | |
| in_header: false | |
| - entity: sensor.adaptive_ac_off_time | |
| color: black | |
| name: "Off" | |
| show: | |
| legend_value: true | |
| datalabels: percent | |
| in_header: false | |
| - entity: input_number.adaptive_ac_lookback_period | |
| name: Lookback Period | |
| show: | |
| in_chart: false | |
| in_header: raw | |
| as_duration: second | |
| - entity: input_number.adaptive_ac_active_time_floor | |
| name: Time Floor | |
| show: | |
| in_chart: false | |
| in_header: raw | |
| as_duration: second | |
| - entity: input_number.adaptive_ac_active_time_ceiling | |
| name: Time Ceiling | |
| show: | |
| in_chart: false | |
| in_header: raw | |
| as_duration: second |
评论
?
参与讨论