###################################################################### # @CCOSTAN - Follow Me on X # For more info visit https://www.vcloudinfo.com/click-here # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Infrastructure Templates - custom:button-card # Related Issue: 1560 # Shared `button_card_templates` used across Infrastructure views. # ------------------------------------------------------------------- # Notes: Keep templates generic; view-specific tuning stays in the view partials. ###################################################################### bearstone_infra_base: show_icon: true show_name: true show_state: true tap_action: action: more-info styles: card: - border-radius: 18px - padding: 14px - box-shadow: none - background: var(--ha-card-background, var(--card-background-color)) - border: 1px solid var(--divider-color) - overflow: hidden grid: - grid-template-areas: "\"i n\" \"i s\"" - grid-template-columns: 40px 1fr - grid-template-rows: min-content min-content icon: - width: 22px - color: var(--primary-text-color) name: - font-weight: 700 - font-size: 14px - justify-self: start - padding-bottom: 2px state: - font-size: 12px - opacity: 0.75 - justify-self: start bearstone_infra_chip: template: bearstone_infra_base show_state: true styles: card: - border-radius: 999px - padding: 10px 12px - border: 1px solid var(--divider-color) - background: var(--ha-card-background, var(--card-background-color)) grid: - grid-template-areas: "\"i n\" \"i s\"" - grid-template-columns: 28px 1fr icon: - width: 18px name: - font-weight: 700 - font-size: 12px state: - font-size: 11px - opacity: 0.7 bearstone_infra_chip_running: template: bearstone_infra_chip state: - value: 'on' styles: card: - border-color: rgba(67,160,71,0.45) - background: rgba(232,245,233,0.85) icon: - color: rgba(46,125,50,1) - value: 'off' styles: card: - border-color: rgba(229,57,53,0.35) - background: rgba(255,235,238,0.85) icon: - color: rgba(198,40,40,1) - value: unavailable styles: card: - border-color: rgba(229,57,53,0.35) - background: rgba(255,235,238,0.85) icon: - color: rgba(198,40,40,1) bearstone_infra_tile: template: bearstone_infra_base show_label: true label: '[[[ return variables.label ? variables.label : "" ]]]' styles: grid: - grid-template-areas: "\"i n\" \"i l\"" - grid-template-rows: min-content min-content label: - font-size: 12px - opacity: 0.8 - justify-self: start state: - value: unavailable styles: card: - border-color: rgba(229,57,53,0.35) - background: rgba(255,235,238,0.85) bearstone_infra_reboot: template: bearstone_infra_tile show_state: false icon: mdi:restart label: '[[[ return variables.subtitle ? variables.subtitle : "Hold to reboot" ]]]' hold_action: action: call-service service: button.press service_data: entity_id: '[[[ return variables.button_entity ]]]' confirmation: text: '[[[ return "Reboot " + (variables.name ? variables.name : "device") + "?" ]]]' styles: icon: - color: var(--secondary-text-color) bearstone_infra_device_tile: template: bearstone_infra_tile show_state: true label: '[[[ return variables.subtitle ? variables.subtitle : "" ]]]' hold_action: action: call-service service: button.press service_data: entity_id: '[[[ return variables.button_entity ]]]' confirmation: text: '[[[ return "Restart " + (variables.name ? variables.name : "device") + "?" ]]]' styles: state: - font-weight: 700 bearstone_infra_prune_tile: template: bearstone_infra_tile show_state: true icon: mdi:image-sync label: >- [[[ return variables.subtitle ? variables.subtitle : "Hold to prune unused images"; ]]] hold_action: action: call-service service: button.press service_data: entity_id: '[[[ return variables.prune_button ]]]' confirmation: text: '[[[ return "Prune unused images on " + (variables.name ? variables.name : "host") + "?" ]]]' styles: icon: - color: var(--secondary-text-color) state: - font-weight: 700 bearstone_infra_volume_prune_tile: template: bearstone_infra_tile show_state: true icon: mdi:database-remove label: >- [[[ return variables.subtitle ? variables.subtitle : "Hold to prune unused volumes"; ]]] hold_action: action: call-service service: button.press service_data: entity_id: '[[[ return variables.prune_button ]]]' confirmation: text: '[[[ return "Prune unused volumes on " + (variables.name ? variables.name : "host") + "?" ]]]' styles: icon: - color: var(--secondary-text-color) state: - font-weight: 700 bearstone_infra_apt_prune_tile: template: bearstone_infra_prune_tile show_state: true icon: mdi:server label: >- [[[ const lastSensor = variables.last_update_sensor ? variables.last_update_sensor : ''; const lastValue = lastSensor ? (states[lastSensor]?.state ?? 'unknown') : 'unknown'; return "Last update: " + lastValue + " | Hold to prune"; ]]] bearstone_infra_container_row: template: bearstone_infra_list_row show_label: false show_state: false tap_action: action: none hold_action: action: call-service service: button.press service_data: entity_id: > [[[ if (variables && variables.restart_button) return variables.restart_button; const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } if (!key) return ''; const restartCandidates = [ `button.${key}_restart_container`, `button.${key}_restart_container_2`, ]; for (const candidate of restartCandidates) { if (states[candidate]) return candidate; } return restartCandidates[0]; ]]] confirmation: text: > [[[ const friendly = (entity && entity.attributes && entity.attributes.friendly_name) ? String(entity.attributes.friendly_name) : ((entity && entity.entity_id) ? String(entity.entity_id) : 'container'); return "Restart container " + friendly + "?"; ]]] icon: mdi:docker name: > [[[ if (variables && variables.name) return variables.name; const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; const friendly = (entity && entity.attributes && entity.attributes.friendly_name) ? String(entity.attributes.friendly_name) : ''; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } if (friendly && friendly !== 'Container') { return friendly.replace(/\s+Container$/, ''); } return key || friendly || ent; ]]] custom_fields: image: > [[[ const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; const telemetryDegraded = states['binary_sensor.docker_container_telemetry_degraded']?.state === 'on'; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } const stateCandidates = key ? [ `binary_sensor.${key}_status`, `binary_sensor.${key}_status_2`, `sensor.${key}_state`, `sensor.${key}_state_2`, `switch.${key}_container`, `switch.${key}_container_2`, ] : []; const isUnknownLike = (v) => !v || ['unknown', 'unavailable', 'none', ''].includes(String(v).toLowerCase()); let resolvedState = String(entity && entity.state !== undefined ? entity.state : '').toLowerCase(); for (const candidate of stateCandidates) { const candidateState = states[candidate]?.state; if (!isUnknownLike(candidateState)) { resolvedState = String(candidateState).toLowerCase(); break; } } if (isUnknownLike(resolvedState)) { for (const candidate of stateCandidates) { const candidateState = states[candidate]?.state; if (candidateState !== undefined) { resolvedState = String(candidateState).toLowerCase(); break; } } } const imageCandidates = variables.image_sensor ? [variables.image_sensor] : (key ? [`sensor.${key}_image`, `sensor.${key}_image_2`] : []); let imageValue; for (const candidate of imageCandidates) { const candidateState = states[candidate]?.state; if (!isUnknownLike(candidateState)) { imageValue = candidateState; break; } } if (imageValue === undefined) { for (const candidate of imageCandidates) { const candidateState = states[candidate]?.state; if (candidateState !== undefined) { imageValue = candidateState; break; } } } if (isUnknownLike(imageValue)) { if (telemetryDegraded && ['unknown', 'unavailable', ''].includes(resolvedState)) { return 'telemetry: delayed'; } return 'image: n/a'; } return imageValue; ]]] status: > [[[ const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } const candidates = key ? [ `binary_sensor.${key}_status`, `binary_sensor.${key}_status_2`, `sensor.${key}_state`, `sensor.${key}_state_2`, `switch.${key}_container`, `switch.${key}_container_2`, ] : []; const isUnknownLike = (v) => !v || ['unknown', 'unavailable', ''].includes(String(v).toLowerCase()); let s = String(entity && entity.state !== undefined ? entity.state : '').toLowerCase(); for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (!isUnknownLike(candidateState)) { s = String(candidateState).toLowerCase(); break; } } if (isUnknownLike(s)) { for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (candidateState !== undefined) { s = String(candidateState).toLowerCase(); break; } } } const telemetryDegraded = states['binary_sensor.docker_container_telemetry_degraded']?.state === 'on'; if (s === 'on' || s === 'running') return 'RUNNING'; if (s === 'off' || s === 'stopped' || s === 'exited' || s === 'dead') return 'STOPPED'; if (s === 'unavailable') return telemetryDegraded ? 'STALE' : 'OFFLINE'; if (s === 'unknown' || s === '') return telemetryDegraded ? 'STALE' : 'UNKNOWN'; return String(s).toUpperCase(); ]]] styles: grid: - grid-template-areas: "\"i n status\" \"i image status\"" - grid-template-columns: 24px 1fr auto - grid-template-rows: min-content min-content - align-items: center - column-gap: 12px name: - min-width: 0 - overflow: hidden - text-overflow: ellipsis - white-space: nowrap icon: - color: > [[[ const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } const candidates = key ? [ `binary_sensor.${key}_status`, `binary_sensor.${key}_status_2`, `sensor.${key}_state`, `sensor.${key}_state_2`, `switch.${key}_container`, `switch.${key}_container_2`, ] : []; const isUnknownLike = (v) => !v || ['unknown', 'unavailable', ''].includes(String(v).toLowerCase()); let s = String(entity && entity.state !== undefined ? entity.state : '').toLowerCase(); for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (!isUnknownLike(candidateState)) { s = String(candidateState).toLowerCase(); break; } } if (isUnknownLike(s)) { for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (candidateState !== undefined) { s = String(candidateState).toLowerCase(); break; } } } if (s === 'on' || s === 'running') return 'rgba(46,125,50,1)'; if (s === 'off' || s === 'stopped' || s === 'exited' || s === 'dead') return 'rgba(198,40,40,1)'; return 'rgba(230,81,0,1)'; ]]] custom_fields: image: - grid-area: image - justify-self: start - align-self: start - font-size: 11px - opacity: 0.72 - line-height: 1.1 - white-space: nowrap - overflow: hidden - text-overflow: ellipsis - max-width: 100% status: - justify-self: end - align-self: center - font-weight: 900 - font-size: 12px - letter-spacing: 0.04em - padding: 4px 10px - border-radius: 999px - background: > [[[ const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } const candidates = key ? [ `binary_sensor.${key}_status`, `binary_sensor.${key}_status_2`, `sensor.${key}_state`, `sensor.${key}_state_2`, `switch.${key}_container`, `switch.${key}_container_2`, ] : []; const isUnknownLike = (v) => !v || ['unknown', 'unavailable', ''].includes(String(v).toLowerCase()); let s = String(entity && entity.state !== undefined ? entity.state : '').toLowerCase(); for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (!isUnknownLike(candidateState)) { s = String(candidateState).toLowerCase(); break; } } if (isUnknownLike(s)) { for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (candidateState !== undefined) { s = String(candidateState).toLowerCase(); break; } } } if (s === 'on' || s === 'running') return 'rgba(46,125,50,0.12)'; if (s === 'off' || s === 'stopped' || s === 'exited' || s === 'dead') return 'rgba(198,40,40,0.10)'; return 'rgba(230,81,0,0.12)'; ]]] - color: > [[[ const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } const candidates = key ? [ `binary_sensor.${key}_status`, `binary_sensor.${key}_status_2`, `sensor.${key}_state`, `sensor.${key}_state_2`, `switch.${key}_container`, `switch.${key}_container_2`, ] : []; const isUnknownLike = (v) => !v || ['unknown', 'unavailable', ''].includes(String(v).toLowerCase()); let s = String(entity && entity.state !== undefined ? entity.state : '').toLowerCase(); for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (!isUnknownLike(candidateState)) { s = String(candidateState).toLowerCase(); break; } } if (isUnknownLike(s)) { for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (candidateState !== undefined) { s = String(candidateState).toLowerCase(); break; } } } if (s === 'on' || s === 'running') return 'rgba(46,125,50,1)'; if (s === 'off' || s === 'stopped' || s === 'exited' || s === 'dead') return 'rgba(198,40,40,1)'; return 'rgba(230,81,0,1)'; ]]] card: - border-color: > [[[ const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } const candidates = key ? [ `binary_sensor.${key}_status`, `binary_sensor.${key}_status_2`, `sensor.${key}_state`, `sensor.${key}_state_2`, `switch.${key}_container`, `switch.${key}_container_2`, ] : []; const isUnknownLike = (v) => !v || ['unknown', 'unavailable', ''].includes(String(v).toLowerCase()); let s = String(entity && entity.state !== undefined ? entity.state : '').toLowerCase(); for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (!isUnknownLike(candidateState)) { s = String(candidateState).toLowerCase(); break; } } if (isUnknownLike(s)) { for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (candidateState !== undefined) { s = String(candidateState).toLowerCase(); break; } } } if (s === 'on' || s === 'running') return 'rgba(67,160,71,0.45)'; if (s === 'off' || s === 'stopped' || s === 'exited' || s === 'dead') return 'rgba(229,57,53,0.35)'; return 'rgba(245,124,0,0.35)'; ]]] - background: > [[[ const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } const candidates = key ? [ `binary_sensor.${key}_status`, `binary_sensor.${key}_status_2`, `sensor.${key}_state`, `sensor.${key}_state_2`, `switch.${key}_container`, `switch.${key}_container_2`, ] : []; const isUnknownLike = (v) => !v || ['unknown', 'unavailable', ''].includes(String(v).toLowerCase()); let s = String(entity && entity.state !== undefined ? entity.state : '').toLowerCase(); for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (!isUnknownLike(candidateState)) { s = String(candidateState).toLowerCase(); break; } } if (isUnknownLike(s)) { for (const candidate of candidates) { const candidateState = states[candidate]?.state; if (candidateState !== undefined) { s = String(candidateState).toLowerCase(); break; } } } if (s === 'on' || s === 'running') return 'rgba(232,245,233,0.85)'; if (s === 'off' || s === 'stopped' || s === 'exited' || s === 'dead') return 'rgba(255,235,238,0.85)'; return 'rgba(255,243,224,0.85)'; ]]] - display: > [[[ const ent = (entity && entity.entity_id) ? String(entity.entity_id) : ''; let key = ''; if (ent.startsWith('binary_sensor.') && ent.endsWith('_status')) { key = ent.replace('binary_sensor.', '').replace(/_status$/, ''); } else if (ent.startsWith('binary_sensor.') && ent.endsWith('_status_2')) { key = ent.replace('binary_sensor.', '').replace(/_status_2$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container')) { key = ent.replace('switch.', '').replace(/_container$/, ''); } else if (ent.startsWith('switch.') && ent.endsWith('_container_2')) { key = ent.replace('switch.', '').replace(/_container_2$/, ''); } const switchEntity = key ? `switch.${key}_container` : ''; const switchEntityAlt = key ? `switch.${key}_container_2` : ''; const monitored = states['sensor.docker_monitored_switch_inventory']?.attributes?.entity_id; const restartCandidates = key ? [ `button.${key}_restart_container`, `button.${key}_restart_container_2`, ] : []; const hasRestart = restartCandidates.some((candidate) => states[candidate]); const isMonitored = Array.isArray(monitored) ? monitored.includes(switchEntity) || monitored.includes(switchEntityAlt) : (ent.startsWith('switch.') && (ent.endsWith('_container') || ent.endsWith('_container_2'))); return (hasRestart && isMonitored) ? 'block' : 'none'; ]]] bearstone_infra_panel_header: show_icon: false show_state: false show_label: false tap_action: action: none styles: card: - background: transparent - border: none - box-shadow: none - padding: 6px 2px 10px 2px name: - font-size: 18px - font-weight: 800 - letter-spacing: 0.04em - text-transform: uppercase - justify-self: start bearstone_infra_list_row: show_icon: true show_name: true show_state: true show_label: false tap_action: action: more-info styles: card: - border-radius: 14px - padding: 12px 12px - box-shadow: none - border: 1px solid rgba(0,0,0,0.03) - background: rgba(0,0,0,0.02) - width: 100% - box-sizing: border-box grid: - grid-template-areas: "\"i n s\"" - grid-template-columns: 24px 1fr auto - align-items: center - column-gap: 10px icon: - width: 18px - color: var(--primary-color) name: - font-weight: 700 - font-size: 14px - justify-self: start - opacity: 0.95 - min-width: 0 - overflow: hidden - text-overflow: ellipsis - white-space: nowrap state: - justify-self: end - font-weight: 700 - font-size: 13px - opacity: 0.8 - padding-left: 8px - white-space: nowrap bearstone_infra_docker_volume_row: template: bearstone_infra_list_row icon: mdi:database state_display: > [[[ const toMiB = (raw, unitRaw) => { const value = Number(raw); if (!Number.isFinite(value)) return null; const unit = String(unitRaw || 'MiB').trim().toLowerCase(); if (unit === 'b' || unit === 'byte' || unit === 'bytes') return value / 1048576; if (unit === 'kib' || unit === 'kb') return value / 1024; if (unit === 'gib' || unit === 'gb') return value * 1024; if (unit === 'tib' || unit === 'tb') return value * 1048576; return value; }; const formatMiB = (value) => { if (!Number.isFinite(value)) return 'No telemetry'; if (value >= 1048576) return `${(value / 1048576).toFixed(1)} TiB`; if (value >= 1024) return `${(value / 1024).toFixed(1)} GiB`; if (value > 0 && value < 1) return `${(value * 1024).toFixed(1)} KiB`; const precision = value >= 10 ? 1 : 2; return `${value.toFixed(precision)} MiB`; }; if (variables?.not_instrumented === true) return 'Not instrumented'; const totalEntity = variables && variables.total_entity ? states[variables.total_entity] : entity; const total = totalEntity ? toMiB(totalEntity.state, totalEntity.attributes?.unit_of_measurement) : null; if (Number.isFinite(total)) return formatMiB(total); const volumeEntities = Array.isArray(variables?.volume_entities) ? variables.volume_entities : []; let sum = 0; let seen = 0; for (const entityId of volumeEntities) { const volume = states[entityId]; const value = volume ? toMiB(volume.state, volume.attributes?.unit_of_measurement) : null; if (Number.isFinite(value)) { sum += value; seen += 1; } } return seen > 0 ? `${formatMiB(sum)} | ${seen} vols` : 'No telemetry'; ]]] bearstone_infra_alert_row: template: bearstone_infra_list_row # Home view only. Uses JS visibility checks to keep Home "exceptions-only". styles: card: - border-color: rgba(229,57,53,0.25) - background: rgba(255,235,238,0.65) - display: > [[[ const kind = (variables && variables.alert_kind) ? String(variables.alert_kind) : ''; const minAge = (variables && variables.min_age_s !== undefined) ? Number(variables.min_age_s) : 0; const thr = (variables && variables.threshold !== undefined) ? Number(variables.threshold) : null; const stateStr = String(entity && entity.state !== undefined ? entity.state : ''); const lastChanged = entity && entity.last_changed ? new Date(entity.last_changed) : null; // If we can't determine age, do not surface time-gated alerts. const ageS = lastChanged ? ((Date.now() - lastChanged.getTime()) / 1000) : 0; const num = (entId) => { const v = parseFloat(states[entId] ? states[entId].state : ''); return Number.isFinite(v) ? v : null; }; let show = false; if (kind === 'binary_on') show = (stateStr === 'on'); else if (kind === 'binary_off') show = (stateStr === 'off'); else if (kind === 'ap_zero') show = (stateStr === '0' && ageS >= minAge); else if (kind === 'disk_high' || 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'; ]]] bearstone_infra_list_row_running: template: bearstone_infra_list_row state: - value: 'on' styles: card: - border-color: rgba(67,160,71,0.45) - background: rgba(232,245,233,0.85) icon: - color: rgba(46,125,50,1) state: - color: rgba(46,125,50,1) - opacity: 1 - value: 'off' styles: card: - border-color: rgba(229,57,53,0.35) - background: rgba(255,235,238,0.85) icon: - color: rgba(198,40,40,1) state: - color: rgba(198,40,40,1) - opacity: 1 - value: Running styles: icon: - color: rgba(46,125,50,1) state: - color: rgba(46,125,50,1) - opacity: 1 - value: running styles: icon: - color: rgba(46,125,50,1) state: - color: rgba(46,125,50,1) - opacity: 1 - value: Stopped styles: icon: - color: rgba(198,40,40,1) state: - color: rgba(198,40,40,1) - opacity: 1 - value: stopped styles: icon: - color: rgba(198,40,40,1) state: - color: rgba(198,40,40,1) - opacity: 1 - value: unavailable styles: icon: - color: rgba(198,40,40,1) state: - color: rgba(198,40,40,1) - opacity: 1 bearstone_infra_kpi: show_icon: true show_name: true show_state: true show_label: false tap_action: action: more-info styles: card: - border-radius: 14px - padding: 12px - box-shadow: none - border: 1px solid rgba(0,0,0,0.03) - background: rgba(0,0,0,0.02) grid: - grid-template-areas: "\"i\" \"n\" \"s\"" - grid-template-rows: 22px min-content min-content - row-gap: 6px - justify-items: center icon: - width: 18px - color: var(--primary-color) name: - font-size: 11px - font-weight: 800 - opacity: 0.65 - text-transform: uppercase - text-align: center - letter-spacing: 0.6px state: - font-size: 16px - font-weight: 800 - opacity: 0.9 bearstone_infra_status_kpi: template: bearstone_infra_kpi 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'; if (raw === okState) return variables?.healthy_text ?? 'HEALTHY'; if (variables?.alert_text) return variables.alert_text; if (variables?.raw_suffix) return `${entity.state} ${variables.raw_suffix}`; return 'ATTENTION'; ]]] styles: 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)'; ]]] - 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)'; ]]] 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)'; ]]] 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)'; ]]] - opacity: 1 bearstone_infra_update_row: template: bearstone_infra_list_row icon: mdi:package-up show_label: true variables: apt_severity: > [[[ const state = String(entity?.state ?? '').toUpperCase(); if (!state || state.includes('UNKNOWN') || state.includes('UNAVAILABLE') || state.includes('ERROR')) return 'error'; const host = entity?.entity_id?.match(/^sensor\.(docker_\d+)_apt_status$/)?.[1]; const dueRaw = host ? states[`input_datetime.apt_${host}_action_due`]?.state : null; const due = dueRaw && !['unknown', 'unavailable', 'none'].includes(String(dueRaw).toLowerCase()) ? new Date(String(dueRaw).replace(' ', 'T')) : null; const actionable = state.includes('PENDING') || state.includes('REBOOT REQ'); if (actionable && due instanceof Date && !Number.isNaN(due.getTime()) && Date.now() > due.getTime()) return 'overdue'; if (actionable) return 'scheduled'; if (state.includes('NO UPDATES') || state.includes('UPDATED')) return 'healthy'; return 'neutral'; ]]] state_display: > [[[ return String(entity?.state ?? 'NO DATA').toUpperCase(); ]]] label: > [[[ const state = String(entity?.state ?? '').toUpperCase(); const host = entity?.entity_id?.match(/^sensor\.(docker_\d+)_apt_status$/)?.[1]; const dueRaw = host ? states[`input_datetime.apt_${host}_action_due`]?.state : null; const parseDate = (raw) => { if (!raw || ['unknown', 'unavailable', 'none', ''].includes(String(raw).toLowerCase())) return null; const value = new Date(String(raw).replace(' ', 'T')); return Number.isNaN(value.getTime()) ? null : value; }; const formatTime = (value) => value.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }).toUpperCase(); const formatDue = (value) => { const weekday = value.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase(); return `${weekday} ${formatTime(value)}`; }; const actionable = state.includes('PENDING') || state.includes('REBOOT REQ'); const due = parseDate(dueRaw); if (actionable) { const action = state.includes('REBOOT REQ') ? 'REBOOT' : 'UPDATE'; return due ? `${action} ${formatDue(due)}` : `${action} SCHEDULED`; } return 'NO ACTION SCHEDULED'; ]]] 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: > [[[ if (variables.apt_severity === 'overdue') return 'rgba(229,57,53,0.40)'; if (variables.apt_severity === 'error' || variables.apt_severity === 'scheduled') return 'rgba(245,124,0,0.45)'; if (variables.apt_severity === 'healthy') return 'rgba(67,160,71,0.45)'; return 'var(--divider-color)'; ]]] - background: > [[[ if (variables.apt_severity === 'overdue') return 'rgba(255,235,238,0.85)'; if (variables.apt_severity === 'error' || variables.apt_severity === 'scheduled') return 'rgba(255,243,224,0.85)'; if (variables.apt_severity === 'healthy') return 'rgba(232,245,233,0.85)'; return 'rgba(0,0,0,0.02)'; ]]] icon: - color: > [[[ if (variables.apt_severity === 'overdue') return 'rgba(198,40,40,1)'; if (variables.apt_severity === 'error' || variables.apt_severity === 'scheduled') return 'rgba(230,81,0,1)'; if (variables.apt_severity === 'healthy') return 'rgba(46,125,50,1)'; return 'var(--primary-color)'; ]]] state: - color: > [[[ if (variables.apt_severity === 'overdue') return 'rgba(198,40,40,1)'; if (variables.apt_severity === 'error' || variables.apt_severity === 'scheduled') return 'rgba(230,81,0,1)'; if (variables.apt_severity === 'healthy') return 'rgba(46,125,50,1)'; return 'var(--secondary-text-color)'; ]]] label: - grid-area: l - justify-self: start - padding-top: 3px - font-size: 11px - font-weight: 700 - letter-spacing: 0.02em - opacity: 0.72 - white-space: normal - text-align: left - color: > [[[ if (variables.apt_severity === 'overdue') return 'rgba(198,40,40,1)'; if (variables.apt_severity === 'error' || variables.apt_severity === 'scheduled') return 'rgba(230,81,0,1)'; if (variables.apt_severity === 'healthy') return 'rgba(46,125,50,1)'; return 'var(--secondary-text-color)'; ]]]