876 lines
44 KiB
Python
876 lines
44 KiB
Python
"""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 dataclasses import replace
|
||
|
||
from ._model import ConsumableSignature, IntegrationSignature
|
||
from ._shared import (
|
||
FILTER_LIFE_PERCENT,
|
||
PRE_FILTER_CLEANING_PERCENT,
|
||
VENTILATION_FILTER_OPERATING_TIME,
|
||
VENTILATION_FILTER_REMAIN,
|
||
)
|
||
|
||
# One object for the AC-only integrations whose climate entity runs through
|
||
# these HVAC modes (gree, midea_ac, mitsubishi_wf_rac, toshiba_ac,
|
||
# fujitsu_airstage; aux_cloud keys a copy to its AC climate) — shared here,
|
||
# next to its users, so the verbatim-duplicate tripwire holds without a
|
||
# _shared.py entry.
|
||
_AC_FILTER_CLEANING_RUNTIME = ConsumableSignature(
|
||
(),
|
||
"Filter Cleaning",
|
||
"runtime_hours",
|
||
delta_units=100,
|
||
entity_domain="climate",
|
||
on_states=("auto", "cool", "dry", "fan_only", "heat"),
|
||
)
|
||
|
||
SIGNATURES: dict[str, IntegrationSignature] = {
|
||
"hass_dyson": IntegrationSignature(
|
||
name="Dyson",
|
||
verified="2026-07-18 / 2026-09-27 (live keys, deep clean) @ cmgrayb/hass-dyson main",
|
||
source=(
|
||
"cmgrayb/hass-dyson sensor.py (verified @ 3dde06e): the live "
|
||
"filter sensors are DysonHEPAFilterLifeSensor tk "
|
||
"'hepa_filter_life' and DysonCarbonFilterLifeSensor tk "
|
||
"'carbon_filter_life' (PERCENTAGE; the carbon one only when the "
|
||
"device reports a separate carbon filter) — one any-low task "
|
||
"covers both filters. The older DysonFilterLifeSensor (tk "
|
||
"'filter_life') is never instantiated; the catalog matched the "
|
||
"live sensors only through the _filter_life suffix until "
|
||
"2026-09-27. Humidifiers: DysonNextCleaningCycleSensor tk "
|
||
"'next_cleaning_cycle' (cltr, HOURS until the deep-clean cycle, "
|
||
"i.e. the citric-acid descale; unknown at 0) → Descale Appliance "
|
||
"24 h ahead. The filter reset is only the service "
|
||
"hass_dyson.reset_filter (no button)."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(("hepa_filter_life", "carbon_filter_life"), "Replace Filter", "percent_left"),
|
||
ConsumableSignature(("next_cleaning_cycle",), "Descale Appliance", "duration_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=(FILTER_LIFE_PERCENT,),
|
||
),
|
||
"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=(FILTER_LIFE_PERCENT,),
|
||
),
|
||
"daikin": IntegrationSignature(
|
||
name="Daikin AC",
|
||
verified="2026-09-25 @ home-assistant/core dev (+ tag 2026.7.0)",
|
||
source=(
|
||
"home-assistant/core homeassistant/components/daikin/climate.py "
|
||
"(AC-only integration, so the climate entity IS an air "
|
||
"conditioner). hvac_action is only mapped for COOL/HEAT/OFF "
|
||
"(HA_STATE_TO_CURRENT_HVAC) and is None in fan_only, dry and "
|
||
"heat_cool — the former hvac_action-attribute runtime missed "
|
||
"every hour in those modes and never reached its 'fan'/'drying' "
|
||
"states. Runtime on the climate STATE instead: DAIKIN_TO_HA_STATE "
|
||
"maps fan/dry/cool/hot/auto to fan_only/dry/cool/heat/heat_cool — "
|
||
"every mode except off moves air through the filter. 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",
|
||
on_states=("cool", "dry", "fan_only", "heat", "heat_cool"),
|
||
),
|
||
),
|
||
),
|
||
"gree": IntegrationSignature(
|
||
name="Gree AC",
|
||
verified="2026-09-25 @ home-assistant/core dev (+ tag 2026.7.0)",
|
||
source=(
|
||
"home-assistant/core homeassistant/components/gree/climate.py "
|
||
"(AC-only integration, so the climate entity IS an air "
|
||
"conditioner). The entity NEVER sets hvac_action (0 hits at "
|
||
"2026.7.0 and dev) — the former hvac_action-attribute runtime "
|
||
"never accumulated. Runtime on the climate STATE instead: "
|
||
"HVAC_MODES maps Mode.Auto/Cool/Dry/Fan/Heat to "
|
||
"auto/cool/dry/fan_only/heat and _attr_hvac_modes = "
|
||
"[*HVAC_MODES_INVERSE, HVACMode.OFF] — every mode except off "
|
||
"moves air through the filter. Interval per Daikin's official "
|
||
"guidance (clean filters every 2 weeks; ≈100 runtime-hours at "
|
||
"typical in-season use)."
|
||
),
|
||
tasks=(_AC_FILTER_CLEANING_RUNTIME,),
|
||
),
|
||
"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, resets=(("filter_change", "reset_filter"),)),),
|
||
),
|
||
"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=(
|
||
PRE_FILTER_CLEANING_PERCENT,
|
||
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=(
|
||
FILTER_LIFE_PERCENT,
|
||
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-09-02 @ iprak/winix main sensor.py (HACS default)",
|
||
source=(
|
||
"HACS winix: tk 'filter_life' (PERCENTAGE) — % remaining derived "
|
||
"from filter hours vs the model's max filter life "
|
||
"(wrapper.filter_max_life; was filter_alarm_duration before 2026-09)."
|
||
),
|
||
tasks=(FILTER_LIFE_PERCENT,),
|
||
),
|
||
"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=(
|
||
VENTILATION_FILTER_OPERATING_TIME,
|
||
),
|
||
),
|
||
"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). button.py (verified @ 41c3338): "
|
||
"'Reset Filter Life' (no translation_key → suffix "
|
||
"_reset_filter_life, CONFIG) exists only on Pure Cool Link units "
|
||
"(hasattr filter_life) and resets the 'filf' hours behind both "
|
||
"'Filter Life' (h) and 'Filter Life Percentage'; the Pure Cool "
|
||
"carbon/HEPA/combined % sensors have no reset. Humidify+Cool: "
|
||
"'Next Deep Clean' (HOURS until the citric-acid deep-clean cycle, "
|
||
"libdyson cltr) → Descale Appliance."
|
||
),
|
||
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",
|
||
resets=(("filter_life_percentage", "reset_filter_life"),),
|
||
),
|
||
ConsumableSignature(
|
||
("filter_life",),
|
||
"Replace Filter",
|
||
"duration_left",
|
||
below_hours=72,
|
||
resets=(("filter_life", "reset_filter_life"),),
|
||
),
|
||
ConsumableSignature(("next_deep_clean",), "Descale Appliance", "duration_left"),
|
||
),
|
||
),
|
||
"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),),
|
||
),
|
||
# ─── Round 14 (2026-09-25): purifier/HRV filters from the HACS + ────
|
||
# ─── core 2026.9/dev sweep ───────────────────────────────────────────
|
||
"meross_lan": IntegrationSignature(
|
||
name="Meross LAN (MAP100 purifier)",
|
||
verified="2026-09-25 @ krahabb/meross_lan master",
|
||
source=(
|
||
"HACS meross_lan sensor.py MLFilterMaintenanceSensor "
|
||
"(Appliance.Control.FilterMaintenance; entitykey mc.KEY_FILTER "
|
||
"'filter', PERCENTAGE, DIAGNOSTIC, no translation_key). "
|
||
"helpers/entity.py names it entitykey.replace('_', ' ')"
|
||
".capitalize() = 'Filter' (channel 0 is dropped) under "
|
||
"has_entity_name → suffix _filter. The value is the payload's "
|
||
"'life' — REMAINING: the emulator (emulator/mixins/fan.py) starts "
|
||
"it at 100 and counts it down, and a real MAP100 push quoted in "
|
||
"issue #388 reads life: 100 on a fresh filter. No other "
|
||
"meross_lan entity key ends in 'filter' (the match is "
|
||
"platform-scoped)."
|
||
),
|
||
tasks=(ConsumableSignature(("filter",), "Replace Filter", "percent_left"),),
|
||
),
|
||
"tuya_local": IntegrationSignature(
|
||
name="Tuya Local",
|
||
verified="2026-09-25 @ make-all/tuya-local main",
|
||
source=(
|
||
"HACS tuya_local (not in the HACS default store) entity.py: "
|
||
"_attr_translation_key = config.translation_key; devices/*.yaml "
|
||
"carry 'translation_key: filter_life' with unit '%' in 57 device "
|
||
"configs (purifiers, humidifiers, dehumidifiers, robot vacuums, "
|
||
"fans) — the Tuya 'filter' DP, REMAINING life (it sits next to a "
|
||
"'Filter remaining' days DP on the Honeywell/Stadler Form "
|
||
"configs; a used-hours DP was moved off filter_life in #6202). "
|
||
"The duration-unit filter_life variants (d/h/min/s) are NOT "
|
||
"signed: some count UP without the 'invert' mapping "
|
||
"(fresco_hydrateultra_petfountain v1 vs v2). 'entity: lock' in "
|
||
"these configs is the CHILD lock — no lock signature. Resets "
|
||
"(verified @ 986d71e): 37 of the '%' configs carry a button with "
|
||
"translation_key 'filter_reset' (e.g. ap402_airpurifier.yaml), two "
|
||
"robot vacuums a name-only 'Reset filter' button (suffix "
|
||
"_reset_filter); the press writes the yaml's reset DP. A device "
|
||
"with several matching buttons gets none wired."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("filter_life",),
|
||
"Replace Filter",
|
||
"percent_left",
|
||
resets=(("filter_life", "filter_reset"), ("filter_life", "reset_filter")),
|
||
),
|
||
),
|
||
),
|
||
"govee": IntegrationSignature(
|
||
name="Govee (purifiers)",
|
||
verified="2026-09-25 @ lasswellt/govee-homeassistant main",
|
||
source=(
|
||
"HACS govee (lasswellt) sensor.py GoveeFilterLifeSensor: tk "
|
||
"'sensor_filter_life', PERCENTAGE, MEASUREMENT, DIAGNOSTIC — "
|
||
"'remaining filter life %' from the devices.capabilities.property "
|
||
"instance 'filterLifeTime' (models/state.py; H7124/H7126). "
|
||
"LaggAt/hacs-govee shares the 'govee' domain but ships lights "
|
||
"only — no conflicting entity."
|
||
),
|
||
tasks=(ConsumableSignature(("sensor_filter_life",), "Replace Filter", "percent_left"),),
|
||
),
|
||
"duux": IntegrationSignature(
|
||
name="Duux",
|
||
verified="2026-09-25 @ SSmale/Duux-Home-Assistant master",
|
||
source=(
|
||
"HACS duux sensor.py DuuxFilterLifeSensor (Bright 2 purifier): "
|
||
"key 'filter', tk 'filter_life', PERCENTAGE, MEASUREMENT — "
|
||
"translations name it 'HEPA Filter remaining lifespan' "
|
||
"(REMAINING; fixtures/devices.json reports filter: 80)."
|
||
),
|
||
tasks=(FILTER_LIFE_PERCENT,),
|
||
),
|
||
"komfovent": IntegrationSignature(
|
||
name="Komfovent ventilation",
|
||
verified="2026-09-25 @ lnagel/hass-komfovent main",
|
||
source=(
|
||
"HACS komfovent sensor.py: key 'filter_clogging' (Modbus register "
|
||
"917, PERCENTAGE, MEASUREMENT; KomfoventSensor sets "
|
||
"translation_key = key). Clogging climbs to 100 % (warning 0x81 "
|
||
"'Change Air Filter'); the 'Clean Filters Calibration' button "
|
||
"resets it after a change → alert_above 90 % with auto-resolve. "
|
||
"button.py (verified @ e5be948): tk 'clean_filters' (name 'Clean "
|
||
"Filters Calibration', CONFIG) writes register 1051 = 1, the "
|
||
"'Reset filters counter'."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("filter_clogging",),
|
||
"Replace Ventilation Filter",
|
||
"alert_above",
|
||
delta_units=90,
|
||
resets=(("filter_clogging", "clean_filters"),),
|
||
),
|
||
),
|
||
),
|
||
"pluggit": IntegrationSignature(
|
||
name="Pluggit ventilation",
|
||
verified="2026-09-25 @ Tvalley71/pluggit main",
|
||
source=(
|
||
"HACS pluggit device_map.py: key ATTR_FILTER_REMAIN "
|
||
"'filter_remain' (DURATION, unit 'd'; entity.py's translation_key "
|
||
"property returns the key) — Modbus register 554 = filter days "
|
||
"REMAINING (device.py derives the replace level from lifetime − "
|
||
"remain; the translated state 0 reads 'Replace the filter now'). "
|
||
"Absent on Servo_flow units (not_component_class). Button key "
|
||
"'filter_reset' (ATTR_FILTER_RESET, translation_key = key) → "
|
||
"set_filter_reset writes register 558 = 1, restoring the "
|
||
"remaining days (verified @ ee0e13d)."
|
||
),
|
||
tasks=(VENTILATION_FILTER_REMAIN,),
|
||
),
|
||
"dantherm": IntegrationSignature(
|
||
name="Dantherm ventilation",
|
||
verified="2026-09-25 @ Tvalley71/dantherm main",
|
||
source=(
|
||
"HACS dantherm — the same code base as pluggit (device_map.py "
|
||
"key 'filter_remain', DURATION, unit 'd', Modbus register 554 = "
|
||
"filter days REMAINING; translation_key = key) and the same "
|
||
"'filter_reset' button → register 558 (verified @ da9dfaa)."
|
||
),
|
||
tasks=(VENTILATION_FILTER_REMAIN,),
|
||
),
|
||
"ha_carrier": IntegrationSignature(
|
||
name="Carrier Infinity",
|
||
verified="2026-09-25 @ dahlb/ha_carrier main",
|
||
source=(
|
||
"HACS ha_carrier sensor.py FilterUsedSensor: entity_name 'Filter "
|
||
"Remaining' (carrier_entity.py sets _attr_name = entity_name "
|
||
"under has_entity_name, no translation_key → suffix "
|
||
"_filter_remaining), PERCENTAGE = 100 − status.filter_used → "
|
||
"REMAINING."
|
||
),
|
||
tasks=(ConsumableSignature(("filter_remaining",), "Replace Filter", "percent_left"),),
|
||
),
|
||
"localthings": IntegrationSignature(
|
||
name="Samsung (Local Things)",
|
||
verified="2026-09-25 @ mbillow/localthings main",
|
||
source=(
|
||
"HACS localthings registry/capabilities: every % filter sensor "
|
||
"is Samsung's filterUsage = % USED (common.filter_usage_percent: "
|
||
"filterStatus flips to 'wash' at 100); entity.py's "
|
||
"translation_key defaults to the descriptor key. "
|
||
"airconditioner.AIR_FILTER 'air_filter_usage' (AC + dehumidifier "
|
||
"washable dust filter) → Filter Cleaning, gated on its AC-only "
|
||
"sibling 'air_filter_usage_hours' because fridge.AIR_FILTER "
|
||
"reuses the 'air_filter_usage' key for the fridge's deodorizing "
|
||
"filter; air_purifier.HEPA_FILTER 'hepa_filter_usage' + "
|
||
"air_purifier.FILTER 'filter_progress' (counts UP, 100 = change) "
|
||
"→ Replace Filter; range_hood.HOOD_FILTER 'hood_filter_usage' → "
|
||
"Clean Grease Filter (mirrors the core smartthings entry). "
|
||
"Skipped: the shared water-filter key 'filter_usage' (fridge, "
|
||
"dishwasher, water purifier AND the AMF microfiber lint unit on "
|
||
"the washer registry — one key, different duties). Reset "
|
||
"(verified @ 2e942fc): 'air_filter_reset' (common."
|
||
"filter_reset_button, filterReset 'On' to the same "
|
||
"/filter/airdustfilter resource; measured on a RAC board, #449). "
|
||
"The hepa/hood resets are only extrapolated upstream and stay "
|
||
"unwired."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("air_filter_usage",),
|
||
"Filter Cleaning",
|
||
"alert_above",
|
||
delta_units=90,
|
||
require_sibling_keys=("air_filter_usage_hours",),
|
||
resets=(("air_filter_usage", "air_filter_reset"),),
|
||
),
|
||
ConsumableSignature(("hepa_filter_usage", "filter_progress"), "Replace Filter", "alert_above", delta_units=90),
|
||
ConsumableSignature(("hood_filter_usage",), "Clean Grease Filter", "alert_above", delta_units=90),
|
||
),
|
||
),
|
||
"flexit": IntegrationSignature(
|
||
name="Flexit (Modbus)",
|
||
verified="2026-09-25 @ home-assistant/core dev",
|
||
source=(
|
||
"core flexit (Modbus CI66; config flow since 2026.9, the sensor "
|
||
"and binary_sensor platforms are on dev = 2026.10): tk "
|
||
"'air_filter_operating_time' (DURATION, HOURS, TOTAL_INCREASING; "
|
||
"flexit_modbus Measurements.filter_running_hours, input register "
|
||
"8 — the filter timer behind the filter alarm, not a unit "
|
||
"runtime) → usage_above at 4380 h, mirroring flexit_bacnet. A "
|
||
"'filter_alarm' problem binary also exists (adoption path)."
|
||
),
|
||
tasks=(
|
||
VENTILATION_FILTER_OPERATING_TIME,
|
||
),
|
||
),
|
||
# ─── Round 15 part 2 (2026-09-27): HACS HRVs, ACs, thermostats, ──────
|
||
# ─── fragrance diffusers ─────────────────────────────────────────────
|
||
"genvex_connect": IntegrationSignature(
|
||
name="Genvex Connect / Nilan gateway",
|
||
verified="2026-09-27 @ superrob/genvexconnect main (70a1c55) + superrob/genvexnabto 1.5.4",
|
||
source=(
|
||
"HACS genvex_connect entity.py: translation_key = the genvexnabto "
|
||
"value key (plain str constants, every entity). sensor.py: "
|
||
"'filter_days_left' (unit 'd', 'Days left until filter change'; "
|
||
"Nilan CTS400/CTS602/CTS602light) → duration_left 7 days; "
|
||
"'filter_days' (unit 'd', 'Days since filter change'; Genvex "
|
||
"Optima 270/314, counts up until the reset) → usage_above 4,380 h "
|
||
"like the other HRV filter timers. The two never exist on one "
|
||
"model. button.py 'filter_reset' (CONFIG, created when the model "
|
||
"provides FILTER_RESET — every model above) writes the setpoint 1. "
|
||
"The DHW 'sacrificial_anode' enum ('service' state) is left out: "
|
||
"its meaning is only inferred from the option names."
|
||
),
|
||
translation_keys_authoritative=True,
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("filter_days_left",),
|
||
"Replace Ventilation Filter",
|
||
"duration_left",
|
||
below_hours=168,
|
||
resets=(("filter_days_left", "filter_reset"),),
|
||
),
|
||
ConsumableSignature(
|
||
("filter_days",),
|
||
"Replace Ventilation Filter",
|
||
"usage_above",
|
||
above_hours=4380,
|
||
resets=(("filter_days", "filter_reset"),),
|
||
),
|
||
),
|
||
),
|
||
"nilan": IntegrationSignature(
|
||
name="Nilan (CTS602 Modbus)",
|
||
verified="2026-09-27 @ veista/nilan master (6cdbbfe)",
|
||
source=(
|
||
"HACS nilan sensor.py: translation_key = the map name on every "
|
||
"sensor; 'days_to_air_filter_change' (UnitOfTime.DAYS, input "
|
||
"register air_flow_to_filt_day, bus version >= 9) → duration_left "
|
||
"7 days. 'days_since_air_filter_change' is the same duty from the "
|
||
"other side and is not signed twice; the DHW 'anode_state' raw "
|
||
"register (2 = Service) is left out (meaning unconfirmed). No "
|
||
"filter-reset button (button.py has only sync_time)."
|
||
),
|
||
translation_keys_authoritative=True,
|
||
tasks=(ConsumableSignature(("days_to_air_filter_change",), "Replace Ventilation Filter", "duration_left", below_hours=168),),
|
||
),
|
||
"ha_comfoconnectpro": IntegrationSignature(
|
||
name="Zehnder ComfoConnect Pro (Modbus)",
|
||
verified="2026-09-27 @ hstrohmaier/ha_comfoconnectpro main (e03e4b6)",
|
||
source=(
|
||
"HACS ha_comfoconnectpro const.py: 'filter_days_remaining' (input "
|
||
"register 25, unit 'd' → DURATION; translation_key = key for every "
|
||
"generated entity) — days until the filter change, the ComfoAir "
|
||
"Q's own countdown → duration_left 7 days like the core "
|
||
"comfoconnect entry. The 'filter_dirty' binary is the same duty "
|
||
"(not signed twice). No reset entity."
|
||
),
|
||
translation_keys_authoritative=True,
|
||
tasks=(ConsumableSignature(("filter_days_remaining",), "Replace Ventilation Filter", "duration_left", below_hours=168),),
|
||
),
|
||
"heru": IntegrationSignature(
|
||
name="Östberg HERU",
|
||
verified="2026-09-27 @ toringer/home-assistant-heru master (ec1294f)",
|
||
source=(
|
||
"HACS heru const.py (name-based entities under has_entity_name, no "
|
||
"translation_key): 'Filter days left' (input register 3x00020, "
|
||
"unit spelled out as 'days') → suffix _filter_days_left, "
|
||
"duration_left 7 days; button 'Reset filter timer' (coil "
|
||
"0x00006) → suffix _reset_filter_timer resets it."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("filter_days_left",),
|
||
"Replace Ventilation Filter",
|
||
"duration_left",
|
||
below_hours=168,
|
||
resets=(("filter_days_left", "reset_filter_timer"),),
|
||
),
|
||
),
|
||
),
|
||
"midea_ac": IntegrationSignature(
|
||
name="Midea Smart AC (msmart-ng)",
|
||
verified="2026-09-27 @ mill1000/midea-ac-py main (d433bbb)",
|
||
source=(
|
||
"HACS midea_ac climate.py (AC and commercial AC devices only): "
|
||
"_OPERATIONAL_MODE_TO_HVAC_MODE maps auto/cool/dry/heat/fan to "
|
||
"auto/cool/dry/heat/fan_only, off otherwise — every mode except "
|
||
"off moves air through the filter → the same engine-runtime duty "
|
||
"as gree, with its cadence (Daikin's guidance: clean filters every "
|
||
"2 weeks ≈ 100 runtime hours at in-season use). The unit's own "
|
||
"'filter_alert' binary (supports_filter_reminder only) is "
|
||
"device_class problem → problem-sensor adoption."
|
||
),
|
||
tasks=(_AC_FILTER_CLEANING_RUNTIME,),
|
||
),
|
||
"nest_legacy": IntegrationSignature(
|
||
name="Nest (legacy API)",
|
||
verified="2026-09-27 @ tronikos/nest_legacy main (018eb74)",
|
||
source=(
|
||
"HACS nest_legacy sensor.py: tk 'filter_runtime' (thermostats with "
|
||
"an air filter; SECONDS, DURATION, TOTAL_INCREASING) = the "
|
||
"FilterReminder trait's filterRuntime, the HVAC runtime on the "
|
||
"current filter → usage_above 300 h, the furnace-filter cadence of "
|
||
"the venstar duty. Nest's own reminder is the problem-class "
|
||
"'filter_replacement_needed' binary (adoption path); Protect "
|
||
"'replace_by' is a DATE sensor (no direction)."
|
||
),
|
||
tasks=(ConsumableSignature(("filter_runtime",), "Replace Filter", "usage_above", above_hours=300),),
|
||
),
|
||
"pura": IntegrationSignature(
|
||
name="Pura fragrance diffusers",
|
||
verified="2026-09-27 @ natekspencer/ha-pura main (6166f16)",
|
||
source=(
|
||
"HACS pura sensor.py: tk 'fragrance_remaining' (car/mini) and "
|
||
"'bay_fragrance_remaining' (wall/plus, one per bay with the "
|
||
"placeholder {bay}), PERCENTAGE — pypura's bay.remaining.percent, "
|
||
"else (expectedLifeHours − runtime) / expectedLife → % REMAINING. "
|
||
"Two-bay diffusers get one task per vial."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("fragrance_remaining", "bay_fragrance_remaining"),
|
||
"Replace Air Freshener",
|
||
"percent_left",
|
||
per_entity=True,
|
||
),
|
||
),
|
||
),
|
||
# ─── Round 16 (2026-10-03): ventilation units that report the filter ─
|
||
# ─── change DATE (due_date engine), core 2026.9 + 2026.10 ────────────
|
||
# Reviewed and left out (problem-class binaries → problem-sensor
|
||
# adoption): sensibo 'filter_clean' (its 'filter_last_reset' TIMESTAMP is
|
||
# the LAST reset, not a due date; the 'reset_filter' button has no duty to
|
||
# attach to), coolmaster 'clean_filter' (+ 'reset_filter' button),
|
||
# actron_air 'clean_filter', intelliclima 'filter_cleaning', homee
|
||
# 'replace_filter', flexit_bacnet 'air_filter_polluted'. helty: only the
|
||
# 'reset_filter' button — core exposes no filter sensor (pyhelty 0.2.0
|
||
# reads the remaining filter hours but no entity carries them). wemo: the
|
||
# humidifier's filter life is only the 'filter_life' ATTRIBUTE of its fan
|
||
# entity, reset only by the wemo.reset_filter_life service.
|
||
"smarty": IntegrationSignature(
|
||
name="Salda Smarty ventilation",
|
||
verified="2026-10-03 @ home-assistant/core 2026.10.0b0 (sensor.py/button.py identical in 2026.9.0b0 and dev)",
|
||
source=(
|
||
"core smarty sensor.py: tk 'filter_days_left' ('Filter days left', "
|
||
"device_class TIMESTAMP, no unit, enabled) — get_filter_days_left "
|
||
"returns now + pysmarty2 Smarty.filter_timer days (Modbus "
|
||
"'IR_FILTERS_TIMER_DAYS_LEFT'), i.e. the DATE the filter needs to "
|
||
"be replaced → due_date with a week's lead to order filters. "
|
||
"button.py: tk 'reset_filters_timer' ('Reset filters timer', "
|
||
"Smarty.reset_filters_timer writes 'COIL_FILTER_TIMER_RESET') "
|
||
"restarts the timer, which moves the date forward and "
|
||
"auto-completes the task. Every entity sets a translation_key "
|
||
"(the fan 'fan')."
|
||
),
|
||
translation_keys_authoritative=True,
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("filter_days_left",),
|
||
"Replace Ventilation Filter",
|
||
"due_date",
|
||
days_before=7,
|
||
resets=(("filter_days_left", "reset_filters_timer"),),
|
||
),
|
||
),
|
||
),
|
||
"vallox": IntegrationSignature(
|
||
name="Vallox ventilation",
|
||
verified="2026-10-03 @ home-assistant/core 2026.10.0b0 (sensor.py identical in 2026.9.0b0 and dev)",
|
||
source=(
|
||
"core vallox sensor.py: tk 'remaining_time_for_filter' ('Remaining "
|
||
"time for filter', device_class TIMESTAMP, DIAGNOSTIC, enabled) — "
|
||
"ValloxFilterRemainingSensor returns vallox_websocket_api's "
|
||
"next_filter_change_date (last change date "
|
||
"A_CYC_FILTER_CHANGED_DAY/MONTH/YEAR + A_CYC_FILTER_CHANGE_INTERVAL "
|
||
"days) at 13:00 local → due_date with a week's lead. No reset "
|
||
"button: the change date is the CONFIG date entity "
|
||
"'filter_change_date' (date.py, set_filter_change_date) — setting "
|
||
"it after the change moves the due date forward and auto-completes "
|
||
"the task, but a date entity cannot be wired as a button press."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(("remaining_time_for_filter",), "Replace Ventilation Filter", "due_date", days_before=7),
|
||
),
|
||
),
|
||
# ─── Round 16 HACS wave (2026-10-03): air conditioners by engine ─────
|
||
# ─── runtime, decentral HRV filters ──────────────────────────────────
|
||
# Reviewed and left out: ithodaalderop 'airfilter_counter' /
|
||
# 'filter_use_counter' (hours, TOTAL_INCREASING — whether the unit zeroes
|
||
# them at a filter reset is not established in the source); xtend_tuya
|
||
# 'filter_life' (its descriptor manager can also hand it core tuya's
|
||
# category descriptors, so the key is not specific to fresh-air units,
|
||
# and the unit comes from the device's DP).
|
||
"panasonic_cc": IntegrationSignature(
|
||
name="Panasonic Comfort Cloud (air conditioners)",
|
||
verified="2026-10-03 @ sockless-coding/panasonic_cc master (90c653c)",
|
||
source=(
|
||
"HACS panasonic_cc panasonic/climate.py: the AC's climate entity "
|
||
"has translation_key 'climate' (PANASONIC_CLIMATE_DESCRIPTION; "
|
||
"has_entity_name, no translated name → climate.<device>); "
|
||
"convert_operation_mode_to_hvac_mode maps Auto/Cool/Dry/Fan/Heat to "
|
||
"heat_cool (cool under iAUTO-X)/cool/dry/fan_only/heat — every mode "
|
||
"except off moves air through the filter → engine runtime with the "
|
||
"AC cadence of gree/midea_ac (filters cleaned every 2 weeks ≈ 100 "
|
||
"runtime hours in season). The Aquarea heat pumps of the same "
|
||
"integration (aquarea/climate.py) carry per-zone climates with tk "
|
||
"'zone-<id>-climate' and stay out: every panasonic_cc entity sets a "
|
||
"translation_key (PanasonicDataEntity / AquareaDataEntity / "
|
||
"HwsDataEntity bases and the buttons), so the key is authoritative."
|
||
),
|
||
translation_keys_authoritative=True,
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("climate",),
|
||
"Filter Cleaning",
|
||
"runtime_hours",
|
||
delta_units=100,
|
||
entity_domain="climate",
|
||
on_states=("cool", "dry", "fan_only", "heat", "heat_cool"),
|
||
),
|
||
),
|
||
),
|
||
"mitsubishi_wf_rac": IntegrationSignature(
|
||
name="Mitsubishi Heavy Industries (WF-RAC)",
|
||
verified="2026-10-03 @ blues-sechseck/Mitsubishi-WF-RAC-Integration main (3056b27)",
|
||
source=(
|
||
"HACS mitsubishi_wf_rac climate.py: one AircoClimate per device "
|
||
"(translation_key 'mitsubishi_wf_rac', name None); const.py "
|
||
"SUPPORTED_HVAC_MODES off/auto/cool/dry/heat/fan_only — every mode "
|
||
"except off moves air through the filter → the shared AC "
|
||
"engine-runtime duty of gree/midea_ac. No filter entity."
|
||
),
|
||
tasks=(_AC_FILTER_CLEANING_RUNTIME,),
|
||
),
|
||
"toshiba_ac": IntegrationSignature(
|
||
name="Toshiba AC",
|
||
verified="2026-10-03 @ h4de5/home-assistant-toshiba_ac main (b0b893e)",
|
||
source=(
|
||
"HACS toshiba_ac climate.py: one ToshibaClimate per AC device "
|
||
"(has_entity_name, name None); TOSHIBA_TO_HVAC_MODE maps "
|
||
"AUTO/COOL/HEAT/DRY/FAN to auto/cool/heat/dry/fan_only, off "
|
||
"otherwise → the shared AC engine-runtime duty. No filter entity."
|
||
),
|
||
tasks=(_AC_FILTER_CLEANING_RUNTIME,),
|
||
),
|
||
"fujitsu_airstage": IntegrationSignature(
|
||
name="Fujitsu Airstage",
|
||
verified="2026-10-03 @ danielkaldheim/ha_airstage main (12d4dbe)",
|
||
source=(
|
||
"HACS fujitsu_airstage climate.py: one AirstageAC per indoor unit, "
|
||
"each its own device (entity.py AirstageAcEntity identifiers = the "
|
||
"ac_key); FUJITSU_TO_HA_STATE maps FAN/DRY/COOL/HEAT/AUTO to "
|
||
"fan_only/dry/cool/heat/auto → the shared AC engine-runtime duty. "
|
||
"No filter entity."
|
||
),
|
||
tasks=(_AC_FILTER_CLEANING_RUNTIME,),
|
||
),
|
||
"aux_cloud": IntegrationSignature(
|
||
name="AUX Cloud (air conditioners)",
|
||
verified="2026-10-03 @ maeek/ha-aux-cloud main (85ae111)",
|
||
source=(
|
||
"HACS aux_cloud climate.py: an AIR_CONDITIONER device gets "
|
||
"AuxACClimateEntity (AC_DESCRIPTION translation_key 'aux_ac', "
|
||
"strings.json 'Air Conditioner'), a HEAT_PUMP device "
|
||
"AuxHeatPumpClimateEntity (tk 'aux_heater', heat/cool only); "
|
||
"const.py MODE_MAP_AUX_AC_TO_HA maps auto/cooling/heating/dry/fan "
|
||
"to auto/cool/heat/dry/fan_only → the shared AC engine-runtime "
|
||
"duty, keyed to 'aux_ac' so the heat pump's climate stays out."
|
||
),
|
||
tasks=(replace(_AC_FILTER_CLEANING_RUNTIME, keys=("aux_ac",)),),
|
||
),
|
||
"daikin_onecta": IntegrationSignature(
|
||
name="Daikin Onecta (air conditioners)",
|
||
verified=(
|
||
"2026-10-03 @ jwillemsen/daikin_onecta master (e45cc60) + jwillemsen/daikin-onecta main "
|
||
"(models.py model_info = 'modelInfo')"
|
||
),
|
||
source=(
|
||
"HACS daikin_onecta climate.py: one DaikinClimate per setpoint of "
|
||
"the climateControl management point, translation_key from "
|
||
"entity_descriptions.py ENTITY_METADATA ('roomtemperature', "
|
||
"'leavingwateroffset', 'leavingwatertemperature', "
|
||
"'calculatedleavingwatertemperature'); DAIKIN_HVAC_TO_HA maps "
|
||
"fanOnly/dry/cooling/heating/auto/humidification to "
|
||
"fan_only/dry/cool/heat/heat_cool/dry. The 'roomtemperature' "
|
||
"climate also exists on Altherma heat pumps and Daikin gas boilers, "
|
||
"and the type-identifying entities (streamer, econo mode, caution "
|
||
"state) sit on per-management-point sub-devices out of a sibling "
|
||
"gate's reach. Gate: the climate's device model is the gateway "
|
||
"adapter (device.py fill_device_info: gateway modelInfo) — the AC "
|
||
"WLAN adapter families BRP069A4x / BRP069B4x / BRP069C4x-C8x, "
|
||
"while the heat-pump and boiler fixtures report the Altherma "
|
||
"LAN/WLAN adapters BRP069A61/A62/A71/A78 or DRGATEWAYAA (NDJ gas "
|
||
"boiler) — all 20 API fixtures in the integration's tests "
|
||
"checked. Engine runtime with the core daikin cadence (clean "
|
||
"filters every 2 weeks ≈ 100 runtime hours)."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("roomtemperature",),
|
||
"Filter Cleaning",
|
||
"runtime_hours",
|
||
delta_units=100,
|
||
entity_domain="climate",
|
||
on_states=("cool", "dry", "fan_only", "heat", "heat_cool"),
|
||
models=("BRP069A4", "BRP069B4", "BRP069C"),
|
||
),
|
||
),
|
||
),
|
||
"ambientika": IntegrationSignature(
|
||
name="Ambientika (SUEDWIND)",
|
||
verified="2026-10-03 @ ambientika/HomeAssistant-integration-for-Ambientika main (98af980)",
|
||
source=(
|
||
"HACS ambientika sensor.py FilterStatusSensor: translation_key "
|
||
"'filter_status' (translations/en.json 'Filter Status'), "
|
||
"has_entity_name, no unit; the state is const.py FilterStatus "
|
||
"(StrEnum values 'bad' / 'medium' / 'good') from the cloud's "
|
||
"filters_status → event latch on 'bad' ('medium' is the "
|
||
"pre-warning, not latched), cleared when the status recovers. "
|
||
"button.py FilterResetButton: translation_key 'filter_reset' (GET "
|
||
"device/reset-filter) resets it. SUEDWIND decentral heat-recovery "
|
||
"units, one sensor and one button per unit."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("filter_status",),
|
||
"Replace Ventilation Filter",
|
||
"event_present",
|
||
on_states=("bad",),
|
||
resets=(("filter_status", "filter_reset"),),
|
||
),
|
||
),
|
||
),
|
||
"siku": IntegrationSignature(
|
||
name="Siku / Blauberg ventilation fans",
|
||
verified="2026-10-03 @ hmn/siku-integration main (3afa1bb)",
|
||
source=(
|
||
"HACS siku sensor.py SENSORS key 'filter_timer_minutes', name "
|
||
"'Filter timer countdown' (DURATION, native minutes, suggested unit "
|
||
"days; V2 API only — api_v2.py parses register 0x64 as the "
|
||
"days/hours/minutes LEFT until the filter change). SikuEntity sets "
|
||
"neither has_entity_name nor translation_key → "
|
||
"sensor.filter_timer_countdown (exact-object-id pattern; a second "
|
||
"fan's _2 id is matched by the original name) → duration_left 7 "
|
||
"days. button.py 'Reset filter alarm' (key reset_filter_alarm → "
|
||
"button.reset_filter_alarm) sends reset-alarms + 0x65 "
|
||
"reset-filter-timer, which restarts the countdown."
|
||
),
|
||
tasks=(
|
||
ConsumableSignature(
|
||
("filter_timer_countdown",),
|
||
"Replace Ventilation Filter",
|
||
"duration_left",
|
||
below_hours=168,
|
||
resets=(("filter_timer_countdown", "reset_filter_alarm"),),
|
||
),
|
||
),
|
||
),
|
||
"ecovent_v2": IntegrationSignature(
|
||
name="Vents / Blauberg EcoVent (v2)",
|
||
verified="2026-10-03 @ gody01/ecovent_v2 main (4851688)",
|
||
source=(
|
||
"HACS ecovent_v2 sensor_specs.py SensorSpec '_filter_remaining' "
|
||
"('Filter remaining', PERCENTAGE, DIAGNOSTIC, enabled; fans with the "
|
||
"filter_maintenance capability reporting the filter countdown and "
|
||
"its setpoint): sensor.py filter_remaining = countdown hours / "
|
||
"(setpoint days × 24) × 100, clamped 0-100 → % REMAINING. "
|
||
"VentoSensor has_entity_name without translation_key, object id "
|
||
"<fan>_<method> (entity_naming.py) → suffix _filter_remaining. One "
|
||
"duty per filter: the hours countdown ('Filter change in', h; older "
|
||
"ids of it also ended in _filter_remaining — the % unit gate keeps "
|
||
"them out) is the same filter. The timer reset is only the fan "
|
||
"entity SERVICE ecovent_v2.filter_timer_reset (no button)."
|
||
),
|
||
tasks=(ConsumableSignature(("filter_remaining",), "Replace Ventilation Filter", "percent_left"),),
|
||
),
|
||
}
|