diff --git a/codex_skills/homeassistant-dashboard-designer/SKILL.md b/codex_skills/homeassistant-dashboard-designer/SKILL.md index 9eb38e77..f6e7c7f6 100644 --- a/codex_skills/homeassistant-dashboard-designer/SKILL.md +++ b/codex_skills/homeassistant-dashboard-designer/SKILL.md @@ -78,6 +78,8 @@ Tier 2 (fallback, must justify in comments): - No freeform positioning - No layout logic embedded in `card-mod` - For dense tile lists in `type: sections` views, keep the outer panel full-width (`column_span: 4`) and make the inner tile grid responsive using `custom:layout-card` (`4` desktop / `2` mobile unless the user asks otherwise). +- In `type: sections` views, the first section immediately below top chips/badges must be a full-width container (`column_span: 4`) with a single wrapper card that also sets `grid_options.columns: full` to prevent skinny-column whitespace. +- Consolidate upward after removals/moves: do not leave dead space, empty placeholders, or intentionally sparse rows unless the user explicitly asks for whitespace. Note: If the repo/view uses Home Assistant `type: sections`, treat `sections` as the top-level structure and enforce the same container rules inside each section (sections should contain only `grid`/`vertical-stack` cards and their children). @@ -138,14 +140,50 @@ Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, Max layout nesting depth: 2. No horizontal-stack inside grid cells. ``` +## Optional Stitch Handoff (Light Bridge) + +Use this only when the user explicitly asks for visual ideation, layout exploration, or variants. + +- Trigger Stitch path: + - User asks for redesign inspiration, variants, or multiple visual directions. +- Skip Stitch path: + - User asks for direct dashboard refactor/update and no ideation is needed. +- Preconditions: + - Stitch is used for hierarchy/spacing/grouping ideation only. + - Stitch output must never be copied as implementation code. + - Final implementation must still obey all allowed card/layout rules in this skill. + +Handoff artifact (summary only, no code export): + +```yaml +stitch_intent: + layout_pattern: "" + section_hierarchy: "
" + density_target: "" + cards_to_translate: + - "" +``` + +Translation requirement: +- Convert `stitch_intent` into approved Lovelace structures only (`grid`, `vertical-stack`, Tier 1 cards first). +- Re-map any unsupported Stitch concepts to compliant cards/containers and explain fallback choices inline when Tier 2 is required. + +Fallback behavior when Stitch is unavailable: +- Continue with manual ideation using this skill's design system and rules. +- State degraded mode clearly ("Stitch unavailable; proceeding with manual hierarchy/density planning"). +- Do not block dashboard work on Stitch availability. + ## Workflow (Do This In Order) 1. Read the target dashboard/view/partials/templates to understand existing patterns and avoid drift. 2. Determine intent from the user's request and existing dashboard context: `infra` (NOC), `home`, `energy`, or `environment`. Keep one intent per view. 3. Validate entities and services before editing: - Prefer the Home Assistant MCP for live entity/service validation (required when available). + - Record the MCP validation step in the work notes before writing YAML. - If MCP is not available, ask the user to confirm entity IDs and services (do not guess). 4. Draft layout with constraints: a top-level `grid` and optional `vertical-stack` groups. + - If using Stitch, first summarize `stitch_intent` and treat it as advisory input to this step. + - After removals, reflow cards/sections upward to collapse gaps and reduce empty rows. 5. Implement using Tier 1 cards first; reuse existing templates; avoid one-off styles. 6. If fallback cards are necessary, add an inline comment explaining why Tier 1 cannot satisfy the requirement. 7. Validate: @@ -155,6 +193,9 @@ Max layout nesting depth: 2. No horizontal-stack inside grid cells. - If requirements can't be met: state the violated rule and propose a compliant alternative. - If validation fails: stop, surface the error output, and propose corrected YAML. Do not leave invalid config applied. +Example flow: +- User asks for redesign inspiration -> Stitch ideation with required constraints -> summarize `stitch_intent` -> translate to Lovelace-safe YAML -> run validation checks. + ## References - Read `references/dashboard_rules.md` when you need the full constraint set and repo-specific mapping notes. diff --git a/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md b/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md index 8883b657..d25cbd59 100644 --- a/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md +++ b/codex_skills/homeassistant-dashboard-designer/references/dashboard_rules.md @@ -25,9 +25,12 @@ Tier 2 (fallback, only when Tier 1 cannot satisfy the requirement; justify inlin - No freeform positioning - No layout logic embedded in `card-mod` - For dense status/container lists in `type: sections` views, keep the panel full-width (`column_span: 4`) and use a responsive inner grid (`4` desktop / `2` mobile by default). +- In `type: sections` views, always place a full-width wrapper section directly below top chips/badges and set the wrapper card to `grid_options.columns: full`. +- Consolidate layout vertically after any move/remove; dead space is disallowed unless explicitly requested. Sections-mode note: - If a view uses `type: sections`, treat `sections` as the top-level structure and enforce the same container rules inside each section. +- Prefer reducing section count and regrouping cards into earlier rows rather than leaving sparse trailing space. ## Template Architecture (Required) @@ -111,6 +114,37 @@ Card types limited to: custom:button-card, card-mod, custom:flex-horseshoe-card, Max layout nesting depth: 2. No horizontal-stack inside grid cells. ``` +## Optional Stitch Handoff (Light Bridge) + +Decision rule: +- Use Stitch only when the user requests visual ideation, variants, or exploration. +- For normal refactor/update requests, skip Stitch and implement directly with this skill's rules. + +Handoff artifact (advisory summary, not implementation code): + +```yaml +stitch_intent: + layout_pattern: "" + section_hierarchy: "
" + density_target: "" + cards_to_translate: + - "" +``` + +Required translation back to Lovelace: +- Treat `stitch_intent` as input to layout planning only. +- Implement using approved containers/cards and template architecture from this document. +- For any unsupported concept, re-map to compliant cards and justify Tier 2 fallback inline. + +Degraded mode: +- If Stitch or Stitch skills are unavailable, continue with manual hierarchy/spacing ideation. +- Do not block dashboard work on Stitch availability. + +Anti-drift checklist: +- No HTML/CSS export artifacts from Stitch output. +- No nesting-depth violations (max 2). +- No card-type drift outside approved Tier 1/Tier 2 lists. + ## Repo-Specific Dashboard YAML Rules (config/dashboards/**) If working in this repo's `config/dashboards/` tree: diff --git a/config/dashboards/infrastructure/partials/home_sections.yaml b/config/dashboards/infrastructure/partials/home_sections.yaml index 5d60959a..4034fe64 100644 --- a/config/dashboards/infrastructure/partials/home_sections.yaml +++ b/config/dashboards/infrastructure/partials/home_sections.yaml @@ -4,216 +4,626 @@ # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Infrastructure Partial - Home sections -# Rebuilt homepage layout (Stitch-inspired). +# Desktop-first infra overview: full-width hero, exceptions-first alerts. # ------------------------------------------------------------------- -# Notes: Standardized on `custom:button-card` + `custom:mini-graph-card` with `card_mod` polish. +# Notes: Default/light theme only; no dark-mode specific styling. +# Notes: Always keep a full-width container directly below chips. ###################################################################### -- !include /config/dashboards/infrastructure/partials/infra_top_chips_section.yaml +- !include /config/dashboards/infrastructure/partials/infra_top_chips_home_section.yaml +# ------------------------------------------------------------------- +# Home hero (mandatory full-width container under chips) +# ------------------------------------------------------------------- - type: grid column_span: 4 columns: 1 square: false cards: - - type: custom:button-card - template: bearstone_infra_kpi - entity: sensor.total_wifi_clients - name: Wi-Fi Clients - icon: mdi:wifi - -- type: grid - column_span: 3 - columns: 3 - square: false - cards: - - type: custom:vertical-stack-in-card - card_mod: - style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + - 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:button-card - template: bearstone_infra_panel_header - name: Proxmox01 - - type: custom:mini-graph-card - name: CPU / Memory - icon: mdi:server - hours_to_show: 24 - points_per_hour: 2 - line_width: 2 - animate: true - show: - fill: true - legend: false - icon: true - name: true - state: true - entities: - - entity: sensor.node_proxmox1_cpu_used - name: CPU - - entity: sensor.node_proxmox1_memory_used_percentage - name: Memory - - type: grid - columns: 3 - square: false + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml cards: - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_docker69_169_status - name: Docker69 - icon: mdi:docker - label: > - [[[ return "Last boot: " + (states['sensor.qemu_docker69_169_last_boot']?.state ?? 'unknown'); ]]] + template: bearstone_infra_panel_header + name: Alerts + + - type: custom:button-card + template: bearstone_infra_list_row + name: Network + icon: mdi:access-point-network + show_state: false + tap_action: + action: none + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.unifi_ap_office_clients + name: Office AP has 0 clients + icon: mdi:wifi-alert variables: - button_entity: button.qemu_docker69_169_reboot - name: Docker69 - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_carlo_hass_105_status - name: HASS - icon: mdi:home-assistant - label: > - [[[ return "Last boot: " + (states['sensor.qemu_carlo_hass_105_last_boot']?.state ?? 'unknown'); ]]] + alert_kind: ap_zero + min_age_s: 300 + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.unifi_ap_study_clients + name: Study AP has 0 clients + icon: mdi:wifi-alert variables: - button_entity: button.qemu_carlo_hass_105_reboot - name: HASS - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_wireguard_104_status - name: WireGuard - icon: mdi:vpn - label: > - [[[ return "Last boot: " + (states['sensor.qemu_wireguard_104_last_boot']?.state ?? 'unknown'); ]]] + alert_kind: ap_zero + min_age_s: 300 + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.unifi_ap_garage_clients + name: Garage AP has 0 clients + icon: mdi:wifi-alert variables: - button_entity: button.qemu_wireguard_104_reboot - name: WireGuard + alert_kind: ap_zero + min_age_s: 300 - - 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: Proxmox02 - - type: custom:mini-graph-card - name: CPU / Memory - icon: mdi:server - hours_to_show: 24 - points_per_hour: 2 - line_width: 2 - animate: true - show: - fill: true - legend: false - icon: true - name: true - state: true - entities: - - entity: sensor.node_proxmox02_cpu_used - name: CPU - - entity: sensor.node_proxmox02_memory_used_percentage - name: Memory - - type: grid - columns: 1 - square: false - cards: - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_docker2_101_status - name: Frigate - icon: mdi:video - label: > - [[[ return "Last boot: " + (states['sensor.qemu_docker2_101_last_boot']?.state ?? 'unknown'); ]]] + template: bearstone_infra_alert_row + entity: sensor.speedtest_download + name: Internet speed is slow + icon: mdi:speedometer-slow variables: - button_entity: button.qemu_docker2_101_reboot - name: Frigate + alert_kind: speedtest_slow + threshold: 300 + state_display: > + [[[ + const dl = parseFloat(states['sensor.speedtest_download']?.state); + const ul = parseFloat(states['sensor.speedtest_upload']?.state); + const dlText = Number.isFinite(dl) ? (dl.toFixed(0) + ' Mbps') : 'n/a'; + const ulText = Number.isFinite(ul) ? (ul.toFixed(0) + ' Mbps') : 'n/a'; + return `DL ${dlText} | UL ${ulText}`; + ]]] - - 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: Pi-hole - - type: custom:pi-hole - device_id: d69637da16f7d7f3626070582be59808 + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.speedtest_ping + name: WAN latency high + icon: mdi:wan + variables: + alert_kind: disk_high + threshold: 80 + state_display: > + [[[ return `${entity.state} ms`; ]]] + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: switch.pi_hole + name: DNS Pi-hole disabled + icon: mdi:dns-outline + variables: + alert_kind: binary_off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/pihole + state_display: > + [[[ return `Switch ${entity.state}`; ]]] + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.pihole_status + name: DNS Pi-hole service down + icon: mdi:dns + variables: + alert_kind: binary_off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/pihole + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.vcloudinfo_com + name: vcloudinfo.com is down + icon: mdi:web-cancel + variables: + alert_kind: binary_off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.www_kingcrafthomes_com + name: kingcrafthomes.com is down + icon: mdi:web-cancel + variables: + alert_kind: binary_off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + + - type: custom:button-card + template: bearstone_infra_list_row + name: Domain expiration critical + icon: mdi:domain-off + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + state_display: > + [[[ + const ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ]; + let min = null; + ids.forEach((id) => { + const raw = states[id]?.state; + const n = Number(raw); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'No data' : `${Math.round(min)} days remaining`; + ]]] + styles: + card: + - display: > + [[[ + const ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ]; + let min = null; + ids.forEach((id) => { + const n = Number(states[id]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min < 14) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Domain expiration warning + icon: mdi:domain + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + state_display: > + [[[ + const ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ]; + let min = null; + ids.forEach((id) => { + const raw = states[id]?.state; + const n = Number(raw); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'No data' : `${Math.round(min)} days remaining`; + ]]] + styles: + card: + - display: > + [[[ + const ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ]; + let min = null; + ids.forEach((id) => { + const n = Number(states[id]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min >= 14 && min < 30) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Certificate expiration critical + icon: mdi:certificate-outline + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'Not available' : `${Math.round(min)} days remaining`; + ]]] + styles: + card: + - display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min < 14) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Certificate expiration warning + icon: mdi:certificate + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/website-health + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'Not available' : `${Math.round(min)} days remaining`; + ]]] + styles: + card: + - display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min >= 14 && min < 30) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: System + icon: mdi:alert-circle-outline + show_state: false + tap_action: + action: none + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: sensor.disk_use_percent + name: Home Assistant disk usage high + icon: mdi:harddisk + variables: + alert_kind: disk_high + threshold: 80 + state_display: > + [[[ return `${entity.state}% used`; ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Backup stale or failed + icon: mdi:backup-restore + state_display: > + [[[ + const s = states['sensor.dockerconfigs_backup_status']?.state ?? 'unknown'; + const e = states['sensor.dockerconfigs_backup_error_message']?.state ?? ''; + const d = states['sensor.dockerconfigs_backup_date']?.state; + let ageText = 'n/a'; + if (d && !['unknown','unavailable','none',''].includes(String(d).toLowerCase())) { + const dt = new Date(d); + if (!Number.isNaN(dt.getTime())) { + const h = (Date.now() - dt.getTime()) / 3600000; + ageText = `${h.toFixed(1)}h`; + } + } + return `Age ${ageText} | ${s}${e ? ' | error' : ''}`; + ]]] + styles: + card: + - display: > + [[[ + const status = String(states['sensor.dockerconfigs_backup_status']?.state ?? '').toLowerCase(); + const err = String(states['sensor.dockerconfigs_backup_error_message']?.state ?? '').toLowerCase(); + const d = states['sensor.dockerconfigs_backup_date']?.state; + let stale = false; + if (d && !['unknown','unavailable','none',''].includes(String(d).toLowerCase())) { + const dt = new Date(d); + if (!Number.isNaN(dt.getTime())) stale = ((Date.now() - dt.getTime()) / 3600000) > 24; + } + const failed = status.includes('fail') || status.includes('error') || err.length > 0; + return (stale || failed) ? 'block' : 'none'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + name: Services + icon: mdi:docker + show_state: false + tap_action: + action: none + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.docker_10_apt_reboot_required + name: docker_10 needs reboot + icon: mdi:restart-alert + variables: + alert_kind: binary_on + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/docker + state_display: > + [[[ return 'REBOOT REQUIRED'; ]]] + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.docker_14_apt_reboot_required + name: docker_14 needs reboot + icon: mdi:restart-alert + variables: + alert_kind: binary_on + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/docker + state_display: > + [[[ return 'REBOOT REQUIRED'; ]]] + + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.docker_69_apt_reboot_required + name: docker_69 needs reboot + icon: mdi:restart-alert + variables: + alert_kind: binary_on + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/docker + state_display: > + [[[ return 'REBOOT REQUIRED'; ]]] + + - type: custom:auto-entities + show_empty: false + card: + type: custom:layout-card + 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)) + card_param: cards + filter: + include: !include /config/dashboards/infrastructure/partials/docker_container_rows_include.yaml + exclude: + - state: 'on' + + - type: custom:vertical-stack-in-card grid_options: columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Datacenter + - type: custom:mini-graph-card + name: Garage Temp + icon: mdi:thermometer + hours_to_show: 96 + points_per_hour: 1 + line_width: 2 + smoothing: true + show: + legend: false + labels: false + name: true + icon: true + state: true + color_thresholds: + - value: 50 + color: '#f39c12' + - value: 120 + color: '#d35400' + - value: 145 + color: '#c0392b' + entities: + - entity: sensor.proxmox_garage_average_temperature + name: Garage + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml + + - type: custom:layout-card + 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:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_status + name: UPS Status + 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 v = Number(entity?.state); + return Number.isFinite(v) ? `${v.toFixed(0)}%` : (entity?.state ?? 'unknown'); + ]]] + + - 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 secs = parseInt(entity?.state, 10); + if (!Number.isFinite(secs)) return entity?.state ?? 'unknown'; + const hours = Math.floor(secs / 3600); + const mins = Math.floor((secs % 3600) / 60); + return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.garage_ups_status + name: On Battery + icon: mdi:battery-alert-variant-outline + state_display: > + [[[ + const s = String(entity?.state || '').toUpperCase(); + return s.includes('OB') ? 'Yes' : 'No'; + ]]] + +# ------------------------------------------------------------------- +# Overviews (desktop: 2 columns, mobile: stack) +# ------------------------------------------------------------------- +- 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 + name: Wi-Fi Overview + - type: custom:mini-graph-card + name: Clients + icon: mdi:wifi + hours_to_show: 24 + line_width: 2 + points_per_hour: 1 + smoothing: true + show: + graph: line + legend: false + labels: false + name: true + icon: true + state: true + entities: + - entity: sensor.total_wifi_clients + name: Total + show_state: true + show_graph: false + show_line: false + show_points: false + show_fill: false + show_legend: false + - entity: sensor.unifi_ap_office_clients + name: Office AP + show_state: true + - entity: sensor.unifi_ap_study_clients + name: Study AP + show_state: true + - entity: sensor.unifi_ap_garage_clients + name: Garage AP + show_state: true + + - 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: Internet Trend + - type: custom:mini-graph-card + name: Speedtest + icon: mdi:speedometer + hours_to_show: 24 + points_per_hour: 1 + line_width: 2 + smoothing: true + show: + fill: false + legend: false + labels: false + name: true + icon: true + state: true + entities: + - entity: sensor.speedtest_download + name: Download + - entity: sensor.speedtest_upload + name: Upload +# ------------------------------------------------------------------- +# Activity highlights (compact) +# ------------------------------------------------------------------- - type: grid column_span: 4 columns: 1 square: false cards: - type: custom:vertical-stack-in-card + grid_options: + columns: full card_mod: style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml cards: - type: custom:button-card template: bearstone_infra_panel_header - name: Wi-Fi Overview - - type: custom:mini-graph-card - name: Clients - icon: mdi:wifi + name: Activity Highlights + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/activity + - type: logbook + target: + entity_id: + - sensor.activity_feed hours_to_show: 24 - line_width: 2 - points_per_hour: 1 - smoothing: true - show: - graph: line - legend: false - labels: false - name: true - icon: true - state: true - entities: - - entity: sensor.total_wifi_clients - name: Total - show_state: true - show_graph: false - show_line: false - show_points: false - show_fill: false - show_legend: false - - entity: sensor.unifi_ap_office_clients - name: Office AP - show_state: true - - entity: sensor.unifi_ap_study_clients - name: Study AP - show_state: true - - entity: sensor.unifi_ap_garage_clients - name: Garage AP - show_state: true - - type: grid - columns: 3 - square: false - cards: - - type: custom:button-card - template: bearstone_infra_device_tile - name: Garage AP - icon: mdi:access-point - entity: sensor.unifi_ap_garage_clients - label: > - [[[ return "Uptime: " + (states['sensor.unifi_ap_garage_uptime']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.unifi_ap_garage_restart - name: Garage AP - - type: custom:button-card - template: bearstone_infra_device_tile - name: Office AP - icon: mdi:access-point - entity: sensor.unifi_ap_office_clients - label: > - [[[ return "Uptime: " + (states['sensor.unifi_ap_office_uptime']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.unifi_ap_office_restart - name: Office AP - - type: custom:button-card - template: bearstone_infra_device_tile - name: Study AP - icon: mdi:access-point - entity: sensor.unifi_ap_study_clients - label: > - [[[ return "Uptime: " + (states['sensor.unifi_ap_study_uptime']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.unifi_ap_study_restart - name: Study AP diff --git a/config/dashboards/infrastructure/partials/infra_top_chips_home_section.yaml b/config/dashboards/infrastructure/partials/infra_top_chips_home_section.yaml new file mode 100644 index 00000000..8631be6c --- /dev/null +++ b/config/dashboards/infrastructure/partials/infra_top_chips_home_section.yaml @@ -0,0 +1,35 @@ +###################################################################### +# @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 - Top status chips (Home) +# Home-only chips row (no Pi-hole control; DNS health is alert-only on Home). +# ------------------------------------------------------------------- +# Notes: Keep this lightweight; it should render fast on the Home view. +###################################################################### + +type: grid +column_span: 4 +columns: 3 +square: false +cards: +- type: custom:button-card + template: bearstone_infra_chip + entity: binary_sensor.node_proxmox1_updates_packages + name: Proxmox01 + icon: mdi:server + state_display: > + [[[ return entity.state === 'on' ? 'Updates pending' : 'Up to date'; ]]] +- type: custom:button-card + template: bearstone_infra_chip + entity: binary_sensor.node_proxmox02_updates_packages + name: Proxmox02 + icon: mdi:server + state_display: > + [[[ return entity.state === 'on' ? 'Updates pending' : 'Up to date'; ]]] +- type: custom:button-card + template: bearstone_infra_chip + entity: sensor.garage_ups_status + name: Garage UPS + icon: mdi:transmission-tower diff --git a/config/dashboards/infrastructure/partials/pihole_sections.yaml b/config/dashboards/infrastructure/partials/pihole_sections.yaml new file mode 100644 index 00000000..f3846f9f --- /dev/null +++ b/config/dashboards/infrastructure/partials/pihole_sections.yaml @@ -0,0 +1,28 @@ +###################################################################### +# @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 - Pi-hole sections +# Dedicated full-width Pi-hole panel. +# ------------------------------------------------------------------- +# Notes: Uses `custom:pi-hole` with existing device_id. +###################################################################### + +- !include /config/dashboards/infrastructure/partials/infra_top_chips_section.yaml + +- type: grid + column_span: 4 + columns: 1 + square: false + cards: + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:pi-hole + device_id: d69637da16f7d7f3626070582be59808 + grid_options: + columns: full diff --git a/config/dashboards/infrastructure/partials/proxmox_sections.yaml b/config/dashboards/infrastructure/partials/proxmox_sections.yaml index 69799452..97c44ecf 100644 --- a/config/dashboards/infrastructure/partials/proxmox_sections.yaml +++ b/config/dashboards/infrastructure/partials/proxmox_sections.yaml @@ -39,7 +39,7 @@ - type: grid column_span: 4 - columns: 2 + columns: 3 square: false cards: - type: custom:vertical-stack-in-card @@ -93,40 +93,6 @@ name: WireGuard CPU - entity: sensor.qemu_wireguard_104_memory_used_percentage name: WireGuard MEM - - type: grid - columns: 3 - square: false - cards: - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_docker69_169_status - name: Docker69 - icon: mdi:docker - label: > - [[[ return "Last boot: " + (states['sensor.qemu_docker69_169_last_boot']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.qemu_docker69_169_reboot - name: Docker69 - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_carlo_hass_105_status - name: HASS - icon: mdi:home-assistant - label: > - [[[ return "Last boot: " + (states['sensor.qemu_carlo_hass_105_last_boot']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.qemu_carlo_hass_105_reboot - name: HASS - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_wireguard_104_status - name: WireGuard - icon: mdi:vpn - label: > - [[[ return "Last boot: " + (states['sensor.qemu_wireguard_104_last_boot']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.qemu_wireguard_104_reboot - name: WireGuard - type: custom:vertical-stack-in-card card_mod: @@ -171,40 +137,28 @@ name: CPU - entity: sensor.qemu_docker2_101_memory_used_percentage name: MEM - - type: grid - columns: 3 - square: false - cards: - - type: custom:button-card - template: bearstone_infra_device_tile - entity: sensor.qemu_docker2_101_status - name: Frigate - icon: mdi:video - label: > - [[[ return "Last boot: " + (states['sensor.qemu_docker2_101_last_boot']?.state ?? 'unknown'); ]]] - variables: - button_entity: button.qemu_docker2_101_reboot - name: Frigate -- type: grid - column_span: 4 - columns: 1 - square: false - cards: - - type: custom:mini-graph-card - entities: - - entity: sensor.proxmox_garage_average_temperature - name: Garage - - entity: sensor.pirateweather_temperature - name: Outside - name: Garage Temp - hours_to_show: 96 - color_thresholds: - - value: 50 - color: '#f39c12' - - value: 120 - color: '#d35400' - - value: 145 - color: '#c0392b' + - type: custom:vertical-stack-in-card card_mod: - style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Garage Temp + - type: custom:mini-graph-card + entities: + - entity: sensor.proxmox_garage_average_temperature + name: Garage + - entity: sensor.pirateweather_temperature + name: Outside + name: Garage Temp + hours_to_show: 96 + color_thresholds: + - value: 50 + color: '#f39c12' + - value: 120 + color: '#d35400' + - value: 145 + color: '#c0392b' + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml diff --git a/config/dashboards/infrastructure/partials/website_health_sections.yaml b/config/dashboards/infrastructure/partials/website_health_sections.yaml new file mode 100644 index 00000000..5d027871 --- /dev/null +++ b/config/dashboards/infrastructure/partials/website_health_sections.yaml @@ -0,0 +1,236 @@ +###################################################################### +# @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 - Website health sections +# Website uptime + domain expiry + certificate telemetry detail view. +# ------------------------------------------------------------------- +# Notes: Keep a full-width container directly below chips to avoid whitespace. +###################################################################### + +- !include /config/dashboards/infrastructure/partials/infra_top_chips_section.yaml + +# ------------------------------------------------------------------- +# Website hero (mandatory full-width container under chips) +# ------------------------------------------------------------------- +- 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 + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Website Uptime + - type: custom:layout-card + 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:button-card + template: bearstone_infra_list_row_running + entity: binary_sensor.vcloudinfo_com + name: vcloudinfo.com + icon: mdi:web + state_display: > + [[[ return entity.state === 'on' ? 'UP' : (entity.state || 'unknown').toUpperCase(); ]]] + - type: custom:button-card + template: bearstone_infra_list_row_running + entity: binary_sensor.www_kingcrafthomes_com + name: kingcrafthomes.com + icon: mdi:web + state_display: > + [[[ return entity.state === 'on' ? 'UP' : (entity.state || 'unknown').toUpperCase(); ]]] + - type: custom:button-card + template: bearstone_infra_list_row_running + entity: binary_sensor.bear_stone + name: Bear Stone + icon: mdi:web-check + state_display: > + [[[ return entity.state === 'on' ? 'UP' : (entity.state || 'unknown').toUpperCase(); ]]] + + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Domain Expiry + - type: custom:layout-card + 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:button-card + template: bearstone_infra_list_row + entity: sensor.vcloudinfo_com_days_until_expiration + name: vcloudinfo.com + icon: mdi:calendar-clock + state_display: > + [[[ + const days = states['sensor.vcloudinfo_com_days_until_expiration']?.state ?? 'n/a'; + const expRaw = states['sensor.vcloudinfo_com_expires']?.state; + let exp = 'n/a'; + const s = String(expRaw ?? ''); + if (s && !['unknown','unavailable','none',''].includes(s.toLowerCase())) { + if (/^\d{4}-\d{2}-\d{2}/.test(s)) { + exp = s.slice(0, 10); + } else { + const dt = new Date(s); + if (!Number.isNaN(dt.getTime())) { + const y = dt.getFullYear(); + const m = String(dt.getMonth() + 1).padStart(2, '0'); + const d = String(dt.getDate()).padStart(2, '0'); + exp = `${y}-${m}-${d}`; + } + } + } + return `${days}d | ${exp}`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.kingcrafthomes_com_days_until_expiration + name: kingcrafthomes.com + icon: mdi:calendar-clock + state_display: > + [[[ + const days = states['sensor.kingcrafthomes_com_days_until_expiration']?.state ?? 'n/a'; + const expRaw = states['sensor.kingcrafthomes_com_expires']?.state; + let exp = 'n/a'; + const s = String(expRaw ?? ''); + if (s && !['unknown','unavailable','none',''].includes(s.toLowerCase())) { + if (/^\d{4}-\d{2}-\d{2}/.test(s)) { + exp = s.slice(0, 10); + } else { + const dt = new Date(s); + if (!Number.isNaN(dt.getTime())) { + const y = dt.getFullYear(); + const m = String(dt.getMonth() + 1).padStart(2, '0'); + const d = String(dt.getDate()).padStart(2, '0'); + exp = `${y}-${m}-${d}`; + } + } + } + return `${days}d | ${exp}`; + ]]] + +# ------------------------------------------------------------------- +# Certificate health +# ------------------------------------------------------------------- +- type: grid + column_span: 4 + columns: 1 + square: false + cards: + - type: custom:vertical-stack-in-card + grid_options: + columns: full + card_mod: + style: !include /config/dashboards/infrastructure/card_mod/infra_panel.yaml + cards: + - type: custom:button-card + template: bearstone_infra_panel_header + name: Certificate Health + - type: custom:button-card + template: bearstone_infra_list_row + name: Cert telemetry sensors + icon: mdi:certificate-outline + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + return keys.length === 0 ? 'Missing (domain expiry only)' : `${keys.length} sensor(s)`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + name: Minimum cert days remaining + icon: mdi:calendar-alert + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min === null) ? 'Not available' : `${Math.round(min)} days`; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + name: Cert warning (< 30d) + icon: mdi:alert-outline + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min < 30 && min >= 14) ? 'ALERT' : 'OK'; + ]]] + - type: custom:button-card + template: bearstone_infra_list_row + name: Cert critical (< 14d) + icon: mdi:alert-circle + state_display: > + [[[ + const keys = Object.keys(states).filter((k) => + k.startsWith('sensor.') && + /(vcloudinfo|kingcrafthomes)/.test(k) && + /(cert|ssl|tls)/.test(k) + ); + let min = null; + keys.forEach((k) => { + const n = Number(states[k]?.state); + if (Number.isFinite(n)) min = (min === null) ? n : Math.min(min, n); + }); + return (min !== null && min < 14) ? 'ALERT' : 'OK'; + ]]] diff --git a/config/dashboards/infrastructure/templates/button_card_templates.yaml b/config/dashboards/infrastructure/templates/button_card_templates.yaml index e8e17ae2..63f52a86 100644 --- a/config/dashboards/infrastructure/templates/button_card_templates.yaml +++ b/config/dashboards/infrastructure/templates/button_card_templates.yaml @@ -381,6 +381,46 @@ bearstone_infra_list_row: - padding-left: 8px - white-space: nowrap +bearstone_infra_alert_row: + template: bearstone_infra_list_row + # Home view only. Uses JS visibility checks to keep Home "exceptions-only". + styles: + card: + - border-color: rgba(229,57,53,0.25) + - background: rgba(255,235,238,0.65) + - display: > + [[[ + const kind = (variables && variables.alert_kind) ? String(variables.alert_kind) : ''; + const minAge = (variables && variables.min_age_s !== undefined) ? Number(variables.min_age_s) : 0; + const thr = (variables && variables.threshold !== undefined) ? Number(variables.threshold) : null; + + const stateStr = String(entity && entity.state !== undefined ? entity.state : ''); + const lastChanged = entity && entity.last_changed ? new Date(entity.last_changed) : null; + // If we can't determine age, do not surface time-gated alerts. + const ageS = lastChanged ? ((Date.now() - lastChanged.getTime()) / 1000) : 0; + + const num = (entId) => { + const v = parseFloat(states[entId] ? states[entId].state : ''); + return Number.isFinite(v) ? v : null; + }; + + let show = false; + if (kind === 'binary_on') show = (stateStr === 'on'); + else if (kind === 'binary_off') show = (stateStr === 'off'); + else if (kind === 'ap_zero') show = (stateStr === '0' && ageS >= minAge); + else if (kind === 'disk_high') { + const v = parseFloat(stateStr); + show = Number.isFinite(v) && (thr !== null ? (v > thr) : (v > 80)); + } else if (kind === 'speedtest_slow') { + const dl = num('sensor.speedtest_download'); + const ul = num('sensor.speedtest_upload'); + const t = (thr !== null) ? thr : 300; + show = (dl !== null && dl < t) || (ul !== null && ul < t); + } + + return show ? 'block' : 'none'; + ]]] + bearstone_infra_list_row_running: template: bearstone_infra_list_row state: diff --git a/config/dashboards/infrastructure/views/04_pihole.yaml b/config/dashboards/infrastructure/views/04_pihole.yaml new file mode 100644 index 00000000..dfd4d917 --- /dev/null +++ b/config/dashboards/infrastructure/views/04_pihole.yaml @@ -0,0 +1,19 @@ +###################################################################### +# @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 - Pi-hole +# Dedicated Pi-hole view after Docker. +# ------------------------------------------------------------------- +# Notes: Full-width sections layout for Pi-hole telemetry/control card. +###################################################################### + +title: Pi-hole +path: pihole +type: sections +icon: mdi:pi-hole +theme: default +badges: [] +sections: !include /config/dashboards/infrastructure/partials/pihole_sections.yaml +max_columns: 4 diff --git a/config/dashboards/infrastructure/views/04_mariadb.yaml b/config/dashboards/infrastructure/views/05_mariadb.yaml similarity index 100% rename from config/dashboards/infrastructure/views/04_mariadb.yaml rename to config/dashboards/infrastructure/views/05_mariadb.yaml diff --git a/config/dashboards/infrastructure/views/07_website_health.yaml b/config/dashboards/infrastructure/views/07_website_health.yaml new file mode 100644 index 00000000..1f4a3739 --- /dev/null +++ b/config/dashboards/infrastructure/views/07_website_health.yaml @@ -0,0 +1,20 @@ +###################################################################### +# @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 - Website Health +# Website uptime + domain expiry + certificate telemetry details. +# ------------------------------------------------------------------- +# Notes: Home view surfaces only exceptions; this view carries detail. +###################################################################### + +title: Website Health +path: website-health +type: sections +icon: mdi:web-check +theme: default +badges: [] +sections: !include /config/dashboards/infrastructure/partials/website_health_sections.yaml +max_columns: 4 +cards: [] diff --git a/config/dashboards/infrastructure/views/05_activity_feed.yaml b/config/dashboards/infrastructure/views/08_activity_feed.yaml similarity index 100% rename from config/dashboards/infrastructure/views/05_activity_feed.yaml rename to config/dashboards/infrastructure/views/08_activity_feed.yaml diff --git a/config/packages/README.md b/config/packages/README.md index 3c081403..cd7c342f 100755 --- a/config/packages/README.md +++ b/config/packages/README.md @@ -46,6 +46,7 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this | [logbook_activity_feed.yaml](logbook_activity_feed.yaml) | Dummy `sensor.activity_feed` + helper to write clean Activity entries (Issue #1550). | `sensor.activity_feed`, `script.send_to_logbook` | | [mariadb_monitoring.yaml](mariadb_monitoring.yaml) | MariaDB health sensors and Lovelace dashboard snippet for recorder stats. | `sensor.mariadb_status`, `sensor.database_size` | | [docker_infrastructure.yaml](docker_infrastructure.yaml) | Docker host patching + staggered auto-reboot flow + container-down Repairs alerts. | `sensor.docker_*_apt_status`, `repairs.create`, `repairs.remove` | +| [infrastructure_observability.yaml](infrastructure_observability.yaml) | Normalized WAN/DNS/backup/domain/cert health sensors used by the Infrastructure Home + Website Health dashboards. | `binary_sensor.infra_*`, `sensor.infra_*`, `script.send_to_logbook` | | [mariadb.yaml](mariadb.yaml) | MariaDB recorder health and capacity SQL sensors. | `sensor.mariadb_status`, `sensor.database_size` | | [tugtainer_updates.yaml](tugtainer_updates.yaml) | Tugtainer container update notifications via webhook + persistent alerts. | `persistent_notification.create`, `input_datetime.tugtainer_last_update` | | [phynplus.yaml](phynplus.yaml) | Phyn shutoff automations with push + Activity feed + Repairs issues for leak events. | `valve.phyn_shutoff_valve`, `binary_sensor.phyn_leak_test_running`, `repairs.create` | diff --git a/config/packages/infrastructure_observability.yaml b/config/packages/infrastructure_observability.yaml new file mode 100644 index 00000000..66a782dc --- /dev/null +++ b/config/packages/infrastructure_observability.yaml @@ -0,0 +1,266 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Infrastructure Observability - Normalized infra monitoring signals +# WAN/DNS/backup/website/domain/cert state normalized for dashboards. +# ------------------------------------------------------------------- +# Notes: Home dashboard consumes `infra_*` entities for exceptions-only alerts. +# Notes: Domain warning threshold is <30 days; critical threshold is <14 days. +###################################################################### + +command_line: + - sensor: + name: Infra WAN Packet Loss + unique_id: infra_wan_packet_loss + command: >- + ping -q -c 10 -W 1 1.1.1.1 2>/dev/null | + awk -F',' '/packet loss/ {gsub(/%| /, "", $3); print $3; found=1} + END {if (!found) print "unknown"}' + scan_interval: 300 + unit_of_measurement: "%" + + - sensor: + name: Infra WAN Latency Ms + unique_id: infra_wan_latency_ms + command: >- + ping -q -c 10 -W 1 1.1.1.1 2>/dev/null | + awk -F'/' '/^rtt|^round-trip/ {print $5; found=1} + END {if (!found) print "unknown"}' + scan_interval: 300 + unit_of_measurement: "ms" + + - sensor: + name: Infra External IP Fallback + unique_id: infra_external_ip_fallback + command: "curl -fsS https://api.ipify.org || echo unknown" + scan_interval: 900 + +template: + - sensor: + - 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 %} + + - name: "Infra Backup Age Hours" + unique_id: infra_backup_age_hours + unit_of_measurement: "h" + state: >- + {% set stamp = states('sensor.dockerconfigs_backup_date') %} + {% set ts = as_datetime(stamp) %} + {% if ts is not none %} + {{ ((now() - ts).total_seconds() / 3600) | round(1) }} + {% else %} + unknown + {% endif %} + + - name: "Infra Domain Expiry Min Days" + unique_id: infra_domain_expiry_min_days + unit_of_measurement: "d" + state: >- + {% set ids = [ + 'sensor.vcloudinfo_com_days_until_expiration', + 'sensor.ipmer_com_days_until_expiration', + 'sensor.fordst_com_days_until_expiration', + 'sensor.kingcrafthomes_com_days_until_expiration' + ] %} + {% set ns = namespace(min=9999, any=false) %} + {% for id in ids %} + {% if expand(id) | count > 0 %} + {% set raw = states(id) %} + {% if raw not in ['unknown', 'unavailable', 'none', ''] %} + {% set ns.any = true %} + {% set val = raw | float(9999) %} + {% if val < ns.min %} + {% set ns.min = val %} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {% if ns.any %} + {{ ns.min | round(0) }} + {% else %} + unknown + {% endif %} + + - name: "Infra Cert Expiry Min Days" + unique_id: infra_cert_expiry_min_days + unit_of_measurement: "d" + state: >- + {% set ns = namespace(min=9999, any=false) %} + {% for item in states.sensor %} + {% if item.entity_id is search('(vcloudinfo|ipmer|fordst|kingcrafthomes).*(cert|ssl|tls)') %} + {% set raw = item.state %} + {% if raw not in ['unknown', 'unavailable', 'none', ''] %} + {% set value = raw | float(9999) %} + {% if value != 9999 %} + {% set ns.any = true %} + {% if value < ns.min %} + {% set ns.min = value %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {% if ns.any %} + {{ ns.min | round(0) }} + {% else %} + unknown + {% endif %} + + - name: "Infra Cert Telemetry Count" + unique_id: infra_cert_telemetry_count + icon: mdi:counter + state: >- + {% set ns = namespace(count=0) %} + {% for item in states.sensor %} + {% if item.entity_id is search('(vcloudinfo|ipmer|fordst|kingcrafthomes).*(cert|ssl|tls)') %} + {% set ns.count = ns.count + 1 %} + {% endif %} + {% endfor %} + {{ ns.count }} + + - name: "Infra Website Down Count" + unique_id: infra_website_down_count + icon: mdi:counter + state: >- + {% set ids = [ + 'binary_sensor.vcloudinfo_com', + 'binary_sensor.ipmer_com', + 'binary_sensor.fordst_com', + 'binary_sensor.www_kingcrafthomes_com' + ] %} + {% set ns = namespace(count=0) %} + {% for id in ids %} + {% if expand(id) | count > 0 %} + {% set st = states(id) %} + {% if st in ['off', 'unknown', 'unavailable'] %} + {% set ns.count = ns.count + 1 %} + {% endif %} + {% endif %} + {% endfor %} + {{ ns.count }} + + - binary_sensor: + - name: "Infra WAN Quality Degraded" + unique_id: infra_wan_quality_degraded + device_class: problem + state: >- + {% set loss_raw = states('sensor.infra_wan_packet_loss') %} + {% set lat_raw = states('sensor.infra_wan_latency_ms') %} + {% set invalid = loss_raw in ['unknown', 'unavailable', 'none', ''] or + lat_raw in ['unknown', 'unavailable', 'none', ''] %} + {% set loss = loss_raw | float(0) %} + {% set lat = lat_raw | float(0) %} + {{ invalid or loss > 5 or lat > 80 }} + + - name: "Infra Backup Stale Or Failed" + unique_id: infra_backup_stale_or_failed + device_class: problem + state: >- + {% set status = states('sensor.dockerconfigs_backup_status') | lower %} + {% set err = states('sensor.dockerconfigs_backup_error_message') | lower %} + {% set age = states('sensor.infra_backup_age_hours') | float(9999) %} + {% set failed = status in ['failed', 'failure', 'error', 'fatal'] or + 'fail' in status or + 'error' in status or + err not in ['unknown', 'unavailable', 'none', ''] %} + {{ failed or age > 24 }} + + - name: "Infra DNS Pihole Degraded" + unique_id: infra_dns_pihole_degraded + device_class: problem + state: >- + {% set switch_state = states('switch.pi_hole') %} + {% set service_state = states('binary_sensor.pihole_status') %} + {{ switch_state != 'on' or service_state in ['off', 'unavailable', 'unknown'] }} + + - name: "Infra UPS On Battery" + unique_id: infra_ups_on_battery + device_class: problem + state: >- + {% set status = states('sensor.garage_ups_status') | upper %} + {{ 'OB' in status }} + + - name: "Infra Website Degraded" + unique_id: infra_website_degraded + device_class: problem + state: >- + {{ states('sensor.infra_website_down_count') | int(0) > 0 }} + + - name: "Infra Domain Expiry Critical" + unique_id: infra_domain_expiry_critical + device_class: problem + state: >- + {% set d = states('sensor.infra_domain_expiry_min_days') %} + {% if d in ['unknown', 'unavailable', 'none', ''] %} + false + {% else %} + {{ d | float(9999) < 14 }} + {% endif %} + + - name: "Infra Domain Expiry Warning" + unique_id: infra_domain_expiry_warning + device_class: problem + state: >- + {% set d = states('sensor.infra_domain_expiry_min_days') %} + {% if d in ['unknown', 'unavailable', 'none', ''] %} + false + {% else %} + {% set days = d | float(9999) %} + {{ days < 30 and days >= 14 }} + {% endif %} + + - name: "Infra Cert Expiry Critical" + unique_id: infra_cert_expiry_critical + device_class: problem + state: >- + {% set d = states('sensor.infra_cert_expiry_min_days') %} + {% if d in ['unknown', 'unavailable', 'none', ''] %} + false + {% else %} + {{ d | float(9999) < 14 }} + {% endif %} + + - name: "Infra Cert Expiry Warning" + unique_id: infra_cert_expiry_warning + device_class: problem + state: >- + {% set d = states('sensor.infra_cert_expiry_min_days') %} + {% if d in ['unknown', 'unavailable', 'none', ''] %} + false + {% else %} + {% set days = d | float(9999) %} + {{ days < 30 and days >= 14 }} + {% endif %} + +automation: + - alias: "Infrastructure - External IP Change Logbook" + id: infra_external_ip_change_logbook + description: "Log external IP changes into the Activity feed." + mode: queued + trigger: + - platform: state + entity_id: sensor.infra_external_ip + condition: + - condition: template + value_template: "{{ trigger.from_state is not none }}" + - condition: template + value_template: >- + {{ trigger.from_state.state not in ['unknown', 'unavailable', 'none', ''] and + trigger.to_state.state not in ['unknown', 'unavailable', 'none', ''] and + trigger.from_state.state != trigger.to_state.state }} + action: + - service: script.send_to_logbook + data: + topic: "NETWORK" + message: >- + External IP changed from {{ trigger.from_state.state }} to {{ trigger.to_state.state }}.