Updated apps

This commit is contained in:
2026-07-20 22:52:35 -04:00
parent 28a8cb98f6
commit a0c3271743
1164 changed files with 94781 additions and 6892 deletions
@@ -0,0 +1,25 @@
"""Suggested-setups signature catalog (split from integration_signatures.py).
Layout: ``_model`` (dataclasses + matcher/trigger mechanics), one data
module per category, ``_registry`` (merge + duplicate guard),
``_discovery``. The old import path keeps working via the shim."""
from __future__ import annotations
from ._discovery import discover_integration_setups
from ._model import (
ConsumableSignature,
IntegrationSignature,
build_setup_trigger,
task_name_variants,
)
from ._registry import SIGNATURES
__all__ = [
"SIGNATURES",
"ConsumableSignature",
"IntegrationSignature",
"build_setup_trigger",
"discover_integration_setups",
"task_name_variants",
]
@@ -0,0 +1,147 @@
"""Device discovery over the assembled signature catalog."""
from __future__ import annotations
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers import area_registry as ar
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from ._model import (
_entity_matches,
_entity_unit,
_threshold_for,
_unit_compatible,
task_name_variants,
)
from ._registry import SIGNATURES
def discover_integration_setups(hass: HomeAssistant) -> list[dict[str, Any]]:
"""Devices of catalogued integrations with their matchable task wiring.
Groups matched entities per device; carries the maintenance object already
attached to the device (if any) so adoption can extend it instead of
creating a duplicate. Entities already watched by some task's trigger are
skipped — re-running discovery never proposes what is already wired.
"""
from ...templates import localize_template_text
from ..i18n import normalize_language
from ..problem_sensors import _adopted_entity_ids, _object_by_device
lang = normalize_language(hass)
ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)
area_reg = ar.async_get(hass)
already_watched = _adopted_entity_ids(hass)
by_device = _object_by_device(hass)
# Collect the enabled registry entities of cataloged integrations per
# (device, integration) first — the device-type gates need the device's
# FULL entity list (siblings identify the appliance type).
by_device_integration: dict[tuple[str, str], list[er.RegistryEntry]] = {}
for entry in ent_reg.entities.values():
if SIGNATURES.get(entry.platform) is None or not entry.device_id:
continue
if entry.disabled_by is not None:
continue
by_device_integration.setdefault((entry.device_id, entry.platform), []).append(entry)
# device_id → {(integration, task_name, direction): {sig, entity_ids}}.
# The direction is part of the key so an integration that ships one task
# name in two directions (LG ThinQ filter: hours vs percent) stays split.
matched: dict[str, dict[tuple[str, str, str], dict[str, Any]]] = {}
for (device_id, integration), entries in by_device_integration.items():
catalog = SIGNATURES[integration]
device = dev_reg.async_get(device_id)
model = ((device.model or "") if device else "").lower()
for sig in catalog.tasks:
# Device-type gates: registry model substring and/or a
# type-identifying sibling entity (watched siblings still count —
# only the match TARGET must be unwatched).
if sig.models and not any(m.lower() in model for m in sig.models):
continue
if sig.models_exclude and any(m.lower() in model for m in sig.models_exclude):
continue
if sig.require_sibling_keys and not any(
any(_entity_matches(e, key) for key in sig.require_sibling_keys) for e in entries
):
continue
for entry in entries:
if entry.domain != sig.entity_domain:
continue
if entry.entity_id in already_watched:
continue
if not _unit_compatible(sig.direction, _entity_unit(hass, entry)):
continue
# Empty keys (non-sensor domains only, tripwire-enforced) match
# the device's single entity of that domain — THE lawn_mower.
if sig.keys and not any(_entity_matches(entry, key) for key in sig.keys):
continue
group = matched.setdefault(device_id, {}).setdefault(
(integration, sig.task_name, sig.direction),
{"sig": sig, "entity_ids": []},
)
group["entity_ids"].append(entry.entity_id)
# One source entity may back SEVERAL duties (a mower's hours
# counter drives blades AND undercarriage). Adopting any duty
# marks the entity watched — adopt-all is the default,
# deselecting a duty forfeits its later proposal.
out: list[dict[str, Any]] = []
for device_id, sig_map in matched.items():
device = dev_reg.async_get(device_id)
if device is None:
continue
device_name = device.name_by_user or device.name or device_id
area_name = ""
if device.area_id and (area := area_reg.async_get_area(device.area_id)):
area_name = area.name
integration = next(iter(sig_map))[0]
catalog = SIGNATURES[integration]
suggested = by_device.get(device_id)
# Duties already present on the bound object BY NAME (any language) are
# not re-proposed — covers manually created calendar tasks whose
# trigger watches no entity (the entity-watched exclusion misses them).
existing_names: set[str] = set()
if suggested and (target := hass.config_entries.async_get_entry(suggested["entry_id"])):
from ...const import CONF_TASKS
existing_names = {str(t.get("name", "")).lower() for t in target.data.get(CONF_TASKS, {}).values()}
tasks = []
for (_integ, task_name, direction), group in sig_map.items():
entity_ids = sorted(group["entity_ids"])
if not entity_ids:
continue
if existing_names and existing_names & task_name_variants(task_name):
continue
tasks.append(
{
# task_name stays the EN catalog key (adopt selections
# match on it); the dialog renders the localized twin.
"task_name": task_name,
"task_name_localized": localize_template_text(task_name, lang) or task_name,
"entity_ids": entity_ids,
"threshold": _threshold_for(group["sig"], hass, entity_ids[0]),
"direction": direction,
}
)
if not tasks:
continue
tasks.sort(key=lambda t: (t["task_name"], t["direction"]))
out.append(
{
"device_id": device_id,
"device_name": device_name,
"area_name": area_name,
"integration": integration,
"integration_name": catalog.name,
"suggested_entry_id": suggested["entry_id"] if suggested else None,
"suggested_object_name": suggested["name"] if suggested else device_name,
"tasks": tasks,
}
)
out.sort(key=lambda s: (s["integration_name"], s["device_name"]))
return out
@@ -0,0 +1,306 @@
"""Verified maintenance entity signatures of popular integrations (roadmap).
Popular integrations expose consumable/wear entities that map 1:1 onto
maintenance tasks — a Roborock reports *filter time left*, a Brother printer
its *drum remaining life*. This catalog lets discovery propose a maintenance
object **with sensor-based triggers pre-wired** instead of bare calendar
intervals.
METHOD CONTRACT: every signature is verified against the integration's actual
source code — the ``source`` field records where, ``verified`` records when and
against which ref. Evaluation follows the direct→derived ladder in
docs/design/signature-evaluation-scheme.md: inventory ALL entity platforms,
then direct signals (percent/countdown/resettable counter/event), then derived
(lifetime counters via delta, attributes), then engine-derived (runtime on
state entities) — a negative verdict only after all rungs.
Matching uses the entity registry's ``translation_key`` (the stable id from the
integration's EntityDescription, immune to renames) with an entity_id-suffix
fallback for custom integrations that don't set one.
Direction semantics:
* ``duration_left`` — countdown to the next replacement (device_class
duration). Trigger: below N hours, converted into the entity's display unit.
* ``percent_left`` — remaining life/level in percent. Trigger: below N %.
* ``usage_above`` — a wear counter that counts UP since the device's own
last reset (blade usage time, tub-clean cycles). Trigger: a delta counter
from an explicit 0 baseline — absolute semantics at adoption, but a manual
completion re-baselines instead of immediately re-firing, and a device-side
reset both re-baselines (rollover handling) and auto-completes the task.
* ``event_present`` — an ENUM *event* sensor (no unit) that reports an
actionable maintenance state (``present``) vs. ``off``/``confirmed`` — Home
Connect salt/rinse-aid/descale/clean events. Trigger: a state_change latch on
``present`` (not a numeric threshold); the task auto-completes when the event
clears. The appliance emitting the clearing event is required for auto-resolve
— otherwise the task waits for a manual completion.
* ``usage_delta`` — a LIFETIME counter with no reset anywhere (printer
usage hours, burner hours, car odometer). Trigger: a counter trigger in
delta mode — fires every N canonical units (hours for operating-time
counters, kilometres for odometers) of accumulated use since the task was
last completed; completing the task re-baselines the counter. No
auto_complete_on_recovery (a lifetime counter never recovers).
* ``runtime_hours`` — the integration exposes NO usage counter at all, only a
STATE entity (a ``lawn_mower`` reporting ``mowing``). The ENGINE accumulates
the time spent in the given states itself (runtime trigger: persisted every
5 min, restart-safe, paused while unavailable) and fires after N accumulated
hours; completing the task resets the accumulation. Signatures of this
direction set ``entity_domain``/``on_states`` and may leave ``keys`` empty —
meaning "the device's single entity of that domain".
* ``alert_above`` — a MEASUREMENT that signals a maintenance condition
while it is high (AMS humidity → desiccant saturated). Trigger: plain
threshold above N in the entity's own unit (no conversion); performing the
maintenance genuinely lowers the value, so auto_complete_on_recovery is
correct here — unlike wear counters, where a plain above-threshold would
re-fire after a manual completion.
* ``value_below`` — the mirror image: a MEASUREMENT that signals the
condition while LOW (heating-loop pressure → refill water). Plain threshold
below N in the entity's own unit; the maintenance raises the value back.
* ``cycle_count`` — the ENGINE counts state transitions itself: a lock has
no wear sensor, but every transition to ``locked`` is one mechanical cycle.
state_change trigger with ``trigger_target_changes = N``; completing the
task resets the counter. No auto-complete (cycles don't recover).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
# Hours a duration-countdown may still hold when the task should trigger.
_DEFAULT_BELOW_HOURS = 24
# Usage-hours a wear counter may accumulate before the task should trigger.
_DEFAULT_ABOVE_HOURS = 100
# Canonical units between services for lifetime counters (usage_delta mode):
# hours for operating-time counters, kilometres for odometers.
_DEFAULT_DELTA_UNITS = 500
# Percent floor for percent-remaining consumables (ink, toner, drum, brush %).
_DEFAULT_BELOW_PERCENT = 10
@dataclass(frozen=True)
class ConsumableSignature:
"""One maintenance task backed by 1..n verified consumable entities."""
keys: tuple[str, ...] # translation_key values (also matched as _<key> entity-id suffix)
task_name: str # EN task name; localized through templates_i18n
direction: str # duration_left | percent_left | usage_above | event_present | usage_delta | runtime_hours
below_hours: int = _DEFAULT_BELOW_HOURS
below_percent: int = _DEFAULT_BELOW_PERCENT
above_hours: int = _DEFAULT_ABOVE_HOURS
delta_units: int = _DEFAULT_DELTA_UNITS
# runtime_hours signatures target a non-sensor STATE entity; empty keys
# then mean "the device's single entity of this domain".
entity_domain: str = "sensor"
on_states: tuple[str, ...] = ()
# runtime signatures may track an ATTRIBUTE instead of the state — a
# climate entity's hvac_action says whether it actually conditions.
attribute: str = ""
# Device-type gates. Some integrations reuse one entity key across ALL
# appliance types (Miele's status sensor) — require_sibling_keys restricts
# the signature to devices that ALSO carry a type-identifying entity
# (a washer has twin_dos/spin_speed). models gates on the device
# registry's model string (case-insensitive substring — Bambu X1C vs A1).
require_sibling_keys: tuple[str, ...] = ()
models: tuple[str, ...] = ()
# Substring exclusion applied AFTER models: ("AMS",) matches "AMS Lite"
# too, so the desiccant duty excludes it explicitly (the Lite has no
# desiccant compartment).
models_exclude: tuple[str, ...] = ()
@dataclass(frozen=True)
class IntegrationSignature:
"""All verified signatures of one integration domain."""
name: str # human-readable integration name
source: str # where the entity keys were verified
# When and against which ref the source was read (branch head at that
# date, not a pinned commit) — the audit trail for "verified against what".
verified: str = ""
tasks: tuple[ConsumableSignature, ...] = field(default_factory=tuple)
def task_name_variants(task_name: str) -> set[str]:
"""The EN signature task name plus all its localizations, lowercased —
used to recognise an equivalent EXISTING task on the target object
regardless of the language it was created in."""
from ...templates_i18n import _T
variants = {task_name.lower()}
variants.update(v.lower() for v in _T.get(task_name, {}).values())
return variants
def _entity_matches(entry: er.RegistryEntry, key: str) -> bool:
"""translation_key match, with an entity_id-suffix fallback for custom
integrations that don't set translation_key on their descriptions.
Third pattern: xiaomi_home embeds the MIoT property name mid-entity_id with
a ``_p_{siid}_{piid}`` tail (``..._filter_life_level_p_4_1``) and sets no
translation_key — matched via the distinctive ``_<key>_p_`` infix. Matching
is already scoped to the signature's integration (entry.platform), so this
cannot bleed across integrations."""
if entry.translation_key == key:
return True
if entry.entity_id.endswith(f"_{key}"):
return True
if f"_{key}_p_" in entry.entity_id:
return True
# Fourth pattern: integrations that name entities WITHOUT a device prefix
# (bosch thermostat: ``sensor.system_pressure``) — exact object-id match.
# Platform scoping keeps this from bleeding across integrations.
return entry.entity_id.split(".", 1)[1] == key
def _entity_unit(hass: HomeAssistant, entry: er.RegistryEntry) -> str | None:
"""The entity's live display unit, falling back to the registry unit."""
state = hass.states.get(entry.entity_id)
if state and (unit := state.attributes.get("unit_of_measurement")):
return str(unit)
reg_unit = entry.unit_of_measurement
return str(reg_unit) if reg_unit is not None else None
def _unit_compatible(direction: str, unit: str | None) -> bool:
"""Whether an entity's unit fits a signature's direction.
Some integrations (LG ThinQ) reuse ONE translation_key for both an
hours-remaining and a percent-remaining sensor; the key alone can't say
which direction applies. A concrete unit disambiguates: percent_left wants
``%``; the duration/counter directions want anything else. The check is
lenient — a missing unit (disabled/just-added entity) never rejects a
key match, so existing single-shape signatures are unaffected. The one
strict case is ``event_present``: ENUM event sensors carry no unit, so a
unit-bearing entity that happens to share the key is NOT an event."""
if direction in ("event_present", "runtime_hours", "cycle_count"):
return unit is None # ENUM events and state entities carry no unit
if direction in ("alert_above", "value_below"):
return True # measurement alert in the entity's own unit (any unit)
if unit is None:
return True
if direction == "percent_left":
return unit == "%"
return unit != "%"
def _threshold_for(sig: ConsumableSignature, hass: HomeAssistant, entity_id: str) -> float:
"""The trigger threshold in the entity's CURRENT display unit.
Duration values are stored in the signature as hours; HA may present the
state in s/min/h/d depending on the entity's unit settings.
"""
if sig.direction == "event_present":
return 0.0 # ENUM event latch — no numeric threshold
if sig.direction in ("runtime_hours", "alert_above", "value_below", "cycle_count"):
# Engine-accumulated hours resp. a raw measurement threshold in the
# entity's own unit — no conversion in either case.
return float(sig.delta_units)
if sig.direction == "percent_left":
return float(sig.below_percent)
state = hass.states.get(entity_id)
unit = (state.attributes.get("unit_of_measurement") if state else None) or "h"
# Canonical → display unit: time counters are stored in hours, odometers in
# kilometres; the entity may display s/min/d resp. miles.
factor = {
"s": 3600.0,
"min": 60.0,
"h": 1.0,
"d": 1 / 24,
"km": 1.0,
"mi": 0.62137,
# energy counters are canonical in kWh (wallbox cable inspection)
"Wh": 1000.0,
"kWh": 1.0,
"MWh": 0.001,
}.get(unit, 1.0)
hours = {
"usage_above": sig.above_hours,
"usage_delta": sig.delta_units,
}.get(sig.direction, sig.below_hours)
return round(hours * factor, 3)
def build_setup_trigger(sig: ConsumableSignature, hass: HomeAssistant, entity_ids: list[str]) -> dict[str, Any]:
"""A pre-wired trigger for one signature's matched entities.
Numeric signatures build a threshold trigger with ``entity_logic: any``
(any low consumable triggers) and auto-complete on recovery — replacing the
consumable resets the countdown/percentage (or, for ``usage_above`` wear
counters, resetting the counter drops it back below the threshold), which
resolves the task just like a cleared problem sensor.
``event_present`` signatures build a single-entity state-change LATCH on the
``present`` state (Home Connect salt/rinse-aid/descale/clean events): the
task activates while the event is present and auto-completes when the
appliance clears it (to ``off``/``confirmed``).
"""
if sig.direction == "event_present":
return {
"type": "state_change",
"entity_id": entity_ids[0], # state latch watches a single entity
"entity_ids": list(entity_ids),
# Home Connect events use "present"; other integrations latch on
# their own alert state (Dolphin filter bag: "full").
"trigger_to_state": sig.on_states[0] if sig.on_states else "present",
"trigger_target_changes": 1,
"auto_complete_on_recovery": True,
}
if sig.direction == "cycle_count":
# Engine-counted mechanical cycles: every transition into on_states[0]
# increments; the task fires at N and completing it resets the count.
return {
"type": "state_change",
"entity_id": entity_ids[0],
"entity_ids": list(entity_ids),
"trigger_to_state": sig.on_states[0],
"trigger_target_changes": int(_threshold_for(sig, hass, entity_ids[0])),
}
if sig.direction == "runtime_hours":
# The engine accumulates the time the entity spends in on_states
# itself (no integration counter needed); completing the task resets
# the accumulation.
runtime_trigger: dict[str, Any] = {
"type": "runtime",
"entity_id": entity_ids[0],
"entity_ids": list(entity_ids),
"trigger_on_states": list(sig.on_states) or ["on"],
"trigger_runtime_hours": _threshold_for(sig, hass, entity_ids[0]),
}
if sig.attribute:
runtime_trigger["attribute"] = sig.attribute
return runtime_trigger
if sig.direction in ("usage_delta", "usage_above"):
# Counter trigger in delta mode for both wear-counter flavours — a
# plain trigger_above threshold would re-fire immediately after a
# manual completion (the counter is still past the mark), whereas the
# delta baseline moves on completion.
# * usage_delta (lifetime counter): baseline = current value at setup;
# the task is due every N units from the adoption/completion point.
# * usage_above (counts since the device's own reset): explicit 0
# baseline keeps absolute semantics at adoption (80 h old blades are
# 80 h old), manual completion re-baselines, and a device-side reset
# drops the value below the baseline — the rollover handling
# re-baselines and the deactivation auto-completes the task.
trigger: dict[str, Any] = {
"type": "counter",
"entity_id": entity_ids[0], # counter watches a single entity
"entity_ids": list(entity_ids),
"trigger_delta_mode": True,
"trigger_target_value": _threshold_for(sig, hass, entity_ids[0]),
}
if sig.direction == "usage_above":
trigger["trigger_baseline_value"] = 0
trigger["auto_complete_on_recovery"] = True
return trigger
threshold_key = (
"trigger_above" if sig.direction == "alert_above" else "trigger_below"
) # value_below + consumables use trigger_below
return {
"type": "threshold",
"entity_ids": list(entity_ids),
threshold_key: _threshold_for(sig, hass, entity_ids[0]),
"entity_logic": "any",
"auto_complete_on_recovery": True,
}
@@ -0,0 +1,30 @@
"""Assembles the full signature catalog from the category data modules."""
from __future__ import annotations
from . import (
air,
cars,
garden,
heating,
home_it,
kitchen,
locks,
personal,
pets,
printers,
transports,
vacuums,
wallboxes,
xiaomi,
)
from ._model import IntegrationSignature
_MODULES = (air, cars, garden, heating, home_it, kitchen, locks, personal, pets, printers, transports, vacuums, wallboxes, xiaomi)
SIGNATURES: dict[str, IntegrationSignature] = {}
for _mod in _MODULES:
for _domain, _sig in _mod.SIGNATURES.items():
if _domain in SIGNATURES: # pragma: no cover — tripwired
raise ValueError(f"duplicate signature domain {_domain!r} in {_mod.__name__}")
SIGNATURES[_domain] = _sig
@@ -0,0 +1,256 @@
"""Air treatment — purifiers, ACs and HRV/ventilation filters.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"hass_dyson": IntegrationSignature(
name="Dyson",
verified="2026-07-18 @ cmgrayb/hass-dyson main",
source=(
"cmgrayb/hass-dyson sensor.py DysonFilterLifeSensor "
"(translation_key 'filter_life' for BOTH hepa and carbon "
"instances, PERCENTAGE) — one any-low task covers both filters."
),
tasks=(ConsumableSignature(("filter_life",), "Replace Filter", "percent_left"),),
),
"dreo": IntegrationSignature(
name="Dreo",
verified="2026-07-18 @ JeffSteinbok/hass-dreo main",
source=(
"JeffSteinbok/hass-dreo sensor.py (translation_key 'filter_life', unit '%', humidifiers with FILTERTIME support)."
),
tasks=(ConsumableSignature(("filter_life",), "Replace Filter", "percent_left"),),
),
"vesync": IntegrationSignature(
name="VeSync (Levoit)",
verified="2026-07-19 @ core/dev vesync/sensor.py",
source="core vesync: tk 'filter_life', PERCENTAGE, MEASUREMENT (Levoit purifiers).",
tasks=(ConsumableSignature(("filter_life",), "Replace Filter", "percent_left"),),
),
"daikin": IntegrationSignature(
name="Daikin AC",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/daikin/climate.py "
"(climate platform verified present; AC-only integration, so the "
"climate entity IS an air conditioner). Runtime on the hvac_action "
"ATTRIBUTE — the state only reports the standby mode. Interval per Daikin's official guidance (clean filters every 2 weeks; ≈100 runtime-hours at typical in-season use)."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=100,
entity_domain="climate",
attribute="hvac_action",
on_states=("cooling", "heating", "fan", "drying"),
),
),
),
"gree": IntegrationSignature(
name="Gree AC",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/gree/climate.py "
"(climate platform verified present; AC-only integration, so the "
"climate entity IS an air conditioner). Runtime on the hvac_action "
"ATTRIBUTE — the state only reports the standby mode. Interval per Daikin's official guidance (clean filters every 2 weeks; ≈100 runtime-hours at typical in-season use)."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=100,
entity_domain="climate",
attribute="hvac_action",
on_states=("cooling", "heating", "fan", "drying"),
),
),
),
"comfoconnect": IntegrationSignature(
name="Zehnder ComfoAirQ",
verified="2026-07-19 @ core/dev comfoconnect/sensor.py",
source=("core comfoconnect: key 'days_to_replace_filter', UnitOfTime.DAYS (name-style, no tk → suffix match)."),
tasks=(
# 168 canonical hours = warn at 7 days remaining (unit 'd' → ÷24).
ConsumableSignature(("days_to_replace_filter",), "Replace Ventilation Filter", "duration_left", below_hours=168),
),
),
"renson": IntegrationSignature(
name="Renson Endura Delta",
verified="2026-07-19 @ core/dev renson/sensor.py",
source="core renson: tk 'filter_change', DURATION, DAYS, MEASUREMENT.",
tasks=(ConsumableSignature(("filter_change",), "Replace Ventilation Filter", "duration_left", below_hours=168),),
),
"philips_airpurifier_coap": IntegrationSignature(
name="Philips AirPurifier (CoAP)",
verified="2026-07-19 @ kongo09/philips-airpurifier-coap master sensor.py+const.py",
source=(
"HACS philips-airpurifier-coap: PhilipsFilterSensor reports "
"PERCENT when the filter total is known, else HOURS remaining — "
"the LG dual-unit pattern, split per direction. tks: pre_filter "
"(cleaning cycle), hepa_filter / active_carbon_filter / "
"nanoprotect_filter (replacements), wick (humidifier "
"evaporation wick)."
),
tasks=(
ConsumableSignature(("pre_filter",), "Filter Cleaning", "percent_left"),
ConsumableSignature(("pre_filter",), "Filter Cleaning", "duration_left", below_hours=72),
ConsumableSignature(
("hepa_filter", "active_carbon_filter", "nanoprotect_filter"),
"Replace Filter",
"percent_left",
),
ConsumableSignature(
("hepa_filter", "active_carbon_filter", "nanoprotect_filter"),
"Replace Filter",
"duration_left",
below_hours=72,
),
# Humidifier models: the evaporation wick (tk 'wick'), same
# dual-unit shape as the filters.
ConsumableSignature(("wick",), "Replace Wick", "percent_left"),
ConsumableSignature(("wick",), "Replace Wick", "duration_left", below_hours=72),
),
),
"dirigera_platform": IntegrationSignature(
name="IKEA DIRIGERA (STARKVIND)",
verified="2026-07-19 @ sanjoyg/dirigera_platform main sensor.py",
source=(
"HACS dirigera_platform: STARKVIND 'Filter Elapsed Time' sensor "
"(suffix filter_elapsed_time, MINUTES, DURATION) counts UP and "
"resets on IKEA's filter-change reset -> usage_above at 4,320 h "
"(= IKEA's 259,200-minute filter lifetime). The sibling "
"'Filter Lifetime' sensor is the constant total — unusable."
),
tasks=(ConsumableSignature(("filter_elapsed_time",), "Replace Filter", "usage_above", above_hours=4320),),
),
"ha_blueair": IntegrationSignature(
name="Blueair",
verified="2026-07-19 @ dahlb/ha_blueair master sensor.py (HACS default)",
source=(
"HACS ha_blueair: name-derived 'Filter Life' / 'Wick Life' / "
"'Water Refresher Life' (PERCENTAGE). Verified % REMAINING: the "
"device-aws coordinator returns 100 - filter_usage_percentage. "
"A filter_expired problem binary also exists (adoption path)."
),
tasks=(
ConsumableSignature(("filter_life",), "Replace Filter", "percent_left"),
ConsumableSignature(("wick_life",), "Replace Wick", "percent_left"),
ConsumableSignature(("water_refresher_life",), "Replace Water Refresher", "percent_left"),
),
),
"coway": IntegrationSignature(
name="Coway IoCare",
verified="2026-07-19 @ robertd502/home-assistant-iocare main sensor.py (HACS default)",
source=(
"HACS coway: name-derived entities, % remaining — 'Pre filter' "
"(AIRMEGA odor variant: 'Charcoal filter') and 'MAX2 filter' "
"(AP-1512HHS EU/UK models: 'HEPA filter'); both name variants "
"listed as keys."
),
tasks=(
ConsumableSignature(("pre_filter", "charcoal_filter"), "Filter Cleaning", "percent_left"),
ConsumableSignature(("max2_filter", "hepa_filter"), "Replace Filter", "percent_left"),
),
),
"winix": IntegrationSignature(
name="Winix",
verified="2026-07-19 @ iprak/winix master sensor.py (HACS default)",
source=(
"HACS winix: tk 'filter_life' (PERCENTAGE) — % remaining derived "
"device-side from filter hours vs the model's "
"filter_alarm_duration."
),
tasks=(ConsumableSignature(("filter_life",), "Replace Filter", "percent_left"),),
),
"duco": IntegrationSignature(
name="Duco ventilation",
verified="2026-07-20 @ home-assistant/core dev",
source=("core duco: tk 'filter_remaining' (DURATION, DAYS) — the box's own filter countdown."),
tasks=(ConsumableSignature(("filter_remaining",), "Replace Ventilation Filter", "duration_left", below_hours=168),),
),
"flexit_bacnet": IntegrationSignature(
name="Flexit Nordic",
verified="2026-07-20 @ home-assistant/core dev",
source=(
"core flexit_bacnet: tk 'air_filter_operating_time' "
"(TOTAL_INCREASING, HOURS — counts UP, reset on filter change) → "
"usage_above at 4380 h (Flexit: change the filter every 6-12 "
"months). An 'air_filter_polluted' problem binary also exists "
"(adoption path)."
),
tasks=(
ConsumableSignature(
("air_filter_operating_time",),
"Replace Ventilation Filter",
"usage_above",
above_hours=4380,
),
),
),
"tradfri": IntegrationSignature(
name="IKEA Trådfri (STARKVIND)",
verified="2026-07-20 @ home-assistant/core dev",
source=(
"core tradfri: tk 'filter_life_remaining' (MEASUREMENT, HOURS "
"remaining) — STARKVIND purifiers on the NATIVE IKEA gateway "
"(the DIRIGERA path is covered separately)."
),
tasks=(ConsumableSignature(("filter_life_remaining",), "Replace Filter", "duration_left", below_hours=72),),
),
"dyson_local": IntegrationSignature(
name="Dyson (local)",
verified="2026-07-20 @ libdyson-wg/ha-dyson main sensor.py (HACS default)",
source=(
"HACS dyson_local (libdyson-wg — the maintained fork): "
"name-derived 'Filter Life' (HOURS remaining) and 'Filter Life "
"Percentage' / 'Carbon Filter Life' / 'HEPA Filter Life' / "
"'Combined Filter Life' (PERCENTAGE, value/4300 h budget). The "
"percent suffixes end in _filter_life too — the unit-aware "
"matcher routes each entity to the right direction (the "
"lg_thinq dual-unit pattern)."
),
tasks=(
# The Pure Cool "combined" sensor is NAMED plain 'Filter Life'
# (suffix _filter_life) but reports PERCENT, while older models'
# 'Filter Life' reports HOURS — the same suffix appears in BOTH
# key tuples and the unit check routes each entity.
ConsumableSignature(
(
"filter_life_percentage",
"carbon_filter_life",
"hepa_filter_life",
"filter_life",
),
"Replace Filter",
"percent_left",
),
ConsumableSignature(("filter_life",), "Replace Filter", "duration_left", below_hours=72),
),
),
"venstar": IntegrationSignature(
name="Venstar thermostat",
verified="2026-07-20 @ home-assistant/core dev",
source=(
"core venstar CONSUMABLE_ENTITIES: key 'filterHours' carries tk "
"'filter_install_time' (HOURS of filter RUNTIME, counts UP, "
"user-reset on change) — 300 h ≈ the typical 1-3-month "
"furnace-filter guidance. The sibling 'filterDays'/tk "
"'filter_usage' (CALENDAR days since install) ships alongside it "
"and is skipped: same duty, weaker wear proxy, and two "
"same-direction signatures of one task name would collide in "
"discovery's per-device dedupe."
),
tasks=(ConsumableSignature(("filter_install_time",), "Replace Filter", "usage_above", above_hours=300),),
),
}
@@ -0,0 +1,255 @@
"""Cars and EVs — odometer-driven service duties.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"kia_uvo": IntegrationSignature(
name="Hyundai / Kia Connect",
verified="2026-07-18 @ Hyundai-Kia-Connect/kia_uvo master",
source=(
"Hyundai-Kia-Connect/kia_uvo custom_components/kia_uvo/sensor.py "
"(translation_key 'odometer', DISTANCE, TOTAL_INCREASING, dynamic "
"km/mi unit). next/last_service_distance exist but their semantics "
"(target vs remaining) are unverified — odometer delta instead."
),
tasks=(
ConsumableSignature(("odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"tesla_custom": IntegrationSignature(
name="Tesla (custom)",
verified="2026-07-18 @ alandtse/tesla dev",
source=(
"alandtse/tesla custom_components/tesla_custom/sensor.py "
"TeslaCarOdometer (type='odometer' → entity_id suffix, no "
"translation_key; DISTANCE, TOTAL_INCREASING, native miles)."
),
tasks=(
ConsumableSignature(("odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"renault": IntegrationSignature(
name="Renault",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/renault/sensor.py "
"(translation_key 'mileage', DISTANCE, TOTAL_INCREASING, km)."
),
tasks=(
ConsumableSignature(("mileage",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("mileage",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"mbapi2020": IntegrationSignature(
name="Mercedes-Benz",
verified="2026-07-18 @ ReneNulschDE/mbapi2020 master",
source=(
"ReneNulschDE/mbapi2020 const.py SENSORS 'odometer' (name "
"'Odometer' → entity_id suffix; attributes carry "
"serviceintervaldays/distance) — lifetime km counter."
),
tasks=(
ConsumableSignature(("odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"vw_eu_data_act": IntegrationSignature(
name="VW Group (EU Data Act)",
verified="2026-07-18 @ mikrohard/hass-vw-eu-data-act main",
source=(
"mikrohard/hass-vw-eu-data-act data.py CuratedSensor('mileage', "
"'Mileage', 'distance', 'km', 'total_increasing') — official EU "
"Data Act portal data for VW/Audi/Škoda/SEAT/Cupra/Bentley (the "
"unofficial WeConnect APIs were locked down upstream)."
),
tasks=(
ConsumableSignature(("mileage",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("mileage",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"subaru": IntegrationSignature(
name="Subaru",
verified="2026-07-18 @ home-assistant/core dev",
source=("home-assistant/core homeassistant/components/subaru/sensor.py (key sc.ODOMETER, translation_key 'odometer')."),
tasks=(
ConsumableSignature(("odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"volvo": IntegrationSignature(
name="Volvo",
verified="2026-07-18 @ home-assistant/core dev",
source=("home-assistant/core homeassistant/components/volvo/sensor.py (key 'odometer', api_field 'odometer')."),
tasks=(
ConsumableSignature(("odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"polestar_api": IntegrationSignature(
name="Polestar",
verified="2026-07-19 @ pypolestar/polestar_api main sensor.py",
source=(
"HACS polestar_api: key 'current_odometer' (native METERS, "
"suggested display KILOMETERS — the unit-aware threshold reads "
"the display unit)."
),
tasks=(
ConsumableSignature(("current_odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("current_odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"fordpass": IntegrationSignature(
name="Ford (FordPass)",
verified="2026-07-19 @ itchannel/fordpass-ha master sensor.py",
source="HACS fordpass: dict-key 'odometer' sensor (name-style, suffix match).",
tasks=(
ConsumableSignature(("odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"toyota": IntegrationSignature(
name="Toyota Connected",
verified="2026-07-19 @ DurgNomis-drol/ha_toyota master sensor.py",
source="HACS toyota: tk 'odometer', DISTANCE, TOTAL_INCREASING.",
tasks=(
ConsumableSignature(("odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"mg_saic": IntegrationSignature(
name="MG/SAIC iSMART",
verified="2026-07-19 @ ad-ha/mg-saic-ha main sensor.py (HACS default)",
source=(
"HACS mg_saic: 'Mileage' sensor (suffix _mileage; the sibling "
"'Mileage Since Last Charge' does not end in _mileage — no clash)."
),
tasks=(
ConsumableSignature(("mileage",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("mileage",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"myskoda": IntegrationSignature(
name="Škoda (MySkoda)",
verified="2026-07-19 @ skodaconnect/homeassistant-myskoda main sensor.py",
source=(
"HACS myskoda: tk 'mileage' (key 'milage', km, TOTAL_INCREASING); "
"tk 'inspection' (DAYS) / 'inspection_in_km' (km) and "
"'oil_service_in_days' / 'oil_service_in_km' — the car's own "
"maintenance_report *_due_in countdowns (remaining until due). "
"The countdown replaces a generic odometer service duty, so no "
"editorial 15000 km interval here."
),
tasks=(
ConsumableSignature(("mileage",), "Tire Rotation", "usage_delta", delta_units=10000),
ConsumableSignature(("inspection",), "Annual Service", "duration_left", below_hours=336),
ConsumableSignature(("inspection_in_km",), "Annual Service", "value_below", delta_units=1000),
ConsumableSignature(("oil_service_in_days",), "Oil Service", "duration_left", below_hours=336),
ConsumableSignature(("oil_service_in_km",), "Oil Service", "value_below", delta_units=1000),
),
),
"audiconnect": IntegrationSignature(
name="Audi Connect",
verified="2026-07-19 @ audiconnect/audi_connect_ha master sensor.py",
source=(
"HACS audiconnect (name-derived entity ids, no translation_key): "
"'Mileage' (km, TOTAL_INCREASING); 'Service inspection time' "
"(days) / 'Service inspection distance' (km) and 'Oil change "
"time' / 'Oil change distance' — VAG API inspectionDue_*/"
"oilServiceDue_* remaining-until countdowns (audi_models.py). "
"Countdowns replace the generic odometer service duty."
),
tasks=(
ConsumableSignature(("mileage",), "Tire Rotation", "usage_delta", delta_units=10000),
ConsumableSignature(("service_inspection_time",), "Annual Service", "duration_left", below_hours=336),
ConsumableSignature(("service_inspection_distance",), "Annual Service", "value_below", delta_units=1000),
ConsumableSignature(("oil_change_time",), "Oil Service", "duration_left", below_hours=336),
ConsumableSignature(("oil_change_distance",), "Oil Service", "value_below", delta_units=1000),
),
),
# The three core Tesla integrations share the same entity pattern:
# translation_key = description key (entity.py `_attr_translation_key =
# self.key`), odometer in native MILES — the unit-aware threshold
# converts. Complements the HACS tesla_custom already covered.
"tesla_fleet": IntegrationSignature(
name="Tesla Fleet",
verified="2026-07-20 @ home-assistant/core dev",
source=("core tesla_fleet: key/tk 'vehicle_state_odometer' (TOTAL_INCREASING, MILES, DISTANCE)."),
tasks=(
ConsumableSignature(("vehicle_state_odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("vehicle_state_odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"teslemetry": IntegrationSignature(
name="Teslemetry",
verified="2026-07-20 @ home-assistant/core dev",
source=("core teslemetry: key/tk 'vehicle_state_odometer' (TOTAL_INCREASING, MILES, DISTANCE)."),
tasks=(
ConsumableSignature(("vehicle_state_odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("vehicle_state_odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"tessie": IntegrationSignature(
name="Tessie",
verified="2026-07-20 @ home-assistant/core dev",
source=("core tessie: key/tk 'vehicle_state_odometer' (TOTAL_INCREASING, MILES, DISTANCE)."),
tasks=(
ConsumableSignature(("vehicle_state_odometer",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("vehicle_state_odometer",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"ituran": IntegrationSignature(
name="Ituran",
verified="2026-07-20 @ home-assistant/core dev",
source=("core ituran: tk 'mileage' (KILOMETERS, DISTANCE) — fleet-tracker odometer."),
tasks=(
ConsumableSignature(("mileage",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("mileage",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"starline": IntegrationSignature(
name="StarLine",
verified="2026-07-20 @ home-assistant/core dev",
source=("core starline: tk 'mileage' (KILOMETERS, TOTAL_INCREASING) — alarm-system odometer."),
tasks=(
ConsumableSignature(("mileage",), "Annual Service", "usage_delta", delta_units=15000),
ConsumableSignature(("mileage",), "Tire Rotation", "usage_delta", delta_units=10000),
),
),
"bosch_ebike": IntegrationSignature(
name="Bosch eBike",
verified="2026-07-20 @ Phil-Barker/hass-bosch-ebike + marq24/ha-bosch-ebike-flow main (HACS default)",
source=(
"HACS bosch_ebike (both forks share the domain and the "
"'total_distance' key/tk, KILOMETERS, TOTAL_INCREASING) — the "
"eBike's odometer. Chain lubrication every ~250 km (bicycle "
"maintenance standard) and a drivetrain service every ~2,000 km "
"(Bosch eBike's service-interval guidance). Odometer delta "
"re-baselines on completion, like the car duties."
),
tasks=(
ConsumableSignature(("total_distance",), "Lubricate Chain", "usage_delta", delta_units=250),
ConsumableSignature(("total_distance",), "Bike Service", "usage_delta", delta_units=2000),
),
),
"stromer": IntegrationSignature(
name="Stromer eBike",
verified="2026-07-20 @ CoMPaTech/stromer main sensor.py (HACS default)",
source=(
"HACS stromer: tk 'total_distance' (KILOMETERS, TOTAL_INCREASING) "
"— the eBike's odometer. Same drivetrain duties as Bosch eBike."
),
tasks=(
ConsumableSignature(("total_distance",), "Lubricate Chain", "usage_delta", delta_units=250),
ConsumableSignature(("total_distance",), "Bike Service", "usage_delta", delta_units=2000),
),
),
}
@@ -0,0 +1,199 @@
"""Robot lawn mowers.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"husqvarna_automower": IntegrationSignature(
name="Husqvarna Automower",
verified="2026-07-17 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/husqvarna_automower/sensor.py "
"(translation_key 'cutting_blade_usage_time', DURATION s→h; matching reset button exists)"
),
tasks=(
ConsumableSignature(("cutting_blade_usage_time",), "Replace Blades", "usage_above"),
# Lifetime statistics sensors (SECONDS, suggested h) carry two more
# duties: undercarriage washing by mowing time, contact cleaning by
# docking cycles (unitless counter -> delta target is the count).
ConsumableSignature(("total_cutting_time",), "Clean Undercarriage", "usage_delta", delta_units=25),
ConsumableSignature(
("number_of_charging_cycles",),
"Clean Charging Contacts",
"usage_delta",
delta_units=100,
),
),
),
"landroid_cloud": IntegrationSignature(
name="Worx Landroid",
verified="2026-07-17 @ MTrab/landroid_cloud master",
source=(
"MTrab/landroid_cloud custom_components/landroid_cloud/sensor.py "
"(translation_key 'blade_runtime_current' — since last reset, DURATION min→h)"
),
tasks=(
ConsumableSignature(("blade_runtime_current",), "Replace Blades", "usage_above"),
ConsumableSignature(("mower_runtime_total",), "Clean Undercarriage", "usage_delta", delta_units=25),
),
),
"gardena_smart_system": IntegrationSignature(
name="Gardena Smart System",
verified="2026-07-18 @ py-smart-gardena/hass-gardena-smart-system master",
source=(
"py-smart-gardena/hass-gardena-smart-system sensor.py "
"GardenaMowerOperatingHoursSensor (entity_id "
"'{device.id}_{service.id}_operating_hours', UnitOfTime.HOURS, "
"TOTAL_INCREASING lifetime — no reset anywhere) → usage_delta."
),
tasks=(
# Sileno mowers: pivoting razor blades wear by mowing time — every
# 100 operating hours since the last change (delta re-baselines on
# completion, matching the Husqvarna default).
ConsumableSignature(("operating_hours",), "Replace Blades", "usage_delta", delta_units=100),
ConsumableSignature(("operating_hours",), "Clean Undercarriage", "usage_delta", delta_units=25),
),
),
# Same source entity, second duty — the matcher allows multi-duty.
"navimow": IntegrationSignature(
name="Segway Navimow",
verified="2026-07-18 @ pgoutsos/NavimowHA main",
source=(
"pgoutsos/NavimowHA lawn_mower.py (one LawnMower entity per "
"device) + const.py MOWER_STATUS_TO_ACTIVITY ('mowing'"
"LawnMowerActivity.MOWING). The integration exposes NO usage "
"counter — the ENGINE accumulates mowing time itself via the "
"runtime trigger on the lawn_mower entity."
),
tasks=(
ConsumableSignature(
(),
"Replace Blades",
"runtime_hours",
delta_units=100,
entity_domain="lawn_mower",
on_states=("mowing",),
),
ConsumableSignature(
(),
"Clean Undercarriage",
"runtime_hours",
delta_units=25,
entity_domain="lawn_mower",
on_states=("mowing",),
),
),
),
"sunseeker": IntegrationSignature(
name="Sunseeker mowers",
verified="2026-07-20 @ Sdahl1234/Sunseeker-lawn-mower main sensor.py (HACS default)",
source=(
"HACS sunseeker (also Ambrogio/Techline via ZCS): REAL blade-wear "
"sensors — 'Blade time left' / 'Cutterplade time left' / 'Small "
"blade time left' (UnitOfTime.HOURS remaining, tk "
"sunseeker_*_time_left) and the matching '*_health' (PERCENTAGE "
"remaining). Dual-unit like the LG filter: hours→duration_left, "
"percent→percent_left, both the one blade-replacement duty."
),
tasks=(
ConsumableSignature(
(
"blade_time_left",
"cutterplade_time_left",
"small_blade_time_left",
"sunseeker_blade_time_left",
"sunseeker_cutterplade_time_left",
"sunseeker_small_blade_time_left",
),
"Replace Blades",
"duration_left",
below_hours=24,
),
ConsumableSignature(
(
"blade_health",
"cutterplade_health",
"small_blade_health",
"sunseeker_blade_health",
"sunseeker_cutterplade_health",
"sunseeker_small_blade_health",
),
"Replace Blades",
"percent_left",
),
),
),
"husqvarna_automower_ble": IntegrationSignature(
name="Husqvarna Automower BLE",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/husqvarna_automower_ble/"
"lawn_mower.py (lawn_mower entity; the BLE variant exposes no blade "
"counter) — the ENGINE accumulates mowing time."
),
tasks=(
ConsumableSignature(
(),
"Replace Blades",
"runtime_hours",
delta_units=100,
entity_domain="lawn_mower",
on_states=("mowing",),
),
ConsumableSignature(
(),
"Clean Undercarriage",
"runtime_hours",
delta_units=25,
entity_domain="lawn_mower",
on_states=("mowing",),
),
),
),
"rainbird": IntegrationSignature(
name="Rain Bird irrigation",
verified="2026-07-20 @ home-assistant/core dev",
source=(
"core rainbird: switch.py creates one irrigation-zone switch per "
"zone, each on its own 'Rain Bird Sprinkler <n>' device (single "
"switch entity per device). No consumable sensors; the rainsensor "
"binary carries no device_class (not adoptable) and raindelay is "
"operational — the ENGINE accumulates actual watering time on the "
"zone switch instead. Cadence per Rain Bird's maintenance "
"guidance: inspect/clean heads and (drip) filters at least once a "
"season — ~30 h of watering at typical in-season use."
),
tasks=(
ConsumableSignature(
(),
"Clean Sprinkler Heads",
"runtime_hours",
delta_units=30,
entity_domain="switch",
on_states=("on",),
),
),
),
# Pool chlorinator salt: refill when the water's salt concentration
# drops below the generator's operating band (Pentair: 2600-4500 ppm;
# low-salt cells stop producing chlorine). Topping up raises the value
# back — auto-resolve.
"screenlogic": IntegrationSignature(
name="Pentair ScreenLogic",
verified="2026-07-20 @ home-assistant/core dev",
source=("core screenlogic: tk 'salt_ppm' (MEASUREMENT, ppm) — IntelliChlor salt concentration."),
tasks=(ConsumableSignature(("salt_ppm",), "Refill Pool Salt", "value_below", delta_units=2700),),
),
"ondilo_ico": IntegrationSignature(
name="Ondilo ICO",
verified="2026-07-20 @ home-assistant/core dev",
source=("core ondilo_ico: tk 'salt' (mg/L ≡ ppm numerically) — pool salt concentration."),
tasks=(ConsumableSignature(("salt",), "Refill Pool Salt", "value_below", delta_units=2700),),
),
}
@@ -0,0 +1,214 @@
"""Boilers, heating & water treatment.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"vicare": IntegrationSignature(
name="Viessmann ViCare",
verified="2026-07-17 (filter) / 2026-07-18 (burner) @ home-assistant/core dev + openviess/PyViCare master",
source=(
"home-assistant/core homeassistant/components/vicare/sensor.py "
"(GLOBAL_SENSORS translation_key 'filter_remaining_hours', UnitOfTime.HOURS, "
"disabled-by-default; PyViCare ventilation.filter.runtime.remainingHours; "
"BURNER_SENSORS/COMPRESSOR_SENSORS 'burner_hours'/'compressor_hours', "
"UnitOfTime.HOURS, TOTAL_INCREASING lifetime → usage_delta)."
),
tasks=(
ConsumableSignature(("filter_remaining_hours",), "Replace Filter", "duration_left"),
# Boiler/heat-pump service by accumulated operating hours since the
# last service — the counters are lifetime (no reset), which is
# exactly what the delta-baseline trigger models.
ConsumableSignature(
("burner_hours", "compressor_hours"),
"Annual Inspection",
"usage_delta",
delta_units=2000,
),
),
),
"bosch": IntegrationSignature(
name="Bosch/Buderus heating",
verified="2026-07-18 @ bosch-thermostat/home-assistant-bosch-custom-component master + live RC300 registry",
source=(
"bosch-thermostat custom component: sensors are DYNAMIC (named "
"from the device's XMPP data, sensor/base.py builds names without "
"a device prefix → entity ids like sensor.system_pressure; no "
"translation_keys). Key verified against a live Buderus RC300 "
"registry; matched via the exact-object-id pattern."
),
tasks=(
# Heating-loop pressure: refill water when it drops below 1 bar;
# topping up raises the value back (auto-resolve).
ConsumableSignature(("system_pressure",), "Refill Heating Water", "value_below", delta_units=1),
),
),
# ─── Research round 4 (2026-07-19): boiler pressure, HRV filters, ───
# ─── purifiers, espresso, pet tech, Klipper ─────────────────────────
"opentherm_gw": IntegrationSignature(
name="OpenTherm Gateway",
verified="2026-07-19 @ core/dev opentherm_gw/sensor.py",
source=(
"core opentherm_gw: tk 'central_heating_pressure', BAR, MEASUREMENT — generic for EVERY OpenTherm-connected boiler."
),
tasks=(ConsumableSignature(("central_heating_pressure",), "Refill Heating Water", "value_below", delta_units=1),),
),
"plugwise": IntegrationSignature(
name="Plugwise (Anna/Adam)",
verified="2026-07-19 @ core/dev plugwise/sensor.py",
source="core plugwise: tk 'water_pressure', BAR, MEASUREMENT (boiler loop).",
tasks=(ConsumableSignature(("water_pressure",), "Refill Heating Water", "value_below", delta_units=1),),
),
"incomfort": IntegrationSignature(
name="Intergas InComfort",
verified="2026-07-19 @ core/dev incomfort/sensor.py",
source=(
"core incomfort: key 'cv_pressure', BAR, MEASUREMENT. NOTE: "
"entity_registry_enabled_default=False — the suggestion appears "
"once the user enables the sensor."
),
tasks=(ConsumableSignature(("cv_pressure",), "Refill Heating Water", "value_below", delta_units=1),),
),
"atag": IntegrationSignature(
name="ATAG One",
verified="2026-07-19 @ core/dev atag/sensor.py",
source=(
"core atag: legacy name-based sensors ('CH Water Pressure'"
"object id ch_water_pressure), BAR — matched via suffix or the "
"exact-object-id pattern."
),
tasks=(ConsumableSignature(("ch_water_pressure",), "Refill Heating Water", "value_below", delta_units=1),),
),
"bwt_perla": IntegrationSignature(
name="BWT Perla",
verified="2026-07-19 @ dkarv/ha-bwt-perla main sensor.py (HACS default)",
source=(
"HACS bwt_perla (dkarv): tk 'regenerativ_level' (salt reserve, "
"PERCENTAGE) and tk 'regenerativ_days' (days of salt left, "
"UnitOfTime.DAYS) — refilling raises both (auto-resolve)."
),
tasks=(
ConsumableSignature(("regenerativ_level",), "Refill Softener Salt", "percent_left"),
ConsumableSignature(("regenerativ_days",), "Refill Softener Salt", "duration_left", below_hours=168),
),
),
"ecowater_softener": IntegrationSignature(
name="EcoWater softener",
verified="2026-07-19 @ barleybobs/homeassistant-ecowater-softener master (HACS default)",
source=(
"HACS ecowater_softener (barleybobs): key 'salt_level_percentage' "
"(PERCENTAGE) and key 'out_of_salt_days' (days) — name-style "
"entities, suffix-matched."
),
tasks=(
ConsumableSignature(("salt_level_percentage",), "Refill Softener Salt", "percent_left"),
ConsumableSignature(("out_of_salt_days",), "Refill Softener Salt", "duration_left", below_hours=168),
),
),
"wolflink": IntegrationSignature(
name="Wolf SmartSet",
verified="2026-07-19 @ core/dev wolflink/sensor.py",
source="core wolflink: key 'pressure', BAR (heating loop).",
tasks=(ConsumableSignature(("pressure",), "Refill Heating Water", "value_below", delta_units=1),),
),
"palazzetti": IntegrationSignature(
name="Palazzetti pellet stove",
verified="2026-07-19 @ core/dev palazzetti/sensor.py",
source=(
"core palazzetti: tk 'pellet_quantity' (KILOGRAMS consumed, "
"cumulative) -> usage_delta; ash-pan cadence ~100 kg of pellets "
"(editorial: roughly weekly in season; manuals prescribe "
"calendar-based cleaning). 'pellet_level' is a CM tank gauge — "
"inventory, not wear; skipped."
),
tasks=(ConsumableSignature(("pellet_quantity",), "Empty Ash Pan", "usage_delta", delta_units=100),),
),
"mypyllant": IntegrationSignature(
name="Vaillant (myVAILLANT)",
verified="2026-07-19 @ signalkraft/mypyllant-component main sensor.py",
source=(
"HACS mypyllant: SystemWaterPressureSensor (name '... System "
"Water Pressure' -> suffix system_water_pressure) and the "
"device-level operational-data variant (suffix water_pressure), "
"both BAR — matched any-low."
),
tasks=(
ConsumableSignature(
("system_water_pressure", "water_pressure"),
"Refill Heating Water",
"value_below",
delta_units=1,
),
),
),
"grohe_smarthome": IntegrationSignature(
name="Grohe Blue",
verified="2026-07-19 @ flo-schilli/ha-grohe_smarthome main (HACS default)",
source=(
"HACS grohe_smarthome, yaml-driven name-derived entities "
"(config/config.yaml, GroheBlueHome/GroheBlueProf): 'Remaining "
"Filter' (%) and 'Remaining CO2' (%) — reset by the device's own "
"filter/CO2 reset commands. The sibling 'Remaining Filter (App)' "
"slugs to _remaining_filter_app and cannot clash with the exact "
"_remaining_filter suffix."
),
tasks=(
ConsumableSignature(("remaining_filter",), "Replace Water Filter", "percent_left"),
ConsumableSignature(("remaining_co2",), "Replace CO2 Bottle", "percent_left"),
),
),
"iqua_softener": IntegrationSignature(
name="iQua softener",
verified="2026-07-20 @ mutilator/homeassistant-iqua-softener master sensor.py (HACS default)",
source=(
"HACS iqua_softener: 'Salt level' (PERCENTAGE, name-style -> "
"suffix _salt_level). 'Out of salt estimated day' is a DATE "
"sensor - parked for the date direction."
),
tasks=(ConsumableSignature(("salt_level",), "Refill Softener Salt", "percent_left"),),
),
"fumis": IntegrationSignature(
name="Fumis (pellet stoves)",
verified="2026-07-20 @ home-assistant/core dev",
source=(
"core fumis: tk 'time_to_service' (DURATION, HOURS remaining) — "
"the controller's own service countdown for Fumis-driven pellet "
"stoves/boilers."
),
tasks=(ConsumableSignature(("time_to_service",), "Annual Service", "duration_left"),),
),
"rehlko": IntegrationSignature(
name="Rehlko / Kohler generators",
verified="2026-07-20 @ home-assistant/core dev",
source=(
"core rehlko: tk 'runtime_since_last_maintenance' (HOURS since "
"the last maintenance, resets when maintenance is recorded) → "
"usage_above at 100 h — Kohler's oil-change interval (every "
"100 run-hours or annually). An 'oil_pressure' problem binary "
"also exists (adoption path)."
),
tasks=(ConsumableSignature(("runtime_since_last_maintenance",), "Oil Service", "usage_above", above_hours=100),),
),
"aquacell": IntegrationSignature(
name="AquaCell softener",
verified="2026-07-20 @ home-assistant/core dev",
source=(
"core aquacell: tk 'salt_left_side_percentage' and "
"'salt_right_side_percentage' (PERCENTAGE) — dual salt tanks, "
"matched any-low."
),
tasks=(
ConsumableSignature(
("salt_left_side_percentage", "salt_right_side_percentage"),
"Refill Softener Salt",
"percent_left",
),
),
),
}
@@ -0,0 +1,34 @@
"""NAS & home IT.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"synology_dsm": IntegrationSignature(
name="Synology NAS",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/synology_dsm/sensor.py "
"STORAGE_VOL_SENSORS (translation_key 'volume_percentage_used', "
"PERCENTAGE). Disk-health thresholds ship as device_class: safety "
"binaries → covered by problem-sensor adoption (widened to safety)."
),
tasks=(ConsumableSignature(("volume_percentage_used",), "Storage Cleanup", "alert_above", delta_units=85),),
),
"qnap": IntegrationSignature(
name="QNAP NAS",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/qnap/sensor.py "
"(translation_key 'volume_percentage_used', PERCENTAGE — same key "
"shape as synology_dsm)."
),
tasks=(ConsumableSignature(("volume_percentage_used",), "Storage Cleanup", "alert_above", delta_units=85),),
),
}
@@ -0,0 +1,263 @@
"""Kitchen & household appliances incl. espresso machines.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"lg_thinq": IntegrationSignature(
name="LG ThinQ",
verified="2026-07-17 @ home-assistant/core dev + thinq-connect/pythinqconnect main",
source=(
"home-assistant/core homeassistant/components/lg_thinq/sensor.py "
"(ThinQProperty StrEnum translation_key; FILTER_LIFETIME is shared by "
"an HOURS description and a PERCENTAGE one — the unit-aware matcher "
"routes each entity to the right direction) + "
"thinq-connect/pythinqconnect devices/const.py Property members"
),
tasks=(
# AC filter reports hours-remaining; air-purifier/RAC filters report
# percent — both under translation_key 'filter_lifetime'. Two
# directions, unit-disambiguated at match time.
ConsumableSignature(("filter_lifetime", "top_filter_remain_percent"), "Replace Filter", "percent_left"),
ConsumableSignature(("filter_lifetime",), "Replace Filter", "duration_left"),
ConsumableSignature(
(
"water_filter_1_remain_percent",
"water_filter_2_remain_percent",
"water_filter_3_remain_percent",
),
"Replace Water Filter",
"percent_left",
),
),
),
"smartthinq_sensors": IntegrationSignature(
name="LG ThinQ (SmartThinQ)",
verified="2026-07-17 / re-audited 2026-07-20 @ ollo69/ha-smartthinq-sensors master",
source=(
"ollo69/ha-smartthinq-sensors custom_components/smartthinq_sensors/sensor.py "
"(legacy name= entities, NO translation_key → matched by entity_id suffix; "
"FILTER_*_LIFE / *_REMAIN_PERC are percent via wideq device.py "
"_get_filter_life(); TUBCLEAN_COUNT counts up per wash cycle and the "
"machine resets it when a tub-clean course runs). binary_sensor.py: "
"dishwasher RINSEREFILL/SALTREFILL binaries carry NO device_class "
"(and are disabled-by-default) → not adoptable, latched here "
"instead; the washer DETERGENTLOW/SOFTENERLOW binaries ARE "
"problem-class (adoption path)."
),
tasks=(
ConsumableSignature(
(
"filter_remaining_life",
"filter_remaining_life_main",
"filter_remaining_life_bottom",
"filter_remaining_life_dust",
"filter_remaining_life_middle",
"filter_remaining_life_top",
"fresh_air_filter_remaining",
),
"Replace Filter",
"percent_left",
),
ConsumableSignature(("water_filter_remaining",), "Replace Water Filter", "percent_left"),
# Unitless wash-cycle counter: above_hours here is the cycle count
# (~monthly cadence); resetting on a tub-clean course resolves it.
ConsumableSignature(("tub_clean_counter",), "Clean Tub", "usage_above", above_hours=30),
# Dishwasher refill alerts: plain binaries (no problem class) →
# state latch; the appliance clearing them after a refill resolves
# the task. Enable the entities first (disabled-by-default).
ConsumableSignature(
("rinse_refill",),
"Refill Rinse Aid",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
ConsumableSignature(
("salt_refill",),
"Refill Salt",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
),
),
# Enclosed printers only (device registry model = the device_type
# enum: X1C/X1E/P1S/H2*) — the activated-carbon/chamber filter
# duty makes no sense on open-frame A1/A1MINI/P1P.
# Model-aware duties (Bambu maintenance guides): the CoreXY
# X1/P1 series runs on carbon rods that want regular wipe-downs;
# the A1 bed-slingers have a replaceable purge wiper. Intervals
# are tunable print-hour defaults.
# AMS desiccant by MEASURED humidity: the AMS/AMS 2 Pro/AMS HT are
# separate devices (model = 'AMS'/'AMS 2 Pro'/'AMS HT') with a
# humidity sensor; saturated desiccant shows as high humidity and
# replacing it brings the value down (auto-resolve). The AMS Lite
# has NO desiccant compartment and is excluded.
"home_connect": IntegrationSignature(
name="Home Connect",
verified="2026-07-17 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/home_connect/sensor.py "
"EVENT_SENSORS (HomeConnectEventSensor, device_class ENUM, "
"EVENT_OPTIONS ['confirmed','off','present']; translation_key per "
"EventKey). No percent/countdown consumables exist (coffee counters "
"are lifetime, no reset) — these actionable events are the only "
"maintenance-usable signal, matched as a state latch on 'present'."
),
tasks=(
ConsumableSignature(("salt_nearly_empty",), "Refill Salt", "event_present"),
ConsumableSignature(("rinse_aid_nearly_empty",), "Refill Rinse Aid", "event_present"),
ConsumableSignature(("device_should_be_descaled",), "Descale Appliance", "event_present"),
ConsumableSignature(("device_should_be_cleaned",), "Clean Appliance", "event_present"),
ConsumableSignature(("grease_filter_max_saturation_reached",), "Clean Grease Filter", "event_present"),
),
),
"miele": IntegrationSignature(
name="Miele",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/miele/sensor.py "
"(dishwasher salt_level/rinse_aid_level/power_disk_level PERCENTAGE "
"fill levels; washer twin_dos_1/2_level PERCENTAGE detergent "
"containers). Coffee descaling/degreasing counters are lifetime "
"tallies of PERFORMED maintenance — unclear delta semantics, "
"skipped."
),
tasks=(
ConsumableSignature(("salt_level",), "Refill Salt", "percent_left"),
ConsumableSignature(("rinse_aid_level",), "Refill Rinse Aid", "percent_left"),
# PowerDisk (dishwasher AutoDos) and TwinDos (washer) are both
# detergent reservoirs — one any-low task per device.
ConsumableSignature(
("power_disk_level", "twin_dos_1_level", "twin_dos_2_level"),
"Refill Detergent",
"percent_left",
),
ConsumableSignature(
("status",),
"Clean Tub",
"runtime_hours",
delta_units=60,
on_states=("in_use",),
require_sibling_keys=("twin_dos_1_level", "twin_dos_2_level", "spin_speed"),
),
),
),
"electrolux_status": IntegrationSignature(
name="Electrolux / AEG",
verified="2026-07-18 @ albaintor/homeassistant_electrolux_status master",
source=(
"albaintor/homeassistant_electrolux_status catalog_purifier.py "
"'FilterLife' (PERCENTAGE) + entity.py entity_id = "
"f'..._{entity_attr}' — HA slugifies the raw 'FilterLife' tail, so "
"both slug forms are matched."
),
tasks=(ConsumableSignature(("filterlife", "filter_life"), "Replace Filter", "percent_left"),),
),
"midea_ac_lan": IntegrationSignature(
name="Midea (LAN)",
verified="2026-07-18 @ wuwentao/midea_ac_lan master",
source=(
"wuwentao/midea_ac_lan midea_devices.py + midea_entity.py "
"(_attr_translation_key from the per-attribute config; entity_id = "
"f'{device_id}_{entity_key}'). 0xED water purifier: filter1/2/3_life "
"PERCENTAGE; 0xC2: filter_life PERCENTAGE. The filterN_days "
"countdowns describe the SAME filters — percent only, no duplicate "
"tasks. Filter cleaning/change reminders (A1/CE/AC full_dust) are "
"device_class problem binaries — covered by problem-sensor adoption."
),
tasks=(
ConsumableSignature(
("filter1_life", "filter2_life", "filter3_life"),
"Replace Water Filter",
"percent_left",
),
ConsumableSignature(("filter_life",), "Replace Filter", "percent_left"),
),
),
"lamarzocco": IntegrationSignature(
name="La Marzocco",
verified="2026-07-19 @ core/dev lamarzocco/sensor.py",
source=(
"core lamarzocco: tk 'total_coffees_made', TOTAL_INCREASING "
"lifetime shot counter — one entity, two duties (intervals are "
"intervals cross-checked 2026-07-19 against home-barista guidance: detergent backflush every 4-6 weeks ≈ 100 shots at 3/day; "
"water filter ≈ 1000 shots (editorial)."
),
tasks=(
ConsumableSignature(("total_coffees_made",), "Backflush Espresso Group", "usage_delta", delta_units=100),
ConsumableSignature(("total_coffees_made",), "Replace Water Filter", "usage_delta", delta_units=1000),
),
),
"hon": IntegrationSignature(
name="Haier hOn (Haier/Candy/Hoover)",
verified="2026-07-19 @ Andre0512/hon main sensor.py (1.5k stars; open #101 ask)",
source=(
"HACS hon: purifiers (type AP) expose tk 'filter_life' (main "
"filter, %) and tk 'filter_cleaning' (pre-filter, %); washers "
"(WM/WD) expose tk 'cycles_total' (lifetime wash-cycle counter) — "
"tub-clean cadence reuses LG's manufacturer value of 30 cycles."
),
tasks=(
ConsumableSignature(("filter_life",), "Replace Filter", "percent_left"),
ConsumableSignature(("filter_cleaning",), "Filter Cleaning", "percent_left"),
ConsumableSignature(("cycles_total",), "Clean Tub", "usage_delta", delta_units=30),
),
),
"whirlpool": IntegrationSignature(
name="Whirlpool",
verified="2026-07-19 @ core/dev whirlpool/sensor.py",
source=(
"core whirlpool: tk 'washer_state' ENUM incl. 'running_maincycle' "
"— no cycle counter exists, so the ENGINE accumulates wash time "
"(the Miele Clean-Tub pattern; 60 h of washing ~= LG's 30-cycle "
"cadence at a typical 2-h cycle). The dryer's distinct "
"'dryer_state' tk cannot match."
),
tasks=(
ConsumableSignature(
("washer_state",),
"Clean Tub",
"runtime_hours",
delta_units=60,
on_states=("running_maincycle",),
),
),
),
"ha_washdata": IntegrationSignature(
name="WashData (smart-plug cycles)",
verified="2026-07-20 @ 3dg1luk43/ha_washdata main sensor.py (HACS default)",
source=(
"HACS ha_washdata: tk 'cycle_count' (unit 'cycles') — lifetime "
"count of appliance cycles DETECTED from smart-plug power "
"monitoring. Brings the tub-clean cadence to washers with no "
"smarts at all (LG's official 30-cycle interval)."
),
tasks=(ConsumableSignature(("cycle_count",), "Clean Tub", "usage_delta", delta_units=30),),
),
"traeger": IntegrationSignature(
name="Traeger grill",
verified="2026-07-20 @ njobrien1006/hass_traeger master + johnvoipguy/Traeger-WiFire main (HACS default, shared sensor map)",
source=(
"HACS traeger (both default-store forks share the domain and "
"sensor map): 'Cook Cycle' sensor (usage;cook_cycles — lifetime "
"counter, suffix _cook_cycle, disabled-by-default DIAGNOSTIC; the "
"suggestion appears once the user enables it). Cadence per "
"Traeger's official maintenance guidance: grease management every "
"few cooks, deep clean ~every 20 cooks / twice a grilling season. "
"'Pellet Level' (%) is hopper inventory, not wear — skipped (same "
"rationale as Palazzetti's pellet_level)."
),
tasks=(
ConsumableSignature(("cook_cycle",), "Clean Grease Trap", "usage_delta", delta_units=5),
ConsumableSignature(("cook_cycle",), "Clean Appliance", "usage_delta", delta_units=20),
),
),
}
@@ -0,0 +1,240 @@
"""Smart locks — cycle-count lubrication duties.
Interval audit 2026-07-19: Nuki's official guidance is ANNUAL cylinder
lubrication — 2,000 cycles ≈ a year at a typical main door (5-6
cycles/day); heavy doors reach it sooner, matching wear.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"nuki": IntegrationSignature(
name="Nuki Smart Lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/nuki/lock.py "
"(NukiLockEntity, one lock entity per device). No wear sensor — "
"the ENGINE counts locking cycles on the lock entity."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"tedee": IntegrationSignature(
name="Tedee Smart Lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/tedee/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"august": IntegrationSignature(
name="August lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/august/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"yale": IntegrationSignature(
name="Yale lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/yale/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"switchbot": IntegrationSignature(
name="SwitchBot Lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/switchbot/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"loqed": IntegrationSignature(
name="LOQED Smart Lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/loqed/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"homematicip_cloud": IntegrationSignature(
name="Homematic IP lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/homematicip_cloud/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"schlage": IntegrationSignature(
name="Schlage lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/schlage/lock.py "
"(lock platform verified present) — engine-counted locking cycles."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"sesame": IntegrationSignature(
name="Sesame lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/sesame/lock.py "
"(lock platform verified present) — engine-counted locking cycles."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"yalexs_ble": IntegrationSignature(
name="Yale/August BLE lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/yalexs_ble/lock.py "
"(lock platform verified present) — engine-counted locking cycles."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"dormakaba_dkey": IntegrationSignature(
name="dormakaba dKey lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/dormakaba_dkey/lock.py "
"(lock platform verified present) — engine-counted locking cycles."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"homematic": IntegrationSignature(
name="Homematic KeyMatic",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/homematic/lock.py "
"(lock platform verified present) — engine-counted locking cycles, entity_domain-gated so the hub's other device types are untouched."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
}
@@ -0,0 +1,33 @@
"""Personal-care devices.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"oralb": IntegrationSignature(
name="Oral-B toothbrush",
verified="2026-07-19 @ core/dev oralb/sensor.py",
source=(
"core oralb (BLE): tk 'toothbrush_state', ENUM incl. 'running'"
"the engine accumulates brushing time (the per-session 'time' "
"sensor is session-scoped, unusable for deltas). 6 h of brushing "
"= the dentist's 3 months at 2x2 minutes a day. BLE gaps pause "
"the runtime trigger; brushing only happens while connected."
),
tasks=(
ConsumableSignature(
("toothbrush_state",),
"Replace Brush Head",
"runtime_hours",
delta_units=6,
on_states=("running",),
),
),
),
}
@@ -0,0 +1,52 @@
"""Pet tech — feeders, fountains, litter boxes.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"petkit": IntegrationSignature(
name="PetKit",
verified="2026-07-19 @ Jezza34000/homeassistant_petkit main sensor.py",
source=(
"HACS petkit (Jezza34000): tk 'desiccant_left_days' (feeder, "
"UnitOfTime.DAYS) and tk 'filter_percent' (water fountain, "
"PERCENTAGE)."
),
tasks=(
# 48 canonical hours = warn at 2 days of desiccant left.
ConsumableSignature(("desiccant_left_days",), "Replace Desiccant", "duration_left", below_hours=48),
ConsumableSignature(("filter_percent",), "Replace Water Filter", "percent_left"),
),
),
"litterrobot": IntegrationSignature(
name="Litter-Robot",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/litterrobot/sensor.py "
"(waste_drawer_level tk 'waste_drawer' % FULL -> alert_above; "
"litter_level tk 'litter_level' % remaining (LR4/5) -> "
"percent_left; total_cycles lifetime counter -> usage_delta)."
),
tasks=(
ConsumableSignature(("waste_drawer",), "Empty Waste Drawer", "alert_above", delta_units=90),
ConsumableSignature(("litter_level",), "Refill Litter", "percent_left"),
ConsumableSignature(("total_cycles",), "Wash Litter Box", "usage_delta", delta_units=150),
),
),
"eheimdigital": IntegrationSignature(
name="EHEIM Digital (aquarium)",
verified="2026-07-20 @ home-assistant/core dev",
source=(
"core eheimdigital: tk 'service_hours' (DURATION, HOURS remaining "
"to the next filter service, suggested display DAYS) — the "
"filter's own service countdown."
),
tasks=(ConsumableSignature(("service_hours",), "Filter Cleaning", "duration_left"),),
),
}
@@ -0,0 +1,163 @@
"""2D and 3D printers incl. Klipper via Moonraker.
Threshold audit 2026-07-19: the AMS desiccant trigger targets the REAL
percentage humidity sensor (tk 'humidity', PERCENTAGE + HUMIDITY class,
exists only on hygrometer-equipped AMS units via Features.AMS_HUMIDITY) —
NOT the 1-5 'humidity_index' scale sensor (distinct tk; an endswith
'_humidity' match cannot hit '_humidity_index' either). Bambu publishes
no official RH threshold (their desiccant status is color-based), so
>40 % RH stays an editorial trip point: fresh desiccant holds an AMS at
~10-20 % RH, 40 % means it is spent.
Interval audit 2026-07-19: rail/rod lubrication follows Prusa's OFFICIAL
200-print-hour maintenance interval (octoprint/prusalink); Bambu's 500 h
stays editorial (different motion system, wiki unfetchable).
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"bambu_lab": IntegrationSignature(
name="Bambu Lab",
verified="2026-07-18 @ greghesp/ha-bambulab main",
source=(
"greghesp/ha-bambulab definitions.py (key/translation_key "
"'total_usage_hours', UnitOfTime.HOURS, TOTAL_INCREASING lifetime "
"usage → usage_delta every 500 print-hours; filament remaining is "
"only a tray-sensor attribute and hms/print_error are device_class "
"problem → problem-sensor adoption)."
),
tasks=(
ConsumableSignature(
("total_usage_hours",),
"Lubricate Rails and Rods",
"usage_delta",
delta_units=500,
),
ConsumableSignature(
("total_usage_hours",),
"Replace Filter",
"usage_delta",
delta_units=300,
models=("X1C", "X1E", "P1S", "H2"),
),
ConsumableSignature(
("total_usage_hours",),
"Clean Carbon Rods",
"usage_delta",
delta_units=100,
models=("X1", "P1S", "P1P"),
),
ConsumableSignature(
("total_usage_hours",),
"Replace Purge Wiper",
"usage_delta",
delta_units=300,
models=("A1",),
),
ConsumableSignature(
("humidity",),
"Replace Desiccant",
"alert_above",
delta_units=40,
models=("AMS",),
models_exclude=("AMS Lite",),
),
),
),
"octoprint": IntegrationSignature(
name="OctoPrint",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/octoprint/"
"binary_sensor.py (OctoPrintPrintingBinarySensor named 'Printing' "
"-> entity suffix _printing; no lifetime counter exists) — the "
"ENGINE accumulates print time."
),
tasks=(
ConsumableSignature(
("printing",),
"Lubricate Rails and Rods",
"runtime_hours",
delta_units=200,
entity_domain="binary_sensor",
on_states=("on",),
),
),
),
"prusalink": IntegrationSignature(
name="PrusaLink",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/prusalink/sensor.py "
"(translation_key 'printer_state', ENUM incl. 'printing') — the "
"ENGINE accumulates print time on the state sensor."
),
tasks=(
ConsumableSignature(
("printer_state",),
"Lubricate Rails and Rods",
"runtime_hours",
delta_units=200,
on_states=("printing",),
),
),
),
"moonraker": IntegrationSignature(
name="Moonraker (Klipper)",
verified="2026-07-19 @ marcolivierarsenault/moonraker-home-assistant main sensor.py+base.py",
source=(
"HACS moonraker: name 'Totals Filament Used' (has_entity_name → "
"suffix totals_filament_used), METERS, TOTAL_INCREASING lifetime. "
"NOTE: 'Totals Print Time' is a formatted STRING — unusable. "
"Nozzle interval is an editorial default (~1000 m on brass)."
),
tasks=(ConsumableSignature(("totals_filament_used",), "Replace Nozzle", "usage_delta", delta_units=1000),),
),
"ipp": IntegrationSignature(
name="IPP printer",
verified="2026-07-16 @ home-assistant/core dev",
source="home-assistant/core homeassistant/components/ipp/sensor.py (marker_<i>, translation_key 'marker', %)",
tasks=(
# Every marker (each ink/toner) shares translation_key "marker" —
# ONE task watches them all with entity_logic any.
ConsumableSignature(("marker",), "Replace Ink or Toner", "percent_left"),
),
),
"brother": IntegrationSignature(
name="Brother printer",
verified="2026-07-16 @ home-assistant/core dev",
source="home-assistant/core homeassistant/components/brother/sensor.py (*_toner_remaining / *_remaining_life, %)",
tasks=(
ConsumableSignature(
(
"black_toner_remaining",
"cyan_toner_remaining",
"magenta_toner_remaining",
"yellow_toner_remaining",
),
"Replace Toner",
"percent_left",
),
ConsumableSignature(
(
"drum_remaining_life",
"black_drum_remaining_life",
"cyan_drum_remaining_life",
"magenta_drum_remaining_life",
"yellow_drum_remaining_life",
),
"Replace Drum Unit",
"percent_left",
),
ConsumableSignature(("belt_unit_remaining_life",), "Replace Belt Unit", "percent_left"),
ConsumableSignature(("fuser_remaining_life",), "Replace Fuser", "percent_left"),
),
),
}
@@ -0,0 +1,161 @@
"""Protocol/hub transports whose duties are entity-domain-gated.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"matter": IntegrationSignature(
name="Matter lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/matter/lock.py "
"(MatterLock, lock domain entity per device). Matter bridges many "
"device types — the lock entity_domain restricts this signature "
"to locks; every transition to 'locked' is one mechanical cycle."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"zwave_js": IntegrationSignature(
name="Z-Wave lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/zwave_js/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks, so the bridge's other device types are untouched."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"zha": IntegrationSignature(
name="Zigbee (ZHA) lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/zha/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks, so the bridge's other device types are untouched."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"mqtt": IntegrationSignature(
name="MQTT lock (Zigbee2MQTT etc.)",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/mqtt/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks, so the bridge's other device types are untouched."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Replace Blades",
"runtime_hours",
delta_units=100,
entity_domain="lawn_mower",
on_states=("mowing",),
),
ConsumableSignature(
(),
"Clean Undercarriage",
"runtime_hours",
delta_units=25,
entity_domain="lawn_mower",
on_states=("mowing",),
),
),
),
# MQTT vacuums (Valetudo!) and mowers (OpenMower) expose only
# state entities — engine-accumulated usage covers their duties.
"homekit_controller": IntegrationSignature(
name="HomeKit lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/homekit_controller/lock.py "
"(lock platform verified present). Locks carry no wear sensor — "
"the ENGINE counts locking cycles; entity_domain-gated to locks, so the bridge's other device types are untouched."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
"deconz": IntegrationSignature(
name="deCONZ (Zigbee) lock",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/deconz/lock.py "
"(lock platform verified present) — engine-counted locking cycles, entity_domain-gated so the bridge's other device types are untouched."
),
tasks=(
ConsumableSignature(
(),
"Lubricate Cylinder",
"cycle_count",
delta_units=2000,
entity_domain="lock",
on_states=("locked",),
),
),
),
}
@@ -0,0 +1,347 @@
"""Cleaning robots — vacuums, mops and the Dolphin pool robot.
Interval audit 2026-07-19: the sensor-less runtime duties (filter wash
15 h / main-brush clean 30 h of cleaning time) map to Roborock's official
biweekly cleaning cadence at typical 1-2 h/day usage.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"roborock": IntegrationSignature(
name="Roborock",
verified="2026-07-16 @ home-assistant/core dev",
source="home-assistant/core homeassistant/components/roborock/sensor.py (translation_key, duration s→h)",
tasks=(
ConsumableSignature(("main_brush_time_left",), "Replace Main Brush", "duration_left"),
ConsumableSignature(("side_brush_time_left",), "Replace Side Brush", "duration_left"),
ConsumableSignature(("filter_time_left",), "Replace Filter", "duration_left"),
ConsumableSignature(("sensor_time_left",), "Clean Sensors", "duration_left"),
),
),
"xiaomi_miio": IntegrationSignature(
name="Xiaomi Miio",
verified="2026-07-16 @ home-assistant/core dev",
source="home-assistant/core homeassistant/components/xiaomi_miio/sensor.py (consumable_* descriptions, duration s)",
tasks=(
ConsumableSignature(("main_brush_left",), "Replace Main Brush", "duration_left"),
ConsumableSignature(("side_brush_left",), "Replace Side Brush", "duration_left"),
ConsumableSignature(("filter_left",), "Replace Filter", "duration_left"),
ConsumableSignature(("sensor_dirty_left",), "Clean Sensors", "duration_left"),
),
),
"dreame_vacuum": IntegrationSignature(
name="Dreame Vacuum",
verified="2026-07-16 @ Tasshack/dreame-vacuum master",
source="Tasshack/dreame-vacuum custom_components/dreame_vacuum/sensor.py (property keys *_left, percent)",
tasks=(
ConsumableSignature(("main_brush_left",), "Replace Main Brush", "percent_left"),
ConsumableSignature(("side_brush_left",), "Replace Side Brush", "percent_left"),
ConsumableSignature(("filter_left",), "Replace Filter", "percent_left"),
ConsumableSignature(("sensor_dirty_left",), "Clean Sensors", "percent_left"),
),
),
"ecovacs": IntegrationSignature(
name="Ecovacs",
verified="2026-07-17 @ home-assistant/core dev + DeebotUniverse/client.py main",
source=(
"home-assistant/core homeassistant/components/ecovacs/sensor.py "
"(translation_key f'lifespan_{component.name.lower()}', PERCENTAGE) + "
"DeebotUniverse/client.py deebot_client/events LifeSpan enum members"
),
tasks=(
ConsumableSignature(("lifespan_brush",), "Replace Main Brush", "percent_left"),
ConsumableSignature(("lifespan_side_brush",), "Replace Side Brush", "percent_left"),
ConsumableSignature(("lifespan_filter",), "Replace Filter", "percent_left"),
ConsumableSignature(("lifespan_dust_bag",), "Replace Dust Bag", "percent_left"),
ConsumableSignature(("lifespan_round_mop",), "Replace Mop Pads", "percent_left"),
# GOAT robotic mowers report blade lifespan through the same platform.
ConsumableSignature(("lifespan_blade",), "Replace Blades", "percent_left"),
),
),
"weback_vacuum": IntegrationSignature(
name="WeBack Vacuum",
verified="2026-07-18 @ Jezza34000/homeassistant_weback_component main",
source=(
"Jezza34000/homeassistant_weback_component vacuum.py (NO sensors "
"at all — STATE_MAPPING maps all clean modes to STATE_CLEANING) — "
"the ENGINE accumulates cleaning time on the vacuum entity."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
),
),
"roomba": IntegrationSignature(
name="iRobot Roomba",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/roomba/vacuum.py "
"(vacuum platform verified present; no consumable sensors) — the "
"ENGINE accumulates cleaning time."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
("bin_full",),
"Empty Dustbin",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
),
),
# bin_full is a plain binary (no problem device_class, so the
# problem-sensor adoption does NOT cover it) -> event latch.
"neato": IntegrationSignature(
name="Neato Botvac",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/neato/vacuum.py "
"(vacuum platform verified present; no consumable sensors) — the "
"ENGINE accumulates cleaning time."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
),
),
"romy": IntegrationSignature(
name="ROMY Vacuum",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/romy/vacuum.py "
"(vacuum platform verified present; no consumable sensors) — the "
"ENGINE accumulates cleaning time."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
),
),
"tuya": IntegrationSignature(
name="Tuya vacuum",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/tuya/vacuum.py "
"(vacuum platform verified present; no consumable sensors) — the "
"ENGINE accumulates cleaning time, entity_domain-gated so the bridge's other device types are untouched."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
),
),
"switchbot_cloud": IntegrationSignature(
name="SwitchBot vacuum",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/switchbot_cloud/vacuum.py "
"(vacuum platform verified present; no consumable sensors) — the "
"ENGINE accumulates cleaning time, entity_domain-gated so the bridge's other device types are untouched."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
),
),
"smartthings": IntegrationSignature(
name="SmartThings",
verified="2026-07-18 (vacuum) / 2026-07-20 (filters) @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/smartthings/vacuum.py "
"(vacuum platform verified present; no consumable sensors) — the "
"ENGINE accumulates cleaning time, entity_domain-gated so the bridge's other device types are untouched. "
"sensor.py: tk 'water_filter_usage' (custom.waterFilter, "
"PERCENTAGE, MEASUREMENT — Samsung fridge water filter, % USED "
"counting up; replacement resets to 0) and tk 'hood_filter_usage' "
"(SAMSUNG_CE_HOOD_FILTER, PERCENTAGE) — same up-counting shape."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
# Samsung fridge water filter / hood grease filter: usage counts
# UP in percent; replacing/cleaning resets to 0 (auto-resolve).
ConsumableSignature(("water_filter_usage",), "Replace Water Filter", "alert_above", delta_units=90),
ConsumableSignature(("hood_filter_usage",), "Clean Grease Filter", "alert_above", delta_units=90),
),
),
"sharkiq": IntegrationSignature(
name="Shark IQ",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/sharkiq/vacuum.py "
"(vacuum platform verified present; no consumable sensors) — the "
"ENGINE accumulates cleaning time."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
),
),
"tplink": IntegrationSignature(
name="TP-Link Tapo vacuum",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/tplink/vacuum.py "
"(vacuum platform verified present; no consumable sensors) — the "
"ENGINE accumulates cleaning time, entity_domain-gated so the bridge's other device types are untouched."
),
tasks=(
ConsumableSignature(
(),
"Filter Cleaning",
"runtime_hours",
delta_units=15,
entity_domain="vacuum",
on_states=("cleaning",),
),
ConsumableSignature(
(),
"Clean Main Brush",
"runtime_hours",
delta_units=30,
entity_domain="vacuum",
on_states=("cleaning",),
),
),
),
"mydolphin_plus": IntegrationSignature(
name="Maytronics Dolphin",
verified="2026-07-18 @ sh00t2kill/dolphin-robot master",
source=(
"sh00t2kill/dolphin-robot common/consts.py "
"(DATA_KEY_FILTER_STATUS 'Filter Status' -> entity suffix "
"_filter_status; FILTER_BAG_STATUS enum empty/partially_full/"
"getting_full/almost_full/full/fault) — latch on 'full', emptying "
"the bag drops the state back (auto-resolve)."
),
tasks=(
ConsumableSignature(
("filter_status",),
"Filter Cleaning",
"event_present",
on_states=("full",),
),
),
),
}
@@ -0,0 +1,62 @@
"""EV chargers — cable/plug inspection by delivered energy.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"easee": IntegrationSignature(
name="Easee Wallbox",
verified="2026-07-18 @ nordicopen/easee_hass master",
source=(
"nordicopen/easee_hass const.py 'lifetime_energy' "
"(state.lifetimeEnergy, translation_key 'lifetime_energy', kWh "
"lifetime counter) → cable/plug inspection by delivered energy. "
"Core `wallbox` verified NEGATIVE: its added_energy is per-session."
),
tasks=(
ConsumableSignature(
("lifetime_energy",),
"Inspect Cable and Plug",
"usage_delta",
delta_units=5000,
),
),
),
"keba": IntegrationSignature(
name="KEBA Wallbox",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/keba/sensor.py "
"('E total' description, name 'Total Energy' → entity_id suffix "
"_total_energy, kWh, TOTAL_INCREASING lifetime)."
),
tasks=(ConsumableSignature(("total_energy",), "Inspect Cable and Plug", "usage_delta", delta_units=5000),),
),
"goecharger_api2": IntegrationSignature(
name="go-e Charger",
verified="2026-07-18 @ marq24/ha-goecharger-api2 main",
source=(
"marq24/ha-goecharger-api2 const.py Tag.ETO sensor (key 'eto', "
"native WATT_HOUR with suggested kWh display, TOTAL_INCREASING "
"lifetime energy) — the unit map converts the 5,000 kWh target "
"into the live display unit."
),
tasks=(ConsumableSignature(("eto",), "Inspect Cable and Plug", "usage_delta", delta_units=5000),),
),
"openevse": IntegrationSignature(
name="OpenEVSE",
verified="2026-07-18 @ home-assistant/core dev",
source=(
"home-assistant/core homeassistant/components/openevse/sensor.py "
"(translation_key 'usage_total', kWh lifetime; usage_session is "
"per-session and deliberately not used)."
),
tasks=(ConsumableSignature(("usage_total",), "Inspect Cable and Plug", "usage_delta", delta_units=5000),),
),
}
@@ -0,0 +1,47 @@
"""Xiaomi ecosystem integrations (MIoT / Xiaomi Home) — multi-category.
Data module of the suggested-setups signature catalog — see
``helpers/signatures/_model.py`` for the direction semantics and the
method contract (every entry cites and is verified against the
integration's source; drift-probed weekly)."""
from __future__ import annotations
from ._model import ConsumableSignature, IntegrationSignature
SIGNATURES: dict[str, IntegrationSignature] = {
"xiaomi_miot": IntegrationSignature(
name="Xiaomi MIoT",
verified="2026-07-17 @ al-one/hass-xiaomi-miot master",
source=(
"al-one/hass-xiaomi-miot — generic MIoT-spec entities; entity_id "
"suffix = the spec property name (core/miot_spec.py format_name + "
"eid = f'{model}_{mac[-4:]}_{desc_name}'). Cross-device consumables: "
"'filter-life-level' (PERCENTAGE) on air purifiers/humidifiers/water "
"purifiers/vacuums, 'brush-life-level' (PERCENTAGE) on vacuums. The "
"days/used-hours filter counterparts describe the SAME filter, so "
"only the percent signal is cataloged to avoid duplicate tasks."
),
tasks=(
# Matched via the entity_id suffix (translation_key is the noisier
# 'filter-filter_life_level' form). One % task per filter; the side
# brush collides to a '_2' suffix and is intentionally not matched.
ConsumableSignature(("filter_life_level",), "Replace Filter", "percent_left"),
ConsumableSignature(("brush_life_level",), "Replace Main Brush", "percent_left"),
),
),
"xiaomi_home": IntegrationSignature(
name="Xiaomi Home",
verified="2026-07-18 @ XiaoMi/ha_xiaomi_home main",
source=(
"XiaoMi/ha_xiaomi_home miot/miot_device.py gen_prop_entity_id: "
"entity_id = f'{model}_{did}_{model}_{slugify_name(prop)}_p_{siid}_{piid}' "
"(property name mid-string, no translation_key) — matched via the "
"'_<key>_p_' infix. Same MIoT spec properties as hass-xiaomi-miot."
),
tasks=(
ConsumableSignature(("filter_life_level",), "Replace Filter", "percent_left"),
ConsumableSignature(("brush_life_level",), "Replace Main Brush", "percent_left"),
),
),
}