Files
HomeAssistantVS/custom_components/maintenance_supporter/helpers/signatures/kitchen.py
T

777 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
from ._shared import PRE_FILTER_CLEANING_PERCENT
# LG dishwasher "rinse refill needed": a plain binary (no problem class, so
# problem-sensor adoption does not cover it) in both LG integrations — one
# object, the DRY tripwire's verbatim-duplicate rule (both users live here).
LG_RINSE_REFILL_LATCH = ConsumableSignature(
("rinse_refill",),
"Refill Rinse Aid",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
)
# Electrolux fridge water filter: both Electrolux integrations expose the
# appliance's own 'waterFilterState' verdict (title-cased 'Change' when due)
# and its 'waterFilterStateReset' write capability as a button.
ELECTROLUX_WATER_FILTER_STATE = ConsumableSignature(
("waterfilterstate",),
"Replace Water Filter",
"event_present",
on_states=("Change",),
resets=(("waterfilterstate", "waterfilterstatereset"),),
)
SIGNATURES: dict[str, IntegrationSignature] = {
"lg_thinq": IntegrationSignature(
name="LG ThinQ",
verified="2026-07-17 @ home-assistant/core dev + thinq-connect/pythinqconnect main; dishwasher/fridge latches 2026-09-27 @ home-assistant/core 2026.9",
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. "
"2026-09 round: binary_sensor.py RINSE_REFILL (dishwasher, "
"pythinqconnect dish_washer.py dishWashingStatus.rinseRefill, no device_class, "
"no on_key → is_on = the boolean) → latch on 'on'. sensor.py FRESH_AIR_FILTER "
"(refrigerator / kimchi refrigerator, ENUM, created only when the property is "
"READ_ONLY — a writable one is a select and is not matched; strings.json state "
"'replace': 'Replace filter') → latch on 'replace'; its PERCENTAGE twin "
"FRESH_AIR_FILTER_REMAIN_PERCENT shares the translation_key and is kept out by "
"the unit gate (a percent duty on that key would also claim the unit-less ENUM). "
"Skipped: preference settings mCReminder / cleanLReminder / signalLevel "
"(binaries machine_clean_reminder / clean_light_reminder / signal_level) and "
"rinse_level (dispenser SETTING 0-4); used_time (MONTHS, the same water filter as "
"water_filter_*_remain_percent — no gate can keep both off one fridge) and "
"water_filter_state (values undocumented)."
),
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",
),
# Fridge Pure-N-Fresh filter: the ENUM shows 'replace' when due and
# leaves it after the swap + reset on the appliance (auto-resolve).
ConsumableSignature(("fresh_air_filter",), "Replace Filter", "event_present", on_states=("replace",)),
LG_RINSE_REFILL_LATCH,
),
),
"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).
LG_RINSE_REFILL_LATCH,
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 / extended 2026-09-25 @ home-assistant/core dev (2026.9 dishwasher events)",
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; the class sets _attr_entity_registry_enabled_default = "
"False — every event sensor must be enabled first). 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'. "
"2026.9 added the dishwasher events salt_lack / "
"program_blocked_salt_lack / rinse_aid_lack (API: 'supply is "
"empty' resp. 'program blocked due to lack of salt'), "
"machine_care_reminder ('Machine Clean program without dishes is "
"needed'), machine_care_and_filter_cleaning_reminder ('Machine "
"Clean program AND cleaning the filter and spray arm'), "
"machine_care_and_low_maintenance_filter_cleaning_reminder (filter "
"cleaning optional → machine care only) and "
"smart_filter_cleaning_reminder ('filter system may be blocked'). "
"Escalation keys of one condition share ONE any-latch (refilling "
"salt clears all three salt events); coffee Calc'N'Clean "
"(descale + clean in one program) joins the descale duty; the "
"two i-Dos tanks are refilled independently → per-entity duties. "
"Semantics per api-docs.home-connect.com/events. Skipped: the "
"'descaling/calc_n_clean in N cups' pre-warnings and "
"grease_filter_max_saturation_nearly_reached (pre-warnings of the "
"duties below, not the duty itself)."
),
tasks=(
ConsumableSignature(
("salt_nearly_empty", "salt_lack", "program_blocked_salt_lack"),
"Refill Salt",
"event_present",
),
ConsumableSignature(("rinse_aid_nearly_empty", "rinse_aid_lack"), "Refill Rinse Aid", "event_present"),
ConsumableSignature(
(
"device_should_be_descaled",
"device_descaling_overdue",
"device_descaling_blockage",
"device_should_be_calc_n_cleaned",
"device_calc_n_clean_overdue",
"device_calc_n_clean_blockage",
),
"Descale Appliance",
"event_present",
),
# Coffee-maker cleaning + dishwasher Machine Care (a cleaning
# program run without dishes) — one duty name, disjoint appliance
# types, so ONE signature (same name + direction would collide in
# the per-device dedupe).
ConsumableSignature(
(
"device_should_be_cleaned",
"device_cleaning_overdue",
"machine_care_reminder",
"machine_care_and_filter_cleaning_reminder",
"machine_care_and_low_maintenance_filter_cleaning_reminder",
),
"Clean Appliance",
"event_present",
),
# The combined machine-care-AND-filter reminder backs both duties.
ConsumableSignature(
("smart_filter_cleaning_reminder", "machine_care_and_filter_cleaning_reminder"),
"Filter Cleaning",
"event_present",
),
ConsumableSignature(("grease_filter_max_saturation_reached",), "Clean Grease Filter", "event_present"),
ConsumableSignature(
("poor_i_dos_1_fill_level", "poor_i_dos_2_fill_level"),
"Refill Detergent",
"event_present",
per_entity=True,
),
# Roxxter robot vacuum: 'empty the dust box and clean the filter'.
ConsumableSignature(("empty_dust_box_and_clean_filter",), "Empty Dustbin", "event_present"),
),
),
"homeconnect_ws": IntegrationSignature(
name="Home Connect Local",
verified="2026-09-25 @ chris-mc1/homeconnect_local_hass main (HACS default); resets + fridge filter 2026-09-27 @ c20ce8f",
source=(
"HACS homeconnect_ws: entity.py sets translation_key = description "
"key. entity_descriptions/dishcare.py 'sensor_salt' / "
"'sensor_rinse_aid' (HCEventSensor, ENUM options ['empty', "
"'nearly_empty', 'full'] — sensor.py returns 'empty' while "
"SaltLack/RinseAidLack is Present/Confirmed, 'nearly_empty' while "
"the NearlyEmpty event is, else 'full'; never None) → ok-state "
"latch on 'full'. cooking.py 'sensor_grease_filter_saturation' / "
"'sensor_carbon_filter_saturation' (Cooking.Hood.Status.*Saturation, "
"%, counts UP, reset by the hood's filter-reset buttons) → "
"alert_above 90 (the Samsung hood_filter_usage precedent). "
"consumer_products.py 'sensor_countdown_descaling' / "
"'_cleaning' / '_water_filter' (Status.BeverageCountdown*, "
"unitless beverages left until due, disabled by default) → "
"value_below 10. Skipped: 'sensor_countdown_calc_n_clean' (its "
"relation to the separate descale countdown is undocumented), "
"'sensor_machinecare_remaining_runs' and the dishwasher/washer/"
"dryer reminder binaries (device_class problem → problem-sensor "
"adoption). 2026-09-27: cooking.py button 'button_hood_grease_filter_reset' "
"(Cooking.Common.Command.Hood.GreaseFilterReset, CONFIG; press = "
"Command.set_value(True)) resets the grease saturation; refrigeration.py "
"'sensor_water_filter_saturation' (Refrigeration.Common.Status.Dispenser."
"WaterFilterSaturation, %, counts UP) with its reset 'button_water_filter_reset' "
"(Dispenser.WaterFilterReset) → alert_above 90 like the hood filters. The carbon "
"filter reset stays unwired: regenerative-filter hoods also carry "
"'button_hood_regenerative_carbon_filter_reset' / '_lifetime_reset', so which "
"button resets 'sensor_carbon_filter_saturation' is not established."
),
tasks=(
ConsumableSignature(("sensor_salt",), "Refill Salt", "event_present", ok_state="full"),
ConsumableSignature(("sensor_rinse_aid",), "Refill Rinse Aid", "event_present", ok_state="full"),
ConsumableSignature(
("sensor_grease_filter_saturation",),
"Clean Grease Filter",
"alert_above",
delta_units=90,
resets=(("sensor_grease_filter_saturation", "button_hood_grease_filter_reset"),),
),
ConsumableSignature(("sensor_carbon_filter_saturation",), "Replace Filter", "alert_above", delta_units=90),
ConsumableSignature(("sensor_countdown_descaling",), "Descale Appliance", "value_below", delta_units=10),
ConsumableSignature(("sensor_countdown_cleaning",), "Clean Appliance", "value_below", delta_units=10),
ConsumableSignature(("sensor_countdown_water_filter",), "Replace Water Filter", "value_below", delta_units=10),
# Fridge water dispenser filter: saturation counts UP, the fridge's
# own reset brings it back to 0 (coffee machines use the countdown
# above — different appliances, different direction).
ConsumableSignature(
("sensor_water_filter_saturation",),
"Replace Water Filter",
"alert_above",
delta_units=90,
resets=(("sensor_water_filter_saturation", "button_water_filter_reset"),),
),
),
),
"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; fridge filters 2026-09-27 @ bd55519",
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. 2026-09-27: catalog_core.py fridge "
"'waterFilterState' / 'airFilterState' (read, values BUY / CHANGE / CLEAN / "
"GOOD; sensor.py title-cases string states → 'Change') with their write "
"capabilities 'waterFilterStateReset' / 'airFilterStateReset' (values "
"{'RESET'} → ONE button each, api.py: write access = BUTTON) — entity ids "
"end in _waterfilterstate / _waterfilterstatereset (entity.py lowercases the "
"raw id). Latch on 'Change' (BUY is the order-a-spare pre-warning). The "
"'*LifeTime' seconds counters are skipped (used vs left undocumented; the "
"state is the fridge's own verdict against its thresholds)."
),
tasks=(
ConsumableSignature(("filterlife", "filter_life"), "Replace Filter", "percent_left"),
ELECTROLUX_WATER_FILTER_STATE,
ConsumableSignature(
("airfilterstate",),
"Replace Filter",
"event_present",
on_states=("Change",),
resets=(("airfilterstate", "airfilterstatereset"),),
),
),
),
"midea_ac_lan": IntegrationSignature(
name="Midea (LAN)",
verified="2026-07-18 / re-audited 2026-09-25; keys re-checked 2026-09-27 @ wuwentao/midea_ac_lan main (the default branch)",
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}'; device model = "
"f\"{MIDEA_DEVICES[type]['name']} {model}\"). 0xED 'Water Drinking "
"Appliance': filter1/2/3_life PERCENTAGE; 0xC2 'Toilet': "
"filter_life PERCENTAGE. 2026-09-25 fix: 0xFC 'Air Purifier' "
"reuses translation_key filter1_life/filter2_life (PERCENTAGE) — "
"it proposed 'Replace Water Filter' on a purifier; the water "
"duty now excludes the purifier model, which gets 'Replace "
"Filter' instead. 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",
models_exclude=("Air Purifier",),
),
ConsumableSignature(
("filter_life", "filter1_life", "filter2_life"),
"Replace Filter",
"percent_left",
models=("Toilet", "Air Purifier"),
),
),
),
"midea": IntegrationSignature(
name="Midea (core)",
verified="2026-09-25 @ home-assistant/core dev (new in 2026.8) + midea-local 12.1.0",
source=(
"home-assistant/core homeassistant/components/midea/sensor.py "
"SENSOR_ENTITIES (created only when the device reports the "
"attribute): tk 'salt_available' (key left_salt, PERCENTAGE — ED "
"soft-water body 09); tk 'filter_life_level' (PERCENTAGE) is shared "
"by the ED water purifier (keys life1/life2/life3, next to "
"'filter_available_days') and the FC air purifier (keys "
"filter1_life/filter2_life) — routed by the device model, which "
"entity.py sets to device_catalog.MIDEA_DEVICE_NAMES "
"('Water Drinking Appliance' / 'Air Purifier' / 'Toilet'); tk "
"'filter_life' (C2 toilet, midea-local c2/message.py "
"filter_life = 100 - body[19] → remaining %). The 15 virtual "
"brands with supported_by: midea share the domain. "
"binary_sensor.py salt / rinse_aid / filter_cleaning_reminder / "
"full_dust / tank_full are device_class problem → problem-sensor "
"adoption."
),
tasks=(
ConsumableSignature(("salt_available",), "Refill Softener Salt", "percent_left"),
ConsumableSignature(
("filter_life_level",),
"Replace Water Filter",
"percent_left",
models=("Water Drinking Appliance",),
),
ConsumableSignature(
("filter_life", "filter_life_level"),
"Replace Filter",
"percent_left",
models=("Air Purifier", "Toilet"),
),
),
),
"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-09-27 @ gvigroux/hon master (cbc0376, the HACS-default fork) + Andre0512/hon main (70eb6c0, idle since 2024-08)",
source=(
"Two forks share the domain. gvigroux/hon (the store's 'hon' today) "
"sensor.py: no translation_key, _attr_name = f'{nickName} {sensor_name}' → "
"entity-id suffixes; purifiers (AP) HonBaseMainFilter 'Main filter' / "
"HonBasePreFilter 'Pre filter' = 100 − mainFilterStatus / preFilterStatus "
"(PERCENTAGE, life LEFT); washers HonBaseTotalWashCycle 'Total wash cycle' "
"(totalWashCycle − 1, TOTAL_INCREASING). Andre0512/hon (older installs) "
"reports the RAW mainFilterStatus / preFilterStatus under tk 'filter_life' / "
"'filter_cleaning' and tk 'cycles_total'. The raw status counts UP (used): "
"Andre0512/hon#244 — after a filter reset the hOn app shows 100 % life while "
"the raw sensor reads 0 %, which is also why gvigroux inverts it. So the "
"Andre0512 keys are wear gauges (alert_above 90; the former percent_left "
"reading was inverted) and the gvigroux keys are life gauges (percent_left). "
"Tub-clean cadence reuses LG's manufacturer value of 30 cycles."
),
tasks=(
ConsumableSignature(("main_filter",), "Replace Filter", "percent_left"),
ConsumableSignature(("filter_life",), "Replace Filter", "alert_above", delta_units=90),
PRE_FILTER_CLEANING_PERCENT,
ConsumableSignature(("filter_cleaning",), "Filter Cleaning", "alert_above", delta_units=90),
ConsumableSignature(("cycles_total", "total_wash_cycle"), "Clean Tub", "usage_delta", delta_units=30),
),
),
"whirlpool": IntegrationSignature(
name="Whirlpool",
verified="2026-07-19 @ core/dev whirlpool/sensor.py; detergent tank 2026-09-27 @ home-assistant/core 2026.9",
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. 2026-09 round: washer key 'DispenseLevel', "
"tk 'whirlpool_tank' ('Detergent level', ENUM from WASHER_TANK_FILL: "
"empty / 25 / 50 / 100 / active; the bulk-dispenser tank, "
"entity_registry_enabled_default=False) → latch on 'empty'; refilling moves "
"it back to a level (auto-resolve)."
),
tasks=(
ConsumableSignature(
("washer_state",),
"Clean Tub",
"runtime_hours",
delta_units=60,
on_states=("running_maincycle",),
),
ConsumableSignature(("whirlpool_tank",), "Refill Detergent", "event_present", on_states=("empty",)),
),
),
"ha_washdata": IntegrationSignature(
name="WashData (smart-plug cycles)",
verified="2026-09-10 @ 3dg1luk43/ha_washdata main (0.5.5) + branch 0.5.6 sensor.py/manager.py (HACS default)",
source=(
"HACS ha_washdata: tk 'cycle_count' (unit 'cycles'). CAVEAT up to "
"0.5.5: the sensor is len(stored cycle records), capped at "
"DEFAULT_MAX_PAST_CYCLES=200 and lowered when a record is deleted "
"— a delta task freezes once the cap is reached (our D#172, "
"upstream discussion 414). From 0.5.6 (PR #420) the SAME entity "
"reports the monotonic lifetime odometer (old value in attribute "
"'stored_cycles'; state_class total, user-correctable downward), "
"so the value jumps once on that update — a Reset re-anchors the "
"task. Cycles are DETECTED from smart-plug power monitoring. The "
"integration ships its OWN maintenance taxonomy "
"(MAINTENANCE_EVENT_TYPES + DEFAULT_MAINTENANCE_REMINDER_CYCLES: "
"descale 30 / filter_clean 50 / drum_clean 100), but shows it "
"only inside its panel — no due-entity, no notifications. These "
"tasks mirror that taxonomy 1:1, so what its panel counts "
"silently becomes a real reminder here; type-agnostic on purpose "
"because WashData offers the types for every appliance class "
"itself (washer, dryer, dishwasher, air fryer, …)."
),
tasks=(
# 2.94: by WashData's own device_type (const.py DEVICE_TYPE_*,
# stored in the entry's options): no descaling for appliances
# without a water circuit (a heat-pump dryer — reported), no tub
# cleaning where there is no tub. Unknown/generic/other keep all.
ConsumableSignature(
("cycle_count",),
"Descaling",
"usage_delta",
delta_units=30,
exclude_appliance_types=("dryer", "air_fryer", "bread_maker", "pump"),
),
ConsumableSignature(
("cycle_count",), "Filter Cleaning", "usage_delta", delta_units=50, exclude_appliance_types=("bread_maker",)
),
ConsumableSignature(
("cycle_count",),
"Clean Tub",
"usage_delta",
delta_units=100,
exclude_appliance_types=("air_fryer", "bread_maker", "pump"),
),
),
appliance_type_key="device_type",
),
"traeger": IntegrationSignature(
name="Traeger grill",
verified="2026-07-20 @ njobrien1006/hass_traeger master + johnvoipguy/Traeger-WiFire main (HACS default); jv drift fix 2026-09-27 @ 8971c5b",
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). 2026-09-27: the johnvoipguy "
"fork renamed it TraegerSensor 'Cook Cycles' (key 'cook_cycles', explicit "
"entity_id f'sensor.{slugify(grill_id)}_{slugify(name)}' → suffix "
"_cook_cycles) — both suffixes are matched. Its MaintenanceAlert sensors "
"'Grill Clean Alert' / 'Grease Trap Clean Alert' (True/False from "
"usage.*_countdown <= 3600 / <= 5 — the integration's own cut-offs, countdown "
"units undocumented) are skipped."
),
tasks=(
ConsumableSignature(("cook_cycle", "cook_cycles"), "Clean Grease Trap", "usage_delta", delta_units=5),
ConsumableSignature(("cook_cycle", "cook_cycles"), "Clean Appliance", "usage_delta", delta_units=20),
),
),
"electrolux": IntegrationSignature(
name="Electrolux (OCP API)",
verified="2026-09-25 @ TTLucian/ha-electrolux main (HACS default; not the electrolux_status domain); filter states 2026-09-27 @ 60b28fa",
source=(
"HACS electrolux: entity.py sets translation_key = the lowercased "
"capability name (fppn prefix stripped) → 'filterlife', "
"'filterlife_1', 'filterlife_2'. catalogs/catalog_ap.py "
"'FilterLife' (PERCENTAGE, air purifiers) and the UltimateHome 500 "
"pair 'FilterLife_1' / 'FilterLife_2' (int 0-100 %, two slots with "
"their own FilterType_1/_2) → one duty per filter (per_entity). "
"Skipped: AC/fridge/hood filter lifetimes and timers (seconds or "
"minutes with the maintainer's own 'unit is an educated guess' "
"caveat — remaining vs used unclear). 2026-09-27: the filter STATE "
"strings are verified — sensor.py native_value title-cases string "
"values ('CHANGE' → 'Change'). catalogs/catalog_cr.py fridge "
"'waterFilterState' / 'airFilterState' (GOOD / CLEAN / CHANGE / BUY) with "
"write buttons 'waterFilterStateReset' / 'airFilterStateReset' (tk "
"'waterfilterstatereset' / 'airfilterstatereset' in strings.json); "
"catalog_ac.py 'hepaFilterState' (GOOD / BUY / CHANGE, disabled by default) "
"with 'hepaFilterReset' → latch on 'Change' (BUY = order a spare). The AC's "
"main 'filterState' is skipped: it also reports CLEAN for its washable "
"filter and which of CLEAN / CHANGE the 'filterReset' button clears is "
"not established."
),
tasks=(
ConsumableSignature(
("filterlife", "filterlife_1", "filterlife_2"),
"Replace Filter",
"percent_left",
per_entity=True,
),
ELECTROLUX_WATER_FILTER_STATE,
# Fridge air filter / AC HEPA filter — one appliance never has both.
ConsumableSignature(
("airfilterstate", "hepafilterstate"),
"Replace Filter",
"event_present",
on_states=("Change",),
resets=(("airfilterstate", "airfilterstatereset"), ("hepafilterstate", "hepafilterreset")),
),
),
),
"ge_home": IntegrationSignature(
name="GE Home (SmartHQ)",
verified="2026-09-25 @ simbaja/ha_gehome master + simbaja/gehome (gehomesdk 2026.8.0; custom repository, not in the HACS default store)",
source=(
"HACS ge_home: no translation_key — entity names are "
"f'{serial} {ERD title}' (+ ' {Property Title}' for property "
"sensors, entities/common/ge_erd_entity.py + "
"ge_erd_property_sensor.py), matched by entity-id suffix. "
"devices/fridge.py WATER_FILTER_STATUS 'percent_remaining' "
"(uom_override PERCENTAGE → _water_filter_status_percent_remaining); "
"devices/water_filter.py WH_FILTER_LIFE_REMAINING 'life_remaining' "
"(ErdCodeClass.PERCENTAGE → '%', gehomesdk "
"ErdWaterFilterLifeRemainingConverter pct → "
"_wh_filter_life_remaining_life_remaining). Skipped: the water "
"softener binary WH_SOFTENER_LOW_SALT — gehomesdk "
"ErdWaterSoftenerSaltLevel.boolify() returns self == OK, so the "
"'low salt' binary is ON while the salt is fine (inverted; a "
"latch would fire backwards). AC/dehumidifier/ice-maker filter "
"binaries are device_class problem → problem-sensor adoption."
),
tasks=(
ConsumableSignature(
("water_filter_status_percent_remaining", "wh_filter_life_remaining_life_remaining"),
"Replace Water Filter",
"percent_left",
),
),
),
"candy": IntegrationSignature(
name="Candy Simply-Fi",
verified="2026-09-25 @ bigmoby/home-assistant-candy main (1.5.1, HACS default)",
source=(
"HACS candy sensor.py: tk 'wash_total_cycles' (TOTAL_INCREASING; "
"client/model.py sums the wide Temp* wash counters — no wrap, no "
"reset) → Clean Tub every 30 cycles (the hOn/LG cadence). With the "
"integration's opt-in maintenance counters (Simply-Fi app parity) "
"it also exposes tk 'wash_maint_limescale' / 'wash_maint_filter' "
"— cycles REMAINING until due (helpers.cycles_remaining: 0 when "
"due or overdue, back to the full interval after the "
"integration's reset button) → value_below 1 fires exactly when "
"Candy itself reports due. Skipped: 'wash_maint_full_checkup' "
"(button.py WashFullCheckUpButton sends CheckUpState=1 — the machine's "
"self-diagnosis run, not a maintenance action; the integration schedules "
"it itself via checkup_schedule). Resets verified 2026-09-27 @ 5e2999f: "
"button.py tk 'wash_maint_limescale_reset' / 'wash_maint_filter_reset' "
"(DIAGNOSTIC; press stores last_* = total cycles, the counter restarts)."
),
tasks=(
ConsumableSignature(("wash_total_cycles",), "Clean Tub", "usage_delta", delta_units=30),
# 2.95: full-control mode adds the reset buttons (button.py
# WashMaintResetButton, tk '<counter>_reset'); read-only setups have
# none and the duty simply keeps no completion action.
ConsumableSignature(
("wash_maint_limescale",),
"Descaling",
"value_below",
delta_units=1,
resets=(("wash_maint_limescale", "wash_maint_limescale_reset"),),
),
ConsumableSignature(
("wash_maint_filter",),
"Filter Cleaning",
"value_below",
delta_units=1,
resets=(("wash_maint_filter", "wash_maint_filter_reset"),),
),
),
),
"homewhiz": IntegrationSignature(
name="HomeWhiz (Beko / Grundig / Arçelik)",
verified="2026-09-27 @ home-assistant-HomeWhiz/home-assistant-HomeWhiz main (0b7f8d8, HACS default)",
source=(
"home-assistant-HomeWhiz custom_components/homewhiz/appliance_controls.py "
"build_controls_from_warnings: every entry of the appliance's cloud config "
"deviceWarnings becomes a BooleanBitmaskControl with key "
"to_friendly_name(strKey) (lower-cased strKey); binary_sensor.py turns them "
"into binary sensors and entity.py translation_key returns entity_key.lower() "
"for EVERY entity. device_class is the custom string 'homewhiz__<key>' — NOT "
"problem, so problem-sensor adoption does not reach them; the appliance clears "
"the bit after the refill / cleaning (auto-resolve). strKeys verified in the "
"repo's test fixtures: DISHWASHER_WARNING_NO_SALT / _NO_RINSE_AID / "
"_CHECK_THE_FILTER / DISHWASHER_LIQUID_DETERGENT_LOW, WASHER_WARNING_NO_LIQUID_"
"DETERGENT / _NO_POWDER_DETERGENT / _LOW_DETERGENT / _NO_SOFTENER / _LOW_SOFTENER "
"(auto-dosing tanks), DRYER_WARNING_CHECK_THE_FILTER / _CHECK_THE_CONDENSER_FILTER. "
"Skipped: *_NO_WATER / *_DOOR_IS_OPEN / *_CALL_SERVICE (status or faults), "
"DRYER_WARNING_TANKFULL (emptying the condensate tank is per-load operation), "
"OVEN_WARNING_PYRO_CLEANING (a cleaning cycle in progress)."
),
tasks=(
ConsumableSignature(
("dishwasher_warning_no_salt",),
"Refill Salt",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
ConsumableSignature(
("dishwasher_warning_no_rinse_aid",),
"Refill Rinse Aid",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
ConsumableSignature(
("dishwasher_warning_check_the_filter",),
"Filter Cleaning",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
# Auto-dosing tanks: the liquid and the powder tank are refilled
# separately → one task per warning entity.
ConsumableSignature(
(
"dishwasher_liquid_detergent_low",
"washer_warning_low_detergent",
"washer_warning_no_liquid_detergent",
"washer_warning_no_powder_detergent",
),
"Refill Detergent",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
per_entity=True,
),
ConsumableSignature(
("washer_warning_no_softener", "washer_warning_low_softener"),
"Refill Fabric Softener",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
ConsumableSignature(
("dryer_warning_check_the_filter",),
"Lint Filter Cleaning",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
ConsumableSignature(
("dryer_warning_check_the_condenser_filter",),
"Condenser Cleaning",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
),
translation_keys_authoritative=True,
),
"connectlife": IntegrationSignature(
name="ConnectLife (Hisense / Gorenje / ASKO)",
verified="2026-09-27 @ oyvindwe/connectlife-ha main (fa830d2, HACS default)",
source=(
"oyvindwe/connectlife-ha: entities come from data_dictionaries/<type>.yaml; "
"entity.py to_translation_key = property lower-cased (spaces → '_'), set on "
"every sensor/binary description; 'optional: true' → disabled by default. "
"012.yaml (hood) 'GreaseFilterUsedHours' and 010.yaml (hob with extractor) "
"'Grease_filter_used_hours' (DURATION h, total_increasing — hours since the "
"filter counter was reset; 012's reset is the select 'GreaseFilterResetCounter', "
"not a button) → usage_above, 30 h (editorial; the hood's own interval is "
"exposed as 'GreaseFilterCleaningIntervalInHours'). 012 "
"'RecirculationFilter1UsedHours' / '…2UsedHours' and 010 "
"'Recirculation_filter_1_used_hours' (carbon filters, h) → usage_above 120 h "
"(editorial), one task per filter. 015.yaml (dishwasher) "
"'Alarm_run_selfcleaning' (binary, NO device_class, options 2 = on) → latch. "
"Skipped: every other alarm (Alarm_salt_refill, rinse aid, clean filters, "
"condenser, grease, descale, detergent/softener states, AC 'f-filter') is "
"device_class problem → problem-sensor adoption; fridge filter_state (% "
"direction unclear)."
),
tasks=(
ConsumableSignature(
("greasefilterusedhours", "grease_filter_used_hours"),
"Clean Grease Filter",
"usage_above",
above_hours=30,
),
ConsumableSignature(
("recirculationfilter1usedhours", "recirculationfilter2usedhours", "recirculation_filter_1_used_hours"),
"Replace Filter",
"usage_above",
above_hours=120,
per_entity=True,
),
ConsumableSignature(
("alarm_run_selfcleaning",),
"Clean Appliance",
"event_present",
entity_domain="binary_sensor",
on_states=("on",),
),
),
translation_keys_authoritative=True,
),
}