From 8414b628097098b982e0d7bc2aebe2dc5031e768 Mon Sep 17 00:00:00 2001 From: Carlo Costanzo Date: Tue, 15 Sep 2026 17:22:49 -0400 Subject: [PATCH] Expand infrastructure telemetry dashboards --- .../dashboards/infrastructure/dashboard.yaml | 1 + .../infrastructure/partials/car_sections.yaml | 476 ++++++++++++++++++ .../partials/home_sections.yaml | 132 +++-- .../infrastructure/partials/ups_sections.yaml | 457 +++++++++++++++++ .../infrastructure/views/01z_car.yaml | 23 + .../infrastructure/views/04_ups.yaml | 22 + .../infrastructure/views/11_vacuum_map.yaml | 23 +- config/logbook.yaml | 2 +- config/packages/docker_infrastructure.yaml | 1 - config/packages/infrastructure.yaml | 45 +- config/packages/pihole_ha.yaml | 2 +- config/recorder.yaml | 8 +- 12 files changed, 1142 insertions(+), 50 deletions(-) create mode 100644 config/dashboards/infrastructure/partials/car_sections.yaml create mode 100644 config/dashboards/infrastructure/partials/ups_sections.yaml create mode 100644 config/dashboards/infrastructure/views/01z_car.yaml create mode 100644 config/dashboards/infrastructure/views/04_ups.yaml diff --git a/config/dashboards/infrastructure/dashboard.yaml b/config/dashboards/infrastructure/dashboard.yaml index 15fcdc8f..41060386 100644 --- a/config/dashboards/infrastructure/dashboard.yaml +++ b/config/dashboards/infrastructure/dashboard.yaml @@ -8,6 +8,7 @@ # ------------------------------------------------------------------- # Notes: Stable dashboard key/path remains `dashboard-infrastructure`; the user-facing name is Systems. # Notes: Water, HVAC, and Vacuum are promoted production views; Health remains on Overview. +# Notes: Car combines live Tesla, tire-pressure, odometer, and read-only JuiceBox telemetry. # Notes: Joanna dispatch telemetry view is available at `/dashboard-infrastructure/joanna`. # Notes: Network combines WAN, dual Pi-hole, and website health; `/wan`, `/pihole`, and `/website-health` are detail subviews. ###################################################################### diff --git a/config/dashboards/infrastructure/partials/car_sections.yaml b/config/dashboards/infrastructure/partials/car_sections.yaml new file mode 100644 index 00000000..3d60d03f --- /dev/null +++ b/config/dashboards/infrastructure/partials/car_sections.yaml @@ -0,0 +1,476 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Systems Partial - Car +# Read-only vehicle, tire, odometer, and JuiceBox charging summary. +# ------------------------------------------------------------------- +# Related Issue: 1944 +# Notes: No cards expose Tesla or JuiceBox controls. +# Notes: TeslaMate retains detailed drive, charge, and mileage history. +# Notes: Useful tiles use restored last-valid values without timestamp churn. +# Notes: Raw JuiceBox energy retains long-term history; power is session-only. +# Notes: The Tesla's Home location charge-current setting is intentionally 16 A. +# Notes: JuicePassProxy shows OFFLINE immediately and becomes a Systems fault +# after five continuous minutes; neither dashboard exposes writable controls. +###################################################################### + +# ------------------------------------------------------------------- +# Vehicle health strip (mandatory first full-width wrapper) +# ------------------------------------------------------------------- +- 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: repeat(6, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(2, minmax(0, 1fr)) + cards: + - type: custom:button-card + template: bearstone_nav_back + name: Back to Systems + icon: mdi:arrow-left + view_layout: + grid-column: 1 / -1 + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/home + + - type: custom:button-card + template: bearstone_infra_kpi + entity: sensor.car_battery_last_known + name: Battery + icon: mdi:car-electric + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)}%` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_kpi + entity: sensor.car_odometer_last_known + name: Odometer + icon: mdi:counter + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) + ? `${value.toLocaleString(undefined, {maximumFractionDigits: 0})} mi` + : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_kpi + entity: sensor.car_charging_last_known + name: Charging + icon: mdi:ev-station + state_display: > + [[[ + const raw = String(entity?.state ?? '').toUpperCase(); + if (['UNKNOWN', 'UNAVAILABLE', ''].includes(raw)) return 'NO DATA'; + const live = String(states['sensor.spaceship_charging']?.state ?? '').toLowerCase(); + const prefix = ['unknown', 'unavailable', ''].includes(live) ? 'LAST ' : ''; + return `${prefix}${raw}`; + ]]] + + - type: custom:button-card + template: bearstone_infra_status_kpi + entity: sensor.car_tire_pressure_front_left_last_known + name: Tires + icon: mdi:car-tire-alert + variables: + status_severity: > + [[[ + const ids = [ + 'sensor.car_tire_pressure_front_left_last_known', + 'sensor.car_tire_pressure_front_right_last_known', + 'sensor.car_tire_pressure_rear_left_last_known', + 'sensor.car_tire_pressure_rear_right_last_known' + ]; + const values = ids.map((id) => Number(states[id]?.state)).filter(Number.isFinite); + if (values.length !== ids.length) return 'unknown'; + return Math.min(...values) < 40 ? 'critical' : 'healthy'; + ]]] + state_display: > + [[[ + const ids = [ + 'sensor.car_tire_pressure_front_left_last_known', + 'sensor.car_tire_pressure_front_right_last_known', + 'sensor.car_tire_pressure_rear_left_last_known', + 'sensor.car_tire_pressure_rear_right_last_known' + ]; + const values = ids.map((id) => Number(states[id]?.state)).filter(Number.isFinite); + if (values.length !== ids.length) return 'NO DATA'; + const minimum = Math.min(...values); + return `${minimum < 40 ? 'LOW' : 'MIN'} ${minimum.toFixed(1)} psi`; + ]]] + + - type: custom:button-card + template: bearstone_infra_status_kpi + entity: sensor.juicebox_status_last_known + name: JuiceBox + icon: mdi:ev-plug-type1 + variables: + status_severity: > + [[[ + const raw = String(entity?.state ?? '').toLowerCase(); + return ['unknown', 'unavailable', 'none', ''].includes(raw) ? 'unknown' : 'healthy'; + ]]] + state_display: > + [[[ + const raw = String(entity?.state ?? '').toUpperCase(); + if (['UNKNOWN', 'UNAVAILABLE', 'NONE', ''].includes(raw)) return 'WAITING'; + const live = String(states['sensor.juicebox_status']?.state ?? '').toLowerCase(); + const prefix = ['unknown', 'unavailable', 'none', ''].includes(live) ? 'LAST ' : ''; + return `${prefix}${raw}`; + ]]] + + - type: custom:button-card + template: bearstone_infra_status_kpi + entity: sensor.juicepassproxy_state + name: Proxy + icon: mdi:shield-check + variables: + status_severity: > + [[[ + if (states['binary_sensor.juicepassproxy_monitoring_degraded']?.state === 'on') return 'critical'; + const running = String(entity?.state ?? '').toLowerCase() === 'running'; + const healthy = String(states['sensor.juicepassproxy_health']?.state ?? '').toLowerCase() === 'healthy'; + return running && healthy ? 'healthy' : 'warning'; + ]]] + state_display: > + [[[ + const running = String(entity?.state ?? '').toLowerCase() === 'running'; + const healthy = String(states['sensor.juicepassproxy_health']?.state ?? '').toLowerCase() === 'healthy'; + return running && healthy ? 'ONLINE' : 'OFFLINE'; + ]]] + +# ------------------------------------------------------------------- +# Current vehicle, tires, and charging telemetry +# ------------------------------------------------------------------- +- 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: repeat(3, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + cards: + - 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_icon + name: Spaceship + icon: mdi:car-electric + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.car_location_last_known + name: Location + icon: mdi:map-marker + state_display: > + [[[ return String(entity?.state ?? 'NO DATA').replaceAll('_', ' ').toUpperCase(); ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.car_battery_last_known + name: Battery + icon: mdi:battery-high + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)}%` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.car_odometer_last_known + name: Odometer + icon: mdi:counter + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) + ? `${value.toLocaleString(undefined, {maximumFractionDigits: 0})} mi` + : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.car_charge_cable_last_known + name: Charge Cable + icon: mdi:ev-plug-type1 + state_display: > + [[[ + const raw = String(entity?.state ?? '').toLowerCase(); + if (['unknown', 'unavailable', ''].includes(raw)) return 'NO DATA'; + return raw === 'on' ? 'CONNECTED' : 'DISCONNECTED'; + ]]] + + - 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_icon + name: Tire Pressure + icon: mdi:car-tire-alert + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.car_tire_pressure_front_left_last_known + name: Front Left + icon: mdi:circle-double + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(1)} psi` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.car_tire_pressure_front_right_last_known + name: Front Right + icon: mdi:circle-double + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(1)} psi` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.car_tire_pressure_rear_left_last_known + name: Rear Left + icon: mdi:circle-double + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(1)} psi` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.car_tire_pressure_rear_right_last_known + name: Rear Right + icon: mdi:circle-double + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(1)} psi` : 'NO DATA'; + ]]] + + - 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_icon + name: Charging Session + icon: mdi:battery-charging-high + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_session_energy_last_known + name: JuiceBox Session + icon: mdi:counter + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${(value / 1000).toFixed(2)} kWh` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_power_last_known + name: JuiceBox Power + icon: mdi:flash + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${(value / 1000).toFixed(1)} kW` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_current_last_known + name: JuiceBox Current + icon: mdi:current-ac + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(1)} A` : 'NO DATA'; + ]]] + +# ------------------------------------------------------------------- +# JuiceBox electrical detail +# ------------------------------------------------------------------- +- 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: repeat(2, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + cards: + - 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_icon + name: JuiceBox Electrical + icon: mdi:ev-station + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_voltage_last_known + name: Voltage + icon: mdi:sine-wave + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_frequency_last_known + name: Frequency + icon: mdi:waveform + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_power_factor_last_known + name: Power Factor + icon: mdi:chart-bell-curve-cumulative + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_temperature_last_known + name: Temperature + icon: mdi:thermometer + + - 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_icon + name: JuiceBox Capacity + icon: mdi:gauge + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_current_rating_last_known + name: Hardware Rating + icon: mdi:current-ac + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_current_rating_last_known + name: Stored Circuit Limit + icon: mdi:shield-check + state_display: 24 A + tap_action: + action: none + hold_action: + action: none + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_lifetime_energy_last_known + name: Lifetime Energy + icon: mdi:transmission-tower-export + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${(value / 1000).toFixed(1)} kWh` : 'NO DATA'; + ]]] + +# ------------------------------------------------------------------- +# Charging trends +# ------------------------------------------------------------------- +- 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) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + cards: + - 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_icon + name: Energy Delivered - 30 Days + icon: mdi:chart-bar + + - type: conditional + conditions: + - entity: sensor.juicebox_energy_lifetime + state_not: unavailable + card: + type: statistics-graph + chart_type: bar + days_to_show: 30 + period: day + stat_types: + - change + entities: + - entity: sensor.juicebox_energy_lifetime + name: JuiceBox Energy + + - type: conditional + conditions: + - entity: sensor.juicebox_energy_lifetime + state: unavailable + card: + type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.juicebox_status + name: Energy history starts with first report + icon: mdi:chart-timeline-variant-shimmer + state_display: PENDING diff --git a/config/dashboards/infrastructure/partials/home_sections.yaml b/config/dashboards/infrastructure/partials/home_sections.yaml index c85fc4a7..bb443a1c 100644 --- a/config/dashboards/infrastructure/partials/home_sections.yaml +++ b/config/dashboards/infrastructure/partials/home_sections.yaml @@ -6,7 +6,7 @@ # Systems Partial - operations summary # House-system health strip, active issues, and datacenter status. # ------------------------------------------------------------------- -# Related Issue: 1890, 1817 +# Related Issue: 1890, 1817, 1944 # Notes: The health ribbon mirrors the visible dashboard navigation order and labels. # Notes: DNS and external-monitoring faults roll up under Network instead of duplicating it. # Notes: Network trend panels live on Network so Overview stays focused on broad house status. @@ -16,6 +16,9 @@ # Notes: BearClaw scheduled-job warnings/errors roll up under Joanna and appear as an exceptions-only Active Issue row. # Notes: Water and Systems surface normalized Rheem faults; telemetry-only loss renders amber. # Notes: Back to Overview is the first control in the landing-view navigation strip. +# Notes: UPS health covers both Garage and Office NUT servers and drills into the shared UPS view. +# Notes: A JuicePassProxy outage becomes a critical Systems issue after five +# continuous minutes and drills into the Car view. ###################################################################### # ------------------------------------------------------------------- @@ -74,9 +77,11 @@ critical_entity_ids: - binary_sensor.infra_wan_critical - binary_sensor.infra_dns_pihole_degraded + - binary_sensor.infra_ups_on_battery - binary_sensor.infra_compute_degraded - binary_sensor.infra_storage_degraded - binary_sensor.infra_external_monitoring_degraded + - binary_sensor.juicepassproxy_monitoring_degraded - binary_sensor.rheem_wh_active_alert - binary_sensor.garage_phyn_leak_alert @@ -126,6 +131,37 @@ action: navigate navigation_path: /dashboard-infrastructure/vacuum + - type: custom:button-card + template: bearstone_infra_status_kpi + entity: sensor.car_battery_last_known + name: Car + icon: mdi:car-electric + variables: + status_severity: > + [[[ + const tireIds = [ + 'sensor.car_tire_pressure_front_left_last_known', + 'sensor.car_tire_pressure_front_right_last_known', + 'sensor.car_tire_pressure_rear_left_last_known', + 'sensor.car_tire_pressure_rear_right_last_known' + ]; + const battery = Number(entity?.state); + const tires = tireIds.map((id) => Number(states[id]?.state)).filter(Number.isFinite); + if (states['binary_sensor.juicepassproxy_monitoring_degraded']?.state === 'on') return 'critical'; + if (!Number.isFinite(battery) || tires.length !== tireIds.length) return 'unknown'; + return Math.min(...tires) < 40 ? 'critical' : 'healthy'; + ]]] + state_display: > + [[[ + if (states['binary_sensor.juicepassproxy_monitoring_degraded']?.state === 'on') return 'PROXY OFFLINE'; + const battery = Number(entity?.state); + if (!Number.isFinite(battery)) return 'NO DATA'; + return `${battery.toFixed(0)}%`; + ]]] + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/car + - type: custom:button-card template: bearstone_infra_status_kpi entity: binary_sensor.infra_compute_degraded @@ -175,6 +211,19 @@ action: navigate navigation_path: /dashboard-infrastructure/network + - type: custom:button-card + template: bearstone_infra_status_kpi + entity: binary_sensor.infra_ups_on_battery + name: UPS Power + icon: mdi:battery-heart-variant + variables: + ok_state: 'off' + healthy_text: UTILITY POWER + alert_text: ON BATTERY + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/ups + - type: custom:button-card template: bearstone_infra_status_kpi entity: binary_sensor.powerwall_status @@ -326,6 +375,24 @@ action: navigate navigation_path: /dashboard-infrastructure/network + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.infra_ups_on_battery + name: UPS running on battery + icon: mdi:battery-alert-variant-outline + variables: + alert_kind: binary_on + exception_detail: > + [[[ + const garage = String(entity?.attributes?.garage_status ?? 'UNKNOWN').toUpperCase(); + const office = String(entity?.attributes?.office_status ?? 'UNKNOWN').toUpperCase(); + return `GARAGE ${garage} · OFFICE ${office}`; + ]]] + state_display: ON BATTERY + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/ups + - type: custom:button-card template: bearstone_infra_alert_row entity: binary_sensor.garage_phyn_leak_alert @@ -454,6 +521,19 @@ action: navigate navigation_path: /dashboard-infrastructure/joanna + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.juicepassproxy_monitoring_degraded + name: JuiceBox charging monitor offline + icon: mdi:ev-station-off + variables: + alert_kind: binary_on + exception_detail: DO NOT CHARGE - CHECK OR UNPLUG THE TESLA + state_display: '[[[ return "OFFLINE"; ]]]' + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/car + - type: custom:button-card template: bearstone_infra_alert_row entity: binary_sensor.proxmox1_runtime_healthy @@ -710,46 +790,38 @@ style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml - type: custom:button-card - template: bearstone_infra_list_row_running + template: bearstone_infra_list_row entity: sensor.garage_ups_status - name: UPS Status + name: Garage UPS icon: mdi:battery-heart-variant - - - type: custom:button-card - template: bearstone_infra_list_row - entity: sensor.garage_ups_battery_charge - name: UPS Battery - icon: mdi:battery-70 state_display: > [[[ - const value = Number(entity?.state); - return Number.isFinite(value) ? `${value.toFixed(0)}%` : 'NO DATA'; - ]]] - - - type: custom:button-card - template: bearstone_infra_list_row - entity: sensor.garage_ups_battery_runtime - name: Runtime Remaining - icon: mdi:timer-outline - state_display: > - [[[ - const seconds = Number(entity?.state); - if (!Number.isFinite(seconds)) return 'NO DATA'; - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + const charge = Number(states['sensor.garage_ups_battery_charge']?.state); + const seconds = Number(states['sensor.garage_ups_battery_runtime']?.state); + const battery = Number.isFinite(charge) ? `${charge.toFixed(0)}%` : 'NO BATTERY DATA'; + const runtime = Number.isFinite(seconds) ? `${Math.floor(seconds / 60)}m` : 'NO RUNTIME'; + return `${String(entity?.state ?? 'NO DATA').toUpperCase()} · ${battery} · ${runtime}`; ]]] + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/ups - type: custom:button-card template: bearstone_infra_list_row - entity: sensor.garage_ups_status - name: On Battery - icon: mdi:battery-alert-variant-outline + entity: sensor.office_ups_eaton_550_status + name: Office Eaton 3S 550 + icon: mdi:battery-heart-variant state_display: > [[[ - const status = String(entity?.state ?? '').toUpperCase(); - return status ? (status.includes('OB') ? 'YES' : 'NO') : 'NO DATA'; + const charge = Number(states['sensor.office_ups_eaton_550_battery_charge']?.state); + const seconds = Number(states['sensor.office_ups_eaton_550_battery_runtime']?.state); + const battery = Number.isFinite(charge) ? `${charge.toFixed(0)}%` : 'NO BATTERY DATA'; + const runtime = Number.isFinite(seconds) ? `${Math.floor(seconds / 60)}m` : 'NO RUNTIME'; + return `${String(entity?.state ?? 'NO DATA').toUpperCase()} · ${battery} · ${runtime}`; ]]] + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/ups - type: custom:vertical-stack-in-card card_mod: diff --git a/config/dashboards/infrastructure/partials/ups_sections.yaml b/config/dashboards/infrastructure/partials/ups_sections.yaml new file mode 100644 index 00000000..a4493eb5 --- /dev/null +++ b/config/dashboards/infrastructure/partials/ups_sections.yaml @@ -0,0 +1,457 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Infrastructure Partial - UPS Power +# Garage and Office NUT status with current readings and trends. +# ------------------------------------------------------------------- +# Notes: Raw NUT status data drives the shared on-battery rollup. +# Notes: All graphs use 24-hour windows and explicit threshold colors. +###################################################################### + +# ------------------------------------------------------------------- +# Current UPS health (mandatory first full-width wrapper) +# ------------------------------------------------------------------- +- 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: repeat(3, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + cards: + - type: custom:button-card + template: bearstone_nav_back + name: Back to Systems + icon: mdi:arrow-left + view_layout: + grid-column: 1 / -1 + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/home + + - type: custom:button-card + template: bearstone_infra_status_kpi + entity: sensor.garage_ups_status + name: Garage UPS + icon: mdi:garage-variant + variables: + ok_state: Online + healthy_text: ONLINE + alert_text: CHECK + + - type: custom:button-card + template: bearstone_infra_status_kpi + entity: sensor.office_ups_eaton_550_status + name: Office Eaton 3S 550 + icon: mdi:desk + variables: + ok_state: Online + healthy_text: ONLINE + alert_text: CHECK + + - type: custom:button-card + template: bearstone_infra_status_kpi + entity: binary_sensor.infra_ups_on_battery + name: Utility State + icon: mdi:transmission-tower + variables: + ok_state: 'off' + healthy_text: UTILITY POWER + alert_text: ON BATTERY + +# ------------------------------------------------------------------- +# Current readings +# ------------------------------------------------------------------- +- 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: repeat(2, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(1, minmax(0, 1fr)) + cards: + - 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_icon + name: Garage UPS + icon: mdi:garage-variant + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_status + name: Status + icon: mdi:battery-heart-variant + state_display: > + [[[ return String(entity?.state ?? 'NO DATA').toUpperCase(); ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_battery_charge + name: Battery + icon: mdi:battery-high + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)}%` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_battery_runtime + name: Runtime + icon: mdi:timer-outline + state_display: > + [[[ + const seconds = Number(entity?.state); + if (!Number.isFinite(seconds)) return 'NO DATA'; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_load + name: Load + icon: mdi:gauge + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)}%` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_input_voltage + name: Input Voltage + icon: mdi:sine-wave + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)} V` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_output_voltage + name: Output Voltage + icon: mdi:flash-outline + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)} V` : 'NO DATA'; + ]]] + + - 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_icon + name: Office Eaton 3S 550 + icon: mdi:desk + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.office_ups_eaton_550_status + name: Status + icon: mdi:battery-heart-variant + state_display: > + [[[ return String(entity?.state ?? 'NO DATA').toUpperCase(); ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.office_ups_eaton_550_battery_charge + name: Battery + icon: mdi:battery-high + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)}%` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.office_ups_eaton_550_battery_runtime + name: Runtime + icon: mdi:timer-outline + state_display: > + [[[ + const seconds = Number(entity?.state); + if (!Number.isFinite(seconds)) return 'NO DATA'; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.office_ups_eaton_550_load + name: Load + icon: mdi:gauge + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)}%` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.office_ups_eaton_550_real_power + name: Real Power + icon: mdi:flash + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)} W` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.office_ups_eaton_550_output_voltage + name: Output Voltage + icon: mdi:flash-outline + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(0)} V` : 'NO DATA'; + ]]] + +# ------------------------------------------------------------------- +# Battery and load trends +# ------------------------------------------------------------------- +- 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: repeat(4, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + mediaquery: + "(max-width: 900px)": + grid-template-columns: repeat(2, minmax(0, 1fr)) + cards: + - type: custom:mini-graph-card + name: Garage Battery + icon: mdi:battery-high + hours_to_show: 24 + points_per_hour: 2 + line_width: 2 + smoothing: true + height: 80 + show: + legend: false + labels: false + name: true + icon: true + state: true + color_thresholds: + - value: 0 + color: '#c0392b' + - value: 20 + color: '#f39c12' + - value: 50 + color: '#43a047' + entities: + - entity: sensor.garage_ups_battery_charge + name: Battery + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml + + - type: custom:mini-graph-card + name: Office Battery + icon: mdi:battery-high + hours_to_show: 24 + points_per_hour: 2 + line_width: 2 + smoothing: true + height: 80 + show: + legend: false + labels: false + name: true + icon: true + state: true + color_thresholds: + - value: 0 + color: '#c0392b' + - value: 20 + color: '#f39c12' + - value: 50 + color: '#43a047' + entities: + - entity: sensor.office_ups_eaton_550_battery_charge + name: Battery + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml + + - type: custom:mini-graph-card + name: Garage Load + icon: mdi:gauge + hours_to_show: 24 + points_per_hour: 2 + line_width: 2 + smoothing: true + height: 80 + show: + legend: false + labels: false + name: true + icon: true + state: true + color_thresholds: + - value: 0 + color: '#43a047' + - value: 60 + color: '#f39c12' + - value: 85 + color: '#c0392b' + entities: + - entity: sensor.garage_ups_load + name: Load + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml + + - type: custom:mini-graph-card + name: Office Load + icon: mdi:gauge + hours_to_show: 24 + points_per_hour: 2 + line_width: 2 + smoothing: true + height: 80 + show: + legend: false + labels: false + name: true + icon: true + state: true + color_thresholds: + - value: 0 + color: '#43a047' + - value: 60 + color: '#f39c12' + - value: 85 + color: '#c0392b' + entities: + - entity: sensor.office_ups_eaton_550_load + name: Load + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml + +# ------------------------------------------------------------------- +# Home Assistant power resilience dry run +# ------------------------------------------------------------------- +- 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: repeat(1, minmax(0, 1fr)) + grid-auto-flow: row + grid-auto-rows: min-content + grid-gap: 12px + margin: 0 + cards: + - 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_icon + name: Power Resilience - Home Assistant + icon: mdi:home-assistant + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.power_resilience_recommended_stage + name: Recommended Stage + icon: mdi:signal + state_display: > + [[[ return String(entity?.state ?? 'NO DATA').replaceAll('_', ' '); ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.power_resilience_recommended_stage + name: Control Owner + icon: mdi:brain + state_display: > + [[[ + const owner = String(entity?.attributes?.control_owner ?? 'NO DATA'); + const mode = String(entity?.attributes?.execution_mode ?? 'NO DATA'); + return `${owner.toUpperCase()} / ${mode.replaceAll('_', ' ').toUpperCase()}`; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.power_resilience_recommended_stage + name: Safety State + icon: mdi:shield-lock-outline + state_display: > + [[[ return entity?.attributes?.actions_armed === true ? 'ACTIONS ARMED' : 'NO SHUTDOWN ACTIONS'; ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.power_resilience_recommended_stage + name: Powerwall Inputs + icon: mdi:home-battery-outline + state_display: > + [[[ + const grid = String(entity?.attributes?.grid_status ?? 'NO DATA').toUpperCase(); + const charge = Number(entity?.attributes?.powerwall_charge); + return Number.isFinite(charge) ? `GRID ${grid} / ${charge.toFixed(0)}%` : `GRID ${grid} / NO DATA`; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.power_resilience_recommended_stage + name: Outage Elapsed + icon: mdi:timer-sand + state_display: > + [[[ + const seconds = Number(entity?.attributes?.outage_elapsed_seconds); + if (!Number.isFinite(seconds)) return 'NO DATA'; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + ]]] diff --git a/config/dashboards/infrastructure/views/01z_car.yaml b/config/dashboards/infrastructure/views/01z_car.yaml new file mode 100644 index 00000000..1a059f1c --- /dev/null +++ b/config/dashboards/infrastructure/views/01z_car.yaml @@ -0,0 +1,23 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Systems View - Car +# Read-only Tesla, tire-pressure, mileage, and JuiceBox charging telemetry. +# ------------------------------------------------------------------- +# Related Issue: 1944 +# Notes: TeslaMate remains the source of truth for long-term vehicle history. +# Notes: JuiceBox cards stay unavailable until the charger sends real telemetry. +###################################################################### + +title: Car +path: car +type: sections +icon: mdi:car-electric +theme: default +max_columns: 4 +dense_section_placement: true +badges: [] +sections: !include /config/dashboards/infrastructure/partials/car_sections.yaml +cards: [] diff --git a/config/dashboards/infrastructure/views/04_ups.yaml b/config/dashboards/infrastructure/views/04_ups.yaml new file mode 100644 index 00000000..3a780e5a --- /dev/null +++ b/config/dashboards/infrastructure/views/04_ups.yaml @@ -0,0 +1,22 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Infrastructure View - UPS Power +# Garage and Office backup-power status, capacity, and recent trends. +# ------------------------------------------------------------------- +# Notes: The Office Eaton 3S 550 is served by the headless office-pi NUT host. +# Notes: All graphs use a consistent 24-hour window. +###################################################################### + +title: UPS Power +path: ups +type: sections +icon: mdi:battery-heart-variant +theme: default +max_columns: 4 +dense_section_placement: true +badges: [] +sections: !include /config/dashboards/infrastructure/partials/ups_sections.yaml +cards: [] diff --git a/config/dashboards/infrastructure/views/11_vacuum_map.yaml b/config/dashboards/infrastructure/views/11_vacuum_map.yaml index f0fec3b0..1bf59132 100644 --- a/config/dashboards/infrastructure/views/11_vacuum_map.yaml +++ b/config/dashboards/infrastructure/views/11_vacuum_map.yaml @@ -7,6 +7,7 @@ # Full interactive Dreame map for zone, point, and room cleaning. # ------------------------------------------------------------------- # Notes: Specialized map card is retained because native cards do not expose interactive vacuum-map cleaning. +# Notes: Use the saved BearStone map and its explicit calibration because the current-map camera can omit both. # Notes: Panel view is intentional so the single interactive map can use the full available canvas. ###################################################################### @@ -23,9 +24,27 @@ cards: grid_options: columns: full map_source: - camera: camera.l10s_vacuum_map + camera: camera.l10s_vacuum_map_1 calibration_source: - camera: true + calibration_points: + - vacuum: + x: 0 + y: 0 + map: + x: 870 + y: 1093 + - vacuum: + x: 1000 + y: 0 + map: + x: 950 + y: 1093 + - vacuum: + x: 0 + y: 1000 + map: + x: 870 + y: 1013 entity: vacuum.l10s_vacuum vacuum_platform: Tasshack/dreame-vacuum map_modes: diff --git a/config/logbook.yaml b/config/logbook.yaml index fbe62b09..c63df4c3 100644 --- a/config/logbook.yaml +++ b/config/logbook.yaml @@ -57,6 +57,7 @@ exclude: - number.spaceship_* - select.spaceship_* - sensor.spaceship_* + - sensor.odometer - switch.spaceship_* - update.spaceship_* - input_text.l10s_vacuum_* @@ -109,7 +110,6 @@ exclude: # BEGIN generated Portainer recorder/logbook noise exclusions # Current retained Portainer telemetry is live-only; aggregate Docker history remains recorded. - binary_sensor.nebula_sync_status_2 - - binary_sensor.pihole_secondary_status_2 - binary_sensor.portainer_agent_status_2 - binary_sensor.portainer_status_2 - sensor.nebula_sync_state_2 diff --git a/config/packages/docker_infrastructure.yaml b/config/packages/docker_infrastructure.yaml index e72a3b4b..58a6c7a8 100644 --- a/config/packages/docker_infrastructure.yaml +++ b/config/packages/docker_infrastructure.yaml @@ -155,7 +155,6 @@ switch: - switch.paiges_bookshelf_container - switch.panel_notes_container_2 - switch.pihole_container_2 - - switch.pihole_secondary_container_2 - switch.poker_tracker_container_2 - switch.portainer_agent_container_2 - switch.portainer_container_2 diff --git a/config/packages/infrastructure.yaml b/config/packages/infrastructure.yaml index ebcb1258..849bee5d 100644 --- a/config/packages/infrastructure.yaml +++ b/config/packages/infrastructure.yaml @@ -23,7 +23,7 @@ # Notes: Docker host root disk usage uses Glances-backed normalized sensors; raw Glances sensors are recorder/logbook-filtered. # Notes: Disk-pressure dispatch allows bounded safe cleanup of disposable caches and old generated backup artifacts, but not live data or restarts. # Notes: Warning-level Docker host disk pressure is Joanna-only; Repairs are reserved for critical pressure. -# Notes: Nebula Sync DNS consistency compares primary/backup Pi-hole answers and dispatches Joanna on sustained drift or container loss. +# 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: Garage compute health uses the same 145 F threshold as the existing temperature notification. # Notes: Immediate website-down states create Repairs; the installed UptimeRobot integration exposes current state, not trend rollups. @@ -32,6 +32,7 @@ # Notes: Dashboard aggregate issue count includes compact BearClaw scheduled-job warning/error health. # Notes: Docker host maintenance is counted separately from container runtime health so pending packages and stale host reports surface on Systems. # Notes: Home Water rollup includes Rheem faults/telemetry loss plus active Phyn alerts and valve closure. +# Notes: UPS outage status aggregates the Garage and Office NUT servers from their raw status-data entities. ###################################################################### input_text: @@ -121,7 +122,7 @@ command_line: name: Infra Nebula Sync DNS Consistency unique_id: infra_nebula_sync_dns_consistency command: >- - /bin/bash -c 'primary=192.168.10.10; secondary=192.168.10.14; host=GTG-PF45FK6F; fqdn=GTG-PF45FK6F.fordst.com; ip=192.168.10.117; q(){ dig +time=2 +tries=1 +short @"$1" "$2" A 2>/dev/null | tr -d "\r" | sort | tr "\n" "," | sed "s/,$//"; }; r(){ dig +time=2 +tries=1 +short @"$1" -x "$2" 2>/dev/null | tr -d "\r" | sed "s/\.$//" | sort | tr "\n" "," | sed "s/,$//"; }; p_short=$(q "$primary" "$host"); s_short=$(q "$secondary" "$host"); p_fqdn=$(q "$primary" "$fqdn"); s_fqdn=$(q "$secondary" "$fqdn"); p_rev=$(r "$primary" "$ip"); s_rev=$(r "$secondary" "$ip"); status=mismatch; if [ "$p_short" = "$ip" ] && [ "$s_short" = "$ip" ] && [ "$p_fqdn" = "$ip" ] && [ "$s_fqdn" = "$ip" ] && [ -n "$p_rev" ] && [ "$p_rev" = "$s_rev" ]; then status=ok; fi; printf "{\"status\":\"%s\",\"host\":\"%s\",\"expected_ip\":\"%s\",\"primary_short\":\"%s\",\"secondary_short\":\"%s\",\"primary_fqdn\":\"%s\",\"secondary_fqdn\":\"%s\",\"primary_reverse\":\"%s\",\"secondary_reverse\":\"%s\"}\n" "$status" "$host" "$ip" "$p_short" "$s_short" "$p_fqdn" "$s_fqdn" "$p_rev" "$s_rev"' + /bin/bash -c 'primary=192.168.10.10; secondary=192.168.10.100; host=GTG-PF45FK6F; fqdn=GTG-PF45FK6F.fordst.com; ip=192.168.10.117; q(){ dig +time=2 +tries=1 +short @"$1" "$2" A 2>/dev/null | tr -d "\r" | sort | tr "\n" "," | sed "s/,$//"; }; r(){ dig +time=2 +tries=1 +short @"$1" -x "$2" 2>/dev/null | tr -d "\r" | sed "s/\.$//" | sort | tr "\n" "," | sed "s/,$//"; }; p_short=$(q "$primary" "$host"); s_short=$(q "$secondary" "$host"); p_fqdn=$(q "$primary" "$fqdn"); s_fqdn=$(q "$secondary" "$fqdn"); p_rev=$(r "$primary" "$ip"); s_rev=$(r "$secondary" "$ip"); status=mismatch; if [ "$p_short" = "$ip" ] && [ "$s_short" = "$ip" ] && [ "$p_fqdn" = "$ip" ] && [ "$s_fqdn" = "$ip" ] && [ -n "$p_rev" ] && [ "$p_rev" = "$s_rev" ]; then status=ok; fi; printf "{\"status\":\"%s\",\"host\":\"%s\",\"expected_ip\":\"%s\",\"primary_short\":\"%s\",\"secondary_short\":\"%s\",\"primary_fqdn\":\"%s\",\"secondary_fqdn\":\"%s\",\"primary_reverse\":\"%s\",\"secondary_reverse\":\"%s\"}\n" "$status" "$host" "$ip" "$p_short" "$s_short" "$p_fqdn" "$s_fqdn" "$p_rev" "$s_rev"' scan_interval: 300 value_template: "{{ value_json.status | default('unknown') }}" json_attributes: @@ -138,7 +139,7 @@ command_line: name: Infra Pihole IoT DNS Consistency unique_id: infra_pihole_iot_dns_consistency command: >- - /bin/bash -c 'primary=192.168.10.10; secondary=192.168.10.14; records="rachio.fordst.com=192.168.10.73 econet.fordst.com=192.168.10.92 dreame-vacuum.fordst.com=192.168.10.93 carlo-bed.fordst.com=192.168.10.95 lg-smart-fridge.fordst.com=192.168.10.96 tesla-blackbox-gw.fordst.com=192.168.10.97 bgw210.fordst.com=192.168.10.98"; q(){ dig +time=2 +tries=1 +short @"$1" "$2" A 2>/dev/null | tr -d "\r" | sort | tr "\n" "," | sed "s/,$//"; }; status=ok; checked=0; mismatch_count=0; mismatches=""; for record in $records; do host=${record%%=*}; ip=${record#*=}; p=$(q "$primary" "$host"); s=$(q "$secondary" "$host"); checked=$((checked+1)); if [ "$p" != "$ip" ] || [ "$s" != "$ip" ]; then status=mismatch; mismatch_count=$((mismatch_count+1)); mismatches="${mismatches}${host}:expected=${ip},primary=${p:-none},secondary=${s:-none};"; fi; done; if [ -z "$mismatches" ]; then mismatches=none; fi; printf "{\"status\":\"%s\",\"checked_records\":%s,\"mismatch_count\":%s,\"mismatches\":\"%s\",\"primary_dns\":\"%s\",\"backup_dns\":\"%s\"}\n" "$status" "$checked" "$mismatch_count" "$mismatches" "$primary" "$secondary"' + /bin/bash -c 'primary=192.168.10.10; secondary=192.168.10.100; records="rachio.fordst.com=192.168.10.73 econet.fordst.com=192.168.10.92 dreame-vacuum.fordst.com=192.168.10.93 carlo-bed.fordst.com=192.168.10.95 lg-smart-fridge.fordst.com=192.168.10.96 tesla-blackbox-gw.fordst.com=192.168.10.97 bgw210.fordst.com=192.168.10.98"; q(){ dig +time=2 +tries=1 +short @"$1" "$2" A 2>/dev/null | tr -d "\r" | sort | tr "\n" "," | sed "s/,$//"; }; status=ok; checked=0; mismatch_count=0; mismatches=""; for record in $records; do host=${record%%=*}; ip=${record#*=}; p=$(q "$primary" "$host"); s=$(q "$secondary" "$host"); checked=$((checked+1)); if [ "$p" != "$ip" ] || [ "$s" != "$ip" ]; then status=mismatch; mismatch_count=$((mismatch_count+1)); mismatches="${mismatches}${host}:expected=${ip},primary=${p:-none},secondary=${s:-none};"; fi; done; if [ -z "$mismatches" ]; then mismatches=none; fi; printf "{\"status\":\"%s\",\"checked_records\":%s,\"mismatch_count\":%s,\"mismatches\":\"%s\",\"primary_dns\":\"%s\",\"backup_dns\":\"%s\"}\n" "$status" "$checked" "$mismatch_count" "$mismatches" "$primary" "$secondary"' scan_interval: 300 value_template: "{{ value_json.status | default('unknown') }}" json_attributes: @@ -242,8 +243,10 @@ template: 'binary_sensor.infra_wan_quality_degraded', 'binary_sensor.infra_dns_pihole_degraded', 'binary_sensor.infra_compute_degraded', - 'binary_sensor.infra_storage_degraded', - 'binary_sensor.infra_external_monitoring_degraded', + 'binary_sensor.infra_storage_degraded', + 'binary_sensor.infra_external_monitoring_degraded', + 'binary_sensor.juicepassproxy_monitoring_degraded', + 'binary_sensor.infra_ups_on_battery', 'binary_sensor.docker_host_maintenance_attention', 'binary_sensor.hvac_maintenance_due', 'binary_sensor.home_water_problem', @@ -258,8 +261,10 @@ template: 'binary_sensor.infra_wan_quality_degraded', 'binary_sensor.infra_dns_pihole_degraded', 'binary_sensor.infra_compute_degraded', - 'binary_sensor.infra_storage_degraded', - 'binary_sensor.infra_external_monitoring_degraded', + 'binary_sensor.infra_storage_degraded', + 'binary_sensor.infra_external_monitoring_degraded', + 'binary_sensor.juicepassproxy_monitoring_degraded', + 'binary_sensor.infra_ups_on_battery', 'binary_sensor.docker_host_maintenance_attention', 'binary_sensor.hvac_maintenance_due', 'binary_sensor.home_water_problem', @@ -454,7 +459,7 @@ template: secondary_reverse: "{{ state_attr('sensor.infra_nebula_sync_dns_consistency', 'secondary_reverse') }}" nebula_status: "{{ states('binary_sensor.nebula_sync_status_2') }}" nebula_state: "{{ states('sensor.nebula_sync_state_2') }}" - pihole_secondary_status: "{{ states('binary_sensor.pihole_secondary_status_2') }}" + pihole_secondary_status: "{{ states('binary_sensor.pi_hole_status_2') }}" - name: "Infra Pihole IoT DNS Degraded" unique_id: infra_pihole_iot_dns_degraded @@ -473,8 +478,20 @@ template: unique_id: infra_ups_on_battery device_class: problem state: >- - {% set status = states('sensor.garage_ups_status') | upper %} - {{ 'OB' in status }} + {% set statuses = [ + states('sensor.garage_ups_status_data') | upper, + states('sensor.office_ups_eaton_550_status_data') | upper + ] %} + {% set ns = namespace(on_battery=false) %} + {% for status in statuses %} + {% if 'OB' in status.split() %} + {% set ns.on_battery = true %} + {% endif %} + {% endfor %} + {{ ns.on_battery }} + attributes: + garage_status: "{{ states('sensor.garage_ups_status') }}" + office_status: "{{ states('sensor.office_ups_eaton_550_status') }}" - name: "Infra Website Degraded" unique_id: infra_website_degraded @@ -734,7 +751,7 @@ automation: degraded: "{{ is_state('binary_sensor.infra_nebula_sync_degraded', 'on') }}" nebula_status: "{{ states('binary_sensor.nebula_sync_status_2') }}" nebula_state: "{{ states('sensor.nebula_sync_state_2') }}" - pihole_secondary_status: "{{ states('binary_sensor.pihole_secondary_status_2') }}" + pihole_secondary_status: "{{ states('binary_sensor.pi_hole_status_2') }}" action: - choose: - conditions: "{{ degraded and previous_band != 'warning' }}" @@ -753,7 +770,7 @@ automation: - binary_sensor.infra_nebula_sync_degraded - binary_sensor.nebula_sync_status_2 - sensor.nebula_sync_state_2 - - binary_sensor.pihole_secondary_status_2 + - binary_sensor.pi_hole_status_2 diagnostics: >- issue_id={{ issue_id }}, dns_consistency={{ dns_state }}, @@ -769,9 +786,9 @@ automation: nebula_state={{ nebula_state }}, pihole_secondary_status={{ pihole_secondary_status }}, primary_dns=192.168.10.10, - backup_dns=192.168.10.14 + backup_dns=192.168.10.100 request: >- - Investigate Nebula Sync on docker_14 and the backup Pi-hole sync path. + Investigate Nebula Sync on docker_14 and the physical backup Pi-hole on office-pi. Verify both Pi-holes answer the GTG-PF45FK6F short name, FQDN, and reverse lookup consistently. Check nebula_sync container status, Docker health, recent sync logs, and primary/replica Pi-hole API reachability. If confidence is high, perform safe remediation such as a one-time Nebula Sync run or restarting only the nebula_sync container. diff --git a/config/packages/pihole_ha.yaml b/config/packages/pihole_ha.yaml index a3cda3d5..7f1835a9 100644 --- a/config/packages/pihole_ha.yaml +++ b/config/packages/pihole_ha.yaml @@ -4,7 +4,7 @@ # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Pi-hole HA Sync - Mirror blocking state across DNS nodes -# Keep Pi-hole blocking on docker_10 and docker_14 in lockstep. +# Keep Pi-hole blocking on docker_10 and office-pi in lockstep. # ------------------------------------------------------------------- # Notes: Uses switch.pi_hole + switch.pi_hole_2 from the Pi-hole integration. # Notes: Auto re-enables Pi-hole after 60 minutes of being off. diff --git a/config/recorder.yaml b/config/recorder.yaml index 9a71949a..1ee9f5d4 100755 --- a/config/recorder.yaml +++ b/config/recorder.yaml @@ -11,6 +11,8 @@ # Notes: Keeps 180 days (1/2 year); excludes vcloudinfo pings, noisy connectivity telemetry, countdown-style alarm helpers, low-value script/button/scene history, MariaDB snapshot helpers, raw Docker/Proxmox/Glances host telemetry including dynamic Docker veth sensors, PoE live power, and other high-churn entities; MariaDB via recorder_db_url. # Notes: L10s status/room telemetry is live-only; keep curated VACUUM logbook entries plus error/run context instead. # Notes: Tesla Fleet Spaceship telemetry is live-only in HA; TeslaMate is the historical car database. +# Notes: Persisted vehicle/JuiceBox measurements stay useful without synthetic +# last-report timestamps or transient max-current snapshot history. # Notes: Life360 trackers are explicitly included for map/history queries despite the broad device_tracker exclusion. # Notes: Raw Portainer container/stack status telemetry is live-only; keep Docker aggregate outage sensors, Repairs, and curated logbook events instead. # Notes: Mobile app phone telemetry is live-only except daily pedometer metrics for Carlo and Stacey, retained for fitness history; Life360 keeps location history and notify services remain available. @@ -89,6 +91,7 @@ exclude: - number.spaceship_* - select.spaceship_* - sensor.spaceship_* + - sensor.odometer - switch.spaceship_* - update.spaceship_* - select.sleepnumber_* @@ -216,7 +219,6 @@ exclude: # BEGIN generated Portainer recorder/logbook noise exclusions # Current retained Portainer telemetry is live-only; aggregate Docker history remains recorded. - binary_sensor.nebula_sync_status_2 - - binary_sensor.pihole_secondary_status_2 - binary_sensor.portainer_agent_status_2 - binary_sensor.portainer_status_2 - sensor.nebula_sync_state_2 @@ -273,6 +275,10 @@ exclude: - sensor.pi_hole_dns_queries_today - sensor.rachio_watering_time - sensor.time + - sensor.juicebox_max_current_offline_device + - sensor.juicebox_max_current_online_device + - sensor.juicepassproxy_health + - sensor.juicepassproxy_state - sensor.vcloudinfo_com - switch.bear_stone - switch.alarm_panel_1_screensaver