Add mobile quick access dashboard and infra views

pull/1898/head
Carlo Costanzo 2 months ago
parent 1b00ab869f
commit 160f39fe5c

1
.gitignore vendored

@ -71,6 +71,7 @@ alexa_media
custom_components
output/playwright/
config/www/community
config/www/ookla_speedtest/
config/.cache/
community
image

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

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

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

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

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

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

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

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

@ -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: []

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

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

Loading…
Cancel
Save

Powered by TurnKey Linux.