Home Assistant Adaptive AC

adaptive_ac.yaml

# ┌────────────────────────────────────────────────────────────────────┐
# │ 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

chart.yaml

# 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
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论