diff --git a/.gitignore b/.gitignore index 23474fd2..1e4dbaa3 100755 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ alexa_media custom_components output/playwright/ config/www/community +config/www/ookla_speedtest/ config/.cache/ community image diff --git a/config/dashboards/README.md b/config/dashboards/README.md index e1e89a5d..af2c2653 100644 --- a/config/dashboards/README.md +++ b/config/dashboards/README.md @@ -23,6 +23,8 @@ This folder holds YAML-managed Home Assistant Lovelace dashboards and UI resources. ### Walkthrough +- Mobile quick-access video: [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/ujP-zYLEso8) +- Mobile quick-access companion post: [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/07/home-assistant-mobile-dashboard-big-buttons.html) - Video: [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/aFis2YPeSuY) - Companion post: [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/02/home-assistant-dashboard-design-system-button-card.html) - Kiosk camera tablet video: [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/ChgEu0IDWzc) @@ -47,6 +49,12 @@ This folder holds YAML-managed Home Assistant Lovelace dashboards and UI resourc - `shared/` - Cross-dashboard reuse only. If something is only used by one dashboard, keep it in that dashboard folder. +### Mobile Quick Access +- Mobile is the second visible Overview view, directly after Home and before Cameras. +- Two-column controls use large labels, states, icons, and touch targets for quick use without glasses. +- Primary actions cover garage doors, the front-door lock, thermostats, household alerts, and entry-point status. +- Packages opens the doorbell camera; Systems opens the desktop control center; destination views place a large Back to Home control at the top. + ### Kiosk Camera Dashboard - Logic and recovery package: [![YAML source: kiosk_tablet](https://img.shields.io/static/v1?label=YAML&message=kiosk_tablet&color=lightgrey&logo=github&logoColor=181717)](../packages/kiosk_tablet.yaml) - Dashboard entrypoint: [![YAML source: dashboard](https://img.shields.io/static/v1?label=YAML&message=dashboard&color=lightgrey&logo=github&logoColor=181717)](kiosk/dashboard.yaml) @@ -60,6 +68,7 @@ This folder is referenced from `config/configuration.yaml` via: - `lovelace.dashboards: ...` - Default Overview YAML dashboard: `lovelace.dashboards.lovelace.filename: ui-lovelace.yaml` - Additional YAML dashboards: `filename: dashboards//dashboard.yaml` + - Overview's second visible view is **Mobile**, a phone-first quick-control surface for garage doors, the front-door lock, thermostats, and compact household alerts. - Overview Home uses ordered production sections under `overview/sections/`; approved pilot edits now apply directly there. - Overview Home's **Windows** ribbon opens a dedicated GPIO entry-point subview with perimeter, MQTT, and individual contact status. - `dashboard-infrastructure` is presented as **Systems** while retaining its stable URL; it owns Home Water, Vacuum, network, compute, storage, and service drill-downs, including hidden WAN/Pi-hole/website detail views. diff --git a/config/dashboards/infrastructure/partials/home_sections.yaml b/config/dashboards/infrastructure/partials/home_sections.yaml index 570a5301..07398bda 100644 --- a/config/dashboards/infrastructure/partials/home_sections.yaml +++ b/config/dashboards/infrastructure/partials/home_sections.yaml @@ -10,6 +10,9 @@ # 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. # Notes: Power & Solar shows realtime flow; accumulated history remains in Energy. +# Notes: Scheduled Proxmox maintenance and sustained WAN quality/reset warnings render amber; critical conditions remain red. +# Notes: Active Issue rows enumerate faults, scheduled maintenance, and reset urgency so drill-down is optional for initial triage. +# Notes: Back to Overview is the first control in the landing-view navigation strip. ###################################################################### # ------------------------------------------------------------------- @@ -34,6 +37,16 @@ "(max-width: 900px)": grid-template-columns: repeat(2, minmax(0, 1fr)) cards: + - type: custom:button-card + template: bearstone_nav_back + name: Back to Overview + icon: mdi:arrow-left + view_layout: + grid-column: 1 / -1 + tap_action: + action: navigate + navigation_path: /lovelace/home + - type: custom:button-card template: bearstone_infra_status_kpi entity: sensor.infra_dashboard_active_issue_count @@ -42,7 +55,16 @@ variables: ok_state: '0' healthy_text: ALL CLEAR + warning_text: WARNING raw_suffix: ACTIVE + warning_entity_ids: + - binary_sensor.infra_wan_sustained_degradation + critical_entity_ids: + - binary_sensor.infra_wan_critical + - binary_sensor.infra_dns_pihole_degraded + - binary_sensor.infra_compute_degraded + - binary_sensor.infra_storage_degraded + - binary_sensor.infra_external_monitoring_degraded - type: custom:button-card template: bearstone_infra_status_kpi @@ -76,6 +98,10 @@ icon: mdi:server-network variables: ok_state: 'off' + warning_text: MAINTENANCE + warning_entity_ids: + - binary_sensor.node_proxmox1_updates_packages + - binary_sensor.node_proxmox02_updates_packages tap_action: action: navigate navigation_path: /dashboard-infrastructure/proxmox @@ -98,6 +124,13 @@ icon: mdi:wan variables: ok_state: 'off' + healthy_text: HEALTHY + warning_text: DEGRADED + critical_text: DOWN + warning_entity_ids: + - binary_sensor.infra_wan_sustained_degradation + critical_entity_ids: + - binary_sensor.infra_wan_critical tap_action: action: navigate navigation_path: /dashboard-infrastructure/network @@ -112,7 +145,8 @@ healthy_text: ONLINE alert_text: OFFLINE tap_action: - action: more-info + action: navigate + navigation_path: /energy/overview - type: custom:button-card template: bearstone_infra_status_kpi @@ -134,12 +168,18 @@ variables: ok_state: ok healthy_text: HEALTHY + warning_text: RESET SOON + critical_text: RESET URGENT + warning_entity_ids: + - binary_sensor.codex_reset_credit_expiring_soon + critical_entity_ids: + - binary_sensor.codex_reset_credit_expiring_critical tap_action: action: navigate navigation_path: /dashboard-infrastructure/joanna # ------------------------------------------------------------------- -# Active exceptions and datacenter +# Active issues and datacenter # ------------------------------------------------------------------- - type: grid column_span: 4 @@ -166,7 +206,7 @@ cards: - type: custom:button-card template: bearstone_infra_panel_header - name: Active Exceptions + name: Active Issue - type: custom:button-card template: bearstone_infra_alert_row @@ -175,6 +215,35 @@ icon: mdi:wan variables: alert_kind: binary_on + warning_entity_ids: + - binary_sensor.infra_wan_sustained_degradation + critical_entity_ids: + - binary_sensor.infra_wan_critical + exception_detail: > + [[[ + const value = (id) => { + const raw = String(states[id]?.state ?? '').toLowerCase(); + if (['unknown', 'unavailable', 'none', ''].includes(raw)) return null; + const number = Number(raw); + return Number.isFinite(number) ? number : null; + }; + const latency = value('sensor.infra_wan_latency_ms'); + const loss = value('sensor.infra_wan_packet_loss'); + const reachability = String(states['sensor.infra_wan_reachability']?.state ?? '').toLowerCase(); + const issues = []; + if (reachability === 'offline') issues.push('REACHABILITY OFFLINE'); + else if (reachability === 'partial') issues.push('REACHABILITY PARTIAL'); + if ([latency, loss].some((item) => item === null)) issues.push('QUALITY TELEMETRY MISSING'); + if (latency !== null && latency > 80) issues.push(`LATENCY ${latency.toFixed(0)} > 80 ms`); + if (loss !== null && loss > 5) issues.push(`LOSS ${loss.toFixed(1)}% > 5%`); + return issues.join(' · '); + ]]] + state_display: > + [[[ + return states['binary_sensor.infra_wan_critical']?.state === 'on' + ? 'CRITICAL' + : 'DEGRADED'; + ]]] tap_action: action: navigate navigation_path: /dashboard-infrastructure/network @@ -186,6 +255,19 @@ icon: mdi:dns-outline variables: alert_kind: binary_on + exception_detail: > + [[[ + const labels = { + 'switch.pi_hole': 'PRIMARY BLOCKING', + 'binary_sensor.pi_hole_status': 'PRIMARY SERVICE', + 'switch.pi_hole_2': 'SECONDARY BLOCKING', + 'binary_sensor.pi_hole_status_2': 'SECONDARY SERVICE' + }; + const raw = entity?.attributes?.problem_entities; + const ids = Array.isArray(raw) ? raw : []; + return ids.map((id) => `${labels[id] || id} ${String(states[id]?.state ?? 'unknown').toUpperCase()}`).join(' · '); + ]]] + state_display: '[[[ return "ATTENTION"; ]]]' tap_action: action: navigate navigation_path: /dashboard-infrastructure/network @@ -197,6 +279,8 @@ icon: mdi:pipe-leak variables: alert_kind: binary_on + exception_detail: PHYN REPORTED AN ACTIVE LEAK + state_display: '[[[ return "LEAK"; ]]]' tap_action: action: navigate navigation_path: /dashboard-infrastructure/home-water @@ -209,6 +293,9 @@ variables: alert_kind: state_not ok_state: no_error + exception_detail: > + [[[ return String(entity?.state ?? 'UNKNOWN').replaceAll('_', ' ').toUpperCase(); ]]] + state_display: '[[[ return "ATTENTION"; ]]]' tap_action: action: navigate navigation_path: /dashboard-infrastructure/vacuum @@ -220,6 +307,8 @@ icon: mdi:server-off variables: alert_kind: binary_off + exception_detail: HOST RUNTIME SENSOR IS OFFLINE + state_display: '[[[ return "OFFLINE"; ]]]' tap_action: action: navigate navigation_path: /dashboard-infrastructure/proxmox @@ -231,6 +320,8 @@ icon: mdi:server-off variables: alert_kind: binary_off + exception_detail: HOST RUNTIME SENSOR IS OFFLINE + state_display: '[[[ return "OFFLINE"; ]]]' tap_action: action: navigate navigation_path: /dashboard-infrastructure/proxmox @@ -238,10 +329,25 @@ - type: custom:button-card template: bearstone_infra_alert_row entity: binary_sensor.node_proxmox1_updates_packages - name: Proxmox01 updates pending + name: Proxmox01 maintenance pending icon: mdi:package-up variables: alert_kind: binary_on + warning_only: true + exception_detail: > + [[[ + const count = Number.parseInt(states['sensor.node_proxmox1_total_updates']?.state, 10); + const countText = Number.isFinite(count) + ? `${count} PKG${count === 1 ? '' : 'S'}` + : 'PACKAGE COUNT UNKNOWN'; + const next = new Date(); + next.setHours(2, 15, 0, 0); + if (next <= new Date()) next.setDate(next.getDate() + 1); + const dateText = next.toLocaleDateString(undefined, {month: 'short', day: 'numeric'}).toUpperCase(); + const timeText = next.toLocaleTimeString(undefined, {hour: 'numeric', minute: '2-digit'}); + return `${countText} · NEXT CHECK ${dateText} ${timeText}`; + ]]] + state_display: '[[[ return "SCHEDULED"; ]]]' tap_action: action: navigate navigation_path: /dashboard-infrastructure/proxmox @@ -249,10 +355,25 @@ - type: custom:button-card template: bearstone_infra_alert_row entity: binary_sensor.node_proxmox02_updates_packages - name: Proxmox02 updates pending + name: Proxmox02 maintenance pending icon: mdi:package-up variables: alert_kind: binary_on + warning_only: true + exception_detail: > + [[[ + const count = Number.parseInt(states['sensor.node_proxmox02_total_updates']?.state, 10); + const countText = Number.isFinite(count) + ? `${count} PKG${count === 1 ? '' : 'S'}` + : 'PACKAGE COUNT UNKNOWN'; + const next = new Date(); + next.setHours(2, 15, 0, 0); + if (next <= new Date()) next.setDate(next.getDate() + 1); + const dateText = next.toLocaleDateString(undefined, {month: 'short', day: 'numeric'}).toUpperCase(); + const timeText = next.toLocaleTimeString(undefined, {hour: 'numeric', minute: '2-digit'}); + return `${countText} · NEXT CHECK ${dateText} ${timeText}`; + ]]] + state_display: '[[[ return "SCHEDULED"; ]]]' tap_action: action: navigate navigation_path: /dashboard-infrastructure/proxmox @@ -264,6 +385,12 @@ icon: mdi:docker variables: alert_kind: binary_on + exception_detail: > + [[[ + const unavailable = states['sensor.docker_monitored_unavailable_count']?.state ?? '0'; + const total = states['sensor.docker_monitored_container_count']?.state ?? '0'; + return `${unavailable} OF ${total} MONITORED WORKLOADS UNAVAILABLE`; + ]]] state_display: > [[[ const unavailable = states['sensor.docker_monitored_unavailable_count']?.state ?? '0'; @@ -281,6 +408,26 @@ icon: mdi:database-alert-outline variables: alert_kind: binary_on + exception_detail: > + [[[ + const limits = { + 'sensor.disk_use_percent': ['HOME ASSISTANT', 80], + 'sensor.carlo_nvr_volume_1_volume_used': ['NVR', 85], + 'sensor.carlo_nas01_volume_1_volume_used': ['NAS', 85] + }; + const issues = []; + for (const [id, [label, limit]] of Object.entries(limits)) { + const raw = String(states[id]?.state ?? '').toLowerCase(); + const value = Number(raw); + if (['unknown', 'unavailable', 'none', ''].includes(raw) || !Number.isFinite(value)) { + issues.push(`${label} NO DATA`); + } else if (value > limit) { + issues.push(`${label} ${value.toFixed(1)}% > ${limit}%`); + } + } + return issues.join(' · '); + ]]] + state_display: '[[[ return "ATTENTION"; ]]]' - type: custom:button-card template: bearstone_infra_alert_row @@ -289,10 +436,57 @@ icon: mdi:web-off variables: alert_kind: binary_on + exception_detail: > + [[[ + const raw = states['sensor.infra_website_down_count']?.attributes?.down_entities; + const items = Array.isArray(raw) ? raw : []; + const labels = { + 'binary_sensor.vcloudinfo_com': 'VCLOUDINFO.COM', + 'binary_sensor.ipmer_com': 'IPMER.COM', + 'binary_sensor.fordst_com': 'BEAR STONE', + 'binary_sensor.www_kingcrafthomes_com': 'KINGCRAFTHOMES.COM' + }; + return items.map((item) => { + const [id, state] = String(item).split('='); + return `${labels[id] || id} ${String(state || 'down').toUpperCase()}`; + }).join(' · '); + ]]] + state_display: '[[[ return "DOWN"; ]]]' tap_action: action: navigate navigation_path: /dashboard-infrastructure/network + - type: custom:button-card + template: bearstone_infra_alert_row + entity: binary_sensor.codex_reset_credit_expiring_soon + name: Codex reset expires soon + icon: mdi:calendar-alert + variables: + alert_kind: binary_on + warning_entity_ids: + - binary_sensor.codex_reset_credit_expiring_soon + critical_entity_ids: + - binary_sensor.codex_reset_credit_expiring_critical + exception_detail: > + [[[ + const raw = states['sensor.codex_reset_credit_next_expiration']?.state; + const days = Number(states['sensor.codex_reset_credit_min_days_remaining']?.state); + if (!raw || ['unknown', 'unavailable'].includes(String(raw).toLowerCase())) return 'EXPIRATION DATE UNAVAILABLE'; + const when = new Date(raw).toLocaleString('en-US', { + month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' + }); + return `${when} · ${Number.isFinite(days) ? days.toFixed(1) + ' DAYS' : 'DAYS UNKNOWN'}`; + ]]] + state_display: > + [[[ + return states['binary_sensor.codex_reset_credit_expiring_critical']?.state === 'on' + ? 'RESET URGENT' + : 'RESET SOON'; + ]]] + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/joanna + - type: custom:button-card template: bearstone_infra_alert_row entity: sensor.proxmox_garage_average_temperature diff --git a/config/dashboards/infrastructure/partials/joanna_sections.yaml b/config/dashboards/infrastructure/partials/joanna_sections.yaml index 88c07f67..3165f1fb 100644 --- a/config/dashboards/infrastructure/partials/joanna_sections.yaml +++ b/config/dashboards/infrastructure/partials/joanna_sections.yaml @@ -8,6 +8,7 @@ # ------------------------------------------------------------------- # Notes: The activity drill-down reads the same rolling 24-hour runtime job history as the dispatch counters. # Notes: This view changes HA presentation only; BearClaw runtime behavior is untouched. +# Notes: Reset expiry is amber within 7 days and red within 3 days; the four reset slots remain visible. ###################################################################### # ------------------------------------------------------------------- @@ -214,6 +215,12 @@ icon: mdi:calendar-alert variables: ok_state: 'off' + warning_text: EXPIRING SOON + critical_text: EXPIRING URGENTLY + warning_entity_ids: + - binary_sensor.codex_reset_credit_expiring_soon + critical_entity_ids: + - binary_sensor.codex_reset_credit_expiring_critical state_display: > [[[ const raw = states['sensor.codex_reset_credit_next_expiration']?.state; diff --git a/config/dashboards/infrastructure/partials/network_sections.yaml b/config/dashboards/infrastructure/partials/network_sections.yaml index 7dc7beb7..b36411b6 100644 --- a/config/dashboards/infrastructure/partials/network_sections.yaml +++ b/config/dashboards/infrastructure/partials/network_sections.yaml @@ -9,7 +9,8 @@ # Notes: Pi-hole blocking controls are mirrored by pihole_ha.yaml. # Notes: Detailed WAN, Pi-hole, and website telemetry live in hidden drill-down views. # Notes: The ribbon is status-only; Internet metrics remain in the trend panel. -# Notes: WAN throughput uses a 7-day window so transient and sustained patterns are distinguishable. +# Notes: Ookla capacity snapshots use a 7-day window; continuous latency/loss determine WAN quality. +# Notes: WAN status is green when clear, amber for sustained quality degradation, and red only for corroborated outage or severe quality. ###################################################################### # ------------------------------------------------------------------- @@ -41,6 +42,13 @@ icon: mdi:wan variables: ok_state: 'off' + healthy_text: HEALTHY + warning_text: DEGRADED + critical_text: DOWN + warning_entity_ids: + - binary_sensor.infra_wan_sustained_degradation + critical_entity_ids: + - binary_sensor.infra_wan_critical tap_action: action: navigate navigation_path: /dashboard-infrastructure/wan @@ -104,7 +112,7 @@ name: Internet - type: custom:mini-graph-card - name: Internet Throughput + name: Ookla Capacity Snapshots icon: mdi:speedometer hours_to_show: 168 points_per_hour: 1 @@ -118,10 +126,10 @@ icon: true state: false entities: - - entity: sensor.speedtest_download + - entity: sensor.ookla_speedtest_download name: Download show_legend_state: true - - entity: sensor.speedtest_upload + - entity: sensor.ookla_speedtest_upload name: Upload show_legend_state: true card_mod: @@ -129,8 +137,8 @@ - type: custom:button-card template: bearstone_infra_list_row - entity: sensor.speedtest_ping - name: Current Latency + entity: sensor.infra_wan_latency_ms + name: Continuous Latency icon: mdi:wan state_display: > [[[ diff --git a/config/dashboards/infrastructure/partials/proxmox_sections.yaml b/config/dashboards/infrastructure/partials/proxmox_sections.yaml index e5d0e079..0559fbd3 100644 --- a/config/dashboards/infrastructure/partials/proxmox_sections.yaml +++ b/config/dashboards/infrastructure/partials/proxmox_sections.yaml @@ -60,7 +60,9 @@ icon: mdi:package-up variables: ok_state: 'off' + warning_state: 'on' healthy_text: CURRENT + warning_text: SCHEDULED alert_text: PENDING pending_detail: true pending_count_entity: sensor.node_proxmox1_total_updates @@ -74,7 +76,9 @@ icon: mdi:package-up variables: ok_state: 'off' + warning_state: 'on' healthy_text: CURRENT + warning_text: SCHEDULED alert_text: PENDING pending_detail: true pending_count_entity: sensor.node_proxmox02_total_updates diff --git a/config/dashboards/infrastructure/partials/wan_detail_sections.yaml b/config/dashboards/infrastructure/partials/wan_detail_sections.yaml index ef12bdf6..aa805e19 100644 --- a/config/dashboards/infrastructure/partials/wan_detail_sections.yaml +++ b/config/dashboards/infrastructure/partials/wan_detail_sections.yaml @@ -4,10 +4,10 @@ # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Infrastructure Partial - WAN Detail -# Immediate outage detection, sustained degradation, and 14-day trends. +# Corroborated reachability, continuous quality, Ookla capacity snapshots, and trends. # ------------------------------------------------------------------- -# Notes: Near-zero throughput or 90% packet loss alerts immediately. -# Notes: Other threshold breaches must remain continuous for 6 hours. +# Notes: All four ICMP/HTTPS probes must fail for 2 minutes before WAN is down. +# Notes: Ookla capacity is advisory; continuous latency/loss determine degraded and critical quality. ###################################################################### # ------------------------------------------------------------------- @@ -34,45 +34,70 @@ cards: - type: custom:button-card template: bearstone_infra_status_kpi - entity: binary_sensor.infra_wan_quality_degraded - name: WAN + entity: sensor.infra_wan_reachability + name: Reachability icon: mdi:wan variables: - ok_state: 'off' + ok_state: online + warning_state: partial + healthy_text: ONLINE + warning_text: PARTIAL + critical_text: DOWN - type: custom:button-card template: bearstone_infra_status_kpi - entity: binary_sensor.infra_wan_outage - name: Immediate Outage - icon: mdi:lan-disconnect + entity: binary_sensor.infra_wan_quality_degraded + name: Continuous Quality + icon: mdi:pulse variables: ok_state: 'off' - healthy_text: CLEAR - alert_text: OUTAGE + healthy_text: GOOD + warning_text: DEGRADED + critical_text: SEVERE + warning_entity_ids: + - binary_sensor.infra_wan_sustained_degradation + critical_entity_ids: + - binary_sensor.infra_wan_critical - type: custom:button-card template: bearstone_infra_status_kpi - entity: binary_sensor.infra_wan_sustained_degradation - name: Sustained Performance - icon: mdi:timer-alert-outline + entity: binary_sensor.infra_wan_capacity_degraded + name: Ookla Capacity + icon: mdi:speedometer variables: ok_state: 'off' - healthy_text: CLEAR - alert_text: DEGRADED + warning_state: 'on' + healthy_text: FULL + warning_text: SLOW + warning_entity_ids: + - binary_sensor.infra_wan_capacity_stale state_display: > [[[ + if (states['binary_sensor.infra_wan_capacity_stale']?.state === 'on') return 'STALE'; const raw = String(entity?.state ?? '').toLowerCase(); - if (['unknown', 'unavailable', 'none', ''].includes(raw)) return 'OBSERVING'; - return raw === 'on' ? 'DEGRADED' : 'CLEAR'; + if (['unknown', 'unavailable', 'none', ''].includes(raw)) return 'NO DATA'; + return raw === 'on' ? 'SLOW' : 'FULL'; ]]] - type: custom:button-card - template: bearstone_infra_kpi - entity: binary_sensor.infra_wan_sustained_degradation - name: Evaluation Window - icon: mdi:timer-sand + template: bearstone_infra_status_kpi + entity: binary_sensor.infra_wan_capacity_stale + name: Test Freshness + icon: mdi:clock-check-outline + variables: + ok_state: 'off' + warning_state: 'on' + healthy_text: CURRENT + warning_text: STALE state_display: > - [[[ return '6 HOURS'; ]]] + [[[ + if (entity?.state === 'on') return 'STALE'; + const raw = states['sensor.ookla_speedtest_last_test']?.state; + const when = raw ? new Date(raw) : null; + if (!when || Number.isNaN(when.getTime())) return 'NO DATA'; + const minutes = Math.max(0, Math.floor((Date.now() - when.getTime()) / 60000)); + return minutes < 60 ? `${minutes} MIN AGO` : `${Math.floor(minutes / 60)} HR AGO`; + ]]] # ------------------------------------------------------------------- # Current values and explicit alert rules @@ -106,8 +131,8 @@ - type: custom:button-card template: bearstone_infra_list_row - entity: sensor.speedtest_download - name: Download + entity: sensor.ookla_speedtest_download + name: Ookla Download icon: mdi:download-network-outline state_display: > [[[ @@ -117,8 +142,8 @@ - type: custom:button-card template: bearstone_infra_list_row - entity: sensor.speedtest_upload - name: Upload + entity: sensor.ookla_speedtest_upload + name: Ookla Upload icon: mdi:upload-network-outline state_display: > [[[ @@ -128,8 +153,8 @@ - type: custom:button-card template: bearstone_infra_list_row - entity: sensor.speedtest_ping - name: Speedtest Latency + entity: sensor.ookla_speedtest_ping + name: Ookla Idle Latency icon: mdi:speedometer state_display: > [[[ @@ -137,6 +162,28 @@ return Number.isFinite(value) ? `${value.toFixed(1)} ms` : 'NO DATA'; ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.ookla_speedtest_download_ping + name: Loaded Download Latency + icon: mdi:download-network + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(1)} ms` : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.ookla_speedtest_jitter + name: Ookla Jitter + icon: mdi:pulse + state_display: > + [[[ + const value = Number(entity?.state); + return Number.isFinite(value) ? `${value.toFixed(1)} ms` : 'NO DATA'; + ]]] + - type: custom:button-card template: bearstone_infra_list_row entity: sensor.infra_wan_latency_ms @@ -159,75 +206,134 @@ return Number.isFinite(value) ? `${value.toFixed(1)}%` : 'NO DATA'; ]]] + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.infra_wan_reachability + name: Reachability Checks + icon: mdi:lan-check + state_display: > + [[[ + const passed = Number(entity?.attributes?.checks_passed); + const total = Number(entity?.attributes?.checks_total); + return Number.isFinite(passed) && Number.isFinite(total) + ? `${passed}/${total} PASSED` + : 'NO DATA'; + ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.ookla_speedtest_server + name: Ookla Server Used + icon: mdi:server-network + + - type: custom:button-card + template: bearstone_infra_list_row + entity: sensor.ookla_speedtest_last_test + name: Run Ookla Capacity Test + icon: mdi:play-speed + state_display: > + [[[ + const when = entity?.state ? new Date(entity.state) : null; + if (!when || Number.isNaN(when.getTime())) return 'NO PRIOR TEST'; + const minutes = Math.max(0, Math.floor((Date.now() - when.getTime()) / 60000)); + return minutes < 60 ? `LAST ${minutes} MIN AGO` : `LAST ${Math.floor(minutes / 60)} HR AGO`; + ]]] + tap_action: + action: call-service + service: ookla_speedtest.run_speedtest + confirmation: + text: Run a full Ookla test? This briefly saturates the WAN connection. + - 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: Why WAN Would Alert + name: How WAN Is Evaluated - type: custom:button-card template: bearstone_infra_list_row entity: binary_sensor.infra_wan_quality_degraded - name: Current Threshold Breach + name: Current Evaluation icon: mdi:alert-decagram-outline state_display: > [[[ const invalid = (value) => ['unknown', 'unavailable', 'none', ''].includes(String(value ?? '').toLowerCase()); const numeric = (id) => invalid(states[id]?.state) ? null : Number(states[id]?.state); - const download = numeric('sensor.speedtest_download'); - const upload = numeric('sensor.speedtest_upload'); + const download = numeric('sensor.ookla_speedtest_download'); + const upload = numeric('sensor.ookla_speedtest_upload'); const latency = numeric('sensor.infra_wan_latency_ms'); const loss = numeric('sensor.infra_wan_packet_loss'); + const reachability = String(states['sensor.infra_wan_reachability']?.state ?? '').toLowerCase(); const reasons = []; - if ([download, upload, latency, loss].some((value) => value === null || !Number.isFinite(value))) reasons.push('NO DATA'); - if (download !== null && download < 300) reasons.push('DOWNLOAD'); - if (upload !== null && upload < 300) reasons.push('UPLOAD'); - if (latency !== null && latency > 80) reasons.push('LATENCY'); - if (loss !== null && loss > 5) reasons.push('PACKET LOSS'); + if (reachability === 'offline') reasons.push('OUTAGE'); + else if (reachability === 'partial') reasons.push('PARTIAL REACHABILITY'); + else if (!['online'].includes(reachability)) reasons.push('REACHABILITY NO DATA'); + if ([latency, loss].some((value) => value === null || !Number.isFinite(value))) reasons.push('QUALITY NO DATA'); + if (latency !== null && latency > 80) reasons.push(latency > 250 ? 'SEVERE LATENCY' : 'LATENCY'); + if (loss !== null && loss > 5) reasons.push(loss > 25 ? 'SEVERE LOSS' : 'PACKET LOSS'); + if (states['binary_sensor.infra_wan_capacity_stale']?.state === 'on') reasons.push('OOKLA STALE'); + else if ([download, upload].some((value) => value === null || !Number.isFinite(value))) reasons.push('CAPACITY NO DATA'); + else if (download < 300 || upload < 300) reasons.push('CAPACITY SNAPSHOT'); return reasons.length ? reasons.join(' · ') : 'NONE'; ]]] - type: custom:button-card template: bearstone_infra_list_row entity: binary_sensor.infra_wan_outage - name: Immediate Outage Rule + name: Corroborated Outage icon: mdi:lan-disconnect state_display: > - [[[ return '≤1 Mbit/s OR ≥90% LOSS'; ]]] + [[[ return '0/4 PROBES · 2 MINUTES'; ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: binary_sensor.infra_wan_severe_degradation + name: Severe Quality Rule + icon: mdi:alert-octagon-outline + state_display: > + [[[ return '>250 ms OR >25% LOSS'; ]]] + + - type: custom:button-card + template: bearstone_infra_list_row + entity: binary_sensor.infra_wan_severe_degradation + name: Severe Persistence + icon: mdi:timer-alert + state_display: > + [[[ return '30 CONTINUOUS MINUTES'; ]]] - type: custom:button-card template: bearstone_infra_list_row entity: binary_sensor.infra_wan_sustained_degradation - name: Performance Persistence - icon: mdi:timer-alert-outline + name: Sustained Quality Rule + icon: mdi:pulse state_display: > - [[[ return '6 CONTINUOUS HOURS'; ]]] + [[[ return '>80 ms OR >5% LOSS'; ]]] - type: custom:button-card template: bearstone_infra_list_row - entity: sensor.speedtest_download - name: Throughput Rule - icon: mdi:speedometer-slow + entity: binary_sensor.infra_wan_sustained_degradation + name: Quality Persistence + icon: mdi:timer-alert-outline state_display: > - [[[ return '<300 / 300 Mbit/s'; ]]] + [[[ return '6 CONTINUOUS HOURS'; ]]] - type: custom:button-card template: bearstone_infra_list_row - entity: sensor.infra_wan_latency_ms - name: Latency Rule - icon: mdi:timer-outline + entity: binary_sensor.infra_wan_capacity_degraded + name: Capacity Advisory + icon: mdi:speedometer-slow state_display: > - [[[ return '>80 ms'; ]]] + [[[ return '<300 / 300 Mbit/s · LATEST TEST'; ]]] - type: custom:button-card template: bearstone_infra_list_row - entity: sensor.infra_wan_packet_loss - name: Packet Loss Rule - icon: mdi:chart-bell-curve-cumulative + entity: binary_sensor.infra_wan_capacity_stale + name: Capacity Freshness + icon: mdi:clock-alert-outline state_display: > - [[[ return '>5%'; ]]] + [[[ return 'STALE AFTER 14 HOURS'; ]]] # ------------------------------------------------------------------- # Multi-day trend and alert history @@ -263,7 +369,7 @@ name: Download - 7 Days icon: mdi:download-network-outline entities: - - entity: sensor.speedtest_download + - entity: sensor.ookla_speedtest_download name: Download hours_to_show: 168 points_per_hour: 1 @@ -283,7 +389,7 @@ name: Upload - 7 Days icon: mdi:upload-network-outline entities: - - entity: sensor.speedtest_upload + - entity: sensor.ookla_speedtest_upload name: Upload hours_to_show: 168 points_per_hour: 1 @@ -345,8 +451,12 @@ hours_to_show: 336 entities: - entity: binary_sensor.infra_wan_outage - name: Immediate Outage + name: Corroborated Outage + - entity: binary_sensor.infra_wan_severe_degradation + name: Severe Quality - entity: binary_sensor.infra_wan_sustained_degradation - name: Sustained Degradation + name: Sustained Quality + - entity: binary_sensor.infra_wan_capacity_degraded + name: Capacity Advisory card_mod: style: !include /config/dashboards/infrastructure/card_mod/infra_card.yaml diff --git a/config/dashboards/infrastructure/templates/button_card_templates.yaml b/config/dashboards/infrastructure/templates/button_card_templates.yaml index 1f89fd3c..28324456 100644 --- a/config/dashboards/infrastructure/templates/button_card_templates.yaml +++ b/config/dashboards/infrastructure/templates/button_card_templates.yaml @@ -9,6 +9,7 @@ # ------------------------------------------------------------------- # Notes: Keep templates generic; view-specific tuning stays in the view partials. # Notes: The compact ribbon template supports Overview status and daily controls. +# Notes: Mobile templates extend the ribbon with larger accessible tap targets and text. ###################################################################### bearstone_infra_base: @@ -711,10 +712,40 @@ bearstone_infra_list_row: bearstone_infra_alert_row: template: bearstone_infra_list_row # Home view only. Uses JS visibility checks to keep Home "exceptions-only". + show_label: true + label: > + [[[ return String(variables?.exception_detail ?? ''); ]]] + variables: + alert_severity: > + [[[ + const normalizeIds = (value) => { + if (Array.isArray(value)) return value.map(String); + if (typeof value === 'string' && value.trim()) return [value.trim()]; + return []; + }; + const anyOn = (ids) => ids.some((id) => String(states[id]?.state ?? '').toLowerCase() === 'on'); + if (anyOn(normalizeIds(variables?.critical_entity_ids))) return 'critical'; + if (anyOn(normalizeIds(variables?.warning_entity_ids)) || variables?.warning_only) return 'warning'; + return 'critical'; + ]]] styles: + grid: + - grid-template-areas: '"i n s" "i l l"' + - grid-template-columns: 24px 1fr auto + - grid-template-rows: min-content min-content card: - - border-color: rgba(229,57,53,0.25) - - background: rgba(255,235,238,0.65) + - border-color: > + [[[ + return variables?.alert_severity === 'warning' + ? 'rgba(245,124,0,0.35)' + : 'rgba(229,57,53,0.25)'; + ]]] + - background: > + [[[ + return variables?.alert_severity === 'warning' + ? 'rgba(255,243,224,0.75)' + : 'rgba(255,235,238,0.65)'; + ]]] - display: > [[[ const kind = (variables && variables.alert_kind) ? String(variables.alert_kind) : ''; @@ -726,11 +757,6 @@ bearstone_infra_alert_row: // 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'); @@ -742,15 +768,34 @@ bearstone_infra_alert_row: else if (kind === 'disk_high' || kind === 'numeric_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'; ]]] + icon: + - color: > + [[[ + return variables?.alert_severity === 'warning' + ? 'rgba(230,81,0,1)' + : 'rgba(198,40,40,1)'; + ]]] + state: + - color: > + [[[ + return variables?.alert_severity === 'warning' + ? 'rgba(230,81,0,1)' + : 'rgba(198,40,40,1)'; + ]]] + label: + - grid-area: l + - justify-self: start + - padding-top: 3px + - font-size: 11px + - font-weight: 700 + - line-height: 14px + - opacity: 0.78 + - white-space: normal + - text-align: left bearstone_infra_list_row_running: template: bearstone_infra_list_row @@ -847,12 +892,31 @@ bearstone_infra_kpi: bearstone_infra_status_kpi: template: bearstone_infra_kpi + variables: + status_severity: > + [[[ + const raw = String(entity?.state ?? '').toLowerCase(); + const unavailable = ['unknown', 'unavailable', 'none', ''].includes(raw); + if (unavailable) return 'unknown'; + const normalizeIds = (value) => { + if (Array.isArray(value)) return value.map(String); + if (typeof value === 'string' && value.trim()) return [value.trim()]; + return []; + }; + const anyOn = (ids) => ids.some((id) => String(states[id]?.state ?? '').toLowerCase() === 'on'); + const critical = anyOn(normalizeIds(variables?.critical_entity_ids)); + const warning = anyOn(normalizeIds(variables?.warning_entity_ids)); + const warningState = String(variables?.warning_state ?? '').toLowerCase(); + const okState = String(variables?.ok_state ?? 'off').toLowerCase(); + if (critical) return 'critical'; + if (warning || (warningState && raw === warningState)) return 'warning'; + return raw === okState ? 'healthy' : 'critical'; + ]]] state_display: > [[[ const raw = String(entity?.state ?? '').toLowerCase(); - const unavailable = ['unknown', 'unavailable', 'none', ''].includes(raw); - const okState = String(variables?.ok_state ?? 'off').toLowerCase(); - if (unavailable) return 'NO DATA'; + const severity = String(variables?.status_severity ?? ''); + if (severity === 'unknown') return 'NO DATA'; if (variables?.pending_detail && raw === 'on') { const countEntity = String(variables?.pending_count_entity ?? '').trim(); const countRaw = countEntity ? states[countEntity]?.state : ''; @@ -867,7 +931,9 @@ bearstone_infra_status_kpi: const timeText = next.toLocaleTimeString(undefined, {hour: 'numeric', minute: '2-digit'}); return `${countText} · NEXT CHECK ${dateText} ${timeText}`; } - if (raw === okState) return variables?.healthy_text ?? 'HEALTHY'; + if (severity === 'warning') return variables?.warning_text ?? variables?.alert_text ?? 'DEGRADED'; + if (severity === 'healthy') return variables?.healthy_text ?? 'HEALTHY'; + if (variables?.critical_text) return variables.critical_text; if (variables?.alert_text) return variables.alert_text; if (variables?.raw_suffix) return `${entity.state} ${variables.raw_suffix}`; return 'ATTENTION'; @@ -876,33 +942,33 @@ bearstone_infra_status_kpi: card: - border-color: > [[[ - const raw = String(entity?.state ?? '').toLowerCase(); - const okState = String(variables?.ok_state ?? 'off').toLowerCase(); - if (['unknown', 'unavailable', 'none', ''].includes(raw)) return 'rgba(245,124,0,0.45)'; - return raw === okState ? 'rgba(67,160,71,0.45)' : 'rgba(229,57,53,0.40)'; + const severity = String(variables?.status_severity ?? ''); + if (severity === 'healthy') return 'rgba(67,160,71,0.45)'; + if (severity === 'critical') return 'rgba(229,57,53,0.40)'; + return 'rgba(245,124,0,0.45)'; ]]] - background: > [[[ - const raw = String(entity?.state ?? '').toLowerCase(); - const okState = String(variables?.ok_state ?? 'off').toLowerCase(); - if (['unknown', 'unavailable', 'none', ''].includes(raw)) return 'rgba(255,243,224,0.85)'; - return raw === okState ? 'rgba(232,245,233,0.85)' : 'rgba(255,235,238,0.85)'; + const severity = String(variables?.status_severity ?? ''); + if (severity === 'healthy') return 'rgba(232,245,233,0.85)'; + if (severity === 'critical') return 'rgba(255,235,238,0.85)'; + return 'rgba(255,243,224,0.85)'; ]]] icon: - color: > [[[ - const raw = String(entity?.state ?? '').toLowerCase(); - const okState = String(variables?.ok_state ?? 'off').toLowerCase(); - if (['unknown', 'unavailable', 'none', ''].includes(raw)) return 'rgba(245,124,0,1)'; - return raw === okState ? 'rgba(46,125,50,1)' : 'rgba(198,40,40,1)'; + const severity = String(variables?.status_severity ?? ''); + if (severity === 'healthy') return 'rgba(46,125,50,1)'; + if (severity === 'critical') return 'rgba(198,40,40,1)'; + return 'rgba(230,81,0,1)'; ]]] state: - color: > [[[ - const raw = String(entity?.state ?? '').toLowerCase(); - const okState = String(variables?.ok_state ?? 'off').toLowerCase(); - if (['unknown', 'unavailable', 'none', ''].includes(raw)) return 'rgba(230,81,0,1)'; - return raw === okState ? 'rgba(46,125,50,1)' : 'rgba(198,40,40,1)'; + const severity = String(variables?.status_severity ?? ''); + if (severity === 'healthy') return 'rgba(46,125,50,1)'; + if (severity === 'critical') return 'rgba(198,40,40,1)'; + return 'rgba(230,81,0,1)'; ]]] - opacity: 1 @@ -912,6 +978,8 @@ bearstone_overview_ribbon_control: ok_states: [] active_states: [] warning_states: [] + warning_entity_ids: [] + critical_entity_ids: [] state_labels: {} raw_suffix: '' display_mode: state @@ -924,6 +992,24 @@ bearstone_overview_ribbon_control: ]]] mobile_compact: > [[[ return window.innerWidth <= 600; ]]] + status_severity: > + [[[ + const raw = String(entity?.state ?? '').toLowerCase(); + const normalizeIds = (value) => { + if (Array.isArray(value)) return value.map(String); + if (typeof value === 'string' && value.trim()) return [value.trim()]; + return []; + }; + const anyOn = (ids) => ids.some((id) => String(states[id]?.state ?? '').toLowerCase() === 'on'); + const ok = Array.isArray(variables?.ok_states) ? variables.ok_states.map(String) : []; + const active = Array.isArray(variables?.active_states) ? variables.active_states.map(String) : []; + const warning = Array.isArray(variables?.warning_states) ? variables.warning_states.map(String) : []; + if (anyOn(normalizeIds(variables?.critical_entity_ids))) return 'critical'; + if (anyOn(normalizeIds(variables?.warning_entity_ids))) return 'warning'; + if (variables?.cooling_active || active.includes(raw)) return 'active'; + if (['unknown', 'unavailable', 'none', ''].includes(raw) || warning.includes(raw)) return 'warning'; + return ok.includes(raw) ? 'healthy' : 'critical'; + ]]] state_display: > [[[ const raw = String(entity?.state ?? 'unknown').toLowerCase(); @@ -952,27 +1038,21 @@ bearstone_overview_ribbon_control: [[[ return variables?.mobile_compact ? '4px 5px' : '10px 12px'; ]]] - border-color: > [[[ - const raw = String(entity?.state ?? '').toLowerCase(); - const ok = Array.isArray(variables?.ok_states) ? variables.ok_states.map(String) : []; - const active = Array.isArray(variables?.active_states) ? variables.active_states.map(String) : []; - const warning = Array.isArray(variables?.warning_states) ? variables.warning_states.map(String) : []; + const severity = String(variables?.status_severity ?? 'critical'); const mobile = Boolean(variables?.mobile_compact); - if (variables?.cooling_active) return 'rgba(30,136,229,0.45)'; - if (['unknown', 'unavailable', 'none', ''].includes(raw) || warning.includes(raw)) return 'rgba(245,124,0,0.45)'; - if (active.includes(raw)) return 'rgba(30,136,229,0.45)'; - return ok.includes(raw) ? 'rgba(67,160,71,0.45)' : mobile ? 'rgba(183,28,28,1)' : 'rgba(229,57,53,0.40)'; + if (severity === 'active') return 'rgba(30,136,229,0.45)'; + if (severity === 'warning') return 'rgba(245,124,0,0.45)'; + if (severity === 'healthy') return 'rgba(67,160,71,0.45)'; + return mobile ? 'rgba(183,28,28,1)' : 'rgba(229,57,53,0.40)'; ]]] - background: > [[[ - const raw = String(entity?.state ?? '').toLowerCase(); - const ok = Array.isArray(variables?.ok_states) ? variables.ok_states.map(String) : []; - const active = Array.isArray(variables?.active_states) ? variables.active_states.map(String) : []; - const warning = Array.isArray(variables?.warning_states) ? variables.warning_states.map(String) : []; + const severity = String(variables?.status_severity ?? 'critical'); const mobile = Boolean(variables?.mobile_compact); - if (variables?.cooling_active) return 'rgba(227,242,253,0.90)'; - if (['unknown', 'unavailable', 'none', ''].includes(raw) || warning.includes(raw)) return 'rgba(255,243,224,0.85)'; - if (active.includes(raw)) return 'rgba(227,242,253,0.90)'; - return ok.includes(raw) ? 'rgba(232,245,233,0.85)' : mobile ? 'rgba(183,28,28,0.96)' : 'rgba(255,235,238,0.85)'; + if (severity === 'active') return 'rgba(227,242,253,0.90)'; + if (severity === 'warning') return 'rgba(255,243,224,0.85)'; + if (severity === 'healthy') return 'rgba(232,245,233,0.85)'; + return mobile ? 'rgba(183,28,28,0.96)' : 'rgba(255,235,238,0.85)'; ]]] grid: - grid-template-areas: > @@ -993,27 +1073,21 @@ bearstone_overview_ribbon_control: [[[ return variables?.mobile_compact ? 'center' : 'start'; ]]] - color: > [[[ - const raw = String(entity?.state ?? '').toLowerCase(); - const ok = Array.isArray(variables?.ok_states) ? variables.ok_states.map(String) : []; - const active = Array.isArray(variables?.active_states) ? variables.active_states.map(String) : []; - const warning = Array.isArray(variables?.warning_states) ? variables.warning_states.map(String) : []; + const severity = String(variables?.status_severity ?? 'critical'); const mobile = Boolean(variables?.mobile_compact); - if (variables?.cooling_active) return 'rgba(25,118,210,1)'; - if (['unknown', 'unavailable', 'none', ''].includes(raw) || warning.includes(raw)) return 'rgba(245,124,0,1)'; - if (active.includes(raw)) return 'rgba(25,118,210,1)'; - return ok.includes(raw) ? 'rgba(46,125,50,1)' : mobile ? '#ffffff' : 'rgba(198,40,40,1)'; + if (severity === 'active') return 'rgba(25,118,210,1)'; + if (severity === 'warning') return 'rgba(245,124,0,1)'; + if (severity === 'healthy') return 'rgba(46,125,50,1)'; + return mobile ? '#ffffff' : 'rgba(198,40,40,1)'; ]]] name: - color: > [[[ if (!variables?.mobile_compact) return 'var(--primary-text-color)'; - const raw = String(entity?.state ?? '').toLowerCase(); - const ok = Array.isArray(variables?.ok_states) ? variables.ok_states.map(String) : []; - const active = Array.isArray(variables?.active_states) ? variables.active_states.map(String) : []; - const warning = Array.isArray(variables?.warning_states) ? variables.warning_states.map(String) : []; - if (variables?.cooling_active || active.includes(raw)) return '#0d47a1'; - if (['unknown', 'unavailable', 'none', ''].includes(raw) || warning.includes(raw)) return '#e65100'; - return ok.includes(raw) ? '#1b5e20' : '#ffffff'; + const severity = String(variables?.status_severity ?? 'critical'); + if (severity === 'active') return '#0d47a1'; + if (severity === 'warning') return '#e65100'; + return severity === 'healthy' ? '#1b5e20' : '#ffffff'; ]]] - font-size: > [[[ return variables?.mobile_compact ? '12px' : '13px'; ]]] @@ -1034,15 +1108,12 @@ bearstone_overview_ribbon_control: state: - color: > [[[ - const raw = String(entity?.state ?? '').toLowerCase(); - const ok = Array.isArray(variables?.ok_states) ? variables.ok_states.map(String) : []; - const active = Array.isArray(variables?.active_states) ? variables.active_states.map(String) : []; - const warning = Array.isArray(variables?.warning_states) ? variables.warning_states.map(String) : []; + const severity = String(variables?.status_severity ?? 'critical'); const mobile = Boolean(variables?.mobile_compact); - if (variables?.cooling_active) return 'rgba(25,118,210,1)'; - if (['unknown', 'unavailable', 'none', ''].includes(raw) || warning.includes(raw)) return 'rgba(230,81,0,1)'; - if (active.includes(raw)) return 'rgba(25,118,210,1)'; - return ok.includes(raw) ? 'rgba(46,125,50,1)' : mobile ? '#ffffff' : 'rgba(198,40,40,1)'; + if (severity === 'active') return 'rgba(25,118,210,1)'; + if (severity === 'warning') return 'rgba(230,81,0,1)'; + if (severity === 'healthy') return 'rgba(46,125,50,1)'; + return mobile ? '#ffffff' : 'rgba(198,40,40,1)'; ]]] - font-size: > [[[ return variables?.mobile_compact ? '11px' : '12px'; ]]] @@ -1053,6 +1124,62 @@ bearstone_overview_ribbon_control: [[[ return variables?.mobile_compact ? 'center' : 'start'; ]]] - opacity: 1 +bearstone_mobile_header: + template: bearstone_infra_panel_header + styles: + card: + - padding: 10px 2px 8px 2px + name: + - font-size: 22px + - line-height: 26px + +bearstone_nav_back: + template: bearstone_infra_list_row + show_state: false + styles: + card: + - min-height: 60px + - padding: 14px 16px + grid: + - grid-template-areas: '"i n"' + - grid-template-columns: 32px 1fr + icon: + - width: 26px + name: + - font-size: 17px + - line-height: 22px + +bearstone_mobile_control: + template: bearstone_overview_ribbon_control + variables: + mobile_compact: true + styles: + card: + - min-height: 112px + - padding: 12px 10px + - border-radius: 20px + grid: + - grid-template-areas: '"i" "n" "s"' + - grid-template-columns: 1fr + - grid-template-rows: 34px min-content min-content + - row-gap: 6px + icon: + - width: 32px + - justify-self: center + name: + - font-size: 18px + - line-height: 22px + - justify-self: center + - white-space: normal + - overflow: visible + - text-overflow: clip + - text-align: center + state: + - font-size: 15px + - line-height: 18px + - justify-self: center + - text-align: center + bearstone_overview_sleep_summary: template: bearstone_infra_kpi show_icon: false diff --git a/config/dashboards/overview/sections/00_summary.yaml b/config/dashboards/overview/sections/00_summary.yaml index 5dfc90bb..bc251826 100644 --- a/config/dashboards/overview/sections/00_summary.yaml +++ b/config/dashboards/overview/sections/00_summary.yaml @@ -10,6 +10,7 @@ # Notes: Equal-width pills keep the whole ribbon compact and consistent. # Notes: Thermostat pills use the climate cooling helpers for active light-blue shading. # Notes: Mobile uses high-contrast four-column icon tiles with 64px tap targets. +# Notes: Systems mirrors infrastructure severity: sustained degradation is amber and only critical faults are red. ###################################################################### type: grid @@ -44,6 +45,14 @@ cards: state_labels: '0': ALL CLEAR raw_suffix: ACTIVE + warning_entity_ids: + - binary_sensor.infra_wan_sustained_degradation + critical_entity_ids: + - binary_sensor.infra_wan_critical + - binary_sensor.infra_dns_pihole_degraded + - binary_sensor.infra_compute_degraded + - binary_sensor.infra_storage_degraded + - binary_sensor.infra_external_monitoring_degraded tap_action: action: navigate navigation_path: /dashboard-infrastructure/home diff --git a/config/dashboards/overview/views/02_mobile.yaml b/config/dashboards/overview/views/02_mobile.yaml new file mode 100644 index 00000000..5c0af822 --- /dev/null +++ b/config/dashboards/overview/views/02_mobile.yaml @@ -0,0 +1,242 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Overview View - Mobile +# Mobile-first access, climate, and exception status. +# ------------------------------------------------------------------- +# Notes: Visible second Overview tab, after Home and before Camera. +# Notes: Tap access and climate tiles for Home Assistant's native controls. +# Notes: Two-column 112px tiles and large text prioritize use without glasses. +# Notes: Odd final tiles span both columns to avoid small orphan controls. +# Notes: The top Back to Home button preserves visible-tab navigation. +# Video: https://youtu.be/ujP-zYLEso8 +# Companion Post: https://www.vcloudinfo.com/2026/07/home-assistant-mobile-dashboard-big-buttons.html +###################################################################### + +path: mobile +title: Mobile +icon: mdi:cellphone +show_icon_and_title: true +theme: Backend-selected +type: sections +max_columns: 4 +dense_section_placement: true +badges: [] +sections: + +# ------------------------------------------------------------------- +# Quick access +# ------------------------------------------------------------------- +- 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 + align-items: stretch + grid-gap: 12px + margin: 0 + cards: + - type: custom:button-card + template: bearstone_nav_back + name: Back to Home + icon: mdi:arrow-left + view_layout: + grid-column: 1 / -1 + tap_action: + action: navigate + navigation_path: /lovelace/home + + - type: custom:button-card + template: bearstone_mobile_header + name: Quick Access + view_layout: + grid-column: 1 / -1 + + - type: custom:button-card + template: bearstone_mobile_control + entity: cover.large_garage_door + name: Large Garage + icon: mdi:garage + variables: + ok_states: + - closed + warning_states: + - opening + - closing + + - type: custom:button-card + template: bearstone_mobile_control + entity: cover.small_garage_door + name: Small Garage + icon: mdi:garage + variables: + ok_states: + - closed + warning_states: + - opening + - closing + + - type: custom:button-card + template: bearstone_mobile_control + entity: lock.front_door + name: Front Door + icon: mdi:door-closed-lock + view_layout: + grid-column: 1 / -1 + variables: + ok_states: + - locked + +# ------------------------------------------------------------------- +# Climate +# ------------------------------------------------------------------- +- 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 + align-items: stretch + grid-gap: 12px + margin: 0 + cards: + - type: custom:button-card + template: bearstone_mobile_header + name: Climate + view_layout: + grid-column: 1 / -1 + + - type: custom:button-card + template: bearstone_mobile_control + entity: climate.downstairs + name: Downstairs + icon: mdi:thermostat + variables: + display_mode: climate + humidity_entity: sensor.downstairs_humidity + cooling_entity: binary_sensor.downstairs_ac_is_cooling + ok_states: + - auto + - cool + - dry + - fan_only + - heat + - heat_cool + - 'off' + + - type: custom:button-card + template: bearstone_mobile_control + entity: climate.upstairs + name: Upstairs + icon: mdi:thermostat + variables: + display_mode: climate + humidity_entity: sensor.upstairs_humidity + cooling_entity: binary_sensor.upstairs_ac_is_cooling + ok_states: + - auto + - cool + - dry + - fan_only + - heat + - heat_cool + - 'off' + +# ------------------------------------------------------------------- +# Alerts and status +# ------------------------------------------------------------------- +- 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 + align-items: stretch + grid-gap: 12px + margin: 0 + cards: + - type: custom:button-card + template: bearstone_mobile_header + name: Alerts and Status + view_layout: + grid-column: 1 / -1 + + - type: custom:button-card + template: bearstone_mobile_control + entity: sensor.infra_dashboard_active_issue_count + name: Systems + icon: mdi:server-network + variables: + ok_states: + - '0' + state_labels: + '0': ALL CLEAR + raw_suffix: ACTIVE + warning_entity_ids: + - binary_sensor.infra_wan_sustained_degradation + critical_entity_ids: + - binary_sensor.infra_wan_critical + - binary_sensor.infra_dns_pihole_degraded + - binary_sensor.infra_compute_degraded + - binary_sensor.infra_storage_degraded + - binary_sensor.infra_external_monitoring_degraded + tap_action: + action: navigate + navigation_path: /dashboard-infrastructure/home + + - type: custom:button-card + template: bearstone_mobile_control + entity: group.entry_points + name: Entry Points + icon: mdi:window-closed-variant + variables: + ok_states: + - 'off' + state_labels: + 'off': SECURE + 'on': OPEN + tap_action: + action: navigate + navigation_path: /lovelace/windows + + - type: custom:button-card + template: bearstone_mobile_control + entity: binary_sensor.front_door_packages_present + name: Packages + icon: mdi:package-variant-closed + view_layout: + grid-column: 1 / -1 + variables: + ok_states: + - 'off' + state_labels: + 'off': CLEAR + 'on': WAITING + tap_action: + action: more-info + entity: camera.frontdoorbell +cards: [] diff --git a/config/dashboards/resources.yaml b/config/dashboards/resources.yaml index 7a3ad788..7a8342fc 100644 --- a/config/dashboards/resources.yaml +++ b/config/dashboards/resources.yaml @@ -8,6 +8,8 @@ # ------------------------------------------------------------------- # Notes: Generated from config/.storage/lovelace_resources. Edit here after migration; do not edit .storage. # Loaded by Home Assistant via `config/lovelace/resources.yaml`. +# Ookla resources are declared here because the integration's storage-mode registration +# is incompatible with this dashboard's YAML-managed resources. ###################################################################### - type: module @@ -46,3 +48,13 @@ url: /hacsfiles/lovelace-layout-card/layout-card.js?hacstag=156434866247 - type: module url: /hacsfiles/frigate-events-card/frigate-events-card.js?hacstag=1111882479100 +- type: module + url: /local/ookla_speedtest/ookla-speedtest-card.js?v=3.0.5 +- type: module + url: /local/ookla_speedtest/ookla-speedtest-minimal.js?v=3.0.5 +- type: module + url: /local/ookla_speedtest/ookla-speedtest-compact.js?v=3.0.5 +- type: module + url: /local/ookla_speedtest/ookla-speedtest-dashboard.js?v=3.0.5 +- type: module + url: /local/ookla_speedtest/ookla-speedtest-card-simple.js?v=3.0.5 diff --git a/tools/dashboard_entity_allowlist.txt b/tools/dashboard_entity_allowlist.txt index 1c3f70e4..2960fffc 100644 --- a/tools/dashboard_entity_allowlist.txt +++ b/tools/dashboard_entity_allowlist.txt @@ -129,7 +129,6 @@ sensor.sleepnumber_carlo_stacey_heart_rate_average sensor.sleepnumber_carlo_stacey_heart_rate_variability sensor.sleepnumber_carlo_stacey_respiratory_rate_average sensor.sleepnumber_carlo_stacey_sleep_duration -sensor.speedtest_ping sensor.stacey_location_display sensor.total_wifi_clients sensor.water_softener_salt_average_days_between_refills