###################################################################### # @CCOSTAN - Follow Me on X # For more info visit https://www.vcloudinfo.com/click-here # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Docker Infrastructure - Host patching and container alerts # APT webhook results (docker_10/14/17/69) and container down repairs. # ------------------------------------------------------------------- # Related Issue: 1584 # Notes: Hosts run weekly Wed 12:00 APT job and POST JSON to webhooks. # Notes: Reboots are handled directly on each host by apt_weekly.sh. # Notes: Reboot staggering: docker_14 first, docker_69 second, docker_10 third. # Notes: Container monitoring is dynamic with binary_sensor status preferred over switch state. # Notes: Includes Portainer stack status repairs and scheduled image prune. ###################################################################### input_datetime: apt_docker_10_last_check: name: "docker_10 APT last check" has_date: true has_time: true apt_docker_10_last_update: name: "docker_10 APT last update" has_date: true has_time: true apt_docker_17_last_check: name: "docker_17 APT last check" has_date: true has_time: true apt_docker_17_last_update: name: "docker_17 APT last update" has_date: true has_time: true apt_docker_14_last_check: name: "docker_14 APT last check" has_date: true has_time: true apt_docker_14_last_update: name: "docker_14 APT last update" has_date: true has_time: true apt_docker_69_last_check: name: "docker_69 APT last check" has_date: true has_time: true apt_docker_69_last_update: name: "docker_69 APT last update" has_date: true has_time: true docker_container_alerts_snooze_until: name: "Docker container alerts snooze until" has_date: true has_time: true input_text: apt_docker_10_last_result: name: "docker_10 APT last result" max: 255 apt_docker_17_last_result: name: "docker_17 APT last result" max: 255 apt_docker_14_last_result: name: "docker_14 APT last result" max: 255 apt_docker_69_last_result: name: "docker_69 APT last result" max: 255 group: docker_monitored_containers: name: Docker Monitored Containers entities: - switch.cloudflared_kch_container - switch.cloudflared_wp_container - switch.codex_appliance_container - switch.college_budget_app_container - switch.cruise_tracker_container - switch.dashy_container - switch.docker_socket_proxy_container - switch.dozzle_container - switch.dozzle_agent_10_container - switch.dozzle_agent_14_container - switch.dozzle_agent_17_container - switch.dozzle_agent_69_container - switch.duplicati_container - switch.esphome_container - switch.fed437a0f191_tugtainer_socket_proxy_container - switch.foodie_tracker_container - switch.frigate_container - switch.games_hub_container - switch.home_assistant_container - switch.imposter_container - switch.infra_info_container - switch.kingcrafthomes_container - switch.lmediaservices_container - switch.mariadb_container - switch.mariadb_backup_container - switch.matter_server_container - switch.mqtt_container - switch.nebula_sync_container - switch.panel_notes_container - switch.pihole_container - switch.pihole_secondary_container - switch.poker_tracker_container - switch.portainer_container - switch.portainer_agent_container - switch.postgres_webhooks_engine_container - switch.rc_price_checker_container - switch.redis_webhooks_engine_container - switch.rvtools_ppt_web_container - switch.tapple_container - switch.tugtainer_container - switch.tugtainer_agent_container - switch.tugtainer_socket_proxy_container - switch.unifi_container - switch.webhooks_engine_container - switch.wordpress_db_container - switch.wordpress_wp_container - switch.wyze_bridge_container template: - sensor: - name: "docker_10 APT status" unique_id: apt_docker_10_status icon: mdi:package-up state: "{{ states('input_text.apt_docker_10_last_result') }}" - name: "docker_10 APT last check" unique_id: apt_docker_10_last_check device_class: timestamp state: >- {% set stamp = states('input_datetime.apt_docker_10_last_check') %} {% if stamp not in ['unknown', 'unavailable', 'none', ''] %} {{ as_local(as_datetime(stamp)) }} {% endif %} - name: "docker_10 APT last update" unique_id: apt_docker_10_last_update device_class: timestamp state: >- {% set stamp = states('input_datetime.apt_docker_10_last_update') %} {% if stamp not in ['unknown', 'unavailable', 'none', ''] %} {{ as_local(as_datetime(stamp)) }} {% endif %} - name: "docker_14 APT status" unique_id: apt_docker_14_status icon: mdi:package-up state: "{{ states('input_text.apt_docker_14_last_result') }}" - name: "docker_17 APT status" unique_id: apt_docker_17_status icon: mdi:package-up state: "{{ states('input_text.apt_docker_17_last_result') }}" - name: "docker_17 APT last check" unique_id: apt_docker_17_last_check device_class: timestamp state: >- {% set stamp = states('input_datetime.apt_docker_17_last_check') %} {% if stamp not in ['unknown', 'unavailable', 'none', ''] %} {{ as_local(as_datetime(stamp)) }} {% endif %} - name: "docker_17 APT last update" unique_id: apt_docker_17_last_update device_class: timestamp state: >- {% set stamp = states('input_datetime.apt_docker_17_last_update') %} {% if stamp not in ['unknown', 'unavailable', 'none', ''] %} {{ as_local(as_datetime(stamp)) }} {% endif %} - name: "docker_14 APT last check" unique_id: apt_docker_14_last_check device_class: timestamp state: >- {% set stamp = states('input_datetime.apt_docker_14_last_check') %} {% if stamp not in ['unknown', 'unavailable', 'none', ''] %} {{ as_local(as_datetime(stamp)) }} {% endif %} - name: "docker_14 APT last update" unique_id: apt_docker_14_last_update device_class: timestamp state: >- {% set stamp = states('input_datetime.apt_docker_14_last_update') %} {% if stamp not in ['unknown', 'unavailable', 'none', ''] %} {{ as_local(as_datetime(stamp)) }} {% endif %} - name: "docker_69 APT status" unique_id: apt_docker_69_status icon: mdi:package-up state: "{{ states('input_text.apt_docker_69_last_result') }}" - name: "docker_69 APT last check" unique_id: apt_docker_69_last_check device_class: timestamp state: >- {% set stamp = states('input_datetime.apt_docker_69_last_check') %} {% if stamp not in ['unknown', 'unavailable', 'none', ''] %} {{ as_local(as_datetime(stamp)) }} {% endif %} - name: "docker_69 APT last update" unique_id: apt_docker_69_last_update device_class: timestamp state: >- {% set stamp = states('input_datetime.apt_docker_69_last_update') %} {% if stamp not in ['unknown', 'unavailable', 'none', ''] %} {{ as_local(as_datetime(stamp)) }} {% endif %} - sensor: - name: "Docker Monitored Container Count" unique_id: docker_monitored_container_count icon: mdi:format-list-numbered state: >- {{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) | count }} - name: "Docker Monitored Unavailable Count" unique_id: docker_monitored_unavailable_count icon: mdi:lan-disconnect state: >- {% 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(?:_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' %} {% 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 %} {% endfor %} {{ ns.unavailable }} - name: "Docker Containers Down List" unique_id: docker_containers_down_list icon: mdi:docker state: >- {% set ns = namespace(keys=[], down=[]) %} {% 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(?:_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' %} {% 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'] %} {% set ns.down = ns.down + [key] %} {% endif %} {% endfor %} {{ ns.down | sort | join(', ') if (ns.down | count > 0) else 'none' }} - name: "Docker Containers Down Count" unique_id: docker_containers_down_count icon: mdi:counter state: >- {% set down_list = states('sensor.docker_containers_down_list') %} {% set normalized = down_list | lower %} {% if normalized in ['unknown', 'unavailable', 'none', ''] %} 0 {% else %} {{ down_list.split(',') | map('trim') | reject('equalto', '') | list | count }} {% endif %} - name: "Docker Stacks Down List" unique_id: docker_stacks_down_list icon: mdi:package-down state: >- {% set ns = namespace(down=[]) %} {% for item in states.binary_sensor %} {% if item.entity_id is search('^binary_sensor\\..*_stack_status$') %} {% set st = item.state | lower %} {% if st in ['off', 'unknown', 'unavailable'] %} {% set stack = item.entity_id | replace('binary_sensor.', '') | regex_replace('_stack_status$', '') %} {% set ns.down = ns.down + [stack] %} {% endif %} {% endif %} {% endfor %} {{ ns.down | sort | join(', ') if (ns.down | count > 0) else 'none' }} - name: "Docker Stacks Down Count" unique_id: docker_stacks_down_count icon: mdi:counter state: >- {% set down_list = states('sensor.docker_stacks_down_list') %} {% if down_list in ['unknown', 'unavailable', 'none', ''] %} 0 {% else %} {{ down_list.split(',') | map('trim') | reject('equalto', '') | list | count }} {% endif %} - binary_sensor: - name: "Docker Container Telemetry Degraded" unique_id: docker_container_telemetry_degraded device_class: problem icon: mdi:lan-disconnect state: >- {% set total = states('sensor.docker_monitored_container_count') | int(0) %} {% set unavailable = states('sensor.docker_monitored_unavailable_count') | int(0) %} {% set threshold = [3, ((total * 0.6) | round(0, 'ceil') | int(0))] | max %} {{ total > 0 and unavailable >= threshold }} - name: "Docker Container Alerts Snoozed" unique_id: docker_container_alerts_snoozed device_class: problem icon: mdi:bell-sleep state: >- {% set stamp = states('input_datetime.docker_container_alerts_snooze_until') %} {% 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: docker_container_repairs_sync: alias: Docker Container Repairs Sync mode: parallel fields: entity_id: description: Changed Portainer entity (`switch.*_container` or `binary_sensor.*_status`) example: "switch.rc_price_checker_container" operation: description: "Sync operation: create or clear" example: "create" 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', '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(?:_2)?$', '') }} {% elif src_entity.startswith('switch.') %} {{ 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" 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 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: - conditions: "{{ op == 'create' }}" sequence: - choose: - conditions: "{{ wait_minutes > 0 }}" sequence: - delay: minutes: "{{ wait_minutes }}" - variables: 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 value_template: >- {{ effective_state in down_states and not (telemetry_degraded and effective_state in ['unknown', 'unavailable']) }} - condition: state entity_id: binary_sensor.docker_container_alerts_snoozed state: "off" - service: repairs.create data: issue_id: "{{ issue_id }}" title: "Container offline: {{ container_name }}" description: >- {{ container_name }} has been {{ effective_state }} for over 5 minutes. Effective entity: {{ effective_entity }}. severity: warning persistent: true - 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: >- {% 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 }}" - service: repairs.remove continue_on_error: true data: issue_id: "{{ issue_id }}" - service: repairs.remove continue_on_error: true data: 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 mode: parallel fields: entity_id: description: Changed Portainer stack status entity (`binary_sensor.*_stack_status`) example: "binary_sensor.vcloudinfo_stack_status" operation: description: "Sync operation: create or clear" example: "create" 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 }}" issue_id: "docker_stack_{{ stack_key }}_offline" - condition: template value_template: "{{ src_entity.startswith('binary_sensor.') and src_entity.endswith('_stack_status') }}" - condition: template value_template: "{{ tracked_stack and op in ['create', 'clear'] }}" - choose: - conditions: "{{ op == 'create' }}" sequence: - choose: - conditions: "{{ wait_minutes > 0 }}" sequence: - delay: minutes: "{{ wait_minutes }}" - variables: effective_state: "{{ states(src_entity) | lower }}" stack_name: "{{ state_attr(src_entity, 'friendly_name') | default(stack_key, true) }}" - condition: template value_template: "{{ effective_state in down_states }}" - service: repairs.create data: issue_id: "{{ issue_id }}" title: "Stack offline: {{ stack_name }}" description: >- {{ stack_name }} has been {{ effective_state }} for over 2 minutes. Effective entity: {{ src_entity }}. severity: warning persistent: true - 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: effective_state: "{{ states(src_entity) | lower }}" stack_name: "{{ state_attr(src_entity, 'friendly_name') | default(stack_key, true) }}" - condition: template value_template: "{{ effective_state not in down_states }}" - service: repairs.remove continue_on_error: true data: issue_id: "{{ issue_id }}" - 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" id: apt_update_report_docker_hosts description: "Receive docker host APT results and update helpers/logbook." mode: queued trigger: - platform: webhook webhook_id: !secret apt_webhook_docker_10 id: docker_10 allowed_methods: - POST local_only: true - platform: webhook webhook_id: !secret apt_webhook_docker_14 id: docker_14 allowed_methods: - POST local_only: true - platform: webhook webhook_id: !secret apt_webhook_docker_17 id: docker_17 allowed_methods: - POST local_only: true - platform: webhook webhook_id: !secret apt_webhook_docker_69 id: docker_69 allowed_methods: - POST local_only: true variables: host_id: "{{ trigger.id }}" payload: "{{ trigger.json | default({}) }}" success: "{{ payload.get('success', true) | bool }}" updated: "{{ payload.get('updated', false) | bool }}" reboot_required: "{{ payload.get('reboot_required', false) | bool }}" packages: "{{ payload.get('packages', 0) | int(0) }}" message: "{{ payload.get('message', '') | string }}" helpers: docker_10: last_check: input_datetime.apt_docker_10_last_check last_update: input_datetime.apt_docker_10_last_update last_result: input_text.apt_docker_10_last_result docker_14: last_check: input_datetime.apt_docker_14_last_check last_update: input_datetime.apt_docker_14_last_update last_result: input_text.apt_docker_14_last_result docker_17: last_check: input_datetime.apt_docker_17_last_check last_update: input_datetime.apt_docker_17_last_update last_result: input_text.apt_docker_17_last_result docker_69: last_check: input_datetime.apt_docker_69_last_check last_update: input_datetime.apt_docker_69_last_update last_result: input_text.apt_docker_69_last_result host_helpers: "{{ helpers[host_id] if host_id in helpers else none }}" result: >- {% if not success %} ERROR{% if (message | trim) != '' %}: {{ message | trim }}{% endif %} {% elif updated %} UPDATED {{ packages }} PKGS{% if reboot_required %} (REBOOT REQ){% endif %} {% elif reboot_required %} NO UPDATES (REBOOT REQ) {% else %} NO UPDATES {% endif %} log_message: >- {{ host_id }} updated {{ packages }} package{% if packages != 1 %}s{% endif %}{% if reboot_required %}; reboot required{% endif %}. condition: - condition: template value_template: "{{ host_helpers is not none }}" action: - service: input_datetime.set_datetime target: entity_id: "{{ host_helpers.last_check }}" data: datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}" - service: input_text.set_value target: entity_id: "{{ host_helpers.last_result }}" data: value: "{{ result }}" - choose: - conditions: "{{ success and updated }}" sequence: - service: input_datetime.set_datetime target: entity_id: "{{ host_helpers.last_update }}" data: datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}" - service: script.send_to_logbook data: topic: "APT" message: "{{ log_message }}" - 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: "{{ 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: 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: - 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: - 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: - 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 Repairs Reconcile" id: docker_repairs_reconcile description: "Reconcile stale container and stack Repairs issues on startup and every 55 minutes." mode: queued trigger: - platform: homeassistant event: start - platform: time_pattern minutes: "/55" action: - variables: 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 description: "Prompt Carlo to snooze container alerts for maintenance when more than 3 containers are down." mode: single trigger: - platform: numeric_state entity_id: sensor.docker_containers_down_count above: 3 condition: - condition: state entity_id: binary_sensor.docker_container_alerts_snoozed state: "off" action: - service: script.notify_engine_two_button data: title: "Docker Maintenance Check" value1: "{{ states('sensor.docker_containers_down_count') }} containers are currently down." value2: "Down: {{ states('sensor.docker_containers_down_list') }}" who: "carlo" group: "maintenance" title1: "Yes, snooze 1h" action1: "DOCKER_MAINTENANCE_SNOOZE_1H" icon1: "sfsymbols:clock" title2: "No, investigate" action2: "DOCKER_MAINTENANCE_NOT_MAINTENANCE" icon2: "sfsymbols:wrench.and.screwdriver" - service: script.send_to_logbook data: topic: "DOCKER" message: >- Maintenance prompt sent to Carlo ({{ states('sensor.docker_containers_down_count') }} down: {{ states('sensor.docker_containers_down_list') }}). - alias: "Docker Maintenance Snooze 1H" id: docker_maintenance_snooze_1h description: "Snooze dynamic container alerts for one hour from a notification action." mode: single trigger: - platform: event event_type: mobile_app_notification_action event_data: action: DOCKER_MAINTENANCE_SNOOZE_1H variables: snooze_until: "{{ (now() + timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S') }}" action: - service: input_datetime.set_datetime target: entity_id: input_datetime.docker_container_alerts_snooze_until data: datetime: "{{ snooze_until }}" - service: script.send_to_logbook data: topic: "DOCKER" message: "Container alerts snoozed for 1 hour (until {{ snooze_until }})." - alias: "Docker Maintenance Declined" id: docker_maintenance_declined description: "Log when maintenance snooze is declined from the dynamic container prompt." mode: single trigger: - platform: event event_type: mobile_app_notification_action event_data: action: DOCKER_MAINTENANCE_NOT_MAINTENANCE action: - service: script.send_to_logbook data: topic: "DOCKER" message: >- 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." mode: single trigger: - platform: time at: "03:15:00" condition: - condition: time weekday: - sun action: - service: button.press target: entity_id: - button.carlo_hass_prune_unused_images - button.docker17_prune_unused_images - button.docker69_prune_unused_images - button.docker2_prune_unused_images - service: script.send_to_logbook data: topic: "DOCKER" message: "Weekly scheduled prune triggered on docker_10/17/69/14."