parent
656a3106d4
commit
ef6273eb90
@ -0,0 +1,65 @@
|
||||
{# Downstairs cooling policy. Durable intent lives in the climate package helpers.
|
||||
Native Nest scheduling/learning must be cleared/disabled before enabling this policy.
|
||||
Never infer a manual change from an unavailable state or an unconfirmed command. #}
|
||||
{% macro downstairs_matches(expected) -%}
|
||||
{% set mode = states('climate.downstairs') %}
|
||||
{% set preset = state_attr('climate.downstairs', 'preset_mode') %}
|
||||
{% set target = state_attr('climate.downstairs', 'temperature') | float(none) %}
|
||||
{{ (mode == expected.get('mode') and
|
||||
(mode == 'off' or (preset == expected.get('preset') and
|
||||
(preset == 'eco' or target == expected.get('temperature'))))) | to_json }}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro downstairs_policy() -%}
|
||||
{% set stamp = as_timestamp(now()) %}
|
||||
{% set expected = states('input_text.downstairs_hvac_expected') | from_json({}) %}
|
||||
{% set hold = states('input_text.downstairs_hvac_override') | from_json({}) %}
|
||||
{% set phase = states('input_select.downstairs_hvac_period') %}
|
||||
{% set held = hold.get('kind') == 'off' or hold.get('phase') == phase %}
|
||||
{% set grid = states.binary_sensor.powerwall_grid_status %}
|
||||
{% set entries = states.group.entry_points %}
|
||||
{% set family = states.group.family %}
|
||||
{% set grid_age = stamp - as_timestamp(grid.last_changed, stamp) if grid is not none else 0 %}
|
||||
{% set entry_age = stamp - as_timestamp(entries.last_changed, stamp) if entries is not none else 0 %}
|
||||
{% set away = is_state('group.family', 'not_home') and
|
||||
stamp - as_timestamp(family.last_changed) >= 600 and is_state('input_boolean.guest_mode', 'off') %}
|
||||
{% set guest = is_state('input_boolean.guest_mode', 'on') %}
|
||||
{% set ns = namespace(valid=true, mode='cool', preset='none',
|
||||
target=76 if guest else (77 if phase == 'sleep' else 80),
|
||||
reason='guest mode' if guest else phase ~ ' schedule') %}
|
||||
{% if is_state('binary_sensor.powerwall_grid_status', 'off') %}
|
||||
{% set ns.valid = grid_age >= 360 %}
|
||||
{% set ns.mode = 'off' %}
|
||||
{% set ns.reason = 'grid outage' %}
|
||||
{% elif not is_state('binary_sensor.powerwall_grid_status', 'on') or states('group.entry_points') not in ['on', 'off'] %}
|
||||
{% set ns.valid = false %}
|
||||
{% set ns.reason = 'waiting for safety sensors' %}
|
||||
{% elif is_state('group.entry_points', 'on') %}
|
||||
{% set ns.valid = entry_age >= 300 %}
|
||||
{% set ns.mode = 'off' %}
|
||||
{% set ns.reason = 'entry open' %}
|
||||
{% elif (expected.get('reason') == 'grid outage' and grid_age < 600) or
|
||||
(expected.get('reason') == 'entry open' and entry_age < 300) %}
|
||||
{% set ns.valid = false %}
|
||||
{% set ns.reason = 'safety recovery delay' %}
|
||||
{% elif phase not in ['day', 'sleep'] or
|
||||
states('group.family') not in ['home', 'not_home'] or not has_value('input_boolean.guest_mode') %}
|
||||
{% set ns.valid = false %}
|
||||
{% set ns.reason = 'waiting for bed period or presence' %}
|
||||
{% elif held and hold.get('kind') == 'off' %}
|
||||
{% set ns.mode = 'off' %}
|
||||
{% set ns.reason = 'manual off' %}
|
||||
{% elif away %}
|
||||
{% set ns.preset = 'eco' %}
|
||||
{% set ns.reason = 'family away' %}
|
||||
{% elif held and hold.get('kind') == 'eco' %}
|
||||
{% set ns.preset = 'eco' %}
|
||||
{% set ns.reason = 'manual eco' %}
|
||||
{% elif held and hold.get('kind') == 'temperature' %}
|
||||
{% set ns.target = hold.get('temperature', ns.target) %}
|
||||
{% set ns.reason = 'manual temperature' %}
|
||||
{% endif %}
|
||||
{{ dict(valid=ns.valid, mode=ns.mode, preset=ns.preset,
|
||||
temperature=ns.target if ns.mode == 'cool' and ns.preset == 'none' else none,
|
||||
reason=ns.reason, phase=phase, held=held) | to_json }}
|
||||
{%- endmacro %}
|
||||
@ -0,0 +1,77 @@
|
||||
{# Advisory only. Weather uses Pirate Weather and air quality uses Airly CAQI.
|
||||
The dew-point comparison avoids recommending cool but moisture-laden air. #}
|
||||
{% macro fresh_air_sunset() -%}
|
||||
{% set sunset = as_datetime(state_attr('sun.sun', 'next_setting'), none) %}
|
||||
{{ as_timestamp(sunset) if is_state('sun.sun', 'above_horizon') and sunset is not none
|
||||
and as_local(sunset).date() == now().date() and sunset > now() else 0 }}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro fresh_air_conditions() -%}
|
||||
{% set inside = state_attr('climate.downstairs', 'current_temperature') | float(none) %}
|
||||
{% set target = state_attr('climate.downstairs', 'temperature') | float(none) %}
|
||||
{% set humidity = state_attr('climate.downstairs', 'current_humidity') | float(none) %}
|
||||
{% set outside = states('sensor.pirateweather_temperature') | float(none) %}
|
||||
{% set outside_dew = state_attr('weather.pirateweather', 'dew_point') | float(none) %}
|
||||
{% set airly = states.sensor.bear_stone_common_air_quality_index %}
|
||||
{% set caqi = airly.state | float(none) if airly is not none else none %}
|
||||
{% set rain = states('sensor.pirateweather_precip_intensity') | float(none) %}
|
||||
{% set rain_chance = states('sensor.pirateweather_precip_probability') | float(none) %}
|
||||
{% set gust = states('sensor.pirateweather_wind_gust') | float(none) %}
|
||||
{% set alerts = states('sensor.pirateweather_alerts') | int(-1) %}
|
||||
{% set values_valid = inside is not none and 40 <= inside <= 110
|
||||
and humidity is not none and 0 < humidity <= 100
|
||||
and outside is not none and -80 <= outside <= 140 and outside_dew is not none
|
||||
and rain is not none and rain_chance is not none and gust is not none and alerts >= 0
|
||||
and has_value('climate.downstairs') and has_value('weather.pirateweather')
|
||||
and state_attr('climate.downstairs', 'temperature_unit') == '\u00b0F'
|
||||
and state_attr('weather.pirateweather', 'temperature_unit') == '\u00b0F' %}
|
||||
{% set inside_dew = none %}
|
||||
{% if values_valid %}
|
||||
{% set celsius = (inside - 32) * 5 / 9 %}
|
||||
{% set gamma = log(humidity / 100) + 17.625 * celsius / (243.04 + celsius) %}
|
||||
{% set inside_dew = (243.04 * gamma / (17.625 - gamma)) * 9 / 5 + 32 %}
|
||||
{% endif %}
|
||||
{% set weather = states.weather.pirateweather %}
|
||||
{% set climate = states.climate.downstairs %}
|
||||
{% set fresh = values_valid and weather is not none and climate is not none
|
||||
and 0 <= as_timestamp(now()) - as_timestamp(weather.last_reported) <= 3600
|
||||
and 0 <= as_timestamp(now()) - as_timestamp(climate.last_reported) <= 7200 %}
|
||||
{% set daytime = fresh_air_sunset() | float(0) > 0 %}
|
||||
{% set home = is_state('person.carlo', 'home') or is_state('person.stacey', 'home') %}
|
||||
{% set awake = is_state('binary_sensor.sleepnumber_carlo_carlo_is_in_bed', 'off')
|
||||
and is_state('binary_sensor.sleepnumber_carlo_stacey_is_in_bed', 'off') %}
|
||||
{% set slider_closed = is_state('binary_sensor.mcu2_gpio12', 'off') %}
|
||||
{% set entries = expand('group.entry_points') | list %}
|
||||
{% set closed = is_state('group.entry_points', 'off') and entries | count > 0
|
||||
and entries | rejectattr('state', 'eq', 'off') | list | count == 0 %}
|
||||
{% set guests_off = is_state('input_boolean.guest_mode', 'off') %}
|
||||
{% set temperatures = fresh and is_number(target) and outside <= 76
|
||||
and inside - outside >= 2 and target - outside >= 1 %}
|
||||
{% set dry = fresh and outside_dew <= 60 and outside_dew <= inside_dew - 2 %}
|
||||
{% set weather_ok = fresh and 0 <= rain == 0 and 0 <= rain_chance <= 20 and 0 <= gust <= 20
|
||||
and alerts == 0 and states('weather.pirateweather') in ['sunny', 'partlycloudy', 'cloudy'] %}
|
||||
{% set air_ok = airly is not none and caqi is not none and 0 <= caqi <= 25
|
||||
and airly.attributes.get('unit_of_measurement') == 'CAQI'
|
||||
and 0 <= as_timestamp(now()) - as_timestamp(airly.last_reported) <= 7200 %}
|
||||
{% set checks = dict(data_fresh=fresh, daytime=daytime, home=home, awake=awake,
|
||||
slider_closed=slider_closed, windows_closed=closed, guest_mode_off=guests_off, temperatures=temperatures,
|
||||
drier_outside=dry, weather=weather_ok, air_quality=air_ok) %}
|
||||
{% set ns = namespace(blocked=[]) %}
|
||||
{% for name, passes in checks.items() %}
|
||||
{% if not passes %}{% set ns.blocked = ns.blocked + [name] %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ dict(eligible=ns.blocked | count == 0, blocked=ns.blocked,
|
||||
inside_f=inside, target_f=target if is_number(target) else none, indoor_humidity=humidity, outside_f=outside,
|
||||
indoor_dew_point_f=inside_dew | round(1) if inside_dew is not none else none,
|
||||
outdoor_dew_point_f=outside_dew, airly_caqi=caqi) | to_json }}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro fresh_air_message(reading=none) -%}
|
||||
{% set reading = reading if reading is not none else state_attr('binary_sensor.fresh_air_window_opportunity', 'assessment') %}
|
||||
{% if reading is mapping and reading.get('inside_f') is number and reading.get('outside_f') is number %}
|
||||
{{ reading.inside_f | round(0) | int }}F inside, {{ reading.outside_f | round(0) | int }}F outside.
|
||||
Consider opening the main sliding door for cooler, drier air.
|
||||
{% else %}
|
||||
Fresh-air opportunity details are updating.
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
@ -0,0 +1,225 @@
|
||||
######################################################################
|
||||
# @CCOSTAN - Follow Me on X
|
||||
# For more info visit https://www.vcloudinfo.com/click-here
|
||||
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
|
||||
# -------------------------------------------------------------------
|
||||
# Downstairs HVAC - Bed-led cooling, durable overrides, verified commands
|
||||
# One writer owns downstairs; upstairs remains in the shared legacy policy.
|
||||
# Notes: Nest learning and the stored schedule must both be disabled/cleared.
|
||||
# Notes: Physical changes have no reliable user context; compare confirmed
|
||||
# command state after a settling window, never temperature/humidity telemetry.
|
||||
######################################################################
|
||||
|
||||
capture_downstairs_hvac_override:
|
||||
alias: Capture Downstairs HVAC Override
|
||||
mode: queued
|
||||
sequence:
|
||||
- variables:
|
||||
expected: "{{ states('input_text.downstairs_hvac_expected') | from_json({}) }}"
|
||||
current_mode: "{{ states('climate.downstairs') }}"
|
||||
current_preset: "{{ state_attr('climate.downstairs', 'preset_mode') }}"
|
||||
current_target: "{{ state_attr('climate.downstairs', 'temperature') | float(none) }}"
|
||||
phase: "{{ states('input_select.downstairs_hvac_period') }}"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% from 'downstairs_hvac.jinja' import downstairs_matches %}
|
||||
{{ expected.get('confirmed', false) and
|
||||
as_timestamp(now()) >= expected.get('settle_until', 0) and
|
||||
not (downstairs_matches(expected) | from_json) and
|
||||
(current_mode == 'off' or (current_mode == 'cool' and
|
||||
(current_preset == 'eco' or current_target is not none))) }}
|
||||
- variables:
|
||||
override: >-
|
||||
{{ dict(kind='off' if current_mode == 'off' else
|
||||
('eco' if current_preset == 'eco' else 'temperature'),
|
||||
temperature=current_target, phase=phase) }}
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.downstairs_hvac_override
|
||||
data:
|
||||
value: "{{ override | to_json }}"
|
||||
- action: script.send_to_logbook
|
||||
data:
|
||||
topic: CLIMATE
|
||||
message: >-
|
||||
Downstairs {{ override.kind }} override captured
|
||||
(target={{ current_target }}F; {{ 'until resumed' if override.kind == 'off' else 'until bedtime, 08:00, or a Guest Mode change' }}).
|
||||
|
||||
reconcile_downstairs_hvac:
|
||||
alias: Reconcile Downstairs HVAC
|
||||
description: Bed-led cool-only policy; preserve manual intent and verify every command.
|
||||
mode: queued
|
||||
max: 10
|
||||
max_exceeded: silent
|
||||
sequence:
|
||||
- condition: state
|
||||
entity_id: climate.downstairs
|
||||
state: ['cool', 'off', 'heat', 'heat_cool']
|
||||
- if: "{{ capture_override | default(true) }}"
|
||||
then:
|
||||
- action: script.capture_downstairs_hvac_override
|
||||
- variables:
|
||||
desired: >-
|
||||
{% from 'downstairs_hvac.jinja' import downstairs_policy %}
|
||||
{{ downstairs_policy() | from_json }}
|
||||
expected: "{{ states('input_text.downstairs_hvac_expected') | from_json({}) }}"
|
||||
- condition: template
|
||||
value_template: "{{ desired.valid }}"
|
||||
- variables:
|
||||
matched: >-
|
||||
{% from 'downstairs_hvac.jinja' import downstairs_matches %}
|
||||
{{ downstairs_matches(desired) | from_json }}
|
||||
command: >-
|
||||
{{ dict(mode=desired.mode, preset=desired.preset,
|
||||
temperature=desired.temperature, reason=desired.reason) }}
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ matched }}"
|
||||
then:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ not expected.get('confirmed', false) or
|
||||
expected.get('mode') != desired.mode or
|
||||
expected.get('preset') != desired.preset or
|
||||
expected.get('temperature') != desired.temperature or
|
||||
expected.get('reason') != desired.reason }}
|
||||
then:
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.downstairs_hvac_expected
|
||||
data:
|
||||
value: "{{ dict(command, confirmed=true, settle_until=0) | to_json }}"
|
||||
- stop: Downstairs already matches the current policy.
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ as_timestamp(now()) >= expected.get('settle_until', 0)
|
||||
or desired.mode == 'off' }}
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.downstairs_hvac_expected
|
||||
data:
|
||||
value: "{{ dict(command, confirmed=false, settle_until=as_timestamp(now()) + 90) | to_json }}"
|
||||
- choose:
|
||||
- conditions: "{{ desired.mode == 'off' }}"
|
||||
sequence:
|
||||
- action: climate.turn_off
|
||||
target:
|
||||
entity_id: climate.downstairs
|
||||
default:
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ not is_state('climate.downstairs', 'cool') }}"
|
||||
then:
|
||||
- action: climate.set_hvac_mode
|
||||
target:
|
||||
entity_id: climate.downstairs
|
||||
data:
|
||||
hvac_mode: cool
|
||||
- wait_template: "{{ is_state('climate.downstairs', 'cool') }}"
|
||||
timeout: "00:00:20"
|
||||
continue_on_timeout: false
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ state_attr('climate.downstairs', 'preset_mode') != desired.preset }}"
|
||||
then:
|
||||
- action: climate.set_preset_mode
|
||||
target:
|
||||
entity_id: climate.downstairs
|
||||
data:
|
||||
preset_mode: "{{ desired.preset }}"
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ desired.preset == 'none' }}"
|
||||
then:
|
||||
- wait_template: "{{ state_attr('climate.downstairs', 'preset_mode') == 'none' }}"
|
||||
timeout: "00:00:20"
|
||||
continue_on_timeout: false
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ state_attr('climate.downstairs', 'temperature') | float(none) != desired.temperature }}"
|
||||
then:
|
||||
- action: climate.set_temperature
|
||||
target:
|
||||
entity_id: climate.downstairs
|
||||
data:
|
||||
temperature: "{{ desired.temperature }}"
|
||||
- wait_template: >-
|
||||
{% from 'downstairs_hvac.jinja' import downstairs_matches %}
|
||||
{{ downstairs_matches(desired) | from_json }}
|
||||
timeout: "00:00:30"
|
||||
continue_on_timeout: true
|
||||
- variables:
|
||||
verified: >-
|
||||
{% from 'downstairs_hvac.jinja' import downstairs_matches %}
|
||||
{{ downstairs_matches(desired) | from_json }}
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.downstairs_hvac_expected
|
||||
data:
|
||||
value: "{{ dict(command, confirmed=verified, settle_until=as_timestamp(now()) + (15 if verified else 120)) | to_json }}"
|
||||
- action: script.send_to_logbook
|
||||
data:
|
||||
topic: CLIMATE
|
||||
message: >-
|
||||
Downstairs {{ 'verified' if verified else 'NOT CONFIRMED; will retry after settling' }}:
|
||||
{{ desired.mode }}/{{ desired.preset }}, target={{ desired.temperature }}F
|
||||
({{ desired.reason }}).
|
||||
|
||||
resume_downstairs_hvac_schedule:
|
||||
alias: Resume Downstairs HVAC Schedule
|
||||
description: Clear the manual hold and apply the current bed/presence policy.
|
||||
mode: single
|
||||
sequence:
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id:
|
||||
- input_text.downstairs_hvac_override
|
||||
- input_text.downstairs_hvac_expected
|
||||
data:
|
||||
value: "{}"
|
||||
- action: script.reconcile_downstairs_hvac
|
||||
|
||||
set_downstairs_hvac_period:
|
||||
alias: Set Downstairs HVAC Bed Period
|
||||
mode: queued
|
||||
fields:
|
||||
period:
|
||||
description: Day at 08:00, or sleep after both sides are occupied.
|
||||
selector:
|
||||
select:
|
||||
options: [day, sleep]
|
||||
reset_hold:
|
||||
description: Reset at the morning or guest boundary even if the bed period is unchanged.
|
||||
selector:
|
||||
boolean:
|
||||
sequence:
|
||||
- condition: template
|
||||
value_template: "{{ period in ['day', 'sleep'] and (states('input_select.downstairs_hvac_period') != period or reset_hold | default(false)) }}"
|
||||
- action: script.capture_downstairs_hvac_override
|
||||
- variables:
|
||||
hold: "{{ states('input_text.downstairs_hvac_override') | from_json({}) }}"
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ hold.get('kind') != 'off' }}"
|
||||
then:
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.downstairs_hvac_override
|
||||
data:
|
||||
value: "{}"
|
||||
- action: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.downstairs_hvac_period
|
||||
data:
|
||||
option: "{{ period }}"
|
||||
- action: input_datetime.set_datetime
|
||||
target:
|
||||
entity_id: input_datetime.downstairs_hvac_next_wake
|
||||
data:
|
||||
datetime: >-
|
||||
{% set morning = today_at('08:00') %}
|
||||
{{ (morning if now() < morning else morning + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S') }}
|
||||
- action: script.reconcile_downstairs_hvac
|
||||
data:
|
||||
capture_override: false
|
||||
@ -0,0 +1,99 @@
|
||||
######################################################################
|
||||
# @CCOSTAN - Follow Me on X
|
||||
# For more info visit https://www.vcloudinfo.com/click-here
|
||||
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
|
||||
# -------------------------------------------------------------------
|
||||
# Fresh Air Notice - One daily advisory across phones and household screens
|
||||
# Never changes HVAC, windows, or security state.
|
||||
# -------------------------------------------------------------------
|
||||
# Notes: One restoring timestamp reserves the daily send before fan-out.
|
||||
# Qualifying conditions can issue a notice while downstairs is idle or cooling.
|
||||
# The active helper tracks the notice lifecycle, not physical air quality.
|
||||
# A stable tag replaces old pushes; clear commands also remove kiosk copies.
|
||||
######################################################################
|
||||
|
||||
clear_fresh_air_notice:
|
||||
alias: Clear Fresh Air Notice
|
||||
mode: queued
|
||||
sequence:
|
||||
- action: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.fresh_air_notice_active
|
||||
- action: notify.ios_parents
|
||||
continue_on_error: true
|
||||
data:
|
||||
message: clear_notification
|
||||
data:
|
||||
tag: fresh_air_opportunity
|
||||
group: fresh_air
|
||||
- action: persistent_notification.dismiss
|
||||
data:
|
||||
notification_id: fresh_air_opportunity
|
||||
|
||||
reconcile_fresh_air_notice:
|
||||
alias: Reconcile Fresh Air Notice
|
||||
mode: single
|
||||
max_exceeded: silent
|
||||
sequence:
|
||||
- variables:
|
||||
assessment: >-
|
||||
{% from 'fresh_air.jinja' import fresh_air_conditions %}
|
||||
{{ fresh_air_conditions() | from_json }}
|
||||
active: "{{ is_state('input_boolean.fresh_air_notice_active', 'on') }}"
|
||||
sent_today: "{{ states('input_datetime.fresh_air_last_notified')[:10] == now().strftime('%Y-%m-%d') }}"
|
||||
- choose:
|
||||
- conditions: "{{ active and (not assessment.get('eligible', false) or not sent_today) }}"
|
||||
sequence:
|
||||
- action: script.clear_fresh_air_notice
|
||||
- action: script.send_to_logbook
|
||||
data:
|
||||
topic: CLIMATE
|
||||
message: "Fresh-air reminder cleared ({{ assessment.get('blocked', ['data unavailable']) | join(', ') if sent_today else 'previous day' }})."
|
||||
- conditions:
|
||||
- condition: state
|
||||
entity_id: input_boolean.text_notifications
|
||||
state: 'on'
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ assessment.eligible and not sent_today and is_state('climate.downstairs', 'cool')
|
||||
and state_attr('climate.downstairs', 'hvac_action') in ['idle', 'cooling'] }}
|
||||
sequence:
|
||||
# Reserve first: a restart or failed delivery must not duplicate today's push.
|
||||
- action: input_datetime.set_datetime
|
||||
target:
|
||||
entity_id: input_datetime.fresh_air_last_notified
|
||||
data:
|
||||
timestamp: "{{ as_timestamp(now()) }}"
|
||||
- action: script.clear_fresh_air_notice
|
||||
- action: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.fresh_air_notice_active
|
||||
- variables:
|
||||
message: >-
|
||||
{% from 'fresh_air.jinja' import fresh_air_message %}
|
||||
{{ fresh_air_message(assessment) | trim }}
|
||||
- action: persistent_notification.create
|
||||
data:
|
||||
notification_id: fresh_air_opportunity
|
||||
title: Good weather for fresh air
|
||||
message: "{{ message }}"
|
||||
- action: script.notify_engine
|
||||
continue_on_error: true
|
||||
data:
|
||||
who: parents
|
||||
title: Good weather for fresh air
|
||||
value1: "{{ message }}"
|
||||
group: fresh_air
|
||||
tag: fresh_air_opportunity
|
||||
level: passive
|
||||
notification_icon: mdi:currency-usd
|
||||
color: '#00897B'
|
||||
url: /?more-info-entity-id=binary_sensor.fresh_air_window_opportunity
|
||||
- action: script.send_to_logbook
|
||||
data:
|
||||
topic: CLIMATE
|
||||
message: >-
|
||||
Fresh-air reminder issued to Carlo/Stacey and household screens:
|
||||
inside={{ assessment.inside_f }}F/{{ assessment.indoor_humidity }}%,
|
||||
outside={{ assessment.outside_f }}F, dew points={{ assessment.indoor_dew_point_f }}/{{ assessment.outdoor_dew_point_f }}F,
|
||||
Airly CAQI={{ assessment.airly_caqi }}. Daily allowance reserved.
|
||||
Loading…
Reference in new issue