Opening OneNote
+Joanna is handing this note to the OneNote app now.
+ + +If the app does not open automatically in a second or two, tap the OneNote button.
+diff --git a/.gitignore b/.gitignore index f8ef6c46..c2fb628e 100755 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ panel-notes docker_14 docker_69 .codex_tmp/ + diff --git a/config/.HA_VERSION b/config/.HA_VERSION index 7675d388..cb90feee 100755 --- a/config/.HA_VERSION +++ b/config/.HA_VERSION @@ -1 +1 @@ -2026.3.2 \ No newline at end of file +2026.4.1 \ No newline at end of file diff --git a/config/dashboards/infrastructure/partials/docker_containers_sections.yaml b/config/dashboards/infrastructure/partials/docker_containers_sections.yaml index 1cc86964..9ceed273 100644 --- a/config/dashboards/infrastructure/partials/docker_containers_sections.yaml +++ b/config/dashboards/infrastructure/partials/docker_containers_sections.yaml @@ -194,11 +194,13 @@ card_param: cards filter: include: - - group: group.docker_monitored_containers + - entity_id: "/^switch\\..*_container(_2)?$/" options: type: custom:button-card template: bearstone_infra_container_row icon: mdi:docker + exclude: + - state: unavailable + - state: unknown sort: method: name - diff --git a/config/dashboards/infrastructure/partials/home_sections.yaml b/config/dashboards/infrastructure/partials/home_sections.yaml index d9fcec1e..2c67b332 100644 --- a/config/dashboards/infrastructure/partials/home_sections.yaml +++ b/config/dashboards/infrastructure/partials/home_sections.yaml @@ -449,7 +449,7 @@ card_param: cards filter: include: - - group: group.docker_monitored_containers + - entity_id: "/^switch\\..*_container(_2)?$/" options: type: custom:button-card template: bearstone_infra_container_row @@ -636,4 +636,3 @@ name: Download - entity: sensor.speedtest_upload name: Upload - diff --git a/config/dashboards/infrastructure/templates/button_card_templates.yaml b/config/dashboards/infrastructure/templates/button_card_templates.yaml index 26c99747..48f936bf 100644 --- a/config/dashboards/infrastructure/templates/button_card_templates.yaml +++ b/config/dashboards/infrastructure/templates/button_card_templates.yaml @@ -614,13 +614,15 @@ bearstone_infra_container_row: } const switchEntity = key ? `switch.${key}_container` : ''; const switchEntityAlt = key ? `switch.${key}_container_2` : ''; - const monitored = states['group.docker_monitored_containers']?.attributes?.entity_id || []; + const monitored = states['sensor.docker_monitored_switch_inventory']?.attributes?.entity_id; const restartCandidates = key ? [ `button.${key}_restart_container`, `button.${key}_restart_container_2`, ] : []; const hasRestart = restartCandidates.some((candidate) => states[candidate]); - const isMonitored = monitored.includes(switchEntity) || monitored.includes(switchEntityAlt); + const isMonitored = Array.isArray(monitored) + ? monitored.includes(switchEntity) || monitored.includes(switchEntityAlt) + : (ent.startsWith('switch.') && (ent.endsWith('_container') || ent.endsWith('_container_2'))); return (hasRestart && isMonitored) ? 'block' : 'none'; ]]] diff --git a/config/group/sensors.yaml b/config/group/sensors.yaml deleted file mode 100755 index 92ef1b07..00000000 --- a/config/group/sensors.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# Sensors: -# entities: -# # - binary_sensor.aeotec_dsb04100_doorwindow_sensor_sensor_3_0 diff --git a/config/packages/README.md b/config/packages/README.md index d77f05d2..c1dbd832 100755 --- a/config/packages/README.md +++ b/config/packages/README.md @@ -53,13 +53,14 @@ Live collection of plug-and-play Home Assistant packages. Each YAML file in this | [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, plus event-based Joanna dispatch when reports include `### Available:` (24h cooldown via `mode: single` + delay, 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, ingest `/api/bearclaw/status` telemetry, and expose dispatch KPI sensors for Infrastructure dashboards. | `rest_command.bearclaw_*`, `sensor.bearclaw_status_telemetry`, `sensor.joanna_*`, `automation.bearclaw_*`, `script.send_to_logbook` | -| [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` | +| [bearclaw.yaml](bearclaw.yaml) | Joanna/BearClaw bridge automations that forward Telegram commands to codex_appliance, include LLM-first routing context for freeform text, relay replies back, ingest `/api/bearclaw/status` telemetry, and expose dispatch KPI sensors for Infrastructure dashboards. | `rest_command.bearclaw_*`, `sensor.bearclaw_status_telemetry`, `sensor.joanna_*`, `automation.bearclaw_*`, `script.send_to_logbook` | +| [telegram_bot.yaml](telegram_bot.yaml) | Telegram script wrappers used by BearClaw and other ops flows (UI config remains source of truth; wrappers work with polling or webhook transport). | `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` | | [water_delivery.yaml](water_delivery.yaml) | ReadyRefresh delivery date helper with night-before + garage door Alexa reminders, plus helper-change audit logging and Telegram confirmations. | `input_datetime.water_delivery_date`, `script.send_to_logbook`, `script.joanna_send_telegram`, `notify.alexa_media_garage` | | [vacation_mode.yaml](vacation_mode.yaml) | Auto-enable vacation mode after 24 hours away or no bed use, track sitter analytics/secure-house checks, and deliver Chromecast-first vacation briefings with a garage Alexa welcome. | `input_boolean.vacation_mode`, `input_boolean.house_sitter_present`, `sensor.vacation_house_sitter_*`, `group.garage_doors`, `lock.front_door`, `script.notify_engine`, `script.joanna_send_telegram` | | [maintenance_log.yaml](maintenance_log.yaml) | Joanna maintenance webhook ingest for water softener salt with idempotent event handling, Activity feed logging, and recorder-backed helper history for long-term graphing. | `automation.maintenance_log_joanna_webhook_ingest`, `input_number.water_softener_salt_total_added_lb`, `counter.water_softener_salt_event_count`, `sensor.water_softener_salt_days_since_last_add` | | [powerwall.yaml](powerwall.yaml) | Track Tesla Powerwall grid status, push live outage tracking to mobile targets, and shed loads automatically when off-grid (alerts include Activity feed + Repairs). | `binary_sensor.powerwall_grid_status`, `sensor.powerwall_*`, `script.notify_live_activity`, `repairs.create` | +| [tesla_model_y.yaml](tesla_model_y.yaml) | Remind the garage and parents to plug in the Model Y after low-battery arrivals and after 8 PM when it is home but not charging. | `sensor.spaceship_battery_level`, `switch.spaceship_charge`, `notify.alexa_media_garage`, `script.notify_engine` | | [vacuum.yaml](vacuum.yaml) | Dreame vacuum orchestration with room tracking, push alerts, Activity feed, Repairs issues on errors, and Alexa one-off room-clean switches. | `input_select.l10s_vacuum_phase`, `sensor.l10s_vacuum_error`, `repairs.create` | | [hass_agent_homepc.yaml](hass_agent_homepc.yaml) | Mirrors PC lock/unlock state from HASS.Agent to the office lamp for instant desk presence cues. | `sensor.carlo_homepc_carlo_homepc_sessionstate`, `switch.office_lamp_switch` | | [sleepiq.yaml](sleepiq.yaml) | Sleep Number presence/snore automations; Overview Health consumes direct SleepIQ integration entities for scores, vitals, pressure, and bed controls. | `sensor.sleepnumber_carlo_carlo_sleep_score`, `sensor.sleepnumber_carlo_stacey_sleep_score`, `number.sleepnumber_carlo_carlo_firmness`, `select.sleepnumber_carlo_foundation_preset_right` | diff --git a/config/packages/august.yaml b/config/packages/august.yaml index 99763dc1..130c6ed4 100755 --- a/config/packages/august.yaml +++ b/config/packages/august.yaml @@ -38,7 +38,7 @@ automation: trigger: - platform: numeric_state entity_id: sensor.front_door_battery - below: 25 + below: 20 action: - service: script.notify_engine @@ -51,24 +51,3 @@ automation: topic: "BATTERY" message: "August Door lock battery low: {{ states('sensor.front_door_battery') }}%" - # - alias: 'FrontDoor Bell Camera on Front Door Lock' - # id: Doorbell_camera_front_door - # mode: single - # trigger: - # - platform: state - # entity_id: lock.front_door - # to: 'unlocked' - # action: - # - service: media_player.play_media - # target: - # entity_id: media_player.kitchen - # data: - # media_content_id: 'show front doorbell camera' - # media_content_type: custom - # - delay: '00:20:00' - # - service: media_player.play_media - # target: - # entity_id: media_player.kitchen - # data: - # media_content_id: 'hide front doorbell camera' - # media_content_type: custom diff --git a/config/packages/bearclaw.yaml b/config/packages/bearclaw.yaml index 0875d445..03ef3578 100644 --- a/config/packages/bearclaw.yaml +++ b/config/packages/bearclaw.yaml @@ -14,6 +14,8 @@ # Notes: Inbound Telegram handling enforces user_id + chat_id allowlists from secrets CSV values. # Notes: Reply webhook writes JOANNA activity entries to logbook for traceability. # Notes: Status telemetry polling expects !secret bearclaw_status_url (token header stays !secret bearclaw_token). +# Notes: Telegram freeform input now includes LLM-first routing context to improve intent understanding before entity lookups. +# Notes: Command payload supports async_only for automation-first queueing when immediate inline handling is not required. # Notes: Blog: https://www.vcloudinfo.com/2026/03/joanna-dispatch-telemetry-home-assistant-infrastructure-dashboard/ ###################################################################### @@ -21,6 +23,7 @@ rest_command: bearclaw_command: url: !secret bearclaw_command_url method: post + timeout: 30 content_type: application/json headers: x-codex-token: !secret bearclaw_token @@ -30,7 +33,8 @@ rest_command: "user": {{ user | default('carlo') | tojson }}, "source": {{ source | default('home_assistant') | tojson }}, "context": {{ context | default(none) | tojson }}, - "callback": {{ callback | default(none) | tojson }} + "callback": {{ callback | default(none) | tojson }}, + "async_only": {{ async_only | default(false) | tojson }} } bearclaw_ingest: @@ -341,7 +345,7 @@ automation: - id: bearclaw_telegram_text_no_slash_needed alias: BearClaw Telegram Text No Slash Needed - description: Treats plain Telegram text as BearClaw command input. + description: Treats plain Telegram text as BearClaw command input and forwards LLM-first routing context. mode: queued trigger: - platform: event @@ -363,12 +367,36 @@ automation: action: - variables: plain_text: "{{ trigger.event.data.text | default('') | trim }}" + plain_text_lower: "{{ plain_text | lower }}" from_user: "{{ (trigger.event.data.from_first | default('carlo')) | lower }}" + command_like_request: >- + {% set action_verbs = ['disable', 'enable', 'turn off', 'turn on', 'stop', 'start', 'restart', 'review', 'fix', 'change', 'update', 'set', 'open', 'close'] %} + {{ action_verbs | select('in', plain_text_lower) | list | count > 0 }} + status_like_request: >- + {{ (plain_text_lower.startswith('is ') + or plain_text_lower.startswith('are ') + or plain_text_lower.startswith('what is ') + or plain_text_lower.startswith("what's ") + or ' status' in plain_text_lower + or plain_text_lower.startswith('status ')) + and not command_like_request }} + llm_route_hint: >- + {% if command_like_request %} + llm_first_action + {% elif status_like_request %} + llm_first_status + {% else %} + llm_first_general + {% endif %} + llm_context: >- + telegram_freeform route={{ llm_route_hint | trim }}. + Prefer LLM intent interpretation and clarification for action or automation requests before returning entity status. - service: rest_command.bearclaw_command data: text: "{{ plain_text }}" user: "{{ from_user }}" source: telegram_text + context: "{{ llm_context | trim }}" - id: bearclaw_reply_webhook alias: BearClaw Reply Webhook @@ -383,6 +411,15 @@ automation: action: - variables: message: "{{ trigger.json.message | default('Joanna: empty reply') }}" + telegram_message: "{{ trigger.json.telegram_message | default(message, true) }}" + telegram_parse_mode: >- + {% set raw = trigger.json.telegram_parse_mode | default('plain_text', true) | string | lower | trim %} + {% if raw in ['html', 'plain_text'] %} + {{ raw }} + {% else %} + plain_text + {% endif %} + telegram_disable_preview: "{{ trigger.json.disable_web_page_preview | default(true, true) }}" level: "{{ trigger.json.level | default('active') | lower }}" inline_keyboard_payload: >- {% set kb = trigger.json.inline_keyboard if trigger.json.inline_keyboard is defined else none %} @@ -408,14 +445,16 @@ automation: - service: telegram_bot.send_message data: chat_id: !secret telegram_allowed_chat_id_carlo - message: "{{ message }}" - parse_mode: plain_text - disable_web_page_preview: true + message: "{{ telegram_message }}" + parse_mode: "{{ telegram_parse_mode }}" + disable_web_page_preview: "{{ telegram_disable_preview }}" inline_keyboard: "{{ inline_keyboard_payload }}" default: - service: script.joanna_send_telegram data: - message: "{{ message }}" + message: "{{ telegram_message }}" + parse_mode: "{{ telegram_parse_mode }}" + disable_web_page_preview: "{{ telegram_disable_preview }}" - service: script.send_to_logbook data: topic: JOANNA diff --git a/config/packages/docker_infrastructure.yaml b/config/packages/docker_infrastructure.yaml index 3a043f53..b86abd8a 100644 --- a/config/packages/docker_infrastructure.yaml +++ b/config/packages/docker_infrastructure.yaml @@ -4,13 +4,14 @@ # Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig # ------------------------------------------------------------------- # Docker Infrastructure - Host patching and container alerts +# Related Issue: 1632, 1584 # 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: Weekly Joanna reconcile checks discovered container switches vs configured group members. # Notes: Includes Portainer stack status repairs, 20-minute Joanna dispatch for persistent container outages, and scheduled image prune. ###################################################################### @@ -66,57 +67,120 @@ input_text: name: "docker_69 APT last result" max: 255 -group: - docker_monitored_containers: +switch: + - platform: group name: Docker Monitored Containers + unique_id: docker_monitored_containers entities: + - switch.3a446393a8c5_dozzle_container + - switch.5b3503372936_wordpress_db_container + - switch.7a83770eb321_rc_price_checker_container + - switch.91aca40203c0_portainer_agent_container - switch.cloudflared_kch_container + - switch.cloudflared_kch_container_2 - switch.cloudflared_wp_container + - switch.cloudflared_wp_container_2 - switch.codex_appliance_container + - switch.codex_appliance_container_2 - switch.college_budget_app_container + - switch.college_budget_app_container_2 - switch.cruise_tracker_container + - switch.cruise_tracker_container_2 - switch.dashy_container + - switch.dashy_container_2 - switch.docker_socket_proxy_container - - switch.dozzle_container + - switch.docker_socket_proxy_container_2 - switch.dozzle_agent_10_container + - switch.dozzle_agent_10_container_2 - switch.dozzle_agent_14_container + - switch.dozzle_agent_14_container_2 - switch.dozzle_agent_17_container + - switch.dozzle_agent_17_container_2 - switch.dozzle_agent_69_container + - switch.dozzle_agent_69_container_2 + - switch.dozzle_container + - switch.dozzle_container_2 - switch.duplicati_container + - switch.duplicati_container_2 - switch.esphome_container + - switch.esphome_container_2 - switch.fed437a0f191_tugtainer_socket_proxy_container - switch.foodie_tracker_container + - switch.foodie_tracker_container_2 - switch.frigate_container + - switch.frigate_container_2 - switch.games_hub_container + - switch.games_hub_container_2 - switch.home_assistant_container + - switch.home_assistant_container_2 - switch.imposter_container + - switch.imposter_container_2 - switch.infra_info_container + - switch.infra_info_container_2 - switch.kingcrafthomes_container + - switch.kingcrafthomes_container_2 - switch.lmediaservices_container - - switch.mariadb_container + - switch.lmediaservices_container_2 - switch.mariadb_backup_container + - switch.mariadb_backup_container_2 + - switch.mariadb_container + - switch.mariadb_container_2 - switch.matter_server_container + - switch.matter_server_container_2 - switch.mqtt_container + - switch.mqtt_container_2 - switch.nebula_sync_container + - switch.nebula_sync_container_2 + - switch.onenote_indexer_container + - switch.onenote_indexer_container_2 + - switch.optimistic_mclaren_container - switch.panel_notes_container + - switch.panel_notes_container_2 - switch.pihole_container + - switch.pihole_container_2 - switch.pihole_secondary_container + - switch.pihole_secondary_container_2 - switch.poker_tracker_container - - switch.portainer_container + - switch.poker_tracker_container_2 + - switch.portainer_agent_69_container - switch.portainer_agent_container + - switch.portainer_agent_container_2 + - switch.portainer_container + - switch.portainer_container_2 - switch.postgres_webhooks_engine_container + - switch.postgres_webhooks_engine_container_2 - switch.rc_price_checker_container + - switch.rc_price_checker_container_2 - switch.redis_webhooks_engine_container + - switch.redis_webhooks_engine_container_2 - switch.rvtools_ppt_web_container + - switch.rvtools_ppt_web_container_2 + - switch.steelesharing_home_container - switch.tapple_container - - switch.tugtainer_container + - switch.tapple_container_2 + - switch.tugtainer_agent_14_container + - switch.tugtainer_agent_17_container + - switch.tugtainer_agent_69_container - switch.tugtainer_agent_container + - switch.tugtainer_agent_container_2 + - switch.tugtainer_container + - switch.tugtainer_container_2 + - switch.tugtainer_socket_proxy_14_container + - switch.tugtainer_socket_proxy_17_container + - switch.tugtainer_socket_proxy_69_container - switch.tugtainer_socket_proxy_container + - switch.tugtainer_socket_proxy_container_2 - switch.unifi_container + - switch.unifi_container_2 - switch.webhooks_engine_container + - switch.webhooks_engine_container_2 - switch.wordpress_db_container + - switch.wordpress_db_container_2 - switch.wordpress_wp_container + - switch.wordpress_wp_container_2 - switch.wyze_bridge_container + - switch.wyze_bridge_container_2 + template: - sensor: @@ -202,18 +266,133 @@ template: {% endif %} - sensor: + - name: "Docker Monitored Switch Inventory" + unique_id: docker_monitored_switch_inventory + icon: mdi:docker + state: >- + {% set ns = namespace(items=[]) %} + {% for item in states.switch %} + {% set ent = item.entity_id %} + {% if ent is search('^switch\\..*_container(?:_2)?$') %} + {% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %} + {% set has_companion = (expand('binary_sensor.' ~ key ~ '_status') | count > 0 + and states('binary_sensor.' ~ key ~ '_status') | lower not in ['unknown', 'unavailable', '']) + or (expand('binary_sensor.' ~ key ~ '_status_2') | count > 0 + and states('binary_sensor.' ~ key ~ '_status_2') | lower not in ['unknown', 'unavailable', '']) + or (expand('sensor.' ~ key ~ '_state') | count > 0 + and states('sensor.' ~ key ~ '_state') | lower not in ['unknown', 'unavailable', '']) + or (expand('sensor.' ~ key ~ '_state_2') | count > 0 + and states('sensor.' ~ key ~ '_state_2') | lower not in ['unknown', 'unavailable', '']) %} + {% set switch_state = states(ent) | lower %} + {% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %} + {% if ent not in ns.items %} + {% set ns.items = ns.items + [ent] %} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {{ ns.items | sort | count }} + attributes: + entity_id: >- + {% set ns = namespace(items=[]) %} + {% for item in states.switch %} + {% set ent = item.entity_id %} + {% if ent is search('^switch\\..*_container(?:_2)?$') %} + {% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %} + {% set has_companion = (expand('binary_sensor.' ~ key ~ '_status') | count > 0 + and states('binary_sensor.' ~ key ~ '_status') | lower not in ['unknown', 'unavailable', '']) + or (expand('binary_sensor.' ~ key ~ '_status_2') | count > 0 + and states('binary_sensor.' ~ key ~ '_status_2') | lower not in ['unknown', 'unavailable', '']) + or (expand('sensor.' ~ key ~ '_state') | count > 0 + and states('sensor.' ~ key ~ '_state') | lower not in ['unknown', 'unavailable', '']) + or (expand('sensor.' ~ key ~ '_state_2') | count > 0 + and states('sensor.' ~ key ~ '_state_2') | lower not in ['unknown', 'unavailable', '']) %} + {% set switch_state = states(ent) | lower %} + {% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %} + {% if ent not in ns.items %} + {% set ns.items = ns.items + [ent] %} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {{ ns.items | sort }} + configured_group_members: >- + {{ state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list | sort }} + missing_from_group: >- + {% set discovered_ns = namespace(items=[]) %} + {% for item in states.switch %} + {% set ent = item.entity_id %} + {% if ent is search('^switch\\..*_container(?:_2)?$') %} + {% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %} + {% set has_companion = (expand('binary_sensor.' ~ key ~ '_status') | count > 0 + and states('binary_sensor.' ~ key ~ '_status') | lower not in ['unknown', 'unavailable', '']) + or (expand('binary_sensor.' ~ key ~ '_status_2') | count > 0 + and states('binary_sensor.' ~ key ~ '_status_2') | lower not in ['unknown', 'unavailable', '']) + or (expand('sensor.' ~ key ~ '_state') | count > 0 + and states('sensor.' ~ key ~ '_state') | lower not in ['unknown', 'unavailable', '']) + or (expand('sensor.' ~ key ~ '_state_2') | count > 0 + and states('sensor.' ~ key ~ '_state_2') | lower not in ['unknown', 'unavailable', '']) %} + {% set switch_state = states(ent) | lower %} + {% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %} + {% if ent not in discovered_ns.items %} + {% set discovered_ns.items = discovered_ns.items + [ent] %} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {% set discovered = discovered_ns.items | list %} + {% set configured = state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list %} + {% set ns = namespace(items=[]) %} + {% for ent in discovered %} + {% if ent not in configured %} + {% set ns.items = ns.items + [ent] %} + {% endif %} + {% endfor %} + {{ ns.items | sort }} + stale_group_members: >- + {% set discovered_ns = namespace(items=[]) %} + {% for item in states.switch %} + {% set ent = item.entity_id %} + {% if ent is search('^switch\\..*_container(?:_2)?$') %} + {% set key = ent | replace('switch.', '') | regex_replace('_container(?:_2)?$', '') %} + {% set has_companion = (expand('binary_sensor.' ~ key ~ '_status') | count > 0 + and states('binary_sensor.' ~ key ~ '_status') | lower not in ['unknown', 'unavailable', '']) + or (expand('binary_sensor.' ~ key ~ '_status_2') | count > 0 + and states('binary_sensor.' ~ key ~ '_status_2') | lower not in ['unknown', 'unavailable', '']) + or (expand('sensor.' ~ key ~ '_state') | count > 0 + and states('sensor.' ~ key ~ '_state') | lower not in ['unknown', 'unavailable', '']) + or (expand('sensor.' ~ key ~ '_state_2') | count > 0 + and states('sensor.' ~ key ~ '_state_2') | lower not in ['unknown', 'unavailable', '']) %} + {% set switch_state = states(ent) | lower %} + {% if has_companion or switch_state not in ['unknown', 'unavailable', ''] %} + {% if ent not in discovered_ns.items %} + {% set discovered_ns.items = discovered_ns.items + [ent] %} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {% set discovered = discovered_ns.items | list %} + {% set configured = state_attr('switch.docker_monitored_containers', 'entity_id') | default([], true) | list %} + {% set ns = namespace(items=[]) %} + {% for ent in configured %} + {% if ent not in discovered %} + {% set ns.items = ns.items + [ent] %} + {% endif %} + {% endfor %} + {{ ns.items | sort }} + - 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 }} + {{ state_attr('sensor.docker_monitored_switch_inventory', '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) %} + {% set monitored = state_attr('sensor.docker_monitored_switch_inventory', '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 %} @@ -257,7 +436,7 @@ template: icon: mdi:docker state: >- {% set ns = namespace(keys=[], down=[]) %} - {% set monitored = state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) | list %} + {% set monitored = state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list %} {% 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)?$', '') %} @@ -301,7 +480,7 @@ template: attributes: down_containers: >- {% set ns = namespace(keys=[], down=[]) %} - {% set monitored = state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) | list %} + {% set monitored = state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list %} {% 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)?$', '') %} @@ -437,7 +616,7 @@ script: 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) }}" + monitored_switches: "{{ state_attr('sensor.docker_monitored_switch_inventory', '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 %} @@ -567,7 +746,13 @@ script: effective_state_20m={{ persistent_effective_state }} request: >- Troubleshoot and resolve the persistent Docker container outage if possible. - Use Duplicati and the related host/container telemetry to verify recovery. + Reply with explicit status fields: + resolved=true/false, + root_cause, + action_taken, + verification (entity plus observed state), + next_action_required=true/false. + Use Duplicati and related host/container telemetry to verify recovery. - conditions: "{{ op == 'clear' }}" sequence: - variables: @@ -798,7 +983,7 @@ automation: entity_id: "{{ trigger.event.data.entity_id | default('') }}" old_state: "{{ (trigger.event.data.old_state.state if trigger.event.data.old_state is not none else '') | lower }}" new_state: "{{ (trigger.event.data.new_state.state if trigger.event.data.new_state is not none else '') | lower }}" - monitored_switches: "{{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) | list }}" + monitored_switches: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list }}" is_monitored_container_event: >- {% set ent = entity_id %} {% if ent.startswith('switch.') and (ent.endswith('_container') or ent.endswith('_container_2')) %} @@ -888,7 +1073,7 @@ automation: minutes: "/55" action: - variables: - monitored_switches: "{{ state_attr('group.docker_monitored_containers', 'entity_id') | default([], true) | list }}" + monitored_switches: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list }}" - repeat: for_each: "{{ monitored_switches }}" sequence: @@ -1009,11 +1194,64 @@ automation: - service: homeassistant.update_entity target: entity_id: + - sensor.docker_monitored_switch_inventory - sensor.docker_monitored_unavailable_count - sensor.docker_containers_down_list - sensor.docker_containers_down_count - binary_sensor.docker_container_telemetry_degraded + - alias: "Docker Group Reconcile - Weekly Joanna Review" + id: docker_group_reconcile_weekly_joanna_review + description: "Weekly reconciliation of discovered Docker container entities vs configured group members." + mode: single + trigger: + - platform: time + at: "08:45:00" + condition: + - condition: time + weekday: + - sun + action: + - variables: + discovered_members: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'entity_id') | default([], true) | list }}" + configured_members: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'configured_group_members') | default([], true) | list }}" + missing_members: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'missing_from_group') | default([], true) | list }}" + stale_members: "{{ state_attr('sensor.docker_monitored_switch_inventory', 'stale_group_members') | default([], true) | list }}" + - service: script.send_to_logbook + data: + topic: "DOCKER" + message: >- + Weekly reconcile check: discovered={{ discovered_members | count }}, + configured={{ configured_members | count }}, + missing={{ missing_members | count }}, + stale={{ stale_members | count }}. + - choose: + - conditions: + - condition: template + value_template: "{{ (missing_members | count > 0) or (stale_members | count > 0) }}" + sequence: + - service: script.joanna_dispatch + data: + trigger_context: >- + HA automation docker_group_reconcile_weekly_joanna_review + (Docker Group Reconcile - Weekly Joanna Review) + source: "home_assistant_automation.docker_group_reconcile_weekly_joanna_review" + summary: >- + Docker group membership drift detected (missing={{ missing_members | count }}, + stale={{ stale_members | count }}). + entity_ids: + - sensor.docker_monitored_switch_inventory + - switch.docker_monitored_containers + diagnostics: >- + discovered={{ discovered_members | join(', ') if (discovered_members | count > 0) else 'none' }}; + configured={{ configured_members | join(', ') if (configured_members | count > 0) else 'none' }}; + missing={{ missing_members | join(', ') if (missing_members | count > 0) else 'none' }}; + stale={{ stale_members | join(', ') if (stale_members | count > 0) else 'none' }}. + request: >- + Reconcile Docker monitored group members in + config/packages/docker_infrastructure.yaml so configured monitoring + matches currently discovered container entities. + - alias: "Docker Weekly Prune Unused Images" id: docker_weekly_prune_unused_images description: "Run weekly unguarded prune actions across Docker hosts." diff --git a/config/packages/garadget.yaml b/config/packages/garadget.yaml index ffb19884..c323370c 100755 --- a/config/packages/garadget.yaml +++ b/config/packages/garadget.yaml @@ -16,6 +16,7 @@ mqtt: cover: - name: "Large Garage Door" + unique_id: garadget_large_garage_door_cover device_class: 'garage' command_topic: "garadget/GLarge/command" state_topic: "garadget/GLarge/status" @@ -30,6 +31,7 @@ mqtt: payload_stop: "stop" - name: "Small Garage Door" + unique_id: garadget_small_garage_door_cover device_class: 'garage' command_topic: "garadget/GSmall/command" state_topic: "garadget/GSmall/status" @@ -45,19 +47,23 @@ mqtt: sensor: - name: "Large Garage Door Since" + unique_id: garadget_large_garage_door_since state_topic: "garadget/GLarge/status" value_template: '{{ value_json.time }}' - name: "Large Garage Door Brightness" + unique_id: garadget_large_garage_door_brightness state_topic: "garadget/GLarge/status" unit_of_measurement: '%' value_template: '{{ value_json.bright }}' - name: "Small Garage Door Since" + unique_id: garadget_small_garage_door_since state_topic: "garadget/GSmall/status" value_template: '{{ value_json.time }}' - name: "Small Garage Door Brightness" + unique_id: garadget_small_garage_door_brightness state_topic: "garadget/GSmall/status" unit_of_measurement: '%' value_template: '{{ value_json.bright }}' @@ -241,43 +247,6 @@ automation: topic: "garadget/GLarge/command" payload: "get-status" - - alias: 'Garage Door State Change' - id: afec0987-edb1-4341-a524-a00ae4df9fb7 - mode: restart - trigger: - - platform: state - entity_id: - - cover.large_garage_door - - cover.small_garage_door - from: 'open' - to: 'closed' - for: '00:02:00' - - - platform: state - entity_id: - - cover.large_garage_door - - cover.small_garage_door - from: 'closed' - to: 'open' - for: '00:10:00' - - action: - - choose: - - conditions: "{{ is_state('cover.large_garage_door','closed') }}" - sequence: - - service: script.speech_engine - data: - DoorClosed: "The {{ trigger.entity_id.split('.')[1]|replace('_', ' ') }} is now {{ (trigger.to_state.state)|replace('_', ' ') }}." - call_garage_check: 1 - - delay: "00:10:00" - - default: - - service: script.speech_engine - data: - DoorClosed: "The {{ trigger.entity_id.split('.')[1]|replace('_', ' ') }} is now {{ (trigger.to_state.state)|replace('_', ' ') }}." - call_garage_check: 1 - - ################################### ## Garadget Wind Door Checks - [Garadget](https://amzn.to/2jQLpVQ) ################################### @@ -330,13 +299,8 @@ automation: trigger: - platform: time_pattern - minutes: '/45' - - platform: state - entity_id: - - cover.large_garage_door - - cover.small_garage_door - to: 'open' - for: "00:01:00" + minutes: '/30' + - platform: state entity_id: group.family to: not_home @@ -392,46 +356,3 @@ automation: - service: script.speech_engine data: value1: "Check the garage doors. The Small garage is {{ states('cover.small_garage_door')}} and the large garage is {{ states('cover.large_garage_door')}} [Always mention the specific garage door that is currently open and remind us to close it for the night]" - - # - alias: 'Garage Camera on Alexa Shows' - # id: 4373df2a-77f2-4e19-be7c-46c7b27ca583 - # mode: single - # trigger: - # - platform: state - # entity_id: - # - cover.large_garage_door - # - cover.small_garage_door - # from: 'closed' - # to: 'open' - # for: "00:00:15" - - # - platform: state - # entity_id: binary_sensor.mcu1_gpio12 #interior Garage Doors - # from: 'off' - # to: 'on' - # for: "00:00:05" - - # - platform: state - # entity_id: - # - person.carlo - # - person.stacey - # - person.paige - # - person.justin - # to: 'not_home' - # from: 'home' - - # action: - # - service: media_player.play_media - # target: - # entity_id: media_player.kitchen - # data: - # media_content_id: 'show garage camera from home assistant' - # media_content_type: custom - # - delay: '00:20:00' - # - service: media_player.play_media - # target: - # entity_id: media_player.kitchen - # data: - # media_content_id: 'hide garage camera' - # media_content_type: custom - diff --git a/config/packages/github_watched_repo_scout.yaml b/config/packages/github_watched_repo_scout.yaml new file mode 100644 index 00000000..77cb2666 --- /dev/null +++ b/config/packages/github_watched_repo_scout.yaml @@ -0,0 +1,45 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# GitHub Watched Repo Scout - Nightly Joanna review of watched repos +# Schedules Joanna to review unread watched-repo notifications for HA ideas. +# ------------------------------------------------------------------- +# Notes: Joanna native GitHub scout logic runs in docker_17/codex_appliance. +# Notes: Successfully processed watched-repo notifications are marked read by Joanna. +###################################################################### + +automation: + - id: github_watched_repo_scout_nightly + alias: GitHub Watched Repo Scout - Nightly Joanna Review + description: Ask Joanna nightly to review unread watched-repo GitHub notifications for Home Assistant ideas. + mode: single + trigger: + - platform: time + at: "03:15:00" + variables: + trigger_context: "HA automation github_watched_repo_scout_nightly (GitHub Watched Repo Scout - Nightly Joanna Review)" + action: + - service: script.send_to_logbook + data: + topic: "GITHUB" + message: "Requesting Joanna nightly watched-repo scout review." + - service: script.joanna_dispatch + data: + trigger_context: "{{ trigger_context }}" + source: "home_assistant_automation.github_watched_repo_scout_nightly" + summary: "Nightly GitHub watched-repo scout review for Home Assistant ideas" + diagnostics: >- + schedule=03:15:00, + inbox_scope=unread, + repo_scope=watched, + create_issues=true, + mark_read_on_success=true + request: >- + Review unread GitHub notifications from watched repositories only. + Look for interesting Home Assistant configuration ideas we could apply in + our own repo, send a concise nightly recap, create or refresh one + GitHub issue per strong candidate in CCOSTAN/Home-AssistantConfig, and + mark each watched-repo notification read only after it has been + processed successfully. Leave failed items unread for the next run. diff --git a/config/packages/llmvision.yaml b/config/packages/llmvision.yaml index 9bcab110..ae1db883 100644 --- a/config/packages/llmvision.yaml +++ b/config/packages/llmvision.yaml @@ -7,7 +7,7 @@ # Trigger with input_button.llmvision_garbage_check, input_button.llmvision_front_door_package_check, or front door person activity to update vision-backed package sensors. # ------------------------------------------------------------------- # Notes: LLMVision analyzes camera.garagecam; expects strict "on"/"off" output. -# Notes: Front-door package detection runs 3 minutes after `sensor.frontdoorbell_person_active_count_2` goes above 0. +# Notes: Front-door package detection waits 3 minutes after person activity or 20 seconds after the front door relocks, then analyzes a short live stream for better package accuracy. # Docs: https://llmvision.gitbook.io/getting-started/usage/image-analyzer ###################################################################### @@ -129,30 +129,79 @@ automation: - platform: numeric_state entity_id: sensor.frontdoorbell_person_active_count_2 above: 0 - for: "00:03:00" id: person_count + - platform: state + entity_id: lock.front_door + from: unlocked + to: locked + id: front_door_locked - platform: state entity_id: input_button.llmvision_front_door_package_check id: manual_check variables: + trigger_source: >- + {% if trigger.id == 'manual_check' %} + manual button + {% elif trigger.id == 'front_door_locked' %} + front door relock + {% else %} + front door person activity + {% endif %} prompt_text: >- - Examine the front door camera image for delivery packages. Focus only on boxes, padded envelopes, - or shopping bags left on the porch or by the front door. If one or more packages are clearly visible, - respond exactly: on. If no packages are clearly visible, respond exactly: off. No other words. + Examine these front door camera frames for delivery packages. Focus on the porch and doorstep area + near the wall and doormat in the lower-right part of the image. Treat cardboard boxes, padded mailers, + poly bags, and shopping bags as packages. Ignore the street, cars, landscaping, and anything not resting + on the porch or doorstep. If any package is clearly visible, respond exactly: on. If no package is + clearly visible, respond exactly: off. No other words. action: - - service: llmvision.data_analyzer + - choose: + - conditions: + - condition: template + value_template: "{{ trigger.id == 'person_count' }}" + sequence: + - delay: "00:03:00" + - conditions: + - condition: template + value_template: "{{ trigger.id == 'front_door_locked' }}" + sequence: + - delay: "00:00:20" + - service: llmvision.stream_analyzer response_variable: llmvision_result data: provider: !secret llmvision_provider_entry - model: gpt-4.1-nano + model: gpt-4.1-mini message: "{{ prompt_text }}" - sensor_entity: input_boolean.front_door_packages_present image_entity: - camera.frontdoorbell + duration: 6 + max_frames: 5 include_filename: false - target_width: 1280 + target_width: 1920 max_tokens: 16 expose_images: true + - variables: + normalized_response: "{{ llmvision_result.response_text | default('') | trim | lower }}" + - choose: + - conditions: + - condition: template + value_template: >- + {{ normalized_response in ['on', 'yes', 'true'] + or normalized_response.startswith('on') + or normalized_response.startswith('yes') }} + sequence: + - service: input_boolean.turn_on + target: + entity_id: input_boolean.front_door_packages_present + - conditions: + - condition: template + value_template: >- + {{ normalized_response in ['off', 'no', 'false'] + or normalized_response.startswith('off') + or normalized_response.startswith('no') }} + sequence: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.front_door_packages_present - service: input_text.set_value target: entity_id: input_text.llmvision_front_door_last_response @@ -181,7 +230,7 @@ automation: data: topic: PACKAGES message: >- - Front door package vision check ran via {{ 'manual button' if trigger.id == 'manual_check' else 'front door person activity' }} + Front door package vision check ran via {{ trigger_source }} and returned {{ llmvision_result.response_text | default(states('input_boolean.front_door_packages_present')) | lower }}. - choose: - conditions: diff --git a/config/packages/office_motion.yaml b/config/packages/office_motion.yaml index c3f13aab..456f684e 100755 --- a/config/packages/office_motion.yaml +++ b/config/packages/office_motion.yaml @@ -1,12 +1,13 @@ -#------------------------------------------- -# @CCOSTAN -# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig -# Office Motion - Motion/illuminance triggers for office lighting. -#------------------------------------------- ###################################################################### -## Office motion sensors and automations. +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Office Motion - MQTT motion sensor and office lighting automations +# Office occupancy trigger that drives office light on/off routines. +# ------------------------------------------------------------------- +# Notes: Light sensor node hardware reference https://amzn.to/2oUgj5i ###################################################################### -# Light sensor node: https://amzn.to/2oUgj5i homeassistant: customize: binary_sensor.office_motion: @@ -18,6 +19,7 @@ mqtt: binary_sensor: - state_topic: "NodeMCU4/Motion/Motion" name: "Office_Motion" + unique_id: office_motion_sensor payload_on: 1 payload_off: 0 device_class: motion diff --git a/config/packages/telegram_bot.yaml b/config/packages/telegram_bot.yaml index 240497bb..c3006707 100644 --- a/config/packages/telegram_bot.yaml +++ b/config/packages/telegram_bot.yaml @@ -7,8 +7,9 @@ # Script wrappers for Telegram messaging using UI-configured integration. # ------------------------------------------------------------------- # Notes: Do not add `telegram_bot:` YAML here; integration is UI-only. -# Notes: Joanna transport sends as plain_text to avoid Telegram parse-entity failures. +# Notes: Joanna transport defaults to plain_text, but can opt into HTML when the appliance provides a vetted rich message. # Notes: Keep Skills logic in docker_17/codex_appliance; this package is delivery/transport only. +# Notes: HA Core 2026.4 webhook support is optional; service-call wrappers remain compatible with polling or webhook transport. ###################################################################### script: @@ -20,9 +21,23 @@ script: message: description: Message body to send. example: Joanna is online. + parse_mode: + description: Telegram parse mode (`plain_text` or `html`). + example: html + disable_web_page_preview: + description: Whether Telegram should suppress web page previews. + example: true sequence: - variables: chunk_size: 3400 + requested_parse_mode: >- + {% set raw = parse_mode | default('plain_text', true) | string | lower | trim %} + {% if raw in ['html', 'plain_text'] %} + {{ raw }} + {% else %} + plain_text + {% endif %} + preview_disabled: "{{ disable_web_page_preview | default(true, true) }}" normalized_message: >- {% set raw = message | default('', true) | string %} {{ raw | replace('\r\n', '\n') | replace('\r', '\n') | trim }} @@ -60,8 +75,8 @@ script: data: chat_id: !secret telegram_allowed_chat_id_carlo message: "{{ chunk_message }}" - parse_mode: plain_text - disable_web_page_preview: true + parse_mode: "{{ requested_parse_mode }}" + disable_web_page_preview: "{{ preview_disabled }}" response_variable: telegram_send_response - choose: - conditions: @@ -77,4 +92,4 @@ script: chat_id: !secret telegram_allowed_chat_id_carlo message: "{{ fallback_message if fallback_message | length > 0 else 'Joanna: message delivery fallback (content omitted)' }}" parse_mode: plain_text - disable_web_page_preview: true + disable_web_page_preview: "{{ preview_disabled }}" diff --git a/config/packages/tesla_model_y.yaml b/config/packages/tesla_model_y.yaml new file mode 100644 index 00000000..adbc482a --- /dev/null +++ b/config/packages/tesla_model_y.yaml @@ -0,0 +1,99 @@ +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# Tesla Model Y - Arrival and nightly plug-in reminders +# Garage speech + parents push reminders when the Tesla comes home low. +# ------------------------------------------------------------------- +# Related Issue: 1279 +# Notes: Expects Tesla Fleet UI entities `sensor.spaceship_battery_level` +# and `switch.spaceship_charge`. +# Notes: Uses `device_tracker.spaceship_location` for arrival/home state. +# Notes: The nightly parents reminder starts at 8 PM, waits until the +# car is home, then loops every 30 minutes via a custom event until it +# is charging or no longer home. +###################################################################### + +automation: + - alias: "Tesla Model Y - Garage Plug In Reminder" + id: 6a873341-c823-4d5d-9d42-fc2df52b93db + mode: restart + trigger: + - platform: state + entity_id: device_tracker.spaceship_location + to: 'home' + condition: + - condition: template + value_template: >- + {{ trigger.from_state is not none and + trigger.from_state.state not in ['home', 'unknown', 'unavailable'] }} + - condition: template + value_template: >- + {% set battery = states('sensor.spaceship_battery_level') %} + {{ battery not in ['unknown', 'unavailable', 'none', ''] and + (battery | float(100)) < 50 and + is_state('switch.spaceship_charge', 'off') }} + action: + - wait_for_trigger: + - platform: state + entity_id: cover.large_garage_door + from: 'closed' + to: 'open' + for: '00:03:20' + timeout: '00:10:00' + continue_on_timeout: false + - condition: template + value_template: >- + {% set battery = states('sensor.spaceship_battery_level') %} + {{ battery not in ['unknown', 'unavailable', 'none', ''] and + (battery | float(100)) < 50 and + is_state('switch.spaceship_charge', 'off') }} + - service: notify.alexa_media_garage + data: + message: >- + Reminder: the Tesla battery is at + {{ states('sensor.spaceship_battery_level') | float(0) | round(0) }} percent. + Please plug in the Model Y. + data: + type: announce + - service: script.send_to_logbook + data: + topic: "TESLA" + message: >- + Garage reminder announced to plug in the Model Y at + {{ states('sensor.spaceship_battery_level') | float(0) | round(0) }} percent. + + - alias: "Tesla Model Y - Nightly Plug In Reminder" + id: 8b243b63-f5c3-4436-b596-0ec00a2108ab + mode: single + trigger: + - platform: time + at: '20:00:00' + - platform: event + event_type: event_tesla_model_y_nightly_loop + action: + - wait_template: "{{ is_state('device_tracker.spaceship_location', 'home') }}" + - condition: template + value_template: >- + {% set battery = states('sensor.spaceship_battery_level') %} + {{ is_state('device_tracker.spaceship_location', 'home') and + battery not in ['unknown', 'unavailable', 'none', ''] and + (battery | float(100)) < 50 and + is_state('switch.spaceship_charge', 'off') }} + - service: script.notify_engine + data: + title: "Tesla Plug In Reminder" + value1: >- + The Model Y is home, below 50 percent, and not plugged in. + Current battery: + {{ states('sensor.spaceship_battery_level') | float(0) | round(0) }} percent. + who: 'parents' + group: 'Tesla_Model_Y' + - service: script.send_to_logbook + data: + topic: "TESLA" + message: >- + Nightly plug-in reminder sent because the Model Y is home below 50 percent and not charging. + - delay: '00:30:00' + - event: event_tesla_model_y_nightly_loop diff --git a/config/packages/tugtainer_updates.yaml b/config/packages/tugtainer_updates.yaml index 76ac38b1..9cdb628f 100644 --- a/config/packages/tugtainer_updates.yaml +++ b/config/packages/tugtainer_updates.yaml @@ -10,8 +10,11 @@ # 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: Fires `tugtainer_home_assistant_core_updated` when `### Updated:` includes Home Assistant. +# Notes: Home Assistant changelog dispatch uses core-YYYY.M URL format from parsed/fallback version. # Notes: Joanna dispatch cooldown uses mode=single with a 24-hour delay lockout. -# Notes: Blog post https://www.vcloudinfo.com/2026/02/tugtainer-docker-updates-home-assistant-notifications.html +# - Blog: https://www.vcloudinfo.com/2026/04/joanna-home-assistant-changelog-digest-tugtainer.html +# Notes: Prior background post https://www.vcloudinfo.com/2026/02/tugtainer-docker-updates-home-assistant-notifications.html ###################################################################### input_datetime: @@ -37,6 +40,43 @@ automation: message: "{{ payload.message | default('Update event received') }}" event_type: "{{ payload.type | default('info') }}" has_available_section: "{{ '### available:' in (message | lower) }}" + updated_section: >- + {% set sections = message | regex_findall('(?is)###\\s*updated:\\s*(.*?)(?:\\n###\\s|$)') %} + {{ sections[0] if sections | count > 0 else '' }} + has_updated_section: "{{ updated_section | trim != '' }}" + ha_updated_line: >- + {% set matches = updated_section | regex_findall('(?im)^.*(?:home-assistant|ghcr\\.io/home-assistant/home-assistant).*$') %} + {{ matches[0] if matches | count > 0 else '' }} + ha_core_update_detected: "{{ has_updated_section and (ha_updated_line | trim != '') }}" + ha_version_from_updated_line: >- + {% set arrow_match = ha_updated_line | regex_findall('(?i)(?:->|→|to)\\s*v?([0-9]{4}\\.[0-9]+(?:\\.[0-9]+)?)') %} + {% if arrow_match | count > 0 %} + {{ arrow_match[-1] }} + {% else %} + {% set any_match = ha_updated_line | regex_findall('(?i)v?([0-9]{4}\\.[0-9]+(?:\\.[0-9]+)?)') %} + {{ any_match[-1] if any_match | count > 0 else '' }} + {% endif %} + ha_sensor_version: "{{ states('sensor.ha_installed_version') | string | trim }}" + ha_core_version_full: >- + {% set parsed = ha_version_from_updated_line | trim %} + {% if parsed != '' %} + {{ parsed }} + {% else %} + {% set sensor_matches = ha_sensor_version | regex_findall('([0-9]{4}\\.[0-9]+(?:\\.[0-9]+)?)') %} + {{ sensor_matches[0] if sensor_matches | count > 0 else '' }} + {% endif %} + ha_core_version_minor: >- + {% set minor = ha_core_version_full | trim | regex_findall('^([0-9]{4}\\.[0-9]+)') %} + {{ minor[0] if minor | count > 0 else '' }} + ha_core_changelog_url: >- + {% if ha_core_version_minor | trim != '' %} + https://www.home-assistant.io/changelogs/core-{{ ha_core_version_minor | trim }} + {% else %} + + {% endif %} + ha_report_excerpt: >- + {% set excerpt = ha_updated_line | trim %} + {{ excerpt if excerpt != '' else (message | truncate(280, true)) }} full_message: >- {{ message }}{% if event_type %} ({{ event_type | upper }}){% endif %} action: @@ -59,6 +99,19 @@ automation: title: "{{ title }}" event_type: "{{ event_type }}" message: "{{ message }}" + - conditions: + - condition: template + value_template: "{{ ha_core_update_detected and (ha_core_version_minor | trim != '') }}" + sequence: + - event: tugtainer_home_assistant_core_updated + event_data: + title: "{{ title }}" + event_type: "{{ event_type }}" + message: "{{ message }}" + core_version_full: "{{ ha_core_version_full | trim }}" + core_version_minor: "{{ ha_core_version_minor | trim }}" + changelog_url: "{{ ha_core_changelog_url | trim }}" + report_excerpt: "{{ ha_report_excerpt | trim }}" - alias: "Tugtainer - Dispatch Joanna For Available Updates" id: tugtainer_dispatch_joanna_for_available_updates @@ -93,3 +146,54 @@ automation: Review the Tugtainer report and update all containers listed under the Available section. Report what was updated and any failures. - delay: "24:00:00" + + - alias: "Tugtainer - Dispatch Joanna For Home Assistant Core Digest" + id: tugtainer_dispatch_joanna_for_home_assistant_core_digest + description: "Dispatch Joanna after Home Assistant core is updated so changelog actions are captured in a GitHub digest issue." + mode: queued + trigger: + - platform: event + event_type: tugtainer_home_assistant_core_updated + variables: + report_title: "{{ trigger.event.data.title | default('Tugtainer update') }}" + report_message: "{{ trigger.event.data.message | default('Update event received') }}" + report_event_type: "{{ trigger.event.data.event_type | default('info') }}" + core_version_full: "{{ trigger.event.data.core_version_full | default('', true) | string | trim }}" + core_version_minor: "{{ trigger.event.data.core_version_minor | default('', true) | string | trim }}" + changelog_url: "{{ trigger.event.data.changelog_url | default('', true) | string | trim }}" + report_excerpt: "{{ trigger.event.data.report_excerpt | default('', true) | string | replace('\n', ' ') | replace('\r', ' ') | trim }}" + trigger_context: "HA automation tugtainer_dispatch_joanna_for_home_assistant_core_digest (Tugtainer - Dispatch Joanna For Home Assistant Core Digest)" + structured_request: |- + HA_CORE_UPDATE_DIGEST_REQUEST + core_version_full={{ core_version_full }} + core_version_minor={{ core_version_minor }} + changelog_url={{ changelog_url }} + report_title={{ report_title | string | replace('\n', ' ') | trim }} + report_event_type={{ report_event_type }} + report_excerpt={{ report_excerpt }} + condition: + - condition: template + value_template: "{{ core_version_minor != '' and changelog_url != '' }}" + action: + - service: script.send_to_logbook + data: + topic: "DOCKER" + message: >- + Home Assistant core update detected via Tugtainer ({{ core_version_full }}). + Joanna digest dispatch requested. + - service: script.joanna_dispatch + data: + trigger_context: "{{ trigger_context }}" + source: "home_assistant_automation.tugtainer_home_assistant_core_digest" + summary: >- + Home Assistant core updated to {{ core_version_full }} via Tugtainer. + Build changelog digest and open/refresh the GitHub Update Digest issue. + entity_ids: + - "input_datetime.tugtainer_last_update" + diagnostics: >- + report_title={{ report_title }}, + report_event_type={{ report_event_type }}, + core_version_full={{ core_version_full }}, + core_version_minor={{ core_version_minor }}, + changelog_url={{ changelog_url }} + request: "{{ structured_request }}" diff --git a/config/packages/vacuum.yaml b/config/packages/vacuum.yaml index f81062a0..45a7a2f4 100755 --- a/config/packages/vacuum.yaml +++ b/config/packages/vacuum.yaml @@ -12,7 +12,7 @@ # - Treat 2+ minutes in a room as "being cleaned" and dequeue immediately (queue = remaining rooms). # - Phase changes happen only after verified completion at dock (`task_status: completed`). # - Guarded fallback: if docked with empty queue for 10 minutes but no `completed`, advance with `fallback_advance` log. -# - Avoid reissuing `dreame_vacuum.vacuum_clean_segment` while already cleaning; only send a new segment job when starting/resuming or switching phases. +# - Use `vacuum.clean_area` (HA 2026.3+) and keep room->area mappings aligned with Home Assistant Areas. # - Jinja2 loop scoping: use a `namespace` when building lists (otherwise the queue can appear empty and get cleared). # - If docked+completed still has queue entries, treat queue as stale and clear it before phase advance. # - Mop phases use `sweeping_and_mopping` instead of mop-only. @@ -133,6 +133,30 @@ script: {{ bath_ids }} {% endif %} segments_to_clean: "{{ queue_ints if queue_ints | length > 0 else phase_segments }}" + segment_area_name_map: + 14: Kitchen + 12: "Dining Room" + 10: "Living Room" + 7: "Master Bedroom" + 15: Foyer + 9: "Stacey Office" + 13: Hallway + 8: "Justin Bedroom" + 6: "Paige Bedroom" + 4: "Master Bathroom" + 2: Office + 1: "Pool Bath" + 3: "Kids Bathroom" + cleaning_area_ids: > + {% set ns = namespace(ids=[]) %} + {% for seg in segments_to_clean %} + {% set area_name = segment_area_name_map.get(seg) %} + {% set aid = area_id(area_name) if area_name else none %} + {% if aid %} + {% set ns.ids = ns.ids + [aid] %} + {% endif %} + {% endfor %} + {{ ns.ids }} # 0. Reseed the current phase when queue is empty. - choose: @@ -168,6 +192,19 @@ script: - stop: 'No rooms left to clean today.' default: [] + # 2b. Clean-area needs a mapped Home Assistant area ID for every segment + - choose: + - conditions: + - condition: template + value_template: "{{ cleaning_area_ids | length != segments_to_clean | length }}" + sequence: + - service: script.send_to_logbook + data: + topic: "VACUUM" + message: "Missing area mappings for one or more segments {{ segments_to_clean }}; skipping clean_area." + - stop: "Incomplete Home Assistant area mappings." + default: [] + # 3. Start cleaning (but don't clobber an active job) - choose: - conditions: @@ -177,7 +214,7 @@ script: - service: script.send_to_logbook data: topic: "VACUUM" - message: "Vacuum is already cleaning; queue/phase updated but not issuing a new segment job." + message: "Vacuum is already cleaning; queue/phase updated but not issuing a new clean_area action." - stop: "Already cleaning." default: [] @@ -192,12 +229,12 @@ script: entity_id: vacuum.l10s_vacuum data: fan_speed: Standard - - service: dreame_vacuum.vacuum_clean_segment + - service: vacuum.clean_area target: entity_id: vacuum.l10s_vacuum data: - # Clean the non-bathrooms if any, otherwise clean the bathrooms - segments: "{{ segments_to_clean }}" + # Clean mapped Home Assistant areas for this phase queue. + cleaning_area_id: "{{ cleaning_area_ids }}" ## 3. Automations @@ -294,22 +331,24 @@ automation: id: kids_bathroom variables: room_map: - kitchen: {segment: 14, name: Kitchen} - dining_room: {segment: 12, name: 'Dining Room'} - living_room: {segment: 10, name: 'Living Room'} - master_bedroom: {segment: 7, name: 'Master Bedroom'} - foyer: {segment: 15, name: Foyer} - stacey_office: {segment: 9, name: 'Stacey Office'} - formal_dining: {segment: 17, name: 'Formal Dining'} - hallway: {segment: 13, name: Hallway} - justin_bedroom: {segment: 8, name: 'Justin Bedroom'} - paige_bedroom: {segment: 6, name: 'Paige Bedroom'} - master_bathroom: {segment: 4, name: 'Master Bathroom'} - office: {segment: 2, name: Office} - pool_bath: {segment: 1, name: 'Pool Bath'} - kids_bathroom: {segment: 3, name: 'Kids Bathroom'} + kitchen: {segment: 14, name: Kitchen, area: Kitchen} + dining_room: {segment: 12, name: 'Dining Room', area: 'Dining Room'} + living_room: {segment: 10, name: 'Living Room', area: 'Living Room'} + master_bedroom: {segment: 7, name: 'Master Bedroom', area: 'Master Bedroom'} + foyer: {segment: 15, name: Foyer, area: Foyer} + stacey_office: {segment: 9, name: 'Stacey Office', area: 'Stacey Office'} + formal_dining: {segment: 17, name: 'Formal Dining', area: 'Formal Dining'} + hallway: {segment: 13, name: Hallway, area: Hallway} + justin_bedroom: {segment: 8, name: 'Justin Bedroom', area: 'Justin Bedroom'} + paige_bedroom: {segment: 6, name: 'Paige Bedroom', area: 'Paige Bedroom'} + master_bathroom: {segment: 4, name: 'Master Bathroom', area: 'Master Bathroom'} + office: {segment: 2, name: Office, area: Office} + pool_bath: {segment: 1, name: 'Pool Bath', area: 'Pool Bath'} + kids_bathroom: {segment: 3, name: 'Kids Bathroom', area: 'Kids Bathroom'} room_key: "{{ trigger.id }}" room_name: "{{ room_map[room_key].name }}" + area_name: "{{ room_map[room_key].area }}" + area_id_value: "{{ area_id(area_name) if area_name else none }}" segment_id: "{{ room_map[room_key].segment | int }}" vac_state: "{{ states('vacuum.l10s_vacuum') }}" on_demand: "{{ is_state('input_boolean.l10s_vacuum_on_demand', 'on') }}" @@ -319,7 +358,7 @@ automation: - choose: - conditions: - condition: template - value_template: "{{ can_start }}" + value_template: "{{ can_start and area_id_value is not none }}" sequence: - service: script.send_to_logbook data: @@ -338,17 +377,17 @@ automation: data: fan_speed: Standard - continue_on_error: true - service: dreame_vacuum.vacuum_clean_segment + service: vacuum.clean_area target: entity_id: vacuum.l10s_vacuum data: - segments: "{{ [segment_id] }}" + cleaning_area_id: "{{ [area_id_value] }}" - delay: "00:00:02" default: - service: script.send_to_logbook data: topic: "VACUUM" - message: "One-off clean blocked: {{ room_name }} (vac={{ vac_state }}, on_demand={{ on_demand }}, queue='{{ queue_raw }}')." + message: "One-off clean blocked: {{ room_name }} (area={{ area_name }}, area_id={{ area_id_value }}, vac={{ vac_state }}, on_demand={{ on_demand }}, queue='{{ queue_raw }}')." - service: input_boolean.turn_off data: entity_id: "{{ trigger.entity_id }}" diff --git a/config/script/joanna_dispatch.yaml b/config/script/joanna_dispatch.yaml index 79517f6b..2ff92bdc 100644 --- a/config/script/joanna_dispatch.yaml +++ b/config/script/joanna_dispatch.yaml @@ -8,6 +8,7 @@ # ------------------------------------------------------------------- # Notes: Keep this helper generic so package automations can reuse one schema. # Notes: Source defaults to home_assistant_automation.unknown when omitted. +# Notes: Automation dispatches are async_only by default so HA calls return quickly while BearClaw works in queue. ###################################################################### joanna_dispatch: @@ -64,3 +65,4 @@ joanna_dispatch: user: "{{ normalized_user }}" source: "{{ normalized_source }}" context: "{{ normalized_context }}" + async_only: true diff --git a/config/sensor/MQTT.yaml b/config/sensor/MQTT.yaml index 31de1f74..e03cf255 100755 --- a/config/sensor/MQTT.yaml +++ b/config/sensor/MQTT.yaml @@ -1,15 +1,16 @@ -#------------------------------------------- -# MQTT Sensor Configuration -# Description: Various MQTT sensors for Home Assistant -# -# For more information and updates, visit: -# https://www.vcloudinfo.com/click-here -# -# Original Repository: https://github.com/CCOSTAN/Home-AssistantConfig -#------------------------------------------- +###################################################################### +# @CCOSTAN - Follow Me on X +# For more info visit https://www.vcloudinfo.com/click-here +# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig +# ------------------------------------------------------------------- +# MQTT Sensors - Medication reminder timestamp feed +# MQTT-backed timestamp sensor used by medicine reminder workflows. +# ------------------------------------------------------------------- +###################################################################### mqtt: sensor: - state_topic: "dash/medicine/medicine_time" name: "Medicine time" + unique_id: medicine_time_timestamp device_class: "timestamp" diff --git a/config/templates/speech/briefing.yaml b/config/templates/speech/briefing.yaml index 9ba751b8..ce241a6c 100755 --- a/config/templates/speech/briefing.yaml +++ b/config/templates/speech/briefing.yaml @@ -131,6 +131,12 @@ {%- endif -%} {%- endmacro -%} + {%- macro front_door_packages() -%} + {% if is_state('binary_sensor.front_door_packages_present', 'on') -%} + There appears to be a package waiting at the front door. + {%- endif -%} + {%- endmacro -%} + {%- macro medicine() -%} {% if is_state('input_boolean.medicine', 'off') -%} It looks like Carlo has not taken his medicine yet. Please make sure Carlo takes his medicine now. @@ -293,6 +299,8 @@ {{ window_check() }} {% endif %} + {{ front_door_packages() }} + {{ NewDevice | default }} {% if call_garbage_day == 1 %} diff --git a/config/www/onenote-launch.html b/config/www/onenote-launch.html new file mode 100644 index 00000000..b422dcc8 --- /dev/null +++ b/config/www/onenote-launch.html @@ -0,0 +1,129 @@ + + +
+ + +Joanna is handing this note to the OneNote app now.
+ + +If the app does not open automatically in a second or two, tap the OneNote button.
+