Add thermal and Nest health monitoring

codex/local-holiday-calendars
Carlo Costanzo 1 month ago
parent 0445c21712
commit 39702f7103

3
.gitignore vendored

@ -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

@ -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 %}

@ -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 %}

@ -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

@ -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
# -------------------------------------------------------------------

@ -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

@ -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/') ? `<img src="${url}&v=${stamp}" alt="Last requested Nest preview">` : '<span>Tap to capture</span>';
return `<div class="nest-bezel ${fresh ? '' : 'nest-stale'}"><div class="nest-face"><div class="nest-brand">nest</div><div class="nest-screen">${image}</div></div></div>`;
]]]
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

@ -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
</a></p>
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.

@ -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,

@ -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', '') }}"

@ -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

@ -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

@ -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"

@ -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

@ -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.
Loading…
Cancel
Save

Powered by TurnKey Linux.