From 39702f71035ab927aa3ea16a8dcd347980018222 Mon Sep 17 00:00:00 2001 From: Carlo Costanzo Date: Tue, 13 Oct 2026 17:26:58 -0400 Subject: [PATCH] Add thermal and Nest health monitoring --- .gitignore | 3 + config/custom_templates/garage_thermal.jinja | 37 +++++ config/custom_templates/kiosk_health.jinja | 3 + .../partials/home_sections.yaml | 14 +- .../partials/kiosk_sections.yaml | 107 +++++++++++++- .../partials/proxmox_sections.yaml | 12 +- .../templates/button_card_templates.yaml | 55 +++++++ config/packages/README.md | 6 +- config/packages/docker_infrastructure.yaml | 13 +- config/packages/glances.yaml | 135 +++++++++++++++++- config/packages/infrastructure.yaml | 22 +-- config/packages/kiosk_tablet.yaml | 60 +++++++- config/packages/proxmox.yaml | 43 +----- config/recorder.yaml | 8 ++ docs/garage-thermal-protection.md | 47 ++++++ 15 files changed, 482 insertions(+), 83 deletions(-) create mode 100644 config/custom_templates/garage_thermal.jinja create mode 100644 docs/garage-thermal-protection.md diff --git a/.gitignore b/.gitignore index 927f4d81..2c2d189b 100755 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,6 @@ docker_14 docker_69 .codex_tmp/ + +# Local host-specific thermal control payload (not portable). +/config/shell_scripts/frigate_thermal_control.py diff --git a/config/custom_templates/garage_thermal.jinja b/config/custom_templates/garage_thermal.jinja new file mode 100644 index 00000000..004fdfb6 --- /dev/null +++ b/config/custom_templates/garage_thermal.jinja @@ -0,0 +1,37 @@ +{# One restored incident policy for notifications and compute-health presentation. #} +{% macro garage_thermal_policy(previous, previous_band, returns) -%} + {% set stamp = as_timestamp(now()) %} + {% set sources = ['sensor.proxmox1_package_id_0_temperature', + 'sensor.proxmox2_package_id_0_temperature', + 'sensor.proxmox03_package_id_0_temperature'] %} + {% set ns = namespace(values=[], missing=[], source='') %} + {% for source in sources %} + {% set entity = states[source] %} + {% set value = states(source) | float(none) %} + {% set reported = as_timestamp(entity.last_reported, 0) if entity is not none else 0 %} + {% if value is not none and 41 <= value <= 230 and 0 <= stamp - reported <= 180 %} + {% if not ns.values or value > ns.values | max %}{% set ns.source = source %}{% endif %} + {% set ns.values = ns.values + [value] %} + {% else %} + {% set ns.missing = ns.missing + [source] %} + {% endif %} + {% endfor %} + {% set hottest = ns.values | max if ns.values else none %} + {% set continuous = 0 <= stamp - previous.get('observed_at', 0) <= 120 %} + {% set warning_since = (previous.get('warning_since', 0) or stamp) if continuous and hottest is not none and hottest >= 176 else (stamp if hottest is not none and hottest >= 176 else 0) %} + {% set urgent_since = (previous.get('urgent_since', 0) or stamp) if continuous and hottest is not none and hottest >= 194 else (stamp if hottest is not none and hottest >= 194 else 0) %} + {% set cool_since = (previous.get('cool_since', 0) or stamp) if continuous and not ns.missing and hottest is not none and hottest < 167 else (stamp if not ns.missing and hottest is not none and hottest < 167 else 0) %} + {% set missing_since = (previous.get('missing_since', 0) or stamp) if ns.missing else 0 %} + {% set band = previous_band if previous_band in ['warning', 'urgent'] else 'normal' %} + {% if hottest is not none and (hottest >= 203 or (urgent_since and stamp - urgent_since >= 120)) %} + {% set band = 'urgent' %} + {% elif band == 'normal' and warning_since and stamp - warning_since >= 600 %} + {% set band = 'warning' %} + {% endif %} + {% if cool_since and stamp - cool_since >= 900 %}{% set band = 'normal' %}{% endif %} + {% set tracking = band != 'normal' or warning_since or urgent_since or missing_since %} + {% do returns({'band': band, 'hottest_f': hottest, 'source': ns.source, + 'missing': ns.missing, 'telemetry_degraded': missing_since != 0 and stamp - missing_since >= 300, + 'memory': {'observed_at': stamp if tracking else 0, 'warning_since': warning_since, + 'urgent_since': urgent_since, 'cool_since': cool_since if band != 'normal' else 0, 'missing_since': missing_since}}) %} +{%- endmacro %} diff --git a/config/custom_templates/kiosk_health.jinja b/config/custom_templates/kiosk_health.jinja index 63b324dd..b4fdc51a 100644 --- a/config/custom_templates/kiosk_health.jinja +++ b/config/custom_templates/kiosk_health.jinja @@ -43,6 +43,9 @@ {% set ns.items = ns.items + [room] %} {% endif %} {% endfor %} + {% if not is_state('binary_sensor.living_room_nest_kiosk_needs_attention', 'off') %} + {% set ns.items = ns.items + ['Living Room Nest'] %} + {% endif %} {{ ns.items | to_json }} {%- endmacro %} diff --git a/config/dashboards/infrastructure/partials/home_sections.yaml b/config/dashboards/infrastructure/partials/home_sections.yaml index 105971f4..f93295c7 100644 --- a/config/dashboards/infrastructure/partials/home_sections.yaml +++ b/config/dashboards/infrastructure/partials/home_sections.yaml @@ -529,6 +529,7 @@ if (states['binary_sensor.kitchen_kiosk_needs_attention']?.state === 'on') issues.push(`KITCHEN: ${String(kitchen ?? 'ATTENTION').toUpperCase()}`); if (states['binary_sensor.upstairs_kiosk_needs_attention']?.state === 'on') issues.push(`UPSTAIRS: ${String(upstairs ?? 'ATTENTION').toUpperCase()}`); if (states['binary_sensor.office_kiosk_needs_attention']?.state === 'on') issues.push(`OFFICE: ${String(office ?? 'ATTENTION').toUpperCase()}`); + if (states['binary_sensor.living_room_nest_kiosk_needs_attention']?.state === 'on') issues.push(`LIVING ROOM NEST: ${String(states['binary_sensor.living_room_nest_kiosk_needs_attention']?.attributes?.status ?? 'ATTENTION').toUpperCase()}`); return issues.join(' · ') || 'KIOSK TELEMETRY REQUIRES REVIEW'; ]]] state_display: '[[[ return "ATTENTION"; ]]]' @@ -834,14 +835,13 @@ - type: custom:button-card template: bearstone_infra_alert_row - entity: sensor.proxmox_garage_average_temperature - name: Garage compute temperature high + entity: binary_sensor.proxmox_garage_thermal_alert + name: Sustained garage CPU heat icon: mdi:thermometer-alert variables: - alert_kind: numeric_high - threshold: 145 + alert_kind: binary_on state_display: > - [[[ return `${entity.state} °F`; ]]] + [[[ return `${states['sensor.proxmox_garage_thermal_state']?.state?.toUpperCase()} | ${states['sensor.proxmox_garage_hottest_cpu_temperature']?.state} F`; ]]] tap_action: action: navigate navigation_path: /dashboard-infrastructure/proxmox @@ -1018,9 +1018,9 @@ color_thresholds: - value: 80 color: '#43a047' - - value: 135 + - value: 176 color: '#f39c12' - - value: 145 + - value: 194 color: '#c0392b' entities: - entity: sensor.proxmox_garage_average_temperature diff --git a/config/dashboards/infrastructure/partials/kiosk_sections.yaml b/config/dashboards/infrastructure/partials/kiosk_sections.yaml index 48f40ab8..414c900f 100644 --- a/config/dashboards/infrastructure/partials/kiosk_sections.yaml +++ b/config/dashboards/infrastructure/partials/kiosk_sections.yaml @@ -22,7 +22,7 @@ columns: full layout_type: custom:grid-layout layout: - grid-template-columns: repeat(4, minmax(0, 1fr)) + grid-template-columns: repeat(5, minmax(0, 1fr)) grid-auto-flow: row grid-auto-rows: min-content grid-gap: 12px @@ -118,6 +118,18 @@ action: navigate navigation_path: /dashboard-kiosk-satellite-staging/echo-show5 + - type: custom:button-card + template: bearstone_infra_status_kpi + entity: sensor.living_room_nest_status + name: Living Room Nest + icon: mdi:circle-outline + variables: + ok_state: 'online' + healthy_text: ONLINE + alert_text: NEEDS ATTENTION + state_display: > + [[[ return String(entity?.attributes?.status ?? entity?.state ?? 'unknown').replaceAll('_', ' ').toUpperCase(); ]]] + # ------------------------------------------------------------------- # Latest device screenshots # ------------------------------------------------------------------- @@ -174,6 +186,99 @@ action: navigate navigation_path: /dashboard-kiosk-satellite-staging/echo-show5 +# ------------------------------------------------------------------- +# Living Room Nest - actual circular framebuffer preview +# ------------------------------------------------------------------- +- type: grid + column_span: 4 + columns: 1 + square: false + cards: + - type: custom:layout-card + grid_options: + columns: full + layout_type: custom:grid-layout + layout: + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: minmax(0, 1fr) + cards: + - type: custom:button-card + template: bearstone_infra_round_display + entity: image.living_room_nest_screen + name: Living Room Nest + icon: mdi:circle-outline + triggers_update: + - sensor.living_room_nest_status + - sensor.living_room_nest_last_screenshot + variables: + health_entity: sensor.living_room_nest_status + timestamp_entity: sensor.living_room_nest_last_screenshot + capture_entity: button.living_room_nest_capture_screen + + - type: custom:vertical-stack-in-card + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Nest Display Health + - type: custom:button-card + template: bearstone_infra_list_row + entity: binary_sensor.living_room_nest_connectivity + name: Device connection + icon: mdi:wifi + - type: custom:button-card + template: bearstone_infra_list_row + entity: binary_sensor.living_room_nest_screen + name: Display power + icon: mdi:power + - type: custom:button-card + template: bearstone_infra_list_row + entity: binary_sensor.living_room_nest_alert_feed + name: Alert feed + icon: mdi:message-check + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.living_room_nest_last_screenshot + name: Last requested preview + icon: mdi:camera + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.living_room_nest_status + name: Rotation + icon: mdi:image-multiple + state_display: > + [[[ const a = entity?.attributes ?? {}; return `${a.photo_count ?? '?'} PHOTOS | ${a.active_notices ?? '?'} NOTICES | ${String(a.portrait_theme ?? 'unknown').toUpperCase()}`; ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.living_room_nest_uptime + name: Device uptime + icon: mdi:timer-outline + state_display: > + [[[ const n = Number(entity?.state); return Number.isFinite(n) ? `${Math.floor(n / 3600)}h ${Math.floor(n % 3600 / 60)}m` : 'UNKNOWN'; ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.living_room_nest_status + name: Recoveries + icon: mdi:restore + state_display: > + [[[ return `${entity?.attributes?.recoveries_last_hour ?? '?'} / 3 THIS HOUR`; ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: button.living_room_nest_restart_display + name: Restart display + icon: mdi:restart + show_state: false + tap_action: + action: call-service + service: button.press + target: + entity_id: button.living_room_nest_restart_display + # ------------------------------------------------------------------- # Device telemetry # ------------------------------------------------------------------- diff --git a/config/dashboards/infrastructure/partials/proxmox_sections.yaml b/config/dashboards/infrastructure/partials/proxmox_sections.yaml index 1b4878ea..cd88fe44 100644 --- a/config/dashboards/infrastructure/partials/proxmox_sections.yaml +++ b/config/dashboards/infrastructure/partials/proxmox_sections.yaml @@ -437,9 +437,9 @@ color_thresholds: - value: 0 color: "#43a047" - - value: 135 + - value: 176 color: "#f39c12" - - value: 145 + - value: 194 color: "#c0392b" entities: - entity: sensor.proxmox1_package_id_0_temperature @@ -459,9 +459,9 @@ color_thresholds: - value: 0 color: "#43a047" - - value: 135 + - value: 176 color: "#f39c12" - - value: 145 + - value: 194 color: "#c0392b" entities: - entity: sensor.proxmox2_package_id_0_temperature @@ -481,9 +481,9 @@ color_thresholds: - value: 0 color: "#43a047" - - value: 135 + - value: 176 color: "#f39c12" - - value: 145 + - value: 194 color: "#c0392b" entities: - entity: sensor.proxmox03_package_id_0_temperature diff --git a/config/dashboards/infrastructure/templates/button_card_templates.yaml b/config/dashboards/infrastructure/templates/button_card_templates.yaml index 5d72f571..5db3e9e4 100644 --- a/config/dashboards/infrastructure/templates/button_card_templates.yaml +++ b/config/dashboards/infrastructure/templates/button_card_templates.yaml @@ -45,6 +45,61 @@ bearstone_infra_base: - opacity: 0.75 - justify-self: start +bearstone_infra_round_display: + template: bearstone_infra_base + show_state: false + show_label: true + variables: + health_entity: null + timestamp_entity: null + capture_entity: null + tap_action: + action: call-service + service: button.press + service_data: + entity_id: '[[[ return variables.capture_entity; ]]]' + hold_action: + action: more-info + label: > + [[[ + const health = states[variables.health_entity]?.state ?? 'unknown'; + const timestamp = states[variables.timestamp_entity]?.state; + const time = Date.parse(['unknown', 'unavailable'].includes(timestamp) ? entity?.attributes?.captured_at : timestamp); + const age = Number.isFinite(time) ? Math.max(0, Math.floor((Date.now() - time) / 1000)) : null; + const captured = age === null ? 'No preview yet' : age < 60 ? 'Captured just now' : age < 3600 ? `Captured ${Math.floor(age / 60)}m ago` : `Captured ${Math.floor(age / 3600)}h ago`; + return `${health.replaceAll('_', ' ').toUpperCase()} | TAP TO REFRESH\n${captured}`; + ]]] + custom_fields: + preview: > + [[[ + const url = entity?.attributes?.entity_picture; + const fresh = states[variables.health_entity]?.state === 'online'; + const stamp = encodeURIComponent(entity?.state ?? ''); + const image = url?.startsWith('/api/image_proxy/') ? `Last requested Nest preview` : 'Tap to capture'; + return `
nest
${image}
`; + ]]] + styles: + grid: + - grid-template-areas: '"i n" "preview preview" "l l"' + - grid-template-columns: 28px 1fr + - grid-template-rows: min-content 1fr min-content + label: + - white-space: pre-line + - font-size: 12px + - padding-top: 12px + - color: var(--secondary-text-color) + custom_fields: + preview: + - padding-top: 12px + - justify-self: center + extra_styles: | + .nest-bezel { width: 230px; height: 230px; box-sizing: border-box; padding: 10px; border-radius: 50%; background: linear-gradient(145deg,#eee,#898d90 45%,#e8e8e8 70%,#666); box-shadow: 0 3px 10px #0004; } + .nest-face { height: 100%; border-radius: 50%; background: #101217; display: flex; flex-direction: column; align-items: center; justify-content: center; } + .nest-brand { color: #d6d8dc; font-size: 18px; line-height: 26px; font-weight: 600; } + .nest-screen { width: 160px; height: 160px; border-radius: 50%; overflow: hidden; background: #080808; display: grid; place-items: center; color: #aaa; } + .nest-screen img { width: 100%; height: 100%; object-fit: cover; } + .nest-stale .nest-screen { opacity: .4; } + bearstone_infra_chip: template: bearstone_infra_base show_state: true diff --git a/config/packages/README.md b/config/packages/README.md index a3b0faec..a8253ee3 100755 --- a/config/packages/README.md +++ b/config/packages/README.md @@ -46,7 +46,7 @@ Kitchen, Upstairs and Office voice use Kiosk Satellite's [native Assist satellit Native Show enrollment is centralized in `satellite_devices()` in [kiosk_health.jinja](../custom_templates/kiosk_health.jinja). Each entry supplies its stable maintenance key, native node, entity suffix, dashboard path, volume-policy opt-in, and local idle-sleep exception. The daily 07:00 app-update pass processes followers before the fleet leader, stages a signed Kiosk Satellite update, then checks installed version, connectivity, dashboard, remote administration, voice availability, and a fresh screenshot. A stalled installer or incomplete recovery dispatches the enrolled device to the authorized maintenance agent. The app update scope excludes Android/LineageOS and unrelated software. Upstairs and Office volumes are manual; only Kitchen opts into the bed-dependent volume policy. Screen/event policies and snapshot refresh use the same inventory. ### Featured packages (live today) -The kiosk package also exposes `sensor.kiosk_recent_phone_notifications`: a restoring, bounded feed of shared parent/family phone pushes. All three Shows include those messages in their existing ten-second slide rotation for ten minutes; the Bedroom message band alternates them with household reminders while weather, security, and water-heater safety alerts retain priority. Entries show their sent time, replace by notification tag, and expire automatically within fifteen seconds of their deadline. Phone notification buttons and camera attachments remain on the phones. +The kiosk package also exposes `sensor.kiosk_recent_phone_notifications`: a restoring, bounded feed of shared parent/family phone pushes, including title-only notices such as Life360 arrivals. All three Shows include those messages in their existing ten-second slide rotation for five minutes; the Bedroom message band uses the same five-minute feed and alternates them with household reminders while weather, security, and water-heater safety alerts retain priority. The round Living Room Nest gives shared notices an exclusive two-minute display window, then resumes its photo gallery. Entries show their sent time, replace by notification tag, and expire automatically within fifteen seconds of their deadline. Phone notification buttons and camera attachments remain on the phones. | Package | What it unlocks | Notable entities / services | | --- | --- | --- | @@ -82,7 +82,7 @@ The kiosk package also exposes `sensor.kiosk_recent_phone_notifications`: a rest | [![YAML source: vacation_mode](https://img.shields.io/static/v1?label=YAML&message=vacation_mode&color=lightgrey&logo=github&logoColor=181717)](vacation_mode.yaml) | Auto-enable vacation mode after 24 hours away or no bed use, track sitter analytics/secure-house checks with a short garage-arrival debounce, clear stale visits with an explicit unconfirmed-departure notice, deliver sitter-facing briefings, and send 10 AM/10 PM house digests with Powerwall/security/backup status plus Joanna review. LG ThinQ telemetry loss is labeled as an unverified-temperature monitoring gap and pushed once per outage during Vacation Mode; routine digests omit the extra issues push when nothing needs attention. | `input_boolean.vacation_mode`, `input_boolean.house_sitter_present`, `input_boolean.vacation_refrigerator_telemetry_notified`, `input_datetime.vacation_house_sitter_*`, `input_datetime.vacation_house_status_digest_last_sent`, `sensor.vacation_house_sitter_*`, `binary_sensor.powerwall_grid_status`, `sensor.powerwall_charge`, `group.garage_doors`, `lock.front_door`, `script.vacation_house_sitter_clear_presence`, `script.vacation_house_status_digest`, `script.notify_engine`, `rest_command.bearclaw_command`, `script.joanna_send_telegram` | | [![YAML source: royalcaribbean](https://img.shields.io/static/v1?label=YAML&message=royalcaribbean&color=lightgrey&logo=github&logoColor=181717)](royalcaribbean.yaml) | Receives Cruise Tracker price-drop webhooks and polls a separate sanitized endpoint hourly for the next confirmed cruise countdown shown on the Kitchen Show. Confirmation, cabin, pricing, and watch-list details never enter Home Assistant. | `automation.royal_caribbean_price_drop_alert`, `sensor.next_booked_cruise_countdown`, `script.notify_engine` | | [![YAML source: maintenance_log](https://img.shields.io/static/v1?label=YAML&message=maintenance_log&color=lightgrey&logo=github&logoColor=181717)](maintenance_log.yaml) | Webhook-based water softener salt maintenance logging with idempotent event handling, Activity Feed logging, parser-safe refill cadence history, and dashboard-ready template sensors. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/_OqTv9i-jAs) [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/07/home-assistant-water-softener-salt-automation.html) | `automation.maintenance_log_joanna_webhook_ingest`, `sensor.water_softener_salt_average_days_between_refills`, `sensor.water_softener_salt_refill_count`, `sensor.water_softener_salt_total_added` | -| [![YAML source: kiosk_tablet](https://img.shields.io/static/v1?label=YAML&message=kiosk_tablet&color=lightgrey&logo=github&logoColor=181717)](kiosk_tablet.yaml) | Shares Garage-then-Front-Door priority plus reusable Bedroom header/alert state while keeping each display purpose-built. The Garage view now remains active for three minutes after both doors close before each kiosk returns to its default dashboard. Bedroom retains Fully wake/drift recovery and SleepIQ screen behavior around a clock-free night-watch display; Kitchen retains its clock/forecast, rotating household context, native camera overlays, Kiosk Satellite idle dimming, and an event-driven household policy that sets master volume to 13% while either bed is occupied and restores 75% when both are empty, with reconciliation at 08:00 and startup, and invokes the Dim screensaver while `group.bed` is on or the family is away. Nearby rain or lightning starts one five-minute NOAA radar takeover while the sun is above the horizon; afterward, the rotating Storms Approaching banner can replay the latest 20-minute loop while the event remains active. Actionable banner slides reuse one transient detail timer for NWS instructions, the front-door package view, a person-centered family map, open-entry details, Phyn water status with a confirmed valve-restore action, and hot-water recovery status. Sunset restores the ambient wall immediately. True idle panel-off remains disabled on the Crown LineageOS device until it has a reliable motion-wake input. Fully availability/page/app/power checks plus enrolled Show connectivity, current-page, and screenshot freshness keep the Systems Kiosks tab truthful; a reachable Kitchen display remains online while stale session timestamps remain visible only in the telemetry panel. Kitchen, Upstairs, and Office share the fleet-synced `Joanna Voice` Assist pipeline, with persistent follow-ups and native spoken replies through the shared Joanna human agent. Each satellite is explicitly enrolled with its own credential, assigned personal profile, and conversation history; the custom `joanna_conversation` integration enforces device binding. When a selected Joanna Voice satellite enters processing, one shared automation plays a cached two-second "Let me check on that" clip on that device while the answer continues. Upstairs and Office retain their compact Echo Show 5 view and native ESPHome telemetry. Shared health macros cover every enrolled Show, including foreground-app drift on the Echo Show 5 devices; one screenshot refresh loop covers all native Shows every 15 minutes and after delayed startup recovery. Their faults join the same Systems kiosk rollup and screenshot previews. Available Show updates remain advisory telemetry, while stable attention attributes preserve useful outage transitions without recording every heartbeat. At 07:00, Home Assistant processes enrolled followers before the fleet leader, requests each pending signed installer, and waits two minutes for staging. An incomplete installation dispatches Joanna to the fixed enrolled-device maintenance helper; an unsuccessful update is retried at 07:00 the following day until its native update entity confirms success. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/ChgEu0IDWzc) | `binary_sensor.kiosk_garage_camera_active`, `sensor.kiosk_active_camera_view`, `sensor.bedroom_kiosk_header`, `sensor.kiosk_household_alert`, `binary_sensor.kiosk_rain_radar_weather_event`, `binary_sensor.kiosk_rain_radar_active`, `timer.kitchen_kiosk_radar_display`, `input_text.kitchen_kiosk_detail_view`, `timer.kitchen_kiosk_detail_display`, `script.kitchen_kiosk_show_event`, `script.kitchen_kiosk_show_detail`, `script.kitchen_kiosk_restore_phyn_water`, `binary_sensor.bedroom_kiosk_needs_attention`, `binary_sensor.upstairs_kiosk_needs_attention`, `binary_sensor.office_kiosk_needs_attention`, `binary_sensor.kiosk_devices_need_attention`, `sensor.alarm_panel_1_current_page`, `button.alarm_panel_1_load_start_url`, `binary_sensor.kitchen_kiosk_should_be_on`, `number.kitchen_show_volume_2`, `light.kitchen_show_screen_2`, `button.kitchen_show_postpone_screensaver_2`, `button.kitchen_show_take_screenshot_2`, `sensor.kitchen_show_active_camera_view_2`, `automation.kitchen_kiosk_satellite_update_joanna_dispatch` | +| [![YAML source: kiosk_tablet](https://img.shields.io/static/v1?label=YAML&message=kiosk_tablet&color=lightgrey&logo=github&logoColor=181717)](kiosk_tablet.yaml) | Shares Garage-then-Front-Door priority plus reusable Bedroom header/alert state while keeping each display purpose-built. The Garage view now remains active for three minutes after both doors close before each kiosk returns to its default dashboard. Bedroom retains Fully wake/drift recovery and SleepIQ screen behavior around a clock-free night-watch display; Kitchen retains its clock/forecast, rotating household context, native camera overlays, Kiosk Satellite idle dimming, and an event-driven household policy that sets master volume to 13% while either bed is occupied and restores 75% when both are empty, with reconciliation at 08:00 and startup, and invokes the Dim screensaver while `group.bed` is on or the family is away. Nearby rain or lightning starts one five-minute NOAA radar takeover while the sun is above the horizon; afterward, the rotating Storms Approaching banner can replay the latest 20-minute loop while the event remains active. Actionable banner slides reuse one transient detail timer for NWS instructions, the front-door package view, a person-centered family map, open-entry details, Phyn water status with a confirmed valve-restore action, and hot-water recovery status. Sunset restores the ambient wall immediately. True idle panel-off remains disabled on the Crown LineageOS device until it has a reliable motion-wake input. Fully availability/page/app/power checks plus enrolled Show connectivity, current-page, and screenshot freshness keep the Systems Kiosks tab truthful; a reachable Kitchen display remains online while stale session timestamps remain visible only in the telemetry panel. Kitchen, Upstairs, and Office share the fleet-synced `Joanna Voice` Assist pipeline, with persistent follow-ups and native spoken replies through the shared Joanna human agent. The Living Room Nest adds an actual circular framebuffer preview, MQTT connection/screen/feed freshness, bounded scene recovery, and a durable repair after persistent failure; its attention state joins the kiosk and Systems rollups. Each satellite is explicitly enrolled with its own credential, assigned personal profile, and conversation history; the custom `joanna_conversation` integration enforces device binding. When a selected Joanna Voice satellite enters processing, one shared automation plays a cached two-second "Let me check on that" clip on that device while the answer continues. Upstairs and Office retain their compact Echo Show 5 view and native ESPHome telemetry. Shared health macros cover every enrolled Show, including foreground-app drift on the Echo Show 5 devices; one screenshot refresh loop covers all native Shows every 15 minutes and after delayed startup recovery. Their faults join the same Systems kiosk rollup and screenshot previews. Available Show updates remain advisory telemetry, while stable attention attributes preserve useful outage transitions without recording every heartbeat. At 07:00, Home Assistant processes enrolled followers before the fleet leader, requests each pending signed installer, and waits two minutes for staging. An incomplete installation dispatches Joanna to the fixed enrolled-device maintenance helper; an unsuccessful update is retried at 07:00 the following day until its native update entity confirms success. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/ChgEu0IDWzc) | `binary_sensor.kiosk_garage_camera_active`, `sensor.kiosk_active_camera_view`, `sensor.bedroom_kiosk_header`, `sensor.kiosk_household_alert`, `binary_sensor.kiosk_rain_radar_weather_event`, `binary_sensor.kiosk_rain_radar_active`, `timer.kitchen_kiosk_radar_display`, `input_text.kitchen_kiosk_detail_view`, `timer.kitchen_kiosk_detail_display`, `script.kitchen_kiosk_show_event`, `script.kitchen_kiosk_show_detail`, `script.kitchen_kiosk_restore_phyn_water`, `binary_sensor.bedroom_kiosk_needs_attention`, `binary_sensor.upstairs_kiosk_needs_attention`, `binary_sensor.office_kiosk_needs_attention`, `binary_sensor.kiosk_devices_need_attention`, `sensor.alarm_panel_1_current_page`, `button.alarm_panel_1_load_start_url`, `binary_sensor.kitchen_kiosk_should_be_on`, `number.kitchen_show_volume_2`, `light.kitchen_show_screen_2`, `button.kitchen_show_postpone_screensaver_2`, `button.kitchen_show_take_screenshot_2`, `sensor.kitchen_show_active_camera_view_2`, `automation.kitchen_kiosk_satellite_update_joanna_dispatch` | | [![YAML source: powerwall](https://img.shields.io/static/v1?label=YAML&message=powerwall&color=lightgrey&logo=github&logoColor=181717)](powerwall.yaml) | Track Tesla Powerwall grid status, keep a restart-safe outage anchor, publish monitor-only staged resilience recommendations from Home Assistant, handle immediate household load shedding, and alert if the Powerwall stays low after grid power returns. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/hR_0lFEE2bA) [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/06/tesla-powerwall-home-assistant-watchdog.html) | `binary_sensor.powerwall_grid_status`, `sensor.powerwall_*`, `input_datetime.power_resilience_outage_started`, `sensor.power_resilience_recommended_stage`, `script.notify_live_activity`, `repairs.create` | | [![YAML source: tesla_model_y](https://img.shields.io/static/v1?label=YAML&message=tesla_model_y&color=lightgrey&logo=github&logoColor=181717)](tesla_model_y.yaml) | Keeps garage-driven plug-in/tire reminders and durable dashboard snapshots. Fresh actual home draw above 18 A requests a native Tesla stop, critical push, and investigation; the intended 16 A limit stays unchanged. Source timestamps reject cached Tesla data and retained MQTT. Missing reports dispatch investigation at 3 minutes. Unverified home sessions pause at 5 minutes; safe/stopped sessions receive a quiet monitoring warning at 15 minutes. Fresh post-stop zero current clears the critical incident. A restoring review latch requires human acknowledgment before home charging resumes; no start commands are issued. Monitoring recovery clears its own tagged alert. Plug-in and tire snoozes remain available. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/hDNyZfTfTtE) [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/09/home-assistant-tesla-tire-pressure-alerts.html) | `sensor.tesla_home_charge_guard`, `binary_sensor.tesla_home_charge_critical`, `binary_sensor.tesla_home_charge_attention`, `sensor.juicepassproxy_mqtt_heartbeat`, `binary_sensor.juicepassproxy_investigation_needed`, `binary_sensor.juicepassproxy_monitoring_degraded`, `switch.spaceship_charge`, `input_boolean.tesla_home_charge_restart_required`, `input_boolean.tesla_high_charge_rate_alert_active`, `script.tesla_home_charge_protect`, `script.notify_engine`, `script.joanna_dispatch` | | [![YAML source: vacuum](https://img.shields.io/static/v1?label=YAML&message=vacuum&color=lightgrey&logo=github&logoColor=181717)](vacuum.yaml) | Dreame away-only vacuum orchestration with room tracking, sweep/mop phases, a four-day start guard, family-return task cancellation, push alerts, and Alexa one-off room cleans. Rescue sender icon: [![Notification icon tip](https://img.shields.io/badge/Icons-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/TMfdxv-WprM) [![Notification icon guide](https://img.shields.io/static/v1?label=Icons&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/10/home-assistant-ios-notification-icons.html) | `input_select.l10s_vacuum_phase`, `sensor.l10s_vacuum_error`, `repairs.create` | @@ -177,4 +177,4 @@ These are the devices that power the packages above, plus tracked hardware imple Affiliate Disclosure

-Managed NETGEAR switches use [network_switches.yaml](network_switches.yaml): authenticated SNMPv3 monitors all physical links and PoE delivery, key-link speed/error counters, firmware, uptime and PoE budget utilization. Camera PoE controls omit AP, compute and uplink ports. Current firmware provides no thermal/fan/CPU MIB readings. The retired UniFi switch is disabled in the device registry. [glances.yaml](glances.yaml) averages one CPU package reading per server; the three-node hardware temperatures remain distinct from ambient room temperature. +Managed NETGEAR switches use [network_switches.yaml](network_switches.yaml): authenticated SNMPv3 monitors all physical links and PoE delivery, key-link speed/error counters, firmware, uptime and PoE budget utilization. Camera PoE controls omit AP, compute and uplink ports. Current firmware provides no thermal/fan/CPU MIB readings. The retired UniFi switch is disabled in the device registry. [glances.yaml](glances.yaml) retains the informational CPU-package average and uses the hottest package for sustained heat warnings: 80 C for ten minutes, urgent at 90 C for two minutes or 95 C immediately, and recovery below 75 C for fifteen minutes. These hardware readings remain distinct from ambient room temperature. [Thermal protection](../../docs/garage-thermal-protection.md) describes incident deduplication and graceful optional-camera workload shedding. diff --git a/config/packages/docker_infrastructure.yaml b/config/packages/docker_infrastructure.yaml index 714f585c..97f9d0be 100644 --- a/config/packages/docker_infrastructure.yaml +++ b/config/packages/docker_infrastructure.yaml @@ -32,6 +32,7 @@ # Notes: Paige's Bookshelf is a live monitored service and should remain in the group when its discovery switch is present. # Notes: Panel Notes and OneNote Indexer use their current `_2` discovery switches after stale registry cleanup. # Notes: Retired repair cleanup clears stale legacy codex_appliance and Dozzle repair IDs. +# Notes: Intentional Frigate thermal shedding suppresses its outage repair and recovery dispatch; the native hold also prevents restarts. ###################################################################### input_datetime: @@ -681,7 +682,10 @@ script: issue_id: "docker_container_{{ container_key }}_offline" spook_issue_id: "user_docker_container_{{ container_key }}_offline" - condition: template - value_template: "{{ tracked_container and op in ['create', 'clear'] }}" + value_template: >- + {{ tracked_container and op in ['create', 'clear'] and + (op == 'clear' or container_key != 'frigate' or + is_state('binary_sensor.frigate_thermal_recovery_inhibited', 'off')) }} - choose: - conditions: "{{ op == 'create' }}" sequence: @@ -727,6 +731,8 @@ script: - condition: state entity_id: binary_sensor.docker_container_alerts_snoozed state: "off" + - condition: template + value_template: "{{ container_key != 'frigate' or is_state('binary_sensor.frigate_thermal_recovery_inhibited', 'off') }}" - service: repairs.create data: issue_id: "{{ issue_id }}" @@ -782,6 +788,8 @@ script: - condition: state entity_id: binary_sensor.docker_container_alerts_snoozed state: "off" + - condition: template + value_template: "{{ container_key != 'frigate' or is_state('binary_sensor.frigate_thermal_recovery_inhibited', 'off') }}" - service: script.joanna_dispatch data: trigger_context: >- @@ -802,6 +810,9 @@ script: effective_state_20m={{ persistent_effective_state }} request: >- Troubleshoot and resolve the persistent Docker container outage if possible. + First check binary_sensor.frigate_thermal_recovery_inhibited and sensor.proxmox02_thermal_guard. + If the thermal recovery block is on, unknown, or unavailable, never start Frigate or Docker14/VM 101. + An intentional thermal shutdown requires operator release after verified cooling. Reply with explicit status fields: resolved=true/false, root_cause, diff --git a/config/packages/glances.yaml b/config/packages/glances.yaml index 4613a601..3180cccd 100755 --- a/config/packages/glances.yaml +++ b/config/packages/glances.yaml @@ -9,6 +9,9 @@ # ------------------------------------------------------------------- # Notes: Garage CPU average and maximum use one CPU package reading per host; SSD temperatures stay separate. # Notes: Unavailable readings are excluded; these temperatures are not ambient garage readings. +# Notes: Hottest-package warnings use 80 C/10 min, urgent 90 C/2 min or 95 C immediately, recovery below 75 C/15 min. +# Notes: Restored trigger-sensor attributes retain the incident; missing telemetry cannot clear it. +# Notes: The optional camera host has an independent graceful thermal guard and an operator-release hold. ###################################################################### sensor: @@ -61,19 +64,137 @@ template: unavailable {% endif %} + - triggers: + - trigger: time_pattern + seconds: "/30" + - trigger: state + entity_id: sensor.proxmox_garage_hottest_cpu_temperature + - trigger: homeassistant + event: start + sensor: + - name: "Proxmox Garage Thermal State" + unique_id: proxmox_garage_thermal_state + icon: mdi:thermometer-alert + state: >- + {% from 'garage_thermal.jinja' import garage_thermal_policy %} + {% set policy = garage_thermal_policy | as_function %} + {{ policy(this.attributes.get('memory', {}), this.state)['band'] }} + attributes: + memory: >- + {% from 'garage_thermal.jinja' import garage_thermal_policy %} + {% set policy = garage_thermal_policy | as_function %} + {{ policy(this.attributes.get('memory', {}), this.state)['memory'] }} + warning_threshold_f: 176 + urgent_threshold_f: 194 + immediate_threshold_f: 203 + recovery_threshold_f: 167 + warning_minutes: 10 + urgent_minutes: 2 + recovery_minutes: 15 + telemetry_degraded: >- + {% from 'garage_thermal.jinja' import garage_thermal_policy %} + {% set policy = garage_thermal_policy | as_function %} + {{ policy(this.attributes.get('memory', {}), this.state)['telemetry_degraded'] }} + + - triggers: + - trigger: webhook + webhook_id: !secret proxmox_thermal_status_webhook + allowed_methods: [POST] + local_only: true + sensor: + - name: "Proxmox02 Thermal Guard" + unique_id: proxmox02_thermal_guard + icon: mdi:shield-thermometer + state: "{{ trigger.json.phase }}" + attributes: + hold: "{{ trigger.json.hold }}" + observed_at: "{{ trigger.json.observed_at }}" + stage_since: "{{ trigger.json.stage_since | default(0, true) }}" + last_action: "{{ trigger.json.last_action | default('', true) }}" + error: "{{ trigger.json.error | default('', true) }}" + + - binary_sensor: + - name: "Proxmox Garage Thermal Alert" + unique_id: proxmox_garage_thermal_alert + device_class: problem + state: "{{ states('sensor.proxmox_garage_thermal_state') in ['warning', 'urgent'] }}" + - name: "Proxmox Garage Thermal Telemetry Degraded" + unique_id: proxmox_garage_thermal_telemetry_degraded + device_class: problem + state: "{{ state_attr('sensor.proxmox_garage_thermal_state', 'telemetry_degraded') == true }}" + - name: "Frigate Thermal Recovery Inhibited" + unique_id: frigate_thermal_recovery_inhibited + device_class: problem + state: >- + {% set hold = state_attr('sensor.proxmox02_thermal_guard', 'hold') %} + {% set stamp = state_attr('sensor.proxmox02_thermal_guard', 'observed_at') | float(0) %} + {{ hold != false or as_timestamp(now()) - stamp > 180 }} + automation: - alias: "Garage Proxmox High Temperature Warning" id: proxmox-garage-high-temp-warning-automation - trigger: - - platform: numeric_state - entity_id: sensor.proxmox_garage_average_temperature - above: 145 # Adjust threshold as needed - action: - - service: script.notify_engine + mode: queued + triggers: + - trigger: state + entity_id: sensor.proxmox_garage_thermal_state + conditions: + - condition: template + value_template: >- + {{ trigger.to_state is not none and trigger.from_state is not none and + trigger.to_state.state != trigger.from_state.state and + (trigger.to_state.state in ['warning', 'urgent'] or + (trigger.to_state.state == 'normal' and trigger.from_state.state in ['warning', 'urgent'])) }} + actions: + - action: script.notify_engine data: notification_icon: "mdi:thermometer-alert" color: "#C62828" title: "Garage Server Temperature" - value1: "The average temperature of your Proxmox servers is above 145\u00B0F!" + value1: >- + {% set band = trigger.to_state.state %} + {% if band == 'normal' %}clear_notification + {% else %} + {{ 'Urgent CPU heat' if band == 'urgent' else 'Sustained CPU heat' }}: + {{ states('sensor.proxmox_garage_hottest_cpu_temperature') }} F on + {{ state_attr('sensor.proxmox_garage_hottest_cpu_temperature', 'max_entity_id') | replace('sensor.', '') | replace('_package_id_0_temperature', '') }}. + {{ 'The optional camera host has independent staged thermal protection; essential guests are preserved.' if band == 'urgent' else 'The hottest CPU package has remained at or above 176 F for 10 minutes.' }} + {% endif %} who: 'carlo' group: 'information' + tag: garage_cpu_heat + level: "{{ 'active' if trigger.to_state.state == 'urgent' else 'passive' }}" + - action: script.send_to_logbook + data: + topic: "THERMAL" + message: "Garage CPU thermal state changed to {{ trigger.to_state.state }}." + + - alias: "Garage Thermal Protection Status" + id: garage-thermal-protection-status + mode: queued + triggers: + - trigger: state + entity_id: sensor.proxmox02_thermal_guard + conditions: + - condition: template + value_template: >- + {{ trigger.to_state is not none and trigger.from_state is not none and + (trigger.to_state.state != trigger.from_state.state or + trigger.to_state.attributes.get('error', '') != trigger.from_state.attributes.get('error', '')) and + trigger.to_state.attributes.get('hold', false) }} + actions: + - action: script.notify_engine + data: + title: "Garage Thermal Protection" + notification_icon: "mdi:shield-thermometer" + who: carlo + group: information + tag: garage_cpu_heat + level: "{{ 'active' if trigger.to_state.attributes.get('error') else 'passive' }}" + value1: >- + Camera-host protection: {{ trigger.to_state.state | replace('_', ' ') }}. + {{ trigger.to_state.attributes.get('error', '') }} + Frigate recovery is blocked pending operator review; nothing will restart automatically. + - action: script.send_to_logbook + data: + topic: "THERMAL" + message: "Camera-host thermal protection: {{ trigger.to_state.state }}. {{ trigger.to_state.attributes.get('error', '') }}" diff --git a/config/packages/infrastructure.yaml b/config/packages/infrastructure.yaml index 303cfc3a..251c1e99 100644 --- a/config/packages/infrastructure.yaml +++ b/config/packages/infrastructure.yaml @@ -33,8 +33,9 @@ # Notes: Nebula Sync compares the docker_10 primary with the physical office-pi DNS replica and dispatches Joanna on sustained drift or endpoint loss. # Notes: Promoted IoT DNS consistency compares primary/backup Pi-hole answers for reserved IoT host records. # Notes: DNS probes retry brief resolver interruptions and filter transport diagnostics; timeout budgets also cover persistent resolver loss. -# Notes: Garage compute health uses the same 145 F threshold as the existing temperature notification. +# Notes: Garage compute heat uses the shared sustained hottest-package thermal incident and telemetry health. # Notes: Immediate website-down states create Repairs; the installed UptimeRobot integration exposes current state, not trend rollups. +# Notes: External IP uses the live ipify probe; website rollups include only configured UptimeRobot monitors. # Notes: Bear Stone and website outages dispatch Joanna quietly; no direct phone alerts are sent. # Notes: Dashboard aggregate issue count is live-only; its underlying degraded sensors retain actionable history. # Notes: Dashboard aggregate issue count includes compact BearClaw scheduled-job warning/error health. @@ -193,14 +194,7 @@ template: warning_since: "{{ states('input_text.infra_eaton_low_voltage_since') }}" - name: "Infra External IP" unique_id: infra_external_ip - state: >- - {% set primary = states('sensor.external_ip') | trim %} - {% set fallback = states('sensor.infra_external_ip_fallback') | trim %} - {% if primary not in ['unknown', 'unavailable', 'none', ''] %} - {{ primary }} - {% else %} - {{ fallback }} - {% endif %} + state: "{{ states('sensor.infra_external_ip_fallback') | trim }}" - name: "docker_17 Disk Used Percentage" unique_id: docker_17_disk_used_percentage @@ -233,8 +227,6 @@ template: {% set ids = [ 'binary_sensor.bear_stone', 'binary_sensor.vcloudinfo_com', - 'binary_sensor.ipmer_com', - 'binary_sensor.fordst_com', 'binary_sensor.www_kingcrafthomes_com' ] %} {% set ns = namespace(count=0) %} @@ -252,16 +244,12 @@ template: {{ [ 'binary_sensor.bear_stone', 'binary_sensor.vcloudinfo_com', - 'binary_sensor.ipmer_com', - 'binary_sensor.fordst_com', 'binary_sensor.www_kingcrafthomes_com' ] }} down_entities: >- {% set ids = [ 'binary_sensor.bear_stone', 'binary_sensor.vcloudinfo_com', - 'binary_sensor.ipmer_com', - 'binary_sensor.fordst_com', 'binary_sensor.www_kingcrafthomes_com' ] %} {% set ns = namespace(items=[]) %} @@ -577,8 +565,8 @@ template: states('binary_sensor.proxmox1_runtime_healthy') != 'on' or states('binary_sensor.proxmox02_runtime_healthy') != 'on' or states('binary_sensor.node_proxmox03_status') != 'on' or - states('sensor.proxmox_garage_average_temperature') in ['unknown', 'unavailable', 'none', ''] or - states('sensor.proxmox_garage_average_temperature') | float(0) > 145 }} + is_state('binary_sensor.proxmox_garage_thermal_alert', 'on') or + is_state('binary_sensor.proxmox_garage_thermal_telemetry_degraded', 'on') }} - name: "Infra Storage Degraded" unique_id: infra_storage_degraded diff --git a/config/packages/kiosk_tablet.yaml b/config/packages/kiosk_tablet.yaml index c8f6b102..cb1ea859 100644 --- a/config/packages/kiosk_tablet.yaml +++ b/config/packages/kiosk_tablet.yaml @@ -30,12 +30,13 @@ # Notes: Native Shows share screen, volume, camera, snapshot and 07:00 update policies through satellite_devices. # Notes: Updates run followers first; Joanna receives an exact enrolled device key and preserves its route. # Notes: Kiosk attention sensors expose only their stable status attribute so recorder preserves problem transitions without heartbeat churn. +# Notes: The round Nest uses heartbeat/checksum health and bounded scene recovery; preview age is independent. # Notes: Nearby daytime rain or lightning starts a five-minute NOAA radar view; the ambient banner offers daylight replay while the event remains active. # Notes: Actionable Kitchen banner slides open one shared short-lived detail surface; camera events retain priority. # Notes: Phyn detail can restore a closed main valve only after an explicit on-screen confirmation. # Notes: Rheem faults outrank low-hot-water reminders and reuse the hot-water diagnostic detail surface. # Notes: Verified Health Coach personal bests rotate for seven days; Bedroom safety and reminders retain priority. -# Notes: Shared parent/family phone pushes rotate for ten minutes; tags replace or clear matching entries. +# Notes: Shared parent/family phone pushes rotate for five minutes; tags replace or clear matching entries. # Notes: The bounded notification feed uses sent timestamps, restores across restart, and prunes every 15 seconds. # Notes: Joanna Voice processing plays one cached spoken acknowledgment on the requesting Show; final speech stays in Assist. # Notes: joanna_voice_ack_url points to /local/joanna-voice/acknowledgement.mp3 on the local HA server. @@ -66,6 +67,47 @@ timer: icon: mdi:car-search automation: + - alias: "Living Room Nest - Reconcile Display Repair" + id: living_room_nest_reconcile_display_repair + description: "Keep one durable repair after the device's bounded recovery fails, and clear it on verified recovery." + mode: queued + max: 3 + trigger: + - platform: state + entity_id: binary_sensor.living_room_nest_kiosk_needs_attention + to: "on" + - platform: state + entity_id: binary_sensor.living_room_nest_kiosk_needs_attention + to: "off" + - platform: homeassistant + event: start + action: + - choose: + - conditions: + - condition: state + entity_id: binary_sensor.living_room_nest_kiosk_needs_attention + state: "on" + sequence: + - action: repairs.create + data: + issue_id: living_room_nest_display + title: "Living Room Nest display needs attention" + description: >- + The display monitor reports {{ state_attr('binary_sensor.living_room_nest_kiosk_needs_attention', 'status') }}. + Check the connection, alert feed and latest round preview on the Systems Kiosks view. + Automatic scene recovery is limited to three restarts per hour; offline hardware requires a power/network check. + severity: warning + persistent: true + - conditions: + - condition: state + entity_id: binary_sensor.living_room_nest_kiosk_needs_attention + state: "off" + sequence: + - action: repairs.remove + continue_on_error: true + data: + issue_id: living_room_nest_display + - alias: "Joanna Voice - Acknowledge Heard Request" id: joanna_voice_acknowledge_heard_request mode: parallel @@ -488,7 +530,7 @@ template: {% set tag = data.get('tag', '') | default('', true) | string | trim %} {% set key = 'tag:' ~ tag if tag else 'message:' ~ title ~ '|' ~ message %} {% set clear = message == 'clear_notification' %} - {% set publish = trigger.id == 'notification' and message and not clear + {% set publish = trigger.id == 'notification' and (title or message) and not clear and not message.startswith('command_') %} {% set ns = namespace(items=[]) %} {% for item in state_attr('sensor.kiosk_recent_phone_notifications', 'items') | default([], true) %} @@ -501,7 +543,7 @@ template: {% set icon = data.get('notification_icon', '') | default('', true) | string %} {% set color = data.get('color', '') | default('', true) | string %} {% set ns.items = ns.items + [dict(key=key, title=title or 'Household notification', - message=message | truncate(320, true), sent_at=current, expires_at=current + 600, + message=message | truncate(320, true), sent_at=current, expires_at=current + 300, sent_time=now().strftime('%-I:%M %p'), icon=icon if icon.startswith('mdi:') else 'mdi:message-text-outline', color=color if color | regex_match('^#[0-9a-fA-F]{6}$') else '#1565c0')] %} @@ -838,6 +880,18 @@ template: {% from 'kiosk_health.jinja' import satellite_status %} {{ satellite_status('office_show', '/dashboard-kiosk-satellite-staging/echo-show5', check_foreground=true, include_update=true) | trim }} + - name: "Living Room Nest Kiosk Needs Attention" + unique_id: living_room_nest_kiosk_needs_attention + device_class: problem + icon: mdi:circle-outline + delay_on: "00:02:00" + delay_off: "00:00:30" + state: >- + {{ not is_state('sensor.living_room_nest_status', 'online') }} + attributes: + status: >- + {{ states('sensor.living_room_nest_status') }} + - name: "Kiosk Devices Need Attention" unique_id: kiosk_devices_need_attention device_class: problem diff --git a/config/packages/proxmox.yaml b/config/packages/proxmox.yaml index 453df388..f439bbf3 100644 --- a/config/packages/proxmox.yaml +++ b/config/packages/proxmox.yaml @@ -16,6 +16,7 @@ # Notes: HA placement uses preferences across three nodes; the camera workload yields capacity after node failure. # Notes: Overnight maintenance uses a four-hour fail-safe alert lease and releases its own lease after verified recovery. # Notes: Normalized disk usage sensors expose state_class for long-term trend rollups. +# Notes: Disk percentages use the integration's native measurement; missing telemetry stays unavailable. ###################################################################### template: - sensor: @@ -25,26 +26,9 @@ template: state_class: measurement icon: mdi:harddisk availability: >- - {% set preferred = states('sensor.node_proxmox1_disk_used_percentage') %} - {% set used = states('sensor.node_proxmox1_disk') %} - {% set total = states('sensor.node_proxmox1_max_disk') %} - {{ preferred not in ['unknown', 'unavailable', 'none', ''] or - (used not in ['unknown', 'unavailable', 'none', ''] and - total not in ['unknown', 'unavailable', 'none', ''] and - (total | float(0)) > 0) }} + {{ is_number(states('sensor.node_proxmox1_disk_used_percentage')) }} state: >- - {% set preferred = states('sensor.node_proxmox1_disk_used_percentage') %} - {% if preferred not in ['unknown', 'unavailable', 'none', ''] %} - {{ preferred | float(0) | round(1) }} - {% else %} - {% set used = states('sensor.node_proxmox1_disk') | float(0) %} - {% set total = states('sensor.node_proxmox1_max_disk') | float(0) %} - {% if total > 0 %} - {{ ((used / total) * 100) | round(1) }} - {% else %} - 0 - {% endif %} - {% endif %} + {{ states('sensor.node_proxmox1_disk_used_percentage') | float(0) | round(1) }} - name: "Proxmox02 Disk Used Percentage" unique_id: proxmox02_disk_used_percentage @@ -52,26 +36,9 @@ template: state_class: measurement icon: mdi:harddisk availability: >- - {% set preferred = states('sensor.node_proxmox02_disk_used_percentage') %} - {% set used = states('sensor.node_proxmox02_disk') %} - {% set total = states('sensor.node_proxmox02_max_disk') %} - {{ preferred not in ['unknown', 'unavailable', 'none', ''] or - (used not in ['unknown', 'unavailable', 'none', ''] and - total not in ['unknown', 'unavailable', 'none', ''] and - (total | float(0)) > 0) }} + {{ is_number(states('sensor.node_proxmox02_disk_used_percentage')) }} state: >- - {% set preferred = states('sensor.node_proxmox02_disk_used_percentage') %} - {% if preferred not in ['unknown', 'unavailable', 'none', ''] %} - {{ preferred | float(0) | round(1) }} - {% else %} - {% set used = states('sensor.node_proxmox02_disk') | float(0) %} - {% set total = states('sensor.node_proxmox02_max_disk') | float(0) %} - {% if total > 0 %} - {{ ((used / total) * 100) | round(1) }} - {% else %} - 0 - {% endif %} - {% endif %} + {{ states('sensor.node_proxmox02_disk_used_percentage') | float(0) | round(1) }} - binary_sensor: - name: "Proxmox1 Runtime Healthy" diff --git a/config/recorder.yaml b/config/recorder.yaml index 4ba0600b..beb63595 100755 --- a/config/recorder.yaml +++ b/config/recorder.yaml @@ -86,6 +86,12 @@ exclude: - update - zone entity_globs: + # Live display frames and heartbeats; kiosk problem transitions retain history. + - sensor.living_room_nest_* + - image.living_room_nest_* + - binary_sensor.living_room_nest_connectivity + - binary_sensor.living_room_nest_screen + - binary_sensor.living_room_nest_alert_feed - binary_sensor.*_motion - binary_sensor.*_motion_* - binary_sensor.*is_present @@ -236,6 +242,8 @@ exclude: entities: - automation.update_garage_get_status + # Native thermal guard heartbeat is live-only; final incident and hold retain history. + - sensor.proxmox02_thermal_guard # Source verification is live-only; critical/attention sensors and curated incidents remain recorded. - sensor.tesla_home_charge_guard - binary_sensor.juicepassproxy_live_reports_missing diff --git a/docs/garage-thermal-protection.md b/docs/garage-thermal-protection.md new file mode 100644 index 00000000..dc384b6e --- /dev/null +++ b/docs/garage-thermal-protection.md @@ -0,0 +1,47 @@ +# Garage thermal protection + +The hottest available CPU package controls the incident. An informational +average cannot identify a hot individual machine and does not describe ambient +temperature. + +| Stage | CPU package condition | Behavior | +| --- | --- | --- | +| Warning | At least 80 C / 176 F for ten minutes | Send one quiet warning | +| Urgent | At least 90 C / 194 F for two minutes | Escalate the existing notification | +| Immediate urgent | At least 95 C / 203 F | Skip the urgent dwell | +| Recovery | Every package below 75 C / 167 F for fifteen minutes | Clear the incident and rearm | + +Incident state and dwell timing survive template updates. Short sensor +fluctuations do not rearm the policy. Missing or stale readings cannot clear an +incident; prolonged missing telemetry becomes a separate problem. An +observation gap resets dwell timing while preserving an active incident. +Dashboard graph colors show instantaneous readings and do not represent the +dwell-based alert policy. + +The temperature values are operating thresholds for this installation, not +manufacturer limits. Package/core temperatures and ambient temperatures are +different measurements. + +## Optional workload guard + +An independent host-level guard monitors configured package readings and +protects one site-configured optional camera workload. Its durable hold prevents +a recovery loop after a thermal stop. Site-specific host bindings and access +credentials belong in local configuration outside tracked source. + +At urgent heat, the guard records the hold and requests a graceful stop of the +optional container. If heat persists, it can request a graceful stop of the +configured optional guest. Sustained severe heat can request a normal host +shutdown after fresh temperature readings and configured cluster-safety checks +confirm that shutdown is safe. The guard does not force-stop, start, migrate, or +reboot workloads. An uncertain safety check blocks host shutdown and reports +the reason. + +The hold prevents automatic recovery of the optional workload. Release it only +after verified cooling and review of workload placement, capacity, storage, and +the cooling system. Releasing the hold does not start the workload. A physical +host that has shut down requires operator recovery. + +Validate the Home Assistant configuration and affected dashboards before +rollout. Exercise dwell, stale-telemetry, hold, and graceful-stop cases with +controlled test fixtures. Do not heat equipment artificially to test the guard.