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.
feature/powerwall-live-activity-1598
Carlo Costanzo 2 weeks ago
parent 74f9c23523
commit ef2396f0b8

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

Loading…
Cancel
Save

Powered by TurnKey Linux.