diff --git a/config/dashboards/overview/partials/home_sections.yaml b/config/dashboards/overview/partials/home_sections.yaml index 60c3a2b3..37bb79f3 100644 --- a/config/dashboards/overview/partials/home_sections.yaml +++ b/config/dashboards/overview/partials/home_sections.yaml @@ -177,15 +177,28 @@ - temperature vertical: false features_position: bottom - - type: tile + - type: custom:mushroom-template-card entity: climate.upstairs - show_entity_picture: false - hide_state: false - state_content: - - current_temperature - - temperature - vertical: false - features_position: bottom + primary: Upstairs + secondary: >- + {% set current = state_attr('climate.upstairs', 'current_temperature') %} + {% set target = state_attr('climate.upstairs', 'temperature') %} + {% set humidity = states('sensor.upstairs_humidity') %} + {% set current_text = ((current | float) | round(0) | int) ~ ' °F' if current is number else 'Current n/a' %} + {% set target_text = ((target | float) | round(0) | int) ~ ' °F' if target is number else 'Target n/a' %} + {% set humidity_text = ((humidity | float) | round(0) | int) ~ '% humidity' if is_number(humidity) else 'Humidity n/a' %} + {{ current_text }} · {{ target_text }} · {{ humidity_text }} + icon: mdi:thermostat + icon_color: >- + {% set mode = states('climate.upstairs') %} + {% if mode == 'cool' %}blue{% elif mode == 'heat' %}orange{% elif mode == 'off' %}grey{% else %}green{% endif %} + tap_action: + action: more-info + hold_action: + action: more-info + double_tap_action: + action: none + multiline_secondary: false - type: custom:search-card max_results: 10 - type: custom:mini-graph-card diff --git a/config/dashboards/overview/partials/water_sections.yaml b/config/dashboards/overview/partials/water_sections.yaml index 0a46e757..8e8d1c60 100644 --- a/config/dashboards/overview/partials/water_sections.yaml +++ b/config/dashboards/overview/partials/water_sections.yaml @@ -26,11 +26,25 @@ tap_action: action: more-info entity: script.reset_upstairs_filter - - type: tile + - type: custom:mushroom-template-card + primary: HVAC Condenser Lines + secondary: >- + {% set days = states('sensor.hvac_condenser_lines_days_since_cleaned') | int(0) -%} + {% set raw = states('input_datetime.hvac_condenser_lines_last_cleaned') -%} + {%- set when = as_datetime(raw).strftime('%b %d, %Y') | replace(' 0', ' ') if raw not in ['unknown', 'unavailable', 'none', ''] else 'unknown' -%} + {%- if days < 1 -%}Cleaned today{%- elif days == 1 -%}1 day since {{ when }}{%- else -%}{{ days }} days since {{ when }}{%- endif -%} + icon: mdi:calendar-clock entity: input_datetime.hvac_condenser_lines_last_cleaned + icon_color: blue tap_action: action: more-info entity: script.reset_hvac_condenser_lines_cleaned + hold_action: + action: none + double_tap_action: + action: none + layout: horizontal + fill_container: true - type: custom:power-flow-card-plus entities: battery: @@ -127,4 +141,29 @@ action: none layout: horizontal fill_container: true + - type: custom:mushroom-template-card + primary: >- + {% set days = states('sensor.water_softener_salt_days_since_last_add') | float(0) -%} + {%- if days < 1 -%}Last refill: today{%- elif days == 1 -%}Last refill: 1 day ago{%- else -%}Last refill: {{ days | round(1) }} days ago{%- endif -%} + secondary: "{{ states('sensor.water_softener_salt_last_summary') }}" + icon: mdi:shaker + entity: sensor.water_softener_salt_days_since_last_add + icon_color: blue + tap_action: + action: navigate + navigation_path: /lovelace/salt-trends + hold_action: + action: more-info + double_tap_action: + action: none + layout: horizontal + fill_container: true + - type: tile + entity: sensor.water_softener_salt_average_days_between_refills + name: Salt typically lasts + tap_action: + action: navigate + navigation_path: /lovelace/salt-trends + hold_action: + action: more-info - type: energy-water-graph diff --git a/config/dashboards/overview/views/09_salt_trends.yaml b/config/dashboards/overview/views/09_salt_trends.yaml new file mode 100644 index 00000000..26860379 --- /dev/null +++ b/config/dashboards/overview/views/09_salt_trends.yaml @@ -0,0 +1,59 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Overview Subview - Salt Trends +# Year-scale water softener salt maintenance history. +# ------------------------------------------------------------------- +# Notes: Linked from the Home Maintenance water view salt tiles. +# Notes: Uses native history graph cards because more-info dialogs do not expose a configurable default time window. +###################################################################### + +title: Salt Trends +path: salt-trends +subview: true +back_path: /lovelace/water +type: sections +max_columns: 2 +sections: +- type: grid + column_span: 2 + cards: + - type: heading + heading: Water Softener Salt + heading_style: title + - type: entities + title: Current Salt Maintenance + entities: + - entity: sensor.water_softener_salt_days_since_last_add + name: Days since last refill + - entity: sensor.water_softener_salt_average_days_between_refills + name: Typical duration + - entity: sensor.water_softener_salt_refill_count + name: Refills logged + - entity: sensor.water_softener_salt_total_added + name: Total salt logged + # Fallback card: native history graph is used because the more-info dialog + # does not support a configurable default history range. + - type: history-graph + title: Salt Typically Lasts - Last 365 Days + hours_to_show: 8760 + entities: + - entity: sensor.water_softener_salt_average_days_between_refills + name: Days between refills + # Fallback card: same controlled yearly window for low-frequency salt events. + - type: history-graph + title: Salt Refills Logged - Last 365 Days + hours_to_show: 8760 + entities: + - entity: sensor.water_softener_salt_refill_count + name: Refill count + # Fallback card: same controlled yearly window for low-frequency salt events. + - type: history-graph + title: Salt Logged Total - Last 365 Days + hours_to_show: 8760 + entities: + - entity: sensor.water_softener_salt_total_added + name: Total lb +cards: [] diff --git a/config/logbook.yaml b/config/logbook.yaml index 44c0648c..fcc761a6 100644 --- a/config/logbook.yaml +++ b/config/logbook.yaml @@ -12,6 +12,9 @@ # Notes: Person/location changes and Life360 online status stay out of Activity; recorder keeps location history. # Notes: Raw Portainer container/stack status telemetry is hidden; Docker aggregate outage sensors and curated DOCKER events remain. # Notes: Mobile app phone telemetry stays out of Activity; phone notify services are unaffected. +# Notes: Water-softener salt helpers stay in recorder for restore/math; curated MAINTENANCE events and trend sensors are the visible signal. +# Notes: Rolling maintenance days-since sensors are live-only display helpers. +# Notes: TP-Link lamp voltage diagnostics stay out of Activity; recorder.yaml keeps them live-only. ###################################################################### exclude: @@ -78,6 +81,17 @@ exclude: - automation.detect_lights_and_adjust_the_brightness_when_turned_on_based_on_time - automation.docker_state_sync_repairs_dynamic - automation.docker_telemetry_template_refresh + - sensor.hvac_condenser_lines_days_since_cleaned + - sensor.front_office_lamp_voltage + - sensor.water_softener_salt_days_since_last_add + - input_datetime.water_softener_salt_last_occurred_at + - input_number.water_softener_salt_last_amount_lb + - input_number.water_softener_salt_total_added_lb + - input_text.water_softener_salt_last_note + - input_text.water_softener_salt_recent_event_ids + - input_text.water_softener_salt_recent_events + - counter.water_softener_salt_event_count + - sensor.water_softener_salt_last_summary # BEGIN generated Portainer recorder/logbook noise exclusions # Portainer per-container/status plumbing is live-only; keep aggregate Docker outage sensors historically. - binary_sensor.docker_container_alerts_snoozed diff --git a/config/packages/climate.yaml b/config/packages/climate.yaml index 6cba3c42..d794ae4a 100644 --- a/config/packages/climate.yaml +++ b/config/packages/climate.yaml @@ -7,7 +7,7 @@ # Thermostat helpers for upstairs/downstairs comfort. # ------------------------------------------------------------------- # Related Issue: 1571, 273 -# Notes: Filter due alerts use a 350h runtime threshold, 9-month hard limit, 3-day snooze, and Filter Changed push actions. +# Notes: Filter due alerts use 350h downstairs/450h upstairs runtime thresholds, a 9-month hard limit, 3-day snooze, and Filter Changed push actions. # Notes: Condenser line cleanout uses a single 360-day reminder for both HVAC systems. # Notes: The snooze notification pattern is also covered at https://youtu.be/y47KSflS1aw. # Video: https://youtu.be/nScl2JxYSB8 @@ -91,6 +91,21 @@ template: - name: "Upstairs AC Cooling Numeric" unique_id: upstairs_ac_cooling_numeric state: "{{ 1 if is_state('binary_sensor.upstairs_ac_is_cooling', 'on') else 0 }}" + - name: "HVAC Condenser Lines Days Since Cleaned" + unique_id: hvac_condenser_lines_days_since_cleaned + unit_of_measurement: d + icon: mdi:calendar-clock + availability: >- + {% set raw = states('input_datetime.hvac_condenser_lines_last_cleaned') %} + {{ raw not in ['unknown', 'unavailable', 'none', ''] and as_timestamp(as_datetime(raw), default=none) is not none }} + state: >- + {% set raw = states('input_datetime.hvac_condenser_lines_last_cleaned') %} + {% set cleaned_ts = as_timestamp(as_datetime(raw), default=none) %} + {% if cleaned_ts is not none %} + {{ [((as_timestamp(now()) - cleaned_ts) / 86400) | int(0), 0] | max }} + {% else %} + 0 + {% endif %} input_datetime: downstairs_last_filter_change: @@ -334,7 +349,7 @@ script: ############################################################################## automation: - alias: Notify Climate Filter Change Due - description: Notify when HVAC filter runtime exceeds 350h or age reaches 9 months + description: Notify when HVAC filter runtime exceeds the location threshold or age reaches 9 months trigger: - platform: numeric_state entity_id: sensor.downstairs_ac_runtime_since_last_filter_change @@ -345,7 +360,7 @@ automation: id: downstairs_daily - platform: numeric_state entity_id: sensor.upstairs_ac_runtime_since_last_filter_change - above: 350 + above: 450 id: upstairs_runtime - platform: time at: "09:10:00" @@ -357,15 +372,16 @@ automation: last_filter_entity: "input_datetime.{{ location }}_last_filter_change" snooze_entity: "input_datetime.{{ location }}_filter_snooze_until" runtime_hours: "{{ states(runtime_sensor) | float(0) }}" + runtime_threshold_hours: "{{ 450 if location == 'upstairs' else 350 }}" last_filter_ts: "{{ as_timestamp(states(last_filter_entity), 0) }}" filter_age_days: "{{ ((as_timestamp(now()) - (last_filter_ts | float(0))) / 86400) | round(0) }}" due_reason: >- - {% set runtime_due = runtime_hours | float(0) > 350 %} + {% set runtime_due = runtime_hours | float(0) > (runtime_threshold_hours | float(0)) %} {% set age_due = (last_filter_ts | float(0)) == 0 or (filter_age_days | int(0)) >= 274 %} {% if runtime_due and age_due %} - Runtime has exceeded 350h and the filter is {{ filter_age_days }} days old. + Runtime has exceeded {{ runtime_threshold_hours }}h and the filter is {{ filter_age_days }} days old. {% elif runtime_due %} - Runtime has exceeded 350h. + Runtime has exceeded {{ runtime_threshold_hours }}h. {% else %} Filter age has reached the 9-month hard limit ({{ filter_age_days }} days). {% endif %} @@ -373,7 +389,7 @@ automation: - condition: template value_template: >- {{ - (runtime_hours | float(0) > 350) + (runtime_hours | float(0) > (runtime_threshold_hours | float(0))) or (last_filter_ts | float(0)) == 0 or (filter_age_days | int(0)) >= 274 }} diff --git a/config/packages/maintenance_log.yaml b/config/packages/maintenance_log.yaml index 9350eaec..86ce72c0 100644 --- a/config/packages/maintenance_log.yaml +++ b/config/packages/maintenance_log.yaml @@ -8,8 +8,8 @@ # ------------------------------------------------------------------- # Notes: Webhook id is bearclaw_maintenance_log_v1 (Joanna -> HA contract). # Notes: Duplicate event_id values are ignored to prevent double-count totals. -# Notes: Recent event history string format is "when|amount|note||...". -# Notes: Numeric refill interval sensors expose state_class for long-term trend rollups. +# Notes: Recent event history string format is "YYYY-MM-DD HH:MM:SS|amount|note||...". +# Notes: Long-term stats live on template sensors; helpers are internal state for restore/calculation. ###################################################################### input_number: @@ -65,7 +65,6 @@ template: - name: "Water Softener Salt Days Since Last Add" unique_id: water_softener_salt_days_since_last_add unit_of_measurement: d - state_class: measurement availability: >- {% set raw = states('input_datetime.water_softener_salt_last_occurred_at') %} {% if raw in ['unknown', 'unavailable', 'none', ''] %} @@ -107,7 +106,7 @@ template: state: >- {% set raw = states('input_text.water_softener_salt_recent_events') %} {% if raw in ['unknown', 'unavailable', 'none', ''] %} - 150 + unknown {% else %} {% set entries = raw.split('||') %} {% set ns = namespace(previous_ts=none, total_days=0, sample_count=0) %} @@ -128,10 +127,23 @@ template: {% if ns.sample_count > 0 %} {{ (ns.total_days / ns.sample_count) | round(1) }} {% else %} - 150 + unknown {% endif %} {% endif %} + - name: "Water Softener Salt Refill Count" + unique_id: water_softener_salt_refill_count + icon: mdi:counter + state_class: total + state: "{{ states('counter.water_softener_salt_event_count') | int(0) }}" + + - name: "Water Softener Salt Total Added" + unique_id: water_softener_salt_total_added + unit_of_measurement: lb + device_class: weight + state_class: total + state: "{{ states('input_number.water_softener_salt_total_added_lb') | float(0) | round(1) }}" + automation: - alias: "Maintenance Log - Joanna Webhook Ingest" id: 1c9fba4f-fef5-4da9-82d4-4049deff17cf @@ -216,7 +228,7 @@ automation: note_value: >- {% set text = raw_text if raw_text else 'Logged by Joanna webhook.' %} {{ text | truncate(255, true, '') }} - recent_event_line: "{{ occurred_display }}|{{ effective_amount_lb | round(1) }} lb|{{ note_value }}" + recent_event_line: "{{ occurred_local_datetime }}|{{ effective_amount_lb | round(1) }} lb|{{ note_value }}" next_recent_events: >- {% set current = (states('input_text.water_softener_salt_recent_events') | default('', true) | string).split('||') %} diff --git a/config/recorder.yaml b/config/recorder.yaml index 0b6f577b..419a282e 100755 --- a/config/recorder.yaml +++ b/config/recorder.yaml @@ -12,6 +12,8 @@ # Notes: Life360 trackers are explicitly included for map/history queries despite the broad device_tracker exclusion. # Notes: Raw Portainer container/stack status telemetry is live-only; keep Docker aggregate outage sensors, Repairs, and curated logbook events instead. # Notes: Mobile app phone telemetry is live-only; Life360 keeps location history and notify services remain available. +# Notes: Rolling maintenance days-since sensors are live-only; durable interval/count/total sensors carry the history. +# Notes: TP-Link lamp voltage diagnostics are live-only; useful energy/power sensors stay recorded. ###################################################################### db_url: !secret recorder_db_url purge_keep_days: 180 @@ -169,6 +171,9 @@ exclude: entities: - automation.update_garage_get_status - automation.docker_telemetry_template_refresh + - sensor.hvac_condenser_lines_days_since_cleaned + - sensor.front_office_lamp_voltage + - sensor.water_softener_salt_days_since_last_add # BEGIN generated Portainer recorder/logbook noise exclusions # Portainer per-container/status plumbing is live-only; keep aggregate Docker outage sensors historically. - binary_sensor.docker_container_alerts_snoozed