diff --git a/Common/Dreame Vacuum/README.md b/Common/Dreame Vacuum/README.md index 617331b..49be4ca 100644 --- a/Common/Dreame Vacuum/README.md +++ b/Common/Dreame Vacuum/README.md @@ -1,6 +1,6 @@ # Dreame Vacuum Notifications -Sends customizable notifications for Dreame vacuum events. Requires the [Dreame Vacuum](https://github.com/Tasshack/dreame-vacuum) integration. +Sends customizable notifications for Dreame vacuum events. Requires the [Dreame Vacuum](https://github.com/Tasshack/dreame-vacuum) integration and Home Assistant 2024.7+ (`notify.send_message` action). ## Features @@ -9,7 +9,8 @@ Sends customizable notifications for Dreame vacuum events. Requires the [Dreame - Device warning and error notifications - Informational alerts (e.g., action blocked by Do Not Disturb) - Individual toggle for each event type -- Customizable message templates with variable substitution +- Per-code filter lists for silencing routine warnings, errors, or info messages +- Customizable message templates - Multiple notification targets ## How It Works @@ -24,7 +25,12 @@ The blueprint listens to events fired by the Dreame Vacuum integration: | `dreame_vacuum_error` | Device fault | | `dreame_vacuum_information` | Action blocked by user settings | -Events are filtered by the configured vacuum entity, so only the selected vacuum triggers notifications. +Events are filtered by the configured vacuum: + +1. If the event payload includes `device_id`, it must match the device of the configured vacuum entity. This is the most reliable match. +2. Otherwise the entity_id in the event must equal the configured one, or equal it followed by a purely numeric suffix (e.g., `vacuum.dreame_x10_2`). The integration uses `generate_entity_id()`, so a numeric suffix can appear when the base entity_id is taken. + +This avoids false positives when two vacuums share an entity_id prefix (e.g., `vacuum.dreame_x10` vs. `vacuum.dreame_x10_pro`). ## Configuration @@ -33,8 +39,13 @@ Events are filtered by the configured vacuum entity, so only the selected vacuum | **Vacuum Entity** | The Dreame vacuum entity to monitor | | **Notification Targets** | One or more `notify` entities | | **Event Toggles** | Enable/disable each event type independently | +| **Filtering** | Lists of warning/error/information codes to silence | | **Message Templates** | Customizable message for each event type | +### Filtering + +Each filter input is a list of codes (as strings) that should not produce a notification. The blueprint compares the event's `code` field (cast to string) against the list. Use this to suppress noisy routine events while keeping critical ones. + ## Message Template Variables ### Cleaning Started @@ -84,10 +95,16 @@ Events are filtered by the configured vacuum entity, so only the selected vacuum | --- | --- | | `{vacuum_name}` | Friendly name of the vacuum | | `{information}` | Information message (e.g., Dust Collection, Cleaning Paused) | +| `{code}` | Information code | + +## Notes + +- The default messages contain emojis. Most modern notify integrations (Mobile App, Telegram, Discord) render them correctly; legacy SMS-based integrations may not. +- `notify.send_message` requires Home Assistant 2024.7 or newer. ## Debug Mode -Enable **Debug Notifications** in the Debug section to send persistent notifications with raw event data for troubleshooting. +Enable **Debug Notifications** in the Debug section to send a persistent notification for every trigger. Each debug notification includes the blueprint version, dispatched event kind, and the enable decision — useful for confirming filters are doing what you expect. The notification is keyed per vacuum so debug entries from multiple vacuums do not overwrite each other. ## Author diff --git a/Common/Dreame Vacuum/blueprint.yaml b/Common/Dreame Vacuum/blueprint.yaml index d00ca1d..570502b 100644 --- a/Common/Dreame Vacuum/blueprint.yaml +++ b/Common/Dreame Vacuum/blueprint.yaml @@ -96,6 +96,41 @@ blueprint: selector: boolean: + # ------------------------------------------------------------------------- + # Filtering + # ------------------------------------------------------------------------- + filters_group: + name: "Filtering" + collapsed: true + input: + warning_codes_ignore: + name: Warning Codes to Ignore + description: > + List of warning codes to silence (one entry per code, as text). + Useful for suppressing routine warnings while keeping critical ones. + default: [] + selector: + text: + multiple: true + + error_codes_ignore: + name: Error Codes to Ignore + description: > + List of error codes to silence (one entry per code, as text). + default: [] + selector: + text: + multiple: true + + information_codes_ignore: + name: Information Codes to Ignore + description: > + List of information codes to silence (one entry per code, as text). + default: [] + selector: + text: + multiple: true + # ------------------------------------------------------------------------- # Message Templates # ------------------------------------------------------------------------- @@ -158,7 +193,7 @@ blueprint: name: "Information Message" description: > Message sent for informational alerts. - Variables: `{vacuum_name}`, `{information}` + Variables: `{vacuum_name}`, `{information}`, `{code}` default: "ℹ️ {vacuum_name}: {information}." selector: text: @@ -175,7 +210,7 @@ blueprint: name: Enable Debug Notifications description: > Send persistent notifications for debugging automation behavior. - Shows raw event data and filtering decisions. + Shows raw event data, dispatched event kind, and the enable decision. default: false selector: boolean: @@ -217,6 +252,10 @@ trigger: # VARIABLES # ============================================================================= variables: + # Bumped whenever event-handling logic changes; surfaced in debug output + # so users can confirm which revision is running. + blueprint_version: "1.1.1" + # --------------------------------------------------------------------------- # Input References # --------------------------------------------------------------------------- @@ -232,6 +271,11 @@ variables: enable_error: !input enable_error enable_information: !input enable_information + # Filter lists (lists of strings) + warning_codes_ignore: !input warning_codes_ignore + error_codes_ignore: !input error_codes_ignore + information_codes_ignore: !input information_codes_ignore + # Message templates message_cleaning_started_template: !input message_cleaning_started message_cleaning_completed_template: !input message_cleaning_completed @@ -244,11 +288,13 @@ variables: # Vacuum Info # --------------------------------------------------------------------------- vacuum_name: "{{ state_attr(vacuum_entity, 'friendly_name') | default(vacuum_entity) }}" + vacuum_device: "{{ device_id(vacuum_entity) | default('', true) }}" # --------------------------------------------------------------------------- # Event Data (flat structure — fields are directly on trigger.event.data) # --------------------------------------------------------------------------- event_entity_id: "{{ trigger.event.data.entity_id | default('') }}" + event_device_id: "{{ trigger.event.data.device_id | default('') }}" # Task status fields task_cleaning_mode: "{{ trigger.event.data.cleaning_mode | default('unknown') }}" @@ -270,19 +316,88 @@ variables: # Information fields information_description: "{{ (trigger.event.data.information | default('unknown')) | replace('_', ' ') | title }}" + information_code: "{{ trigger.event.data.code | default('') }}" + + # --------------------------------------------------------------------------- + # Event Dispatch + # --------------------------------------------------------------------------- + # Map the raw trigger to a single logical event kind. Keeping this in one + # place avoids the duplicated condition logic that the old multi-branch + # `choose` had. + event_kind: > + {%- if trigger.id == 'task_status' and not task_completed -%}cleaning_started + {%- elif trigger.id == 'task_status' and task_completed -%}cleaning_completed + {%- elif trigger.id == 'consumable' -%}consumable + {%- elif trigger.id == 'warning' -%}warning + {%- elif trigger.id == 'error' -%}error + {%- elif trigger.id == 'information' -%}information + {%- else -%}none + {%- endif -%} + + # Whether this event should produce a notification, given user toggles + # and any per-code filter lists. Coerce with `| bool(false)` at the + # consumer because folded scalars can render as the string "True"/"False". + event_enabled: > + {%- if event_kind == 'cleaning_started' -%}{{ enable_cleaning_started }} + {%- elif event_kind == 'cleaning_completed' -%}{{ enable_cleaning_completed }} + {%- elif event_kind == 'consumable' -%}{{ enable_consumable }} + {%- elif event_kind == 'warning' -%}{{ enable_warning and (warning_code | string) not in warning_codes_ignore }} + {%- elif event_kind == 'error' -%}{{ enable_error and (error_code | string) not in error_codes_ignore }} + {%- elif event_kind == 'information' -%}{{ enable_information and (information_code | string) not in information_codes_ignore }} + {%- else -%}False + {%- endif -%} + + # Pick the per-event message template. + message_template: > + {%- if event_kind == 'cleaning_started' -%}{{ message_cleaning_started_template }} + {%- elif event_kind == 'cleaning_completed' -%}{{ message_cleaning_completed_template }} + {%- elif event_kind == 'consumable' -%}{{ message_consumable_template }} + {%- elif event_kind == 'warning' -%}{{ message_warning_template }} + {%- elif event_kind == 'error' -%}{{ message_error_template }} + {%- elif event_kind == 'information' -%}{{ message_information_template }} + {%- else -%}{%- endif -%} + + # Render the message. Placeholders that don't apply to this event are + # absent from the chosen template, so `replace()` is a no-op for them. + message: > + {%- set code_for_event = warning_code if event_kind == 'warning' + else (error_code if event_kind == 'error' + else (information_code if event_kind == 'information' else '')) -%} + {{ message_template + | replace('{vacuum_name}', vacuum_name) + | replace('{cleaning_mode}', task_cleaning_mode) + | replace('{status}', task_status_value) + | replace('{cleaned_area}', task_cleaned_area | string) + | replace('{cleaning_time}', task_cleaning_time | string) + | replace('{consumable}', consumable_name) + | replace('{warning}', warning_description) + | replace('{error}', error_description) + | replace('{information}', information_description) + | replace('{code}', code_for_event | string) }} # ============================================================================= # CONDITIONS # ============================================================================= condition: - # Only process events from the configured vacuum entity. - # The Dreame Vacuum integration uses generate_entity_id() for the entity_id - # in event data, which may append a numeric suffix (e.g., _2) since the - # actual vacuum entity already occupies the base entity_id. + # Only process events from the configured vacuum. + # Prefer device_id matching when the event payload carries it (most + # reliable across firmware revisions and avoids prefix collisions when + # multiple Dreame vacuums share an entity_id stem). + # Fall back to entity_id matching: the integration uses generate_entity_id(), + # which may append a numeric suffix (e.g., `_2`) when the base entity_id + # is taken — so we accept the configured entity_id with an optional + # purely numeric suffix and reject non-numeric suffixes (e.g., `_pro`). - condition: template value_template: > - {{ event_entity_id == vacuum_entity - or event_entity_id.startswith(vacuum_entity ~ '_') }} + {%- if event_device_id != '' and vacuum_device != '' -%} + {{ event_device_id == vacuum_device }} + {%- elif event_entity_id == vacuum_entity -%} + true + {%- elif event_entity_id.startswith(vacuum_entity ~ '_') -%} + {{ event_entity_id[(vacuum_entity | length) + 1:].isdigit() }} + {%- else -%} + false + {%- endif -%} # ============================================================================= # ACTIONS @@ -297,127 +412,31 @@ action: - condition: template value_template: "{{ enable_debug_notifications }}" sequence: - - service: persistent_notification.create + - action: persistent_notification.create data: - title: "Dreame Vacuum Debug" + notification_id: "dreame_vacuum_debug_{{ vacuum_entity }}" + title: "Dreame Vacuum Debug — {{ vacuum_name }}" message: > + **Blueprint version:** {{ blueprint_version }} **Trigger:** {{ trigger.id }} + **Event kind:** {{ event_kind }} + **Enabled:** {{ event_enabled }} **Entity:** {{ event_entity_id }} + **Device:** {{ event_device_id }} **Vacuum:** {{ vacuum_name }} **Event Data:** {{ trigger.event.data }} # --------------------------------------------------------------------------- - # Send Notification Based on Event Type + # Send Notification # --------------------------------------------------------------------------- + # Single dispatch — message and the enable decision are computed in the + # variables block above. We just gate on the resolved flags here. - choose: - - # CASE 1: Cleaning Started - conditions: - condition: template - value_template: > - {{ trigger.id == 'task_status' - and not task_completed - and enable_cleaning_started }} + value_template: "{{ event_kind != 'none' and (event_enabled | bool(false)) }}" sequence: - - variables: - message: > - {% set tpl = message_cleaning_started_template %} - {{ tpl | replace('{vacuum_name}', vacuum_name) - | replace('{cleaning_mode}', task_cleaning_mode) - | replace('{status}', task_status_value) }} - - service: notify.send_message - target: - entity_id: "{{ notify_targets }}" - data: - message: "{{ message }}" - - # CASE 2: Cleaning Completed - - conditions: - - condition: template - value_template: > - {{ trigger.id == 'task_status' - and task_completed - and enable_cleaning_completed }} - sequence: - - variables: - message: > - {% set tpl = message_cleaning_completed_template %} - {{ tpl | replace('{vacuum_name}', vacuum_name) - | replace('{cleaning_mode}', task_cleaning_mode) - | replace('{status}', task_status_value) - | replace('{cleaned_area}', task_cleaned_area | string) - | replace('{cleaning_time}', task_cleaning_time | string) }} - - service: notify.send_message - target: - entity_id: "{{ notify_targets }}" - data: - message: "{{ message }}" - - # CASE 3: Consumable Depleted - - conditions: - - condition: template - value_template: > - {{ trigger.id == 'consumable' and enable_consumable }} - sequence: - - variables: - message: > - {% set tpl = message_consumable_template %} - {{ tpl | replace('{vacuum_name}', vacuum_name) - | replace('{consumable}', consumable_name) }} - - service: notify.send_message - target: - entity_id: "{{ notify_targets }}" - data: - message: "{{ message }}" - - # CASE 4: Warning - - conditions: - - condition: template - value_template: > - {{ trigger.id == 'warning' and enable_warning }} - sequence: - - variables: - message: > - {% set tpl = message_warning_template %} - {{ tpl | replace('{vacuum_name}', vacuum_name) - | replace('{warning}', warning_description) - | replace('{code}', warning_code | string) }} - - service: notify.send_message - target: - entity_id: "{{ notify_targets }}" - data: - message: "{{ message }}" - - # CASE 5: Error - - conditions: - - condition: template - value_template: > - {{ trigger.id == 'error' and enable_error }} - sequence: - - variables: - message: > - {% set tpl = message_error_template %} - {{ tpl | replace('{vacuum_name}', vacuum_name) - | replace('{error}', error_description) - | replace('{code}', error_code | string) }} - - service: notify.send_message - target: - entity_id: "{{ notify_targets }}" - data: - message: "{{ message }}" - - # CASE 6: Information - - conditions: - - condition: template - value_template: > - {{ trigger.id == 'information' and enable_information }} - sequence: - - variables: - message: > - {% set tpl = message_information_template %} - {{ tpl | replace('{vacuum_name}', vacuum_name) - | replace('{information}', information_description) }} - - service: notify.send_message + - action: notify.send_message target: entity_id: "{{ notify_targets }}" data: diff --git a/manifest.json b/manifest.json index 7962e7a..302d4b9 100644 --- a/manifest.json +++ b/manifest.json @@ -1,3 +1,3 @@ { - "version": "2.6.4" + "version": "2.9.1" }