Updated apps
This commit is contained in:
@@ -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",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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"),
|
||||
),
|
||||
),
|
||||
}
|
||||
Reference in New Issue
Block a user