feat: translate Dreame Vacuum operation codes and add localization

- Render cleaning_mode/status as friendly labels instead of raw UPPER_SNAKE enum names
- Add optional label_overrides / code_label_overrides inputs and a Russian starter table (localization.ru.yaml)
- Add {code_name} placeholder for warning/error codes (override -> integration text -> empty)
- Fix non-functional information filter (match the information type id, not an absent code)
- Suppress the spurious replace_temporary_map clear warning; humanize bare snake-id warnings
- Remove dead device_id matching branch; strip empty '(label: )' parenthetical (any language)
- Robustness: queue max 10, task_completed bool coercion, notify target via !input, if/then actions
This commit is contained in:
2026-06-21 21:02:10 +03:00
parent fafcf116be
commit c8ab66caf3
4 changed files with 459 additions and 80 deletions
+182 -62
View File
@@ -123,9 +123,11 @@ blueprint:
multiple: true
information_codes_ignore:
name: Information Codes to Ignore
name: Information Types to Ignore
description: >
List of information codes to silence (one entry per code, as text).
List of information message types to silence (one entry per type).
Information events carry a type id, not a numeric code.
Valid values: dust_collection, cleaning_paused.
default: []
selector:
text:
@@ -173,7 +175,7 @@ blueprint:
name: "Warning Message"
description: >
Message sent for device warnings.
Variables: `{vacuum_name}`, `{warning}`, `{code}`
Variables: `{vacuum_name}`, `{warning}`, `{code}`, `{code_name}`
default: "⚠️ {vacuum_name} warning: {warning} (code: {code})."
selector:
text:
@@ -183,7 +185,7 @@ blueprint:
name: "Error Message"
description: >
Message sent for device errors.
Variables: `{vacuum_name}`, `{error}`, `{code}`
Variables: `{vacuum_name}`, `{error}`, `{code}`, `{code_name}`
default: "❌ {vacuum_name} error: {error} (code: {code})."
selector:
text:
@@ -193,12 +195,48 @@ blueprint:
name: "Information Message"
description: >
Message sent for informational alerts.
Variables: `{vacuum_name}`, `{information}`, `{code}`
Variables: `{vacuum_name}`, `{information}`
default: "️ {vacuum_name}: {information}."
selector:
text:
multiline: true
# -------------------------------------------------------------------------
# Localization / Label Overrides
# -------------------------------------------------------------------------
localization_group:
name: "Localization"
collapsed: true
input:
label_overrides:
name: "Label Overrides"
description: >
Optional translation/relabel table for dynamic values, as YAML
key/value pairs. Keys are the raw values the integration emits
(cleaning mode / status are UPPER_SNAKE enum names; consumable /
information are lower-case ids); values are your preferred text in
any language. Applied to {cleaning_mode}, {status}, {consumable}
and {information}. Example:
SWEEPING_AND_MOPPING: Подметание и мытьё
CLEANING: Уборка
BACK_HOME: Возврат на базу
dust_collection: Очистка контейнера
default: {}
selector:
object:
code_label_overrides:
name: "Warning/Error Code Labels"
description: >
Optional table mapping a numeric warning/error code to a short
label, exposed as the {code_name} placeholder. Leave empty to omit
{code_name}. Use unquoted numbers as keys. Example:
68: Снять швабру
47: Робот застрял
default: {}
selector:
object:
# -------------------------------------------------------------------------
# Debug
# -------------------------------------------------------------------------
@@ -215,9 +253,10 @@ blueprint:
selector:
boolean:
# Queued mode to avoid dropping rapid events
# Queued mode to avoid dropping rapid events. A single cleanup-completion pass
# can fire up to 8 events (1 task_status + 7 consumables), so queue up to 10.
mode: queued
max: 5
max: 10
# =============================================================================
# TRIGGERS
@@ -254,13 +293,12 @@ trigger:
variables:
# Bumped whenever event-handling logic changes; surfaced in debug output
# so users can confirm which revision is running.
blueprint_version: "1.1.1"
blueprint_version: "1.2.0"
# ---------------------------------------------------------------------------
# Input References
# ---------------------------------------------------------------------------
vacuum_entity: !input vacuum_entity
notify_targets: !input notify_targets
enable_debug_notifications: !input enable_debug_notifications
# Event toggles
@@ -284,39 +322,119 @@ variables:
message_error_template: !input message_error
message_information_template: !input message_information
# Localization overrides (dicts; default to empty mappings)
label_overrides: !input label_overrides
code_label_overrides: !input code_label_overrides
# ---------------------------------------------------------------------------
# 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('') }}"
# ---------------------------------------------------------------------------
# Friendly label maps
# ---------------------------------------------------------------------------
# The integration emits cleaning_mode and status as raw UPPER_SNAKE enum
# names (e.g. SWEEPING_AND_MOPPING, SEGMENT_CLEANING). These maps translate
# them to readable text. Lookup order at the call site is:
# user label_overrides -> built-in map below -> generic humanizer.
cleaning_mode_labels:
UNKNOWN: "Unknown"
SWEEPING: "Sweeping"
MOPPING: "Mopping"
SWEEPING_AND_MOPPING: "Sweeping & Mopping"
status_labels:
UNKNOWN: "Unknown"
IDLE: "Idle"
PAUSED: "Paused"
CLEANING: "Cleaning"
BACK_HOME: "Returning to dock"
PART_CLEANING: "Spot cleaning"
FOLLOW_WALL: "Following wall"
CHARGING: "Charging"
OTA: "Updating firmware"
FCT: "Factory check"
WIFI_SET: "Wi-Fi setup"
POWER_OFF: "Powered off"
FACTORY: "Factory mode"
ERROR: "Error"
REMOTE_CONTROL: "Remote control"
SLEEPING: "Sleeping"
SELF_TEST: "Self test"
FACTORY_FUNCION_TEST: "Factory function test"
STANDBY: "Standby"
SEGMENT_CLEANING: "Room cleaning"
ZONE_CLEANING: "Zone cleaning"
SPOT_CLEANING: "Spot cleaning"
FAST_MAPPING: "Mapping"
MONITOR_CRUISE: "Patrolling"
MONITOR_SPOT: "Spot monitoring"
SUMMON_CLEAN: "Summon clean"
# ---------------------------------------------------------------------------
# Task status fields
task_cleaning_mode: "{{ trigger.event.data.cleaning_mode | default('unknown') }}"
task_status_value: "{{ trigger.event.data.status | default('unknown') }}"
task_completed: "{{ trigger.event.data.completed | default(false) }}"
# ---------------------------------------------------------------------------
# Raw enum names as fired by the integration.
task_cleaning_mode_raw: "{{ trigger.event.data.cleaning_mode | default('UNKNOWN') }}"
task_status_raw: "{{ trigger.event.data.status | default('UNKNOWN') }}"
# Friendly values: override -> built-in map -> generic humanizer fallback.
task_cleaning_mode: "{{ (label_overrides or {}).get(task_cleaning_mode_raw, cleaning_mode_labels.get(task_cleaning_mode_raw, task_cleaning_mode_raw | replace('_', ' ') | title)) }}"
task_status_value: "{{ (label_overrides or {}).get(task_status_raw, status_labels.get(task_status_raw, task_status_raw | replace('_', ' ') | title)) }}"
# Coerce to a real bool so the started/completed dispatch stays correct.
task_completed: "{{ trigger.event.data.completed | default(false) | bool(false) }}"
task_cleaned_area: "{{ trigger.event.data.cleaned_area | default(0) }}"
task_cleaning_time: "{{ trigger.event.data.cleaning_time | default(0) }}"
# ---------------------------------------------------------------------------
# Consumable fields
consumable_name: "{{ (trigger.event.data.consumable | default('unknown')) | replace('_', ' ') | title }}"
# ---------------------------------------------------------------------------
consumable_name: "{{ (label_overrides or {}).get(trigger.event.data.consumable | default('unknown'), (trigger.event.data.consumable | default('unknown')) | replace('_', ' ') | title) }}"
# ---------------------------------------------------------------------------
# Warning fields
warning_description: "{{ trigger.event.data.warning | default('unknown') }}"
# ---------------------------------------------------------------------------
# Most warnings carry a short English description; the temporary-map events
# instead carry a bare snake id or a markdown blob. Humanize a bare id (and
# honor overrides) while leaving descriptive text / markdown untouched.
warning_description: >
{%- set w = trigger.event.data.warning | default('unknown') -%}
{%- if w and ' ' not in w and '#' not in w -%}{{ (label_overrides or {}).get(w, w | replace('_', ' ') | title) }}{%- else -%}{{ w }}{%- endif -%}
warning_code: "{{ trigger.event.data.code | default('') }}"
# ---------------------------------------------------------------------------
# Error fields
# ---------------------------------------------------------------------------
error_description: "{{ trigger.event.data.error | default('unknown') }}"
error_code: "{{ trigger.event.data.code | default('') }}"
# ---------------------------------------------------------------------------
# Code label (optional, localized)
# ---------------------------------------------------------------------------
# Numeric warning/error code as fired (empty for code-less events). code_name
# is the localized label for that code, exposed as {code_name}: a user override
# if present, otherwise the integration's own English description, otherwise
# empty. No built-in numeric code table is shipped, so there is no risk of
# stale labels across integration versions.
event_code: "{{ trigger.event.data.code | default('') }}"
code_name: >
{%- set lbl = (code_label_overrides or {}).get(event_code, (code_label_overrides or {}).get(event_code | string, '')) -%}
{%- if lbl -%}{{ lbl }}
{%- elif trigger.id == 'warning' -%}{{ warning_description }}
{%- elif trigger.id == 'error' -%}{{ error_description }}
{%- endif -%}
# ---------------------------------------------------------------------------
# Information fields
information_description: "{{ (trigger.event.data.information | default('unknown')) | replace('_', ' ') | title }}"
information_code: "{{ trigger.event.data.code | default('') }}"
# ---------------------------------------------------------------------------
information_description: "{{ (label_overrides or {}).get(trigger.event.data.information | default('unknown'), (trigger.event.data.information | default('unknown')) | replace('_', ' ') | title) }}"
# Information events carry a type id (e.g. dust_collection), never a numeric
# code — this raw id drives the Information ignore filter.
information_code: "{{ trigger.event.data.information | default('') }}"
# ---------------------------------------------------------------------------
# Event Dispatch
@@ -337,11 +455,13 @@ variables:
# 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".
# The warning branch also drops the integration's spurious "replace_temporary_map"
# clear event (fired with no code, including on every restart).
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 == 'warning' -%}{{ enable_warning and (trigger.event.data.warning | default('')) != 'replace_temporary_map' 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
@@ -359,12 +479,14 @@ variables:
# 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.
# {vacuum_name} is substituted last (it is the only user-controlled value)
# so a friendly name containing a literal token cannot be re-expanded. The
# final regex strips an empty "(label: )" parenthetical in any language left
# by a code-less event, e.g. "(code: )" or "(код предупреждения: )".
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)
else (error_code if event_kind == 'error' else '') -%}
{%- set rendered = message_template
| replace('{cleaning_mode}', task_cleaning_mode)
| replace('{status}', task_status_value)
| replace('{cleaned_area}', task_cleaned_area | string)
@@ -373,24 +495,25 @@ variables:
| replace('{warning}', warning_description)
| replace('{error}', error_description)
| replace('{information}', information_description)
| replace('{code}', code_for_event | string) }}
| replace('{code_name}', code_name)
| replace('{code}', code_for_event | string)
| replace('{vacuum_name}', vacuum_name) -%}
{{ rendered | regex_replace('\\s*\\(\\s*[^():]*:\\s*\\)', '') }}
# =============================================================================
# CONDITIONS
# =============================================================================
condition:
# 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`).
# Only process events from the configured vacuum. The integration fires every
# event with an `entity_id` derived from the device NAME via generate_entity_id()
# — events never carry a `device_id`, so matching is entity_id based.
# generate_entity_id() may append a purely numeric suffix (e.g. `_2`) when the
# base id is already taken, so we accept the configured entity_id exactly, or
# followed by a purely-numeric suffix, and reject non-numeric suffixes (e.g. `_pro`).
- condition: template
value_template: >
{%- if event_device_id != '' and vacuum_device != '' -%}
{{ event_device_id == vacuum_device }}
{%- if event_entity_id == '' -%}
false
{%- elif event_entity_id == vacuum_entity -%}
true
{%- elif event_entity_id.startswith(vacuum_entity ~ '_') -%}
@@ -407,37 +530,34 @@ action:
# ---------------------------------------------------------------------------
# Debug Logging
# ---------------------------------------------------------------------------
- choose:
- conditions:
- condition: template
value_template: "{{ enable_debug_notifications }}"
sequence:
- action: persistent_notification.create
data:
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 }}
- if:
- condition: template
value_template: "{{ enable_debug_notifications }}"
then:
- action: persistent_notification.create
data:
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 }}
**Vacuum:** {{ vacuum_name }}
**Event Data:** {{ trigger.event.data }}
# ---------------------------------------------------------------------------
# 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:
- conditions:
- condition: template
value_template: "{{ event_kind != 'none' and (event_enabled | bool(false)) }}"
sequence:
- action: notify.send_message
target:
entity_id: "{{ notify_targets }}"
data:
message: "{{ message }}"
- if:
- condition: template
value_template: "{{ event_kind != 'none' and (event_enabled | bool(false)) }}"
then:
- action: notify.send_message
target:
entity_id: !input notify_targets
data:
message: "{{ message }}"