###################################################################### # @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. # Notes: The compact ribbon template supports Overview status and daily controls. ###################################################################### 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_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 === 'state_not') { const okState = String(variables?.ok_state ?? '').toLowerCase(); show = stateStr.toLowerCase() !== okState; } 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_overview_ribbon_control: template: bearstone_infra_chip variables: ok_states: [] active_states: [] warning_states: [] state_labels: {} raw_suffix: '' display_mode: state humidity_entity: '' state_display: > [[[ const raw = String(entity?.state ?? 'unknown').toLowerCase(); if (variables?.display_mode === 'climate') { const current = Number(entity?.attributes?.current_temperature); const target = Number(entity?.attributes?.temperature); const humidityEntity = String(variables?.humidity_entity ?? '').trim(); const humidity = humidityEntity ? Number(states[humidityEntity]?.state) : Number.NaN; const currentText = Number.isFinite(current) ? `${Math.round(current)}°` : 'n/a'; const targetText = Number.isFinite(target) ? `${Math.round(target)}°` : 'n/a'; const humidityText = Number.isFinite(humidity) ? ` · ${Math.round(humidity)}%` : ''; return `${currentText} / ${targetText}${humidityText}`; } const labels = variables?.state_labels || {}; if (Object.prototype.hasOwnProperty.call(labels, raw)) return labels[raw]; const suffix = String(variables?.raw_suffix ?? '').trim(); const display = raw.replaceAll('_', ' ').toUpperCase(); return suffix ? `${display} ${suffix}` : display; ]]] styles: card: - 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) : []; 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)' : '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) : []; 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)' : 'rgba(255,235,238,0.85)'; ]]] icon: - 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 (['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)' : 'rgba(198,40,40,1)'; ]]] name: - font-size: 13px - font-weight: 900 - opacity: 1 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) : []; 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)' : 'rgba(198,40,40,1)'; ]]] - font-size: 12px - font-weight: 900 - opacity: 1 bearstone_overview_sleep_summary: template: bearstone_infra_kpi show_icon: false show_label: true variables: score_attribute: '' green_threshold: 88 amber_threshold: 65 styles: card: - min-height: 62px - padding: 8px 12px grid: - grid-template-areas: '"n" "s" "l"' - grid-template-rows: min-content min-content 14px - row-gap: 1px name: - font-size: 12px - font-weight: 900 - opacity: 0.75 state: - font-size: 26px - font-weight: 900 - opacity: 1 - color: > [[[ const attribute = String(variables?.score_attribute ?? '').trim(); const score = Number(attribute ? entity?.attributes?.[attribute] : entity?.state); const greenThreshold = Number(variables?.green_threshold ?? 88); const amberThreshold = Number(variables?.amber_threshold ?? 65); if (!Number.isFinite(score)) return 'var(--secondary-text-color)'; if (score >= greenThreshold) return '#2e7d32'; if (score >= amberThreshold) return '#f9a825'; return '#c62828'; ]]] label: - font-size: 11px - font-weight: 700 - line-height: 14px bearstone_health_kpi: template: bearstone_infra_kpi styles: name: - font-size: 12px - font-weight: 900 - opacity: 0.8 state: - font-size: 17px - font-weight: 900 - 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)'; ]]]