mirror of
https://github.com/CCOSTAN/Home-AssistantConfig.git
synced 2026-04-29 03:33:33 +00:00
Update Home Assistant version to 2026.3.1 and enhance Docker infrastructure configurations
- Incremented Home Assistant version in the configuration file. - Refined Docker container handling in templates and automations to support multiple container states. - Improved logic for determining effective states of Docker containers, including handling of alternate status entities. - Enhanced Tugtainer update automation to dispatch Joanna for available updates with a 24-hour cooldown and improved logging. - Updated README and package documentation to reflect recent changes and new features.
This commit is contained in:
@@ -51,6 +51,7 @@ python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.p
|
||||
- Repeated actions/sequence: move to a reusable `script.*`, pass variables.
|
||||
- Repeated conditions: extract to template binary sensors or helper entities.
|
||||
- Repeated triggers: consolidate where behavior is equivalent, or split by intent if readability improves.
|
||||
- For cooldown/throttle behavior, prefer automation-local `this.attributes.last_triggered` with custom event handoff before adding new helper entities, unless shared persistent state is required across automations.
|
||||
|
||||
5. Validate after edits:
|
||||
- Re-run this verifier.
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026.3.0
|
||||
2026.3.1
|
||||
@@ -103,44 +103,6 @@
|
||||
action: navigate
|
||||
navigation_path: '#infra-docker-14'
|
||||
|
||||
- type: grid
|
||||
column_span: 4
|
||||
columns: 4
|
||||
square: false
|
||||
cards:
|
||||
- type: custom:button-card
|
||||
template: bearstone_infra_apt_prune_tile
|
||||
name: docker_10
|
||||
entity: sensor.docker_10_apt_status
|
||||
variables:
|
||||
last_update_sensor: sensor.docker_10_apt_last_update
|
||||
prune_button: button.carlo_hass_prune_unused_images
|
||||
name: docker_10
|
||||
- type: custom:button-card
|
||||
template: bearstone_infra_apt_prune_tile
|
||||
name: docker_17
|
||||
entity: sensor.docker_17_apt_status
|
||||
variables:
|
||||
last_update_sensor: sensor.docker_17_apt_last_update
|
||||
prune_button: button.docker17_prune_unused_images
|
||||
name: docker_17
|
||||
- type: custom:button-card
|
||||
template: bearstone_infra_apt_prune_tile
|
||||
name: docker_69
|
||||
entity: sensor.docker_69_apt_status
|
||||
variables:
|
||||
last_update_sensor: sensor.docker_69_apt_last_update
|
||||
prune_button: button.docker69_prune_unused_images
|
||||
name: docker_69
|
||||
- type: custom:button-card
|
||||
template: bearstone_infra_apt_prune_tile
|
||||
name: docker_14
|
||||
entity: sensor.docker_14_apt_status
|
||||
variables:
|
||||
last_update_sensor: sensor.docker_14_apt_last_update
|
||||
prune_button: button.docker2_prune_unused_images
|
||||
name: docker_14
|
||||
|
||||
- type: grid
|
||||
column_span: 4
|
||||
columns: 1
|
||||
@@ -240,7 +202,3 @@
|
||||
sort:
|
||||
method: name
|
||||
|
||||
- !include /config/dashboards/infrastructure/popups/docker_10_maintenance.yaml
|
||||
- !include /config/dashboards/infrastructure/popups/docker_17_maintenance.yaml
|
||||
- !include /config/dashboards/infrastructure/popups/docker_69_maintenance.yaml
|
||||
- !include /config/dashboards/infrastructure/popups/docker_14_maintenance.yaml
|
||||
|
||||
@@ -186,13 +186,32 @@ bearstone_infra_container_row:
|
||||
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$/, '');
|
||||
}
|
||||
return key ? `button.${key}_restart_container` : '';
|
||||
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: '[[[ return "Restart container " + entity.attributes.friendly_name + "?" ]]]'
|
||||
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: >
|
||||
[[[
|
||||
@@ -204,8 +223,12 @@ bearstone_infra_container_row:
|
||||
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$/, '');
|
||||
@@ -216,20 +239,65 @@ bearstone_infra_container_row:
|
||||
image: >
|
||||
[[[
|
||||
const ent = (entity && entity.entity_id) ? String(entity.entity_id) : '';
|
||||
const stateNow = String(entity && entity.state !== undefined ? entity.state : '').toLowerCase();
|
||||
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 imageEntity = variables.image_sensor
|
||||
? variables.image_sensor
|
||||
: (key ? `sensor.${key}_image` : '');
|
||||
const imageValue = states[imageEntity]?.state;
|
||||
if (!imageValue || ['unknown', 'unavailable', 'none', ''].includes(String(imageValue).toLowerCase())) {
|
||||
if (telemetryDegraded && ['unknown', 'unavailable', ''].includes(stateNow)) {
|
||||
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';
|
||||
@@ -238,13 +306,49 @@ bearstone_infra_container_row:
|
||||
]]]
|
||||
status: >
|
||||
[[[
|
||||
const s = String(entity.state || '').toLowerCase();
|
||||
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') return 'STOPPED';
|
||||
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(entity.state).toUpperCase();
|
||||
return String(s).toUpperCase();
|
||||
]]]
|
||||
styles:
|
||||
grid:
|
||||
@@ -258,6 +362,50 @@ bearstone_infra_container_row:
|
||||
- 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
|
||||
@@ -278,112 +426,203 @@ bearstone_infra_container_row:
|
||||
- letter-spacing: 0.04em
|
||||
- padding: 4px 10px
|
||||
- border-radius: 999px
|
||||
- background: rgba(0,0,0,0.06)
|
||||
- color: var(--secondary-text-color)
|
||||
- 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['group.docker_monitored_containers']?.attributes?.entity_id || [];
|
||||
const restart = key ? `button.${key}_restart_container` : '';
|
||||
return (restart && states[restart] && monitored.includes(switchEntity)) ? 'block' : 'none';
|
||||
const restartCandidates = key ? [
|
||||
`button.${key}_restart_container`,
|
||||
`button.${key}_restart_container_2`,
|
||||
] : [];
|
||||
const hasRestart = restartCandidates.some((candidate) => states[candidate]);
|
||||
const isMonitored = monitored.includes(switchEntity) || monitored.includes(switchEntityAlt);
|
||||
return (hasRestart && isMonitored) ? 'block' : 'none';
|
||||
]]]
|
||||
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)
|
||||
custom_fields:
|
||||
status:
|
||||
- background: rgba(46,125,50,0.12)
|
||||
- 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)
|
||||
custom_fields:
|
||||
status:
|
||||
- background: rgba(198,40,40,0.10)
|
||||
- color: rgba(198,40,40,1)
|
||||
- value: Running
|
||||
styles:
|
||||
card:
|
||||
- border-color: rgba(67,160,71,0.45)
|
||||
- background: rgba(232,245,233,0.85)
|
||||
icon:
|
||||
- color: rgba(46,125,50,1)
|
||||
custom_fields:
|
||||
status:
|
||||
- background: rgba(46,125,50,0.12)
|
||||
- color: rgba(46,125,50,1)
|
||||
- value: running
|
||||
styles:
|
||||
card:
|
||||
- border-color: rgba(67,160,71,0.45)
|
||||
- background: rgba(232,245,233,0.85)
|
||||
icon:
|
||||
- color: rgba(46,125,50,1)
|
||||
custom_fields:
|
||||
status:
|
||||
- background: rgba(46,125,50,0.12)
|
||||
- color: rgba(46,125,50,1)
|
||||
- value: Stopped
|
||||
styles:
|
||||
card:
|
||||
- border-color: rgba(229,57,53,0.35)
|
||||
- background: rgba(255,235,238,0.85)
|
||||
icon:
|
||||
- color: rgba(198,40,40,1)
|
||||
custom_fields:
|
||||
status:
|
||||
- background: rgba(198,40,40,0.10)
|
||||
- color: rgba(198,40,40,1)
|
||||
- value: stopped
|
||||
styles:
|
||||
card:
|
||||
- border-color: rgba(229,57,53,0.35)
|
||||
- background: rgba(255,235,238,0.85)
|
||||
icon:
|
||||
- color: rgba(198,40,40,1)
|
||||
custom_fields:
|
||||
status:
|
||||
- background: rgba(198,40,40,0.10)
|
||||
- color: rgba(198,40,40,1)
|
||||
- value: unavailable
|
||||
styles:
|
||||
card:
|
||||
- border-color: rgba(245,124,0,0.35)
|
||||
- background: rgba(255,243,224,0.85)
|
||||
icon:
|
||||
- color: rgba(230,81,0,1)
|
||||
custom_fields:
|
||||
status:
|
||||
- background: rgba(230,81,0,0.12)
|
||||
- color: rgba(230,81,0,1)
|
||||
- value: unknown
|
||||
styles:
|
||||
card:
|
||||
- border-color: rgba(245,124,0,0.35)
|
||||
- background: rgba(255,243,224,0.85)
|
||||
icon:
|
||||
- color: rgba(230,81,0,1)
|
||||
custom_fields:
|
||||
status:
|
||||
- background: rgba(230,81,0,0.12)
|
||||
- color: rgba(230,81,0,1)
|
||||
|
||||
bearstone_infra_panel_header:
|
||||
show_icon: false
|
||||
|
||||
@@ -51,7 +51,7 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this
|
||||
| [onenote_indexer.yaml](onenote_indexer.yaml) | OneNote indexer health/status monitoring for Joanna, failure-repair automation, and a daily duplicate-delete maintenance request. | `sensor.onenote_indexer_last_job_status`, `binary_sensor.onenote_indexer_last_job_successful` |
|
||||
| [mqtt_status.yaml](mqtt_status.yaml) | Command-line MQTT broker reachability probe with Spook Repairs escalation and Joanna troubleshooting dispatch on outage. | `binary_sensor.mqtt_status_raw`, `binary_sensor.mqtt_broker_problem`, `repairs.create`, `rest_command.bearclaw_command` |
|
||||
| [mariadb.yaml](mariadb.yaml) | MariaDB recorder health and capacity SQL sensors. | `sensor.mariadb_status`, `sensor.database_size` |
|
||||
| [tugtainer_updates.yaml](tugtainer_updates.yaml) | Tugtainer container update notifications via webhook + persistent alerts. | `persistent_notification.create`, `input_datetime.tugtainer_last_update` |
|
||||
| [tugtainer_updates.yaml](tugtainer_updates.yaml) | Tugtainer container update notifications via webhook + persistent alerts, plus event-based Joanna dispatch when reports include `### Available:` (24h cooldown via automation `last_triggered`, no new helpers). | `persistent_notification.create`, `event: tugtainer_available_detected`, `script.joanna_dispatch`, `input_datetime.tugtainer_last_update` |
|
||||
| [bearclaw.yaml](bearclaw.yaml) | Joanna/BearClaw bridge automations that forward Telegram commands to codex_appliance, relay replies back, and write JOANNA webhook reply summaries to Activity feed. | `rest_command.bearclaw_*`, `automation.bearclaw_*`, `script.send_to_logbook`, webhook relay |
|
||||
| [telegram_bot.yaml](telegram_bot.yaml) | Telegram script wrappers used by BearClaw and other ops flows (UI integration remains the source for bot config). | `script.joanna_send_telegram`, `telegram_bot.send_message` |
|
||||
| [phynplus.yaml](phynplus.yaml) | Phyn shutoff automations with push + Activity feed + Repairs issues for leak events. | `valve.phyn_shutoff_valve`, `binary_sensor.phyn_leak_test_running`, `repairs.create` |
|
||||
|
||||
@@ -215,21 +215,37 @@ template:
|
||||
{% set ns = namespace(keys=[], unavailable=0) %}
|
||||
{% set monitored = state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) %}
|
||||
{% for switch_entity in monitored %}
|
||||
{% set key = switch_entity | replace('switch.', '') | regex_replace('_container$', '') %}
|
||||
{% set key = switch_entity | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
|
||||
{% if key not in ns.keys %}
|
||||
{% set ns.keys = ns.keys + [key] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for key in ns.keys %}
|
||||
{% set status_entity = 'binary_sensor.' ~ key ~ '_status' %}
|
||||
{% set status_entity_alt = status_entity ~ '_2' %}
|
||||
{% set state_entity = 'sensor.' ~ key ~ '_state' %}
|
||||
{% set state_entity_alt = state_entity ~ '_2' %}
|
||||
{% set switch_entity = 'switch.' ~ key ~ '_container' %}
|
||||
{% if expand(status_entity) | count > 0 %}
|
||||
{% set effective_state = states(status_entity) | lower %}
|
||||
{% elif expand(switch_entity) | count > 0 %}
|
||||
{% set effective_state = states(switch_entity) | lower %}
|
||||
{% else %}
|
||||
{% set effective_state = 'unknown' %}
|
||||
{% set switch_entity_alt = switch_entity ~ '_2' %}
|
||||
{% set resolver = namespace(state='unknown', chosen=false) %}
|
||||
{% for candidate in [status_entity, status_entity_alt, state_entity, state_entity_alt, switch_entity, switch_entity_alt] %}
|
||||
{% if not resolver.chosen and expand(candidate) | count > 0 %}
|
||||
{% set candidate_state = states(candidate) | lower %}
|
||||
{% if candidate_state not in ['unknown', 'unavailable', ''] %}
|
||||
{% set resolver.state = candidate_state %}
|
||||
{% set resolver.chosen = true %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not resolver.chosen %}
|
||||
{% for candidate in [status_entity, status_entity_alt, state_entity, state_entity_alt, switch_entity, switch_entity_alt] %}
|
||||
{% if not resolver.chosen and expand(candidate) | count > 0 %}
|
||||
{% set resolver.state = states(candidate) | lower %}
|
||||
{% set resolver.chosen = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% set effective_state = resolver.state %}
|
||||
{% if effective_state == 'unavailable' %}
|
||||
{% set ns.unavailable = ns.unavailable + 1 %}
|
||||
{% endif %}
|
||||
@@ -244,21 +260,37 @@ template:
|
||||
{% set monitored = state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) %}
|
||||
{% set telemetry_degraded = is_state('binary_sensor.docker_container_telemetry_degraded', 'on') %}
|
||||
{% for switch_entity in monitored %}
|
||||
{% set key = switch_entity | replace('switch.', '') | regex_replace('_container$', '') %}
|
||||
{% set key = switch_entity | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %}
|
||||
{% if key not in ns.keys %}
|
||||
{% set ns.keys = ns.keys + [key] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for key in ns.keys | sort %}
|
||||
{% set status_entity = 'binary_sensor.' ~ key ~ '_status' %}
|
||||
{% set status_entity_alt = status_entity ~ '_2' %}
|
||||
{% set state_entity = 'sensor.' ~ key ~ '_state' %}
|
||||
{% set state_entity_alt = state_entity ~ '_2' %}
|
||||
{% set switch_entity = 'switch.' ~ key ~ '_container' %}
|
||||
{% if expand(status_entity) | count > 0 %}
|
||||
{% set effective_state = states(status_entity) | lower %}
|
||||
{% elif expand(switch_entity) | count > 0 %}
|
||||
{% set effective_state = states(switch_entity) | lower %}
|
||||
{% else %}
|
||||
{% set effective_state = 'unknown' %}
|
||||
{% set switch_entity_alt = switch_entity ~ '_2' %}
|
||||
{% set resolver = namespace(state='unknown', chosen=false) %}
|
||||
{% for candidate in [status_entity, status_entity_alt, state_entity, state_entity_alt, switch_entity, switch_entity_alt] %}
|
||||
{% if not resolver.chosen and expand(candidate) | count > 0 %}
|
||||
{% set candidate_state = states(candidate) | lower %}
|
||||
{% if candidate_state not in ['unknown', 'unavailable', ''] %}
|
||||
{% set resolver.state = candidate_state %}
|
||||
{% set resolver.chosen = true %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not resolver.chosen %}
|
||||
{% for candidate in [status_entity, status_entity_alt, state_entity, state_entity_alt, switch_entity, switch_entity_alt] %}
|
||||
{% if not resolver.chosen and expand(candidate) | count > 0 %}
|
||||
{% set resolver.state = states(candidate) | lower %}
|
||||
{% set resolver.chosen = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% set effective_state = resolver.state %}
|
||||
{% if effective_state in ['off', 'stopped'] %}
|
||||
{% set ns.down = ns.down + [key] %}
|
||||
{% elif not telemetry_degraded and effective_state in ['unknown', 'unavailable'] %}
|
||||
@@ -323,7 +355,7 @@ template:
|
||||
icon: mdi:bell-sleep
|
||||
state: >-
|
||||
{% set stamp = states('input_datetime.docker_container_alerts_snooze_until') %}
|
||||
{% set until_ts = as_datetime(stamp) %}
|
||||
{% set until_ts = as_local(as_datetime(stamp)) if stamp not in ['unknown', 'unavailable', 'none', ''] else none %}
|
||||
{{ until_ts is not none and now() < until_ts }}
|
||||
|
||||
script:
|
||||
@@ -340,34 +372,52 @@ script:
|
||||
delay_minutes:
|
||||
description: "Optional delay before evaluation (used for create path)"
|
||||
example: 5
|
||||
log_result:
|
||||
description: "Whether to write activity log entries for create/clear actions"
|
||||
example: true
|
||||
sequence:
|
||||
- variables:
|
||||
down_states: ['off', 'stopped', 'unknown', 'unavailable']
|
||||
down_states: ['off', 'stopped', 'exited', 'dead', 'unknown', 'unavailable']
|
||||
src_entity: "{{ entity_id | default('', true) }}"
|
||||
op: "{{ operation | default('create', true) | lower }}"
|
||||
wait_minutes: "{{ delay_minutes | default(0) | int(0) }}"
|
||||
log_enabled: "{{ log_result | default(true) | bool }}"
|
||||
container_key: >-
|
||||
{% if src_entity.startswith('binary_sensor.') %}
|
||||
{{ src_entity | replace('binary_sensor.', '') | regex_replace('_status$', '') }}
|
||||
{{ src_entity | replace('binary_sensor.', '') | regex_replace('_status(?:_2)?$', '') }}
|
||||
{% elif src_entity.startswith('switch.') %}
|
||||
{{ src_entity | replace('switch.', '') | regex_replace('_container$', '') }}
|
||||
{{ src_entity | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') }}
|
||||
{% elif src_entity.startswith('sensor.') %}
|
||||
{{ src_entity | replace('sensor.', '') | regex_replace('_state(?:_2)?$', '') }}
|
||||
{% else %}
|
||||
{{ src_entity }}
|
||||
{% endif %}
|
||||
switch_entity: "switch.{{ container_key }}_container"
|
||||
restart_entity: "button.{{ container_key }}_restart_container"
|
||||
switch_entity_alt: "switch.{{ container_key }}_container_2"
|
||||
status_entity: "binary_sensor.{{ container_key }}_status"
|
||||
status_entity_alt: "binary_sensor.{{ container_key }}_status_2"
|
||||
state_entity: "sensor.{{ container_key }}_state"
|
||||
state_entity_alt: "sensor.{{ container_key }}_state_2"
|
||||
monitored_switches: "{{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) }}"
|
||||
tracked_container: "{{ switch_entity in monitored_switches }}"
|
||||
tracked_container: "{{ switch_entity in monitored_switches or switch_entity_alt in monitored_switches }}"
|
||||
effective_entity: >-
|
||||
{% if expand(status_entity) | count > 0 %}
|
||||
{{ status_entity }}
|
||||
{% elif expand(status_entity_alt) | count > 0 %}
|
||||
{{ status_entity_alt }}
|
||||
{% elif expand(state_entity) | count > 0 %}
|
||||
{{ state_entity }}
|
||||
{% elif expand(state_entity_alt) | count > 0 %}
|
||||
{{ state_entity_alt }}
|
||||
{% elif expand(switch_entity) | count > 0 %}
|
||||
{{ switch_entity }}
|
||||
{% elif expand(switch_entity_alt) | count > 0 %}
|
||||
{{ switch_entity_alt }}
|
||||
{% else %}
|
||||
{{ src_entity }}
|
||||
{% endif %}
|
||||
issue_id: "docker_container_{{ container_key }}_offline"
|
||||
spook_issue_id: "user_docker_container_{{ container_key }}_offline"
|
||||
- condition: template
|
||||
value_template: "{{ tracked_container and op in ['create', 'clear'] }}"
|
||||
- choose:
|
||||
@@ -379,7 +429,27 @@ script:
|
||||
- delay:
|
||||
minutes: "{{ wait_minutes }}"
|
||||
- variables:
|
||||
effective_state: "{{ states(effective_entity) | lower }}"
|
||||
effective_state: >-
|
||||
{% set candidates = [status_entity, status_entity_alt, state_entity, state_entity_alt, switch_entity, switch_entity_alt] %}
|
||||
{% set resolver = namespace(state='unknown', chosen=false) %}
|
||||
{% for candidate in candidates %}
|
||||
{% if not resolver.chosen and expand(candidate) | count > 0 %}
|
||||
{% set candidate_state = states(candidate) | lower %}
|
||||
{% if candidate_state not in ['unknown', 'unavailable', ''] %}
|
||||
{% set resolver.state = candidate_state %}
|
||||
{% set resolver.chosen = true %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not resolver.chosen %}
|
||||
{% for candidate in candidates %}
|
||||
{% if not resolver.chosen and expand(candidate) | count > 0 %}
|
||||
{% set resolver.state = states(candidate) | lower %}
|
||||
{% set resolver.chosen = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ resolver.state }}
|
||||
telemetry_degraded: "{{ is_state('binary_sensor.docker_container_telemetry_degraded', 'on') }}"
|
||||
container_name: "{{ state_attr(effective_entity, 'friendly_name') | default(container_key, true) }}"
|
||||
- condition: template
|
||||
@@ -398,14 +468,37 @@ script:
|
||||
Effective entity: {{ effective_entity }}.
|
||||
severity: warning
|
||||
persistent: true
|
||||
- service: script.send_to_logbook
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: "{{ container_name }} is {{ effective_state }} for over 5 minutes."
|
||||
- choose:
|
||||
- conditions: "{{ log_enabled }}"
|
||||
sequence:
|
||||
- service: script.send_to_logbook
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: "{{ container_name }} is {{ effective_state }} for over 5 minutes."
|
||||
- conditions: "{{ op == 'clear' }}"
|
||||
sequence:
|
||||
- variables:
|
||||
effective_state: "{{ states(effective_entity) | lower }}"
|
||||
effective_state: >-
|
||||
{% set candidates = [status_entity, status_entity_alt, state_entity, state_entity_alt, switch_entity, switch_entity_alt] %}
|
||||
{% set resolver = namespace(state='unknown', chosen=false) %}
|
||||
{% for candidate in candidates %}
|
||||
{% if not resolver.chosen and expand(candidate) | count > 0 %}
|
||||
{% set candidate_state = states(candidate) | lower %}
|
||||
{% if candidate_state not in ['unknown', 'unavailable', ''] %}
|
||||
{% set resolver.state = candidate_state %}
|
||||
{% set resolver.chosen = true %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not resolver.chosen %}
|
||||
{% for candidate in candidates %}
|
||||
{% if not resolver.chosen and expand(candidate) | count > 0 %}
|
||||
{% set resolver.state = states(candidate) | lower %}
|
||||
{% set resolver.chosen = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ resolver.state }}
|
||||
container_name: "{{ state_attr(effective_entity, 'friendly_name') | default(container_key, true) }}"
|
||||
- condition: template
|
||||
value_template: "{{ effective_state not in down_states }}"
|
||||
@@ -413,10 +506,17 @@ script:
|
||||
continue_on_error: true
|
||||
data:
|
||||
issue_id: "{{ issue_id }}"
|
||||
- service: script.send_to_logbook
|
||||
- service: repairs.remove
|
||||
continue_on_error: true
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: "{{ container_name }} recovered ({{ effective_state }})."
|
||||
issue_id: "{{ spook_issue_id }}"
|
||||
- choose:
|
||||
- conditions: "{{ log_enabled }}"
|
||||
sequence:
|
||||
- service: script.send_to_logbook
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: "{{ container_name }} recovered ({{ effective_state }})."
|
||||
|
||||
docker_stack_repairs_sync:
|
||||
alias: Docker Stack Repairs Sync
|
||||
@@ -431,12 +531,16 @@ script:
|
||||
delay_minutes:
|
||||
description: "Optional delay before evaluation (used for create path)"
|
||||
example: 2
|
||||
log_result:
|
||||
description: "Whether to write activity log entries for create/clear actions"
|
||||
example: true
|
||||
sequence:
|
||||
- variables:
|
||||
down_states: ['off', 'unknown', 'unavailable']
|
||||
src_entity: "{{ entity_id | default('', true) }}"
|
||||
op: "{{ operation | default('create', true) | lower }}"
|
||||
wait_minutes: "{{ delay_minutes | default(0) | int(0) }}"
|
||||
log_enabled: "{{ log_result | default(true) | bool }}"
|
||||
stack_key: "{{ src_entity | replace('binary_sensor.', '') | regex_replace('_stack_status$', '') }}"
|
||||
stack_count_entity: "sensor.{{ stack_key }}_stack_containers_count"
|
||||
tracked_stack: "{{ expand(stack_count_entity) | count > 0 }}"
|
||||
@@ -467,10 +571,13 @@ script:
|
||||
Effective entity: {{ src_entity }}.
|
||||
severity: warning
|
||||
persistent: true
|
||||
- service: script.send_to_logbook
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: "{{ stack_name }} stack is {{ effective_state }} for over 2 minutes."
|
||||
- choose:
|
||||
- conditions: "{{ log_enabled }}"
|
||||
sequence:
|
||||
- service: script.send_to_logbook
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: "{{ stack_name }} stack is {{ effective_state }} for over 2 minutes."
|
||||
- conditions: "{{ op == 'clear' }}"
|
||||
sequence:
|
||||
- variables:
|
||||
@@ -482,10 +589,13 @@ script:
|
||||
continue_on_error: true
|
||||
data:
|
||||
issue_id: "{{ issue_id }}"
|
||||
- service: script.send_to_logbook
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: "{{ stack_name }} stack recovered ({{ effective_state }})."
|
||||
- choose:
|
||||
- conditions: "{{ log_enabled }}"
|
||||
sequence:
|
||||
- service: script.send_to_logbook
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: "{{ stack_name }} stack recovered ({{ effective_state }})."
|
||||
|
||||
automation:
|
||||
- alias: "APT Update Report - Docker Hosts"
|
||||
@@ -582,87 +692,128 @@ automation:
|
||||
topic: "APT"
|
||||
message: "{{ log_message }}"
|
||||
|
||||
- alias: "Docker Container State Sync - Repairs (Dynamic)"
|
||||
id: docker_container_state_sync_repairs_dynamic
|
||||
description: "Detect dynamic container state transitions and delegate Repairs sync to script helper."
|
||||
- alias: "Docker State Sync - Repairs (Dynamic)"
|
||||
id: docker_state_sync_repairs_dynamic
|
||||
description: "Detect Docker container/stack state transitions and delegate Repairs sync."
|
||||
mode: parallel
|
||||
trigger:
|
||||
- platform: event
|
||||
event_type: state_changed
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set ent = trigger.event.data.entity_id | default('') %}
|
||||
{{ ent.startswith('switch.') and ent.endswith('_container') or
|
||||
ent.startswith('binary_sensor.') and ent.endswith('_status') }}
|
||||
- condition: template
|
||||
value_template: "{{ trigger.event.data.old_state is not none and trigger.event.data.new_state is not none }}"
|
||||
- condition: template
|
||||
value_template: "{{ trigger.event.data.old_state.state != trigger.event.data.new_state.state }}"
|
||||
action:
|
||||
- variables:
|
||||
down_states: ['off', 'stopped', 'unknown', 'unavailable']
|
||||
entity_id: "{{ trigger.event.data.entity_id }}"
|
||||
entity_id: "{{ trigger.event.data.entity_id | default('') }}"
|
||||
old_state: "{{ trigger.event.data.old_state.state | lower }}"
|
||||
new_state: "{{ trigger.event.data.new_state.state | lower }}"
|
||||
monitored: "{{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) }}"
|
||||
- choose:
|
||||
- conditions: >-
|
||||
{{ new_state in down_states and old_state not in down_states and
|
||||
not (is_state('binary_sensor.docker_container_telemetry_degraded', 'on') and
|
||||
new_state in ['unknown', 'unavailable']) }}
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set ent = entity_id %}
|
||||
{% if ent.startswith('switch.') and (ent.endswith('_container') or ent.endswith('_container_2')) %}
|
||||
{{ ent in monitored }}
|
||||
{% elif ent.startswith('binary_sensor.') and (ent.endswith('_status') or ent.endswith('_status_2')) %}
|
||||
{% set key = ent | replace('binary_sensor.', '') | regex_replace('_status(?:_2)?$', '') %}
|
||||
{{ ('switch.' ~ key ~ '_container') in monitored or ('switch.' ~ key ~ '_container_2') in monitored }}
|
||||
{% elif ent.startswith('sensor.') and (ent.endswith('_state') or ent.endswith('_state_2')) %}
|
||||
{% set key = ent | replace('sensor.', '') | regex_replace('_state(?:_2)?$', '') %}
|
||||
{{ ('switch.' ~ key ~ '_container') in monitored or ('switch.' ~ key ~ '_container_2') in monitored }}
|
||||
{% else %}
|
||||
false
|
||||
{% endif %}
|
||||
sequence:
|
||||
- service: script.docker_container_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ entity_id }}"
|
||||
operation: "create"
|
||||
delay_minutes: 5
|
||||
- conditions: "{{ old_state in down_states and new_state not in down_states }}"
|
||||
- variables:
|
||||
down_states: ['off', 'stopped', 'exited', 'dead', 'unknown', 'unavailable']
|
||||
- choose:
|
||||
- conditions: >-
|
||||
{{ new_state in down_states and old_state not in down_states and
|
||||
not (is_state('binary_sensor.docker_container_telemetry_degraded', 'on') and
|
||||
new_state in ['unknown', 'unavailable']) }}
|
||||
sequence:
|
||||
- service: script.docker_container_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ entity_id }}"
|
||||
operation: "create"
|
||||
delay_minutes: 5
|
||||
- conditions: "{{ old_state in down_states and new_state not in down_states }}"
|
||||
sequence:
|
||||
- service: script.docker_container_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ entity_id }}"
|
||||
operation: "clear"
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set ent = entity_id %}
|
||||
{% if ent.startswith('binary_sensor.') and ent.endswith('_stack_status') %}
|
||||
{% set stack_key = ent | replace('binary_sensor.', '') | regex_replace('_stack_status$', '') %}
|
||||
{{ expand('sensor.' ~ stack_key ~ '_stack_containers_count') | count > 0 }}
|
||||
{% else %}
|
||||
false
|
||||
{% endif %}
|
||||
sequence:
|
||||
- service: script.docker_container_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ entity_id }}"
|
||||
operation: "clear"
|
||||
- variables:
|
||||
down_states: ['off', 'unknown', 'unavailable']
|
||||
- choose:
|
||||
- conditions: "{{ new_state in down_states and old_state not in down_states }}"
|
||||
sequence:
|
||||
- service: script.docker_stack_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ entity_id }}"
|
||||
operation: "create"
|
||||
delay_minutes: 2
|
||||
- conditions: "{{ old_state in down_states and new_state not in down_states }}"
|
||||
sequence:
|
||||
- service: script.docker_stack_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ entity_id }}"
|
||||
operation: "clear"
|
||||
|
||||
- alias: "Docker Stack State Sync - Repairs (Dynamic)"
|
||||
id: docker_stack_state_sync_repairs_dynamic
|
||||
description: "Detect Portainer stack status transitions and delegate Repairs sync."
|
||||
mode: parallel
|
||||
- alias: "Docker Repairs Reconcile"
|
||||
id: docker_repairs_reconcile
|
||||
description: "Reconcile stale container and stack Repairs issues on startup and every 55 minutes."
|
||||
mode: queued
|
||||
trigger:
|
||||
- platform: event
|
||||
event_type: state_changed
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set ent = trigger.event.data.entity_id | default('') %}
|
||||
{{ ent.startswith('binary_sensor.') and ent.endswith('_stack_status') }}
|
||||
- condition: template
|
||||
value_template: "{{ trigger.event.data.old_state is not none and trigger.event.data.new_state is not none }}"
|
||||
- condition: template
|
||||
value_template: "{{ trigger.event.data.old_state.state != trigger.event.data.new_state.state }}"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set stack_key = trigger.event.data.entity_id | replace('binary_sensor.', '') | regex_replace('_stack_status$', '') %}
|
||||
{{ expand('sensor.' ~ stack_key ~ '_stack_containers_count') | count > 0 }}
|
||||
- platform: homeassistant
|
||||
event: start
|
||||
- platform: time_pattern
|
||||
minutes: "/55"
|
||||
action:
|
||||
- variables:
|
||||
down_states: ['off', 'unknown', 'unavailable']
|
||||
entity_id: "{{ trigger.event.data.entity_id }}"
|
||||
old_state: "{{ trigger.event.data.old_state.state | lower }}"
|
||||
new_state: "{{ trigger.event.data.new_state.state | lower }}"
|
||||
- choose:
|
||||
- conditions: "{{ new_state in down_states and old_state not in down_states }}"
|
||||
sequence:
|
||||
- service: script.docker_stack_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ entity_id }}"
|
||||
operation: "create"
|
||||
delay_minutes: 2
|
||||
- conditions: "{{ old_state in down_states and new_state not in down_states }}"
|
||||
sequence:
|
||||
- service: script.docker_stack_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ entity_id }}"
|
||||
operation: "clear"
|
||||
monitored_switches: "{{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) }}"
|
||||
- repeat:
|
||||
for_each: "{{ monitored_switches }}"
|
||||
sequence:
|
||||
- service: script.docker_container_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ repeat.item }}"
|
||||
operation: "clear"
|
||||
log_result: false
|
||||
- variables:
|
||||
stack_status_entities: >-
|
||||
{% set ns = namespace(items=[]) %}
|
||||
{% for item in states.binary_sensor %}
|
||||
{% if item.entity_id is search('^binary_sensor\\..*_stack_status$') %}
|
||||
{% set stack_key = item.entity_id | replace('binary_sensor.', '') | regex_replace('_stack_status$', '') %}
|
||||
{% if expand('sensor.' ~ stack_key ~ '_stack_containers_count') | count > 0 %}
|
||||
{% set ns.items = ns.items + [item.entity_id] %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.items }}
|
||||
- repeat:
|
||||
for_each: "{{ stack_status_entities }}"
|
||||
sequence:
|
||||
- service: script.docker_stack_repairs_sync
|
||||
data:
|
||||
entity_id: "{{ repeat.item }}"
|
||||
operation: "clear"
|
||||
log_result: false
|
||||
|
||||
- alias: "Docker Containers Maintenance Prompt"
|
||||
id: docker_containers_maintenance_prompt
|
||||
@@ -736,6 +887,22 @@ automation:
|
||||
Maintenance snooze declined with {{ states('sensor.docker_containers_down_count') }}
|
||||
containers down ({{ states('sensor.docker_containers_down_list') }}).
|
||||
|
||||
- alias: "Docker Telemetry Template Refresh"
|
||||
id: docker_telemetry_template_refresh
|
||||
description: "Refresh dynamic docker telemetry templates that derive entity IDs at runtime."
|
||||
mode: single
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
minutes: "/1"
|
||||
action:
|
||||
- service: homeassistant.update_entity
|
||||
target:
|
||||
entity_id:
|
||||
- sensor.docker_monitored_unavailable_count
|
||||
- sensor.docker_containers_down_list
|
||||
- sensor.docker_containers_down_count
|
||||
- binary_sensor.docker_container_telemetry_degraded
|
||||
|
||||
- alias: "Docker Weekly Prune Unused Images"
|
||||
id: docker_weekly_prune_unused_images
|
||||
description: "Run weekly unguarded prune actions across Docker hosts."
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
# -------------------------------------------------------------------
|
||||
# Notes: Expects JSON with title/message/type from the Tugtainer template.
|
||||
# Notes: Creates persistent notifications and stamps last-update time.
|
||||
# Notes: Fires `tugtainer_available_detected` when report contains `### Available:`.
|
||||
# Notes: Joanna dispatch cooldown is automation-local (24h) using last_triggered.
|
||||
# Notes: Blog post https://www.vcloudinfo.com/2026/02/tugtainer-docker-updates-home-assistant-notifications.html
|
||||
######################################################################
|
||||
|
||||
@@ -34,6 +36,7 @@ automation:
|
||||
title: "{{ payload.title | default('Tugtainer update') }}"
|
||||
message: "{{ payload.message | default('Update event received') }}"
|
||||
event_type: "{{ payload.type | default('info') }}"
|
||||
has_available_section: "{{ '### available:' in (message | lower) }}"
|
||||
full_message: >-
|
||||
{{ message }}{% if event_type %} ({{ event_type | upper }}){% endif %}
|
||||
action:
|
||||
@@ -46,3 +49,68 @@ automation:
|
||||
data:
|
||||
title: "{{ title }}"
|
||||
message: "{{ full_message }}"
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ has_available_section }}"
|
||||
sequence:
|
||||
- event: tugtainer_available_detected
|
||||
event_data:
|
||||
title: "{{ title }}"
|
||||
event_type: "{{ event_type }}"
|
||||
message: "{{ message }}"
|
||||
|
||||
- alias: "Tugtainer - Dispatch Joanna For Available Updates"
|
||||
id: tugtainer_dispatch_joanna_for_available_updates
|
||||
description: "Dispatch Joanna on Available updates with a 24-hour cooldown and no helper entities."
|
||||
mode: single
|
||||
trigger:
|
||||
- platform: event
|
||||
event_type: tugtainer_available_detected
|
||||
variables:
|
||||
title: "{{ trigger.event.data.title | default('Tugtainer update') }}"
|
||||
message: "{{ trigger.event.data.message | default('Update event received') }}"
|
||||
event_type: "{{ trigger.event.data.event_type | default('info') }}"
|
||||
trigger_context: "HA automation tugtainer_dispatch_joanna_for_available_updates (Tugtainer - Dispatch Joanna For Available Updates)"
|
||||
now_ts: "{{ as_timestamp(now()) | int(0) }}"
|
||||
last_triggered_ts: >-
|
||||
{% if this.attributes.last_triggered %}
|
||||
{{ as_timestamp(this.attributes.last_triggered) | int(0) }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
elapsed_seconds: "{{ (now_ts - last_triggered_ts) | int(0) if last_triggered_ts > 0 else 999999 }}"
|
||||
cooldown_ok: "{{ last_triggered_ts == 0 or elapsed_seconds >= 86400 }}"
|
||||
remaining_seconds: "{{ [86400 - elapsed_seconds, 0] | max }}"
|
||||
action:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ cooldown_ok }}"
|
||||
sequence:
|
||||
- service: script.send_to_logbook
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: >-
|
||||
Tugtainer reported Available container updates. Joanna dispatch requested.
|
||||
- service: script.joanna_dispatch
|
||||
data:
|
||||
trigger_context: "{{ trigger_context }}"
|
||||
source: "home_assistant_automation.tugtainer_dispatch_joanna_for_available_updates"
|
||||
summary: "Tugtainer reported Available container updates requiring manual action"
|
||||
entity_ids:
|
||||
- "input_datetime.tugtainer_last_update"
|
||||
diagnostics: >-
|
||||
title={{ title }},
|
||||
event_type={{ event_type }},
|
||||
message={{ message }}
|
||||
request: >-
|
||||
Review the Tugtainer report and update all containers listed under the
|
||||
Available section. Report what was updated and any failures.
|
||||
default:
|
||||
- service: script.send_to_logbook
|
||||
data:
|
||||
topic: "DOCKER"
|
||||
message: >-
|
||||
Tugtainer Available update dispatch suppressed (24h cooldown active;
|
||||
{{ remaining_seconds }} seconds remaining).
|
||||
|
||||
Reference in New Issue
Block a user