155 files
This commit is contained in:
1 parent
a212615470
commit
0ff8c287a8
155 files changed
+19264
-3148
No files matched your search
@@ -1,8 +1,11 @@
|
||||
"""Helpers for rewriting entity_id references when HA renames an entity.
|
||||
|
||||
Used by the global ``EVENT_ENTITY_REGISTRY_UPDATED`` listener in ``__init__.py``
|
||||
to keep ``trigger_config["entity_id"|"entity_ids"]`` and
|
||||
``adaptive_config["environmental_entity"]`` in sync with the user's renames.
|
||||
to keep ``trigger_config["entity_id"|"entity_ids"]``,
|
||||
``adaptive_config["environmental_entity"]``, calendar schedules, to-do mirror
|
||||
targets, the completion action's target and the battery fleet's lists in sync
|
||||
with the user's renames (the listener itself rewrites the global shopping-list
|
||||
and notify-entity targets).
|
||||
|
||||
Without this, ``async_track_state_change_event`` (which subscribes by literal
|
||||
entity_id) silently misses events on the new id — same dual-storage class of
|
||||
@@ -101,9 +104,38 @@ def rewrite_task(task_data: dict[str, Any], old_id: str, new_id: str) -> tuple[d
|
||||
new_task["mirror_todo_entities"] = [new_id if e == old_id else e for e in mirrors]
|
||||
changed = True
|
||||
|
||||
# The completion action's target — above all the integration's reset
|
||||
# button wired at adoption (2.95): a renamed button used to leave the
|
||||
# counter running behind a repair message until someone re-picked it.
|
||||
action = new_task.get("on_complete_action")
|
||||
if isinstance(action, dict):
|
||||
new_action, action_changed = _rewrite_action(action, old_id, new_id)
|
||||
if action_changed:
|
||||
new_task["on_complete_action"] = new_action
|
||||
changed = True
|
||||
|
||||
return new_task, changed
|
||||
|
||||
|
||||
def _rewrite_action(action: dict[str, Any], old_id: str, new_id: str) -> tuple[dict[str, Any], bool]:
|
||||
"""An action's ``target.entity_id`` and ``data.entity_id`` (a single id or
|
||||
a list) with ``old_id`` replaced."""
|
||||
new_action = dict(action)
|
||||
changed = False
|
||||
for part in ("target", "data"):
|
||||
block = new_action.get(part)
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
eid = block.get("entity_id")
|
||||
if eid == old_id:
|
||||
new_action[part] = {**block, "entity_id": new_id}
|
||||
changed = True
|
||||
elif isinstance(eid, list) and old_id in eid:
|
||||
new_action[part] = {**block, "entity_id": [new_id if e == old_id else e for e in eid]}
|
||||
changed = True
|
||||
return new_action, changed
|
||||
|
||||
|
||||
def rewrite_object(obj: dict[str, Any], old_id: str, new_id: str) -> tuple[dict[str, Any], bool]:
|
||||
"""Rewrite the battery fleet's manual include / exclude lists (#135) —
|
||||
a renamed battery sensor fell out of the fleet (include) or came back
|
||||
|
||||
@@ -15,7 +15,9 @@ then direct signals (percent/countdown/resettable counter/event), then derived
|
||||
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.
|
||||
fallback for custom integrations that don't set one — and, for those, the
|
||||
registry's ``original_name`` (the integration's own entity name), which a
|
||||
user's rename of the entity id leaves alone.
|
||||
|
||||
Direction semantics:
|
||||
* ``duration_left`` — countdown to the next replacement (device_class
|
||||
@@ -78,6 +80,7 @@ from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import slugify
|
||||
|
||||
# Hours a duration-countdown may still hold when the task should trigger.
|
||||
_DEFAULT_BELOW_HOURS = 24
|
||||
@@ -256,7 +259,14 @@ def _entity_matches(entry: er.RegistryEntry, key: str, *, tk_authoritative: bool
|
||||
# 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
|
||||
if entry.entity_id.split(".", 1)[1] == key:
|
||||
return True
|
||||
# Fifth: the integration's own name for the entity. The entity id was
|
||||
# derived from it once, but the user may rename the id — ``original_name``
|
||||
# stays the integration's word ('Filter Life' → filter_life), so a renamed
|
||||
# sensor of an integration without translation keys is still recognised.
|
||||
original = slugify(entry.original_name) if entry.original_name else ""
|
||||
return bool(original) and (original == key or original.endswith(f"_{key}"))
|
||||
|
||||
|
||||
def _entity_unit(hass: HomeAssistant, entry: er.RegistryEntry) -> str | None:
|
||||
|
||||
@@ -7,6 +7,8 @@ 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,
|
||||
@@ -16,8 +18,10 @@ from ._shared import (
|
||||
)
|
||||
|
||||
# One object for the AC-only integrations whose climate entity runs through
|
||||
# these HVAC modes (gree, midea_ac) — shared here, next to its users, so the
|
||||
# verbatim-duplicate tripwire holds without a _shared.py entry.
|
||||
# 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",
|
||||
@@ -620,4 +624,252 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
),
|
||||
),
|
||||
# ─── 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"),),
|
||||
),
|
||||
}
|
||||
@@ -23,7 +23,7 @@ from ._shared import (
|
||||
|
||||
# Duties two integrations of this module name identically (same key, same
|
||||
# countdown) — one object each, so the verbatim-duplicate tripwire holds.
|
||||
# 'Service inspection distance': Audi Connect and Volkswagen (WeConnect).
|
||||
# 'Service inspection distance': Audi Connect, Volkswagen (WeConnect) and PyCupra.
|
||||
_ANNUAL_SERVICE_INSPECTION_DISTANCE = ConsumableSignature(
|
||||
("service_inspection_distance",), "Annual Service", "value_below", delta_units=1000
|
||||
)
|
||||
@@ -31,6 +31,15 @@ _ANNUAL_SERVICE_INSPECTION_DISTANCE = ConsumableSignature(
|
||||
_ANNUAL_SERVICE_DAYS_TO_SERVICE = ConsumableSignature(
|
||||
("days_to_service",), "Annual Service", "duration_left", below_hours=336
|
||||
)
|
||||
# The volkswagencarnet / pycupra dashboard instruments ('Service inspection
|
||||
# days', 'Oil inspection days' / 'distance') — one object per duty for both.
|
||||
_ANNUAL_SERVICE_INSPECTION_DAYS = ConsumableSignature(
|
||||
("service_inspection_days",), "Annual Service", "duration_left", below_hours=336
|
||||
)
|
||||
_OIL_SERVICE_INSPECTION_DAYS = ConsumableSignature(("oil_inspection_days",), "Oil Service", "duration_left", below_hours=336)
|
||||
_OIL_SERVICE_INSPECTION_DISTANCE = ConsumableSignature(
|
||||
("oil_inspection_distance",), "Oil Service", "value_below", delta_units=1000
|
||||
)
|
||||
|
||||
SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"kia_uvo": IntegrationSignature(
|
||||
@@ -497,10 +506,10 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
tasks=(
|
||||
TIRE_ROTATION_ODOMETER,
|
||||
ConsumableSignature(("service_inspection_days",), "Annual Service", "duration_left", below_hours=336),
|
||||
_ANNUAL_SERVICE_INSPECTION_DAYS,
|
||||
_ANNUAL_SERVICE_INSPECTION_DISTANCE,
|
||||
ConsumableSignature(("oil_inspection_days",), "Oil Service", "duration_left", below_hours=336),
|
||||
ConsumableSignature(("oil_inspection_distance",), "Oil Service", "value_below", delta_units=1000),
|
||||
_OIL_SERVICE_INSPECTION_DAYS,
|
||||
_OIL_SERVICE_INSPECTION_DISTANCE,
|
||||
),
|
||||
),
|
||||
"smarthashtag": IntegrationSignature(
|
||||
@@ -541,4 +550,109 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
ConsumableSignature(("oil",), "Oil Service", "percent_left"),
|
||||
),
|
||||
),
|
||||
# ─── Round 16 (2026-10-03): HACS cars, car alarms and the UK MOT ────────
|
||||
"byd_vehicle": IntegrationSignature(
|
||||
name="BYD",
|
||||
verified="2026-10-03 @ jkaberg/hass-byd-vehicle main (0f0cf3f)",
|
||||
source=(
|
||||
"HACS byd_vehicle (custom repository) sensor.py BydSensor: _attr_translation_key = "
|
||||
"description.key on every sensor (every other platform sets one too). tk 'total_mileage' "
|
||||
"(realtime total_mileage, KILOMETERS, DISTANCE, TOTAL_INCREASING, enabled; en 'Odometer' → "
|
||||
"entity id …_odometer) — the lifetime odometer → the usual 15,000 km service / 10,000 km "
|
||||
"tire-rotation pair; no service countdown exists. translation_keys_authoritative keeps the "
|
||||
"suffix fallback off the twins 'total_mileage_v2' and 'energy_cumulative_total_mileage' "
|
||||
"(own keys, DIAGNOSTIC, disabled by default)."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
ConsumableSignature(("total_mileage",), "Annual Service", "usage_delta", delta_units=15000),
|
||||
ConsumableSignature(("total_mileage",), "Tire Rotation", "usage_delta", delta_units=10000),
|
||||
),
|
||||
),
|
||||
"dvla": IntegrationSignature(
|
||||
name="DVLA Vehicle Enquiry (UK)",
|
||||
verified="2026-10-03 @ jampez77/DVLA-Vehicle-Enquiry-Service main (739ebee)",
|
||||
source=(
|
||||
"HACS dvla sensor.py: 'motExpiryDate' ('MOT Expiry Date', device_class DATE) — entity_id "
|
||||
"forced to sensor.dvla_<registration>_motexpirydate (lowercased), no translation_key, no "
|
||||
"has_entity_name, so the original name 'MOT Expiry Date' slugifies differently: both "
|
||||
"'motexpirydate' and 'mot_expiry_date' are keys. Always created: without an MOT date in "
|
||||
"the DVLA record (a car under three years old) update_from_coordinator falls back to the "
|
||||
"first-registration month + 3 years — the first MOT. → Roadworthiness Test (the car "
|
||||
"templates' duty) 30 days ahead; the record's next expiry date clears the trigger after "
|
||||
"the test. Skipped: 'taxDueDate' (vehicle tax, not maintenance) and 'motStatus' / "
|
||||
"'taxStatus' (status)."
|
||||
),
|
||||
tasks=(ConsumableSignature(("motexpirydate", "mot_expiry_date"), "Roadworthiness Test", "due_date", days_before=30),),
|
||||
),
|
||||
"pandora_cas": IntegrationSignature(
|
||||
name="Pandora Car Alarm System",
|
||||
verified="2026-10-03 @ alryaz/hass-pandora-cas master (ca2194d) + pandora-cas 0.0.16; turbulator/pandora-cas master (1392544)",
|
||||
source=(
|
||||
"Two HACS forks share the domain. alryaz/hass-pandora-cas: "
|
||||
"PandoraCASEntityDescription.__post_init__ sets translation_key = key on every "
|
||||
"description, entity_id sensor.<device_id>_<key>. sensor.py 'mileage' ('Mileage', "
|
||||
"KILOMETERS, DISTANCE, state_class TOTAL — the alarm unit's odometer) → Tire Rotation; "
|
||||
"'days_to_maintenance' ('Days to Maintenance', DURATION, UnitOfTime.DAYS, DIAGNOSTIC; "
|
||||
"CurrentState.can_days_to_maintenance = the CAN bus's 'CAN_days_to_maintenance') → Annual "
|
||||
"Service two weeks ahead — the car's own countdown, as for Škoda and Stellantis (the entity "
|
||||
"is unavailable while the car's CAN bus does not report it, entity.py "
|
||||
"update_native_value). translation_keys_authoritative keeps 'can_mileage' / "
|
||||
"'can_mileage_by_battery' / 'can_mileage_to_empty' (own keys) off the mileage duty. "
|
||||
"turbulator/pandora-cas (older, no translation keys): ENTITY_CONFIGS 'mileage' → "
|
||||
"sensor.<pandora_id>_mileage (suffix match); it has no maintenance countdown. Skipped: "
|
||||
"'remaining_engine_runtime' (the remote-start timer)."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
TIRE_ROTATION_MILEAGE,
|
||||
ConsumableSignature(("days_to_maintenance",), "Annual Service", "duration_left", below_hours=336),
|
||||
),
|
||||
),
|
||||
"vag_connect": IntegrationSignature(
|
||||
name="VW Group Connect (vag_connect)",
|
||||
verified="2026-10-03 @ its-me-prash/vwgroup-connect-ha main (3f18940)",
|
||||
source=(
|
||||
"HACS vag_connect (Volkswagen / Audi / Škoda / SEAT / CUPRA / Porsche incl. North America) "
|
||||
"sensor.py SENSOR_DESCRIPTIONS: every description sets translation_key (= key); data "
|
||||
"sensors spawn only once their value arrives (hide_empty_entities, default on). "
|
||||
"'service_due_in_days' (unit 'd', days remaining) and 'service_km' (KILOMETERS remaining — "
|
||||
"vw_eu inspectionDue_km, skoda inspectionDueInKm, porsche MAIN_SERVICE_RANGE) → Annual "
|
||||
"Service; 'oil_service_due_in_days' / 'oil_service_km' (condition 'combustion') → Oil "
|
||||
"Service — the car's own countdowns replace the editorial odometer service, as for Škoda "
|
||||
"and Audi; 'odometer_km' (KILOMETERS, TOTAL_INCREASING) → Tire Rotation. "
|
||||
"translation_keys_authoritative plus the longer-key guard keep the 'oil_service_*' entities "
|
||||
"off the 'service_*' keys. Skipped: the DATE twins 'service_due_at' / 'oil_service_at' "
|
||||
"(the same countdowns as dates) and 'reminder_technical_inspection' (date-or-status mix)."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
ConsumableSignature(("odometer_km",), "Tire Rotation", "usage_delta", delta_units=10000),
|
||||
ConsumableSignature(("service_due_in_days",), "Annual Service", "duration_left", below_hours=336),
|
||||
ConsumableSignature(("service_km",), "Annual Service", "value_below", delta_units=1000),
|
||||
ConsumableSignature(("oil_service_due_in_days",), "Oil Service", "duration_left", below_hours=336),
|
||||
ConsumableSignature(("oil_service_km",), "Oil Service", "value_below", delta_units=1000),
|
||||
),
|
||||
),
|
||||
"pycupra": IntegrationSignature(
|
||||
name="CUPRA / SEAT (PyCupra)",
|
||||
verified="2026-10-03 @ WulfgarW/homeassistant-pycupra main (6187999) + pycupra 0.2.37",
|
||||
source=(
|
||||
"HACS pycupra: entity name = f'{vehicle_name} {instrument.name}' (no has_entity_name, no "
|
||||
"translation_key → entity-id suffixes). The instruments come from the pinned pycupra "
|
||||
"library (>=0.2.36; 0.2.37 dashboard.py) and exist only where the car supports them "
|
||||
"(Instrument.setup → is_<attr>_supported): 'Odometer' (km), 'Service inspection days' "
|
||||
"(unit 'd', maintenance.inspectionDueDays — 'time left until service inspection'), "
|
||||
"'Service inspection distance' (km, inspectionDueKm), 'Oil inspection days' / 'Oil "
|
||||
"inspection distance' (oilServiceDueDays / oilServiceDueKm, only where reported) — the "
|
||||
"volkswagencarnet instruments, names and semantics, so the same duty objects."
|
||||
),
|
||||
tasks=(
|
||||
TIRE_ROTATION_ODOMETER,
|
||||
_ANNUAL_SERVICE_INSPECTION_DAYS,
|
||||
_ANNUAL_SERVICE_INSPECTION_DISTANCE,
|
||||
_OIL_SERVICE_INSPECTION_DAYS,
|
||||
_OIL_SERVICE_INSPECTION_DISTANCE,
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -191,8 +191,13 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
# 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."),
|
||||
verified="2026-07-20 @ home-assistant/core dev; skips re-checked 2026-10-03 @ home-assistant/core 2026.10.0b0",
|
||||
source=(
|
||||
"core screenlogic: tk 'salt_ppm' (MEASUREMENT, ppm) — IntelliChlor salt concentration. "
|
||||
"2026-10-03 skip: 'salt_tds_ppm' is IntelliChem's CONFIGURATION value (a CONFIG "
|
||||
"number; its sensor twin is disabled by default as superseded) — a setting, not a "
|
||||
"reading."
|
||||
),
|
||||
tasks=(ConsumableSignature(("salt_ppm",), "Refill Pool Salt", "value_below", delta_units=2700),),
|
||||
),
|
||||
"ondilo_ico": IntegrationSignature(
|
||||
@@ -471,4 +476,104 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
ConsumableSignature(("salt_to_add",), "Refill Pool Salt", "alert_above", delta_units=5),
|
||||
),
|
||||
),
|
||||
# --- Round 16 (2026-10-03): HACS spas, mowers and pool equipment --------
|
||||
"gecko": IntegrationSignature(
|
||||
name="Gecko spa packs (in.touch)",
|
||||
verified="2026-10-03 @ gazoodle/gecko-home-assistant main (eb3fba4) + geckolib 1.0.16",
|
||||
source=(
|
||||
"HACS gecko sensor.py GeckoReminderSensor: one sensor per reminder the spa pack reports "
|
||||
"(geckolib GeckoReminders.reminders, INVALID slots dropped), name '<spa>: <Type> due' "
|
||||
"(GeckoEntityBase.name; no has_entity_name, no translation_key → entity-id and "
|
||||
"original-name suffixes _rinse_filter_due, _clean_filter_due, _change_water_due, "
|
||||
"_change_ozonator_due, _change_vision_cartridge_due), device_class timestamp, no unit; the "
|
||||
"value is midnight UTC today + the reminder's remaining days (in the past once overdue). "
|
||||
"Resetting the reminder on the spa (or through the CONFIG date twin of the same name, "
|
||||
"date.py set_reminder) moves the date forward, which clears the trigger and auto-completes "
|
||||
"the task — there is no button to wire. The date.* twins carry the same names and are kept "
|
||||
"out by the sensor domain. Rinse / clean / change water map onto the Hot Tub template's "
|
||||
"duties, due on the date itself (the spa's own reminder); the ozonator and the Vision "
|
||||
"cartridge are parts to order → two weeks' lead. Skipped: 'Check Spa due' (a generic "
|
||||
"check, no part or action)."
|
||||
),
|
||||
tasks=(
|
||||
ConsumableSignature(("rinse_filter_due",), "Rinse Filter", "due_date"),
|
||||
ConsumableSignature(("clean_filter_due",), "Deep Clean Filter", "due_date"),
|
||||
ConsumableSignature(("change_water_due",), "Drain and Refill", "due_date"),
|
||||
ConsumableSignature(("change_ozonator_due",), "Replace Ozonator", "due_date", days_before=14),
|
||||
ConsumableSignature(("change_vision_cartridge_due",), "Replace Vision Cartridge", "due_date", days_before=14),
|
||||
),
|
||||
),
|
||||
"terramow": IntegrationSignature(
|
||||
name="TerraMow",
|
||||
verified="2026-10-03 @ TerraMow/TerraMowHA main (045d789)",
|
||||
source=(
|
||||
"TerraMow's own HACS integration (custom repository) sensor.py — has_entity_name and "
|
||||
"_attr_translation_key on every entity class: 'remaining_blade_time' ('Remaining Blade "
|
||||
"Time', de 'Verbleibende Laufzeit der Klingen'; MINUTES, DURATION, DIAGNOSTIC) = "
|
||||
"const.BLADE_MAINTENANCE_CYCLE_MINUTES (14,400 = 240 h) minus dp_126 'blade disk usage "
|
||||
"time', floored at 0; 'remaining_base_station_time' (MINUTES) = "
|
||||
"BASE_STATION_MAINTENANCE_CYCLE_MINUTES (43,200 = 30 days) minus dp_125 'base station usage "
|
||||
"time' — both computed in the integration from the vendor's recommended cycles and both "
|
||||
"restart when the robot's counter is reset in the TerraMow app (dp value 0; no HA "
|
||||
"button). The base-station cycle maps to Clean Charging Contacts (the Robot Lawn Mower "
|
||||
"template's station duty). 'total_mowing_time' (dp_124 statistics duration, SECONDS, "
|
||||
"TOTAL_INCREASING, lifetime) → Clean Undercarriage every 25 h like the other lifetime "
|
||||
"mowing counters. NOTE: docs/en/developers/data_point.md calls both cycles a 'recommended "
|
||||
"cleaning cycle' (and gives 240 minutes for the blade disk where the integration uses 240 "
|
||||
"hours)."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
ConsumableSignature(("remaining_blade_time",), "Replace Blades", "duration_left"),
|
||||
ConsumableSignature(("remaining_base_station_time",), "Clean Charging Contacts", "duration_left"),
|
||||
ConsumableSignature(("total_mowing_time",), "Clean Undercarriage", "usage_delta", delta_units=25),
|
||||
),
|
||||
),
|
||||
"stihl_imow": IntegrationSignature(
|
||||
name="STIHL iMOW",
|
||||
verified="2026-10-03 @ ChrisHaPunkt/ha-stihl-imow main (0fe6c7b)",
|
||||
source=(
|
||||
"HACS stihl_imow (custom repository) maps.py IMOW_SENSORS_MAP + entity.py: has_entity_name, "
|
||||
"translation_key = to_translation_key(property) on every entity → "
|
||||
"'statistics_total_blade_operating_time' (statistics_totalBladeOperatingTime) and "
|
||||
"'statistics_total_operating_time' — DURATION, SECONDS with suggested HOURS, "
|
||||
"TOTAL_INCREASING lifetime counters, enabled (not in DISABLED_BY_DEFAULT_PROPERTIES) → the "
|
||||
"Gardena / Indego pair: blades every 100 h of blade time, undercarriage every 25 h of "
|
||||
"operation. Skipped: 'status_blade_service' (status_bladeService, no type or unit, meaning "
|
||||
"undocumented). No reset button."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
ConsumableSignature(("statistics_total_blade_operating_time",), "Replace Blades", "usage_delta", delta_units=100),
|
||||
ConsumableSignature(("statistics_total_operating_time",), "Clean Undercarriage", "usage_delta", delta_units=25),
|
||||
),
|
||||
),
|
||||
"omnilogic_local": IntegrationSignature(
|
||||
name="Hayward OmniLogic (local)",
|
||||
verified="2026-10-03 @ cryptk/haomnilogic-local main (6a8abb8)",
|
||||
source=(
|
||||
"HACS omnilogic_local (custom repository; not the core 'omnilogic' cloud domain) sensor.py "
|
||||
"CHLORINATOR_SALT_SENSORS 'chlorinator_salt_level_average' (name 'Average Salt Level', "
|
||||
"PARTS_PER_MILLION, MEASUREMENT); the entity is named f'{equipment.name} Average Salt "
|
||||
"Level' under has_entity_name, no translation_key → suffix _average_salt_level. Hayward "
|
||||
"salt cells start their operating range at 2,700 ppm, the IntelliChlor band of the "
|
||||
"ScreenLogic entry → value_below 2,700; topping up raises the reading (auto-resolve). The "
|
||||
"'Instant Salt Level' twin (the raw reading) is not used."
|
||||
),
|
||||
tasks=(ConsumableSignature(("average_salt_level",), "Refill Pool Salt", "value_below", delta_units=2700),),
|
||||
),
|
||||
"fluidra_pool": IntegrationSignature(
|
||||
name="Fluidra Pool (Fluidra Connect)",
|
||||
verified="2026-10-03 @ foXaCe/Fluidra-pool main (fef0d33)",
|
||||
source=(
|
||||
"HACS fluidra_pool (custom repository) sensor/chlorinator.py FluidraUvRunningHoursSensor: "
|
||||
"tk 'uv_running_hours' (DURATION, HOURS, TOTAL_INCREASING — 'a plain integer hour counter "
|
||||
"… the lamp-replacement interval is a running-hours threshold'), created only when the "
|
||||
"chlorinator reports a UV block with running_hours (sensor/__init__.py) → Replace UV Lamp "
|
||||
"every 8,000 h (the UV-lamp rating of the NeoPool entry). Whether the controller restarts "
|
||||
"the counter after a lamp change is not documented, hence usage_delta (completing "
|
||||
"re-baselines either way)."
|
||||
),
|
||||
tasks=(ConsumableSignature(("uv_running_hours",), "Replace UV Lamp", "usage_delta", delta_units=8000),),
|
||||
),
|
||||
}
|
||||
@@ -8,7 +8,7 @@ integration's source; drift-probed weekly)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ._model import ConsumableSignature, IntegrationSignature
|
||||
from ._shared import HEATING_WATER_PRESSURE_LOW, SOFTENER_SALT_LEVEL
|
||||
from ._shared import HEATING_SYSTEM_PRESSURE_LOW, HEATING_WATER_PRESSURE_LOW, SOFTENER_SALT_LEVEL
|
||||
|
||||
SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"vicare": IntegrationSignature(
|
||||
@@ -19,7 +19,14 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"(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)."
|
||||
"UnitOfTime.HOURS, TOTAL_INCREASING lifetime → usage_delta). "
|
||||
"Re-checked 2026-10-03 @ core 2026.10.0b0: 'filter_hours' "
|
||||
"(operatingHours) and 'filter_overdue_hours' (overdueHours) read the "
|
||||
"same ventilation.filter.runtime as the countdown — one duty per "
|
||||
"filter, not signed; 'compressor_hours_loadclass1'-'5', "
|
||||
"'heating_rod_hours' and 'supply_fan_hours' are statistics, not "
|
||||
"duties. No filter-reset button (button.py: one-time DHW charge "
|
||||
"only)."
|
||||
),
|
||||
tasks=(
|
||||
ConsumableSignature(("filter_remaining_hours",), "Replace Filter", "duration_left"),
|
||||
@@ -49,8 +56,9 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
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),
|
||||
# topping up raises the value back (auto-resolve). Shared with
|
||||
# bosch_homecom and nefiteasy (round 16).
|
||||
HEATING_SYSTEM_PRESSURE_LOW,
|
||||
),
|
||||
),
|
||||
# ─── Research round 4 (2026-07-19): boiler pressure, HRV filters, ───
|
||||
@@ -59,7 +67,17 @@ SIGNATURES: dict[str, 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."
|
||||
"core opentherm_gw: tk 'central_heating_pressure', BAR, MEASUREMENT — generic for EVERY OpenTherm-connected boiler. "
|
||||
"Re-checked 2026-10-03 @ core 2026.10.0b0: every sensor and "
|
||||
"binary sensor is registered TWICE, on the 'boiler' and on the "
|
||||
"'thermostat' device (BOILER_/THERMOSTAT_DEVICE_DESCRIPTION, "
|
||||
"identical translation_keys, neither device has a model string), "
|
||||
"so no gate keeps a duty off the thermostat device — the "
|
||||
"burner/pump running-time counters ('total_burner_hours', "
|
||||
"'central_heating_pump_hours', 'hot_water_burner_hours', "
|
||||
"'hot_water_pump_hours'; HOURS, TOTAL) are not signed for that "
|
||||
"reason. 'service_required' is a problem-class binary (adoption "
|
||||
"path)."
|
||||
),
|
||||
tasks=(ConsumableSignature(("central_heating_pressure",), "Refill Heating Water", "value_below", delta_units=1),),
|
||||
),
|
||||
@@ -214,7 +232,11 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"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)."
|
||||
"also exists (adoption path). Re-checked 2026-10-03 @ core "
|
||||
"2026.10.0b0: the sensor is disabled by default (the suggestion "
|
||||
"appears once it is enabled); 'total_runtime' (lifetime HOURS) and "
|
||||
"'next_maintainance' (TIMESTAMP), both disabled by default too, "
|
||||
"describe the same service — one duty, not signed twice."
|
||||
),
|
||||
tasks=(ConsumableSignature(("runtime_since_last_maintenance",), "Oil Service", "usage_above", above_hours=100),),
|
||||
),
|
||||
@@ -388,7 +410,10 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"no interval is set; negative when overdue) → duration_left, "
|
||||
"task at 24 run-hours left. The counter reset "
|
||||
"(ServiceCounterReset) is a library SERVICE topic, not an HA "
|
||||
"button — nothing to wire."
|
||||
"button — nothing to wire. Re-checked 2026-10-03 @ core "
|
||||
"2026.10.0b0: 'generator_total_runtime' (AccumulatedRuntime, "
|
||||
"lifetime h) is the same service seen from the other side — not "
|
||||
"signed twice."
|
||||
),
|
||||
tasks=(ConsumableSignature(("generator_service_counter",), "Oil Service", "duration_left"),),
|
||||
),
|
||||
@@ -434,4 +459,142 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
tasks=(HEATING_WATER_PRESSURE_LOW,),
|
||||
),
|
||||
# ─── Round 16 (2026-10-03): pellet-stove service by burn hours ──────
|
||||
# (hr_energy_qube 'alarm_working_hours' is a problem-class binary →
|
||||
# problem-sensor adoption.)
|
||||
"ecoforest": IntegrationSignature(
|
||||
name="Ecoforest pellet stoves",
|
||||
verified="2026-10-03 @ home-assistant/core 2026.10.0b0 (sensor.py identical in 2026.9.0b0 and dev) + pyecoforest 0.4.0",
|
||||
source=(
|
||||
"core ecoforest sensor.py: tk 'working_hours' ('Working time', "
|
||||
"DURATION, HOURS, no state class, entity_registry_enabled_default="
|
||||
"False) = pyecoforest Device.working_hours, the stove's stats "
|
||||
"field 'Nh' — the TOTAL working hours of the stove (HA docs: 'total "
|
||||
"number of working hours of the device'), never reset → "
|
||||
"usage_delta. Every 1,500 h — editorial: about one heating season "
|
||||
"of a stove used as the main heating, i.e. the yearly 'Stove "
|
||||
"Service' of the Fireplace & Wood Stove template counted in burn "
|
||||
"hours. NOTE: disabled by default — the suggestion appears once the "
|
||||
"user enables the sensor. The 'ignitions' counter and the 'alarm' "
|
||||
"enum (pellets / air depression / CPU overheating) are not "
|
||||
"maintenance duties."
|
||||
),
|
||||
tasks=(ConsumableSignature(("working_hours",), "Stove Service", "usage_delta", delta_units=1500),),
|
||||
),
|
||||
# ─── Round 16 HACS wave (2026-10-03): heating-loop pressure, the ─────
|
||||
# ─── pellet-stove service countdown, a heat pump's vent filter ───────
|
||||
# (Reviewed and left out: the sfstar 'victron' Modbus integration's
|
||||
# generator_servicecounter — dbus-modbustcp returns 0 for an invalid
|
||||
# value, so a GX without a service interval would read "due now".)
|
||||
"ariston": IntegrationSignature(
|
||||
name="Ariston NET (Remotethermo)",
|
||||
verified="2026-10-03 @ fustom/ariston-remotethermo-home-assistant-v3 main (31900cb)",
|
||||
source=(
|
||||
"HACS ariston const.py ARISTON_SENSOR_TYPES "
|
||||
"DeviceProperties.HEATING_CIRCUIT_PRESSURE: name f'{NAME} heating "
|
||||
"circuit pressure' with NAME 'Ariston' and no has_entity_name / "
|
||||
"translation_key → sensor.ariston_heating_circuit_pressure, suffix "
|
||||
"_heating_circuit_pressure (the original name also matches a "
|
||||
"second boiler's _2 id); PRESSURE, MEASUREMENT, DIAGNOSTIC, "
|
||||
"enabled, GALEVO systems only. Value and unit are the cloud item's "
|
||||
"own (ariston library galevo_device.heating_circuit_pressure_value"
|
||||
" / _unit; HeatingCircuitPressure is in bar) → value_below 1 like "
|
||||
"the other boiler-loop duties. Core 'ariston' is only a virtual "
|
||||
"brand entry (supported_by midea) without entities."
|
||||
),
|
||||
tasks=(ConsumableSignature(("heating_circuit_pressure",), "Refill Heating Water", "value_below", delta_units=1),),
|
||||
),
|
||||
"aquarea": IntegrationSignature(
|
||||
name="Panasonic Aquarea (HeishaMon)",
|
||||
verified="2026-10-03 @ kamaradclimber/heishamon-homeassistant main (3a528fd)",
|
||||
source=(
|
||||
"HACS aquarea (HeishaMon, MQTT-fed) definitions.py build_sensors "
|
||||
"TOP115 key f'{mqtt_prefix}main/Water_Pressure', name 'Aquarea "
|
||||
"Water Pressure', PRESSURE, 'bar', entity_registry_enabled_default="
|
||||
"False (K/L series only); sensor.py sets entity_id = "
|
||||
"sensor.<slugify(key)> → sensor.panasonic_heat_pump_main_water_"
|
||||
"pressure (suffix _water_pressure; the original name ends in it "
|
||||
"too). No other HeishaMon key ends in water_pressure (High/Low "
|
||||
"Pressure are the refrigerant side). NOTE: disabled by default — "
|
||||
"the suggestion appears once the user enables the sensor. The "
|
||||
"cloud fork wpatrik14/home-assistant-aquarea shares the domain but "
|
||||
"has no pressure entity (verified @ a588223)."
|
||||
),
|
||||
tasks=(HEATING_WATER_PRESSURE_LOW,),
|
||||
),
|
||||
"maestro_mcz": IntegrationSignature(
|
||||
name="MCZ pellet stoves (Maestro)",
|
||||
verified="2026-10-03 @ Robbe-B/maestro_mcz main (a2ba9cf)",
|
||||
source=(
|
||||
"HACS maestro_mcz maestro/models/models.py supported_sensors: "
|
||||
"SensorMczConfigItem 'Next Maintenance' (status field "
|
||||
"'ore_prox_manut' = hours to the next maintenance, UnitOfTime.HOURS, "
|
||||
"MEASUREMENT, DIAGNOSTIC, enabled; created only when the stove "
|
||||
"reports the field). sensor.py MczSensorEntity has_entity_name with "
|
||||
"_attr_name = the item name, no translation_key → suffix "
|
||||
"_next_maintenance. The stove controller's own service countdown "
|
||||
"(the fumis pattern) → duration_left 24 h; named after the "
|
||||
"technician's 'Stove Service' of the Fireplace & Wood Stove "
|
||||
"template, like the ecoforest duty. The only binary ('Alarm', "
|
||||
"is_in_error) is problem-class."
|
||||
),
|
||||
tasks=(ConsumableSignature(("next_maintenance",), "Stove Service", "duration_left"),),
|
||||
),
|
||||
"bosch_homecom": IntegrationSignature(
|
||||
name="Bosch HomeCom Easy",
|
||||
verified="2026-10-03 @ serbanb11/bosch-homecom-hass main (d9c28a6)",
|
||||
source=(
|
||||
"HACS bosch_homecom sensor.py: K40/ICOM coordinators get "
|
||||
"BoschComK40ExtraSensor('systemPressure', translation_key "
|
||||
"'system_pressure', PRESSURE, 'bar') when the heat source reports "
|
||||
"it (strings.json 'System pressure', has_entity_name). ICOM "
|
||||
"devices additionally get BoschComIcomExtraSensor name "
|
||||
"'hs_system_pressure' (no translation_key, bar) reading the same "
|
||||
"heat_sources.systemPressure — its id ends in _system_pressure too, "
|
||||
"so the duty watches both copies of one reading (any-low). "
|
||||
"value_below 1 bar → the shared bosch duty."
|
||||
),
|
||||
tasks=(HEATING_SYSTEM_PRESSURE_LOW,),
|
||||
),
|
||||
"nefiteasy": IntegrationSignature(
|
||||
name="Nefit Easy (Bosch thermostat)",
|
||||
verified="2026-10-03 @ ksya/ha-nefiteasy dev (28880e4)",
|
||||
source=(
|
||||
"HACS nefiteasy const.py SENSORS: key 'system_pressure', name "
|
||||
"'System pressure', url /system/appliance/systemPressure, "
|
||||
"UnitOfPressure.BAR, PRESSURE, MEASUREMENT; nefit_entity.py sets "
|
||||
"neither has_entity_name nor translation_key → "
|
||||
"sensor.system_pressure (exact-object-id pattern; a second boiler's "
|
||||
"_2 id is matched by the original name). value_below 1 bar → the "
|
||||
"shared bosch duty."
|
||||
),
|
||||
tasks=(HEATING_SYSTEM_PRESSURE_LOW,),
|
||||
),
|
||||
"waterkotte_heatpump": IntegrationSignature(
|
||||
name="Waterkotte heat pumps (BasicVent)",
|
||||
verified="2026-10-03 @ marq24/ha-waterkotte main (f5491c9)",
|
||||
source=(
|
||||
"HACS waterkotte_heatpump const.py "
|
||||
"BASICVENT_FILTER_CHANGE_REMAINING_OPERATING_DAYS_A4504 (DURATION, "
|
||||
"UnitOfTime.DAYS, MEASUREMENT, vent feature only, "
|
||||
"entity_registry_enabled_default=False); __init__.py sets "
|
||||
"translation_key = key.lower() → tk "
|
||||
"'basicvent_filter_change_remaining_operating_days_a4504' (en.json "
|
||||
"'Vent Air-Filter-Change operating hours remaining time') — the "
|
||||
"BasicVent ventilation unit's own filter countdown → duration_left "
|
||||
"7 days. NOTE: disabled by default — the suggestion appears once "
|
||||
"the user enables it. The counter reset "
|
||||
"(BASICVENT_FILTER_CHANGE_OPERATING_HOURS_RESET_D1544) is a SWITCH, "
|
||||
"not a button, and cannot be wired; the 'filter change display' "
|
||||
"binary carries device_class running and is not signed."
|
||||
),
|
||||
tasks=(
|
||||
ConsumableSignature(
|
||||
("basicvent_filter_change_remaining_operating_days_a4504",),
|
||||
"Replace Ventilation Filter",
|
||||
"duration_left",
|
||||
below_hours=168,
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
"""NAS & home IT.
|
||||
"""NAS, home IT & safety detectors.
|
||||
|
||||
Data module of the suggested-setups signature catalog — see
|
||||
``helpers/signatures/_model.py`` for the direction semantics and the
|
||||
@@ -108,4 +108,53 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
tasks=(ConsumableSignature(("pool_usage",), "Storage Cleanup", "alert_above", delta_units=85),),
|
||||
),
|
||||
# Reviewed 2026-10-03 @ core 2026.10.0b0 (round 16), not signed: apcupsd
|
||||
# 'battery_replacement_date' is apcupsd's BATTDATE ('Battery replaced',
|
||||
# plain string sensor) — the date the battery WAS replaced, a last-done
|
||||
# date rather than a due date, so the due_date direction does not fit; the
|
||||
# UPS template's "Replace UPS Battery" covers the duty.
|
||||
# ─── Round 16 HACS wave (2026-10-03): smoke/CO alarms that report ────
|
||||
# ─── their own end of life ───────────────────────────────────────────
|
||||
"nest_protect": IntegrationSignature(
|
||||
name="Google Nest Protect",
|
||||
verified="2026-10-03 @ iMicknl/ha-nest-protect main (8b58fc1)",
|
||||
source=(
|
||||
"HACS nest_protect sensor.py SENSOR_DESCRIPTIONS: key = "
|
||||
"translation_key 'replace_by_date_utc_secs' (strings.json 'Replace "
|
||||
"by'; device_class DATE, DIAGNOSTIC, enabled; created only when the "
|
||||
"device reports the key) = datetime.utcfromtimestamp of the "
|
||||
"alarm's replace-by date, the end of its service life → due_date 30 "
|
||||
"days ahead, time to buy the successor. Core renders the naive "
|
||||
"datetime under DATE as an ISO datetime, which the due_date trigger "
|
||||
"parses. The replacement is a new device, so the task is completed "
|
||||
"by hand. 'last_audio_self_test_end_utc_secs' / "
|
||||
"'latest_manual_test_end_utc_secs' are LAST-done dates (no due "
|
||||
"date); every binary carries a device class (smoke/CO/heat, "
|
||||
"problem-class self-tests → problem-sensor adoption)."
|
||||
),
|
||||
tasks=(ConsumableSignature(("replace_by_date_utc_secs",), "Replace Detectors", "due_date", days_before=30),),
|
||||
),
|
||||
"kidde_homesafe": IntegrationSignature(
|
||||
name="Kidde HomeSafe",
|
||||
verified="2026-10-03 @ snell-evan-itt/Kidde-HomeSafe main (47b9568)",
|
||||
source=(
|
||||
"HACS kidde_homesafe sensor.py KiddeSensorLifeEntity (the API field "
|
||||
"'life', created when the device reports it; has_entity_name, no "
|
||||
"translation_key, MEASUREMENT): LIFE_SENSOR_CONFIG names it per "
|
||||
"mb_model — 'Days to replace' (UnitOfTime.DAYS) on the DETECT "
|
||||
"series (mb_model 46/48), else 'Weeks to replace' "
|
||||
"(UnitOfTime.WEEKS) → suffixes _days_to_replace / "
|
||||
"_weeks_to_replace: the alarm's end-of-life countdown → "
|
||||
"duration_left 30 days (converted into days resp. weeks). The 'End "
|
||||
"of Life Fault' binaries are problem-class (adoption path)."
|
||||
),
|
||||
tasks=(
|
||||
ConsumableSignature(
|
||||
("days_to_replace", "weeks_to_replace"),
|
||||
"Replace Detectors",
|
||||
"duration_left",
|
||||
below_hours=720,
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -35,7 +35,7 @@ ELECTROLUX_WATER_FILTER_STATE = ConsumableSignature(
|
||||
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",
|
||||
verified="2026-07-17 @ home-assistant/core dev + thinq-connect/pythinqconnect main; dishwasher/fridge latches 2026-09-27 @ home-assistant/core 2026.9; skips re-checked 2026-10-03 @ home-assistant/core 2026.10.0b0",
|
||||
source=(
|
||||
"home-assistant/core homeassistant/components/lg_thinq/sensor.py "
|
||||
"(ThinQProperty StrEnum translation_key; FILTER_LIFETIME is shared by "
|
||||
@@ -54,7 +54,11 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"(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)."
|
||||
"water_filter_state (values undocumented). 2026-10-03 (entity descriptions "
|
||||
"unchanged since 2026.9): kimchi-refrigerator binary 'one_touch_filter' "
|
||||
"(refrigeration.oneTouchFilter, on_key 'on', no device_class) is the fresh-air "
|
||||
"filter FUNCTION being on, not a replace flag — the 'fresh_air_filter' latch "
|
||||
"already carries that filter; skipped."
|
||||
),
|
||||
tasks=(
|
||||
# AC filter reports hours-remaining; air-purifier/RAC filters report
|
||||
@@ -367,7 +371,7 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
"midea": IntegrationSignature(
|
||||
name="Midea (core)",
|
||||
verified="2026-09-25 @ home-assistant/core dev (new in 2026.8) + midea-local 12.1.0",
|
||||
verified="2026-09-25 @ home-assistant/core dev (new in 2026.8) + midea-local 12.1.0; skips re-checked 2026-10-03 @ home-assistant/core 2026.10.0b0",
|
||||
source=(
|
||||
"home-assistant/core homeassistant/components/midea/sensor.py "
|
||||
"SENSOR_ENTITIES (created only when the device reports the "
|
||||
@@ -383,7 +387,11 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"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."
|
||||
"adoption. 2026-10-03 skips: 'filter_available_days' (keys filter1/2/3, "
|
||||
"DURATION DAYS = midea-local ED body-01 hours ÷ 24) counts the SAME three "
|
||||
"filters as 'filter_life_level' — one signal per filter; the soft-water "
|
||||
"body-05 'remaining_days' / 'use_days' (meaning undocumented) and the binary "
|
||||
"'arofene_link' (PLUG, module-attached status)."
|
||||
),
|
||||
tasks=(
|
||||
ConsumableSignature(("salt_available",), "Refill Softener Salt", "percent_left"),
|
||||
@@ -804,4 +812,157 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
),
|
||||
),
|
||||
"tami4": IntegrationSignature(
|
||||
name="Tami4 Edge / Edge+",
|
||||
verified="2026-10-03 @ home-assistant/core 2026.10.0b0",
|
||||
source=(
|
||||
"core tami4 sensor.py ENTITY_DESCRIPTIONS (unchanged since 2026.9; every sensor "
|
||||
"and button description sets a translation_key, has_entity_name): tk "
|
||||
"'filter_upcoming_replacement' / 'uv_upcoming_replacement' (device_class DATE, "
|
||||
"no unit, enabled; coordinator.py FlattenedWaterQuality ← Tami4EdgeAPI 3.0 "
|
||||
"water_quality.py QualityInfo.upcoming_replacement = date.fromtimestamp(cloud "
|
||||
"dynamicData filterInfo / uvInfo 'upcomingReplacement' / 1000); HA docs: 'Date "
|
||||
"when the filter / UV lamp needs to be replaced') → due_date. button.py has only "
|
||||
"'boil_water' / 'prepare_drink' — no reset to wire: the Tami4 cloud moves the "
|
||||
"date after the replacement, which clears the trigger (auto-complete). Skipped: "
|
||||
"'filter_litters_passed' (LITERS, TOTAL — water through the currently installed "
|
||||
"filter, the part the date duty already covers) and 'filter_installed' / "
|
||||
"'uv_installed' (booleans, status)."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
# A week's lead time to order the cartridge / lamp (as for Vitesy's filter).
|
||||
ConsumableSignature(("filter_upcoming_replacement",), "Replace Water Filter", "due_date", days_before=7),
|
||||
ConsumableSignature(("uv_upcoming_replacement",), "Replace UV Lamp", "due_date", days_before=7),
|
||||
),
|
||||
),
|
||||
# ─── Round 16 (2026-10-03): HACS water treatment and Polaris / Rusclimate ─
|
||||
"midea_auto_cloud": IntegrationSignature(
|
||||
name="Midea Auto Cloud",
|
||||
verified="2026-10-03 @ sususweet/midea_auto_cloud master (8d0f9a5)",
|
||||
source=(
|
||||
"HACS midea_auto_cloud (not the midea_ac_lan / core midea domains): midea_entity.py sets "
|
||||
"_attr_translation_key = the mapping's translation_key, else the entity key, on every "
|
||||
"entity; platform_setup.py creates every entity the device's mapping lists "
|
||||
"(device_mapping/T0x<type>.py, picked by subtype → sn8 → default_<category>). T0xED water "
|
||||
"purifiers (PERCENTAGE): life_1 / life_2 with tk 'life_fcb' / 'life_ro' "
|
||||
"(default_water_purifier, 63600118 — en 'FCB filter remaining life' / 'RO filter remaining "
|
||||
"life'), 'life_ro' / 'life_pcb' (632009F5, 632009C6; 632009C6 lists translation_key twice, "
|
||||
"the last wins) and 'life_1' / 'life_2_pcb' (632009EN) → one task per cartridge. T0xFC air "
|
||||
"purifiers (571Z3081, 571Z307F): 'deep_filter_percent' ('Filter Life Remaining', "
|
||||
"PERCENTAGE). T0xAC fresh-air units (default_central_fresh_air, 26096947): "
|
||||
"'fresh_filter_time' ('Filter remaining life', %; the default mapping writes "
|
||||
"native_unit_of_measurement, which the entity ignores → no unit, the lenient unit gate) → "
|
||||
"the ventilation filter. Skipped: T0xED 'left_salt' (% salt left) exists only in "
|
||||
"default_water_purifier, which creates its softener, purifier and pipeline-machine "
|
||||
"attributes for every device it serves (issues #48, #129) — a purifier would get a salt "
|
||||
"duty on an entity that never reports; 'remind_maintenance_days' (meaning undocumented); "
|
||||
"the C2 toilet's 'filter_use_per' ('Filter Use Percentage' but BATTERY class — direction "
|
||||
"contradictory); T0xFA 'filter_life_time' / 'dust_life_time' and T0xCE "
|
||||
"'clean_net_used_time' / 'change_net_used_time' (hours, used vs left not established); "
|
||||
"the 'maintenance_remind' / 'filter_value' binaries are device_class problem "
|
||||
"(problem-sensor adoption). button.py has no filter reset."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
ConsumableSignature(
|
||||
("life_fcb", "life_ro", "life_pcb", "life_1", "life_2_pcb"),
|
||||
"Replace Water Filter",
|
||||
"percent_left",
|
||||
per_entity=True,
|
||||
),
|
||||
ConsumableSignature(("deep_filter_percent",), "Replace Filter", "percent_left"),
|
||||
ConsumableSignature(("fresh_filter_time",), "Replace Ventilation Filter", "percent_left"),
|
||||
),
|
||||
),
|
||||
"gruenbeck_cloud": IntegrationSignature(
|
||||
name="Grünbeck softliQ (cloud)",
|
||||
verified="2026-10-03 @ p0l0/hagruenbeck_cloud main (571eb1d)",
|
||||
source=(
|
||||
"HACS gruenbeck_cloud (custom repository) sensor.py SENSORS — every description sets a "
|
||||
"translation_key, GruenbeckCloudEntity has_entity_name: tk 'salt_range' ('Salt Range' / "
|
||||
"'Salzvorrat', UnitOfTime.DAYS, no device class, enabled; pygruenbeck_cloud "
|
||||
"realtime.salt_range = the days the salt supply lasts) → Refill Softener Salt a week ahead "
|
||||
"(the BWT / EcoWater days precedent); tk 'next_service' ('Next Service', source comment "
|
||||
"'Perform maintenance in [days]', UnitOfTime.DAYS, DIAGNOSTIC, "
|
||||
"entity_registry_enabled_default=False — 'Not available at SE devices') → Annual Service "
|
||||
"two weeks ahead, the Water Softener template's service duty; it is proposed once the "
|
||||
"entity is enabled. Skipped: the 'has_error' binary (device_class problem → problem-sensor "
|
||||
"adoption), 'last_service' (DATE of the last service), 'salt_consumption' (kg used). The "
|
||||
"integration has no button platform."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
ConsumableSignature(("salt_range",), "Refill Softener Salt", "duration_left", below_hours=168),
|
||||
ConsumableSignature(("next_service",), "Annual Service", "duration_left", below_hours=336),
|
||||
),
|
||||
),
|
||||
"gruenbeck_softliq_sc": IntegrationSignature(
|
||||
name="Grünbeck softliQ SC (local)",
|
||||
verified="2026-10-03 @ tizianodeg/gruenbeck_softliQ_SC main (c603343)",
|
||||
source=(
|
||||
"HACS gruenbeck_softliq_sc (custom repository; the softliQ:SC's local web interface) "
|
||||
"sensor.py: every SoftQLinkSensorEntityDescription sets translation_key = the controller's "
|
||||
"own parameter id (upper case, matched exactly): 'D_A_2_3' ('Salt range in days') and "
|
||||
"'D_A_2_2' ('days until the next maintenance'), both UnitOfTime.DAYS, DIAGNOSTIC, enabled — "
|
||||
"the same pair as the cloud integration, same leads. button.py has only "
|
||||
"'manual_regeneration' / 'reset_error_memory' — no counter reset."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
ConsumableSignature(("D_A_2_3",), "Refill Softener Salt", "duration_left", below_hours=168),
|
||||
ConsumableSignature(("D_A_2_2",), "Annual Service", "duration_left", below_hours=336),
|
||||
),
|
||||
),
|
||||
"polaris": IntegrationSignature(
|
||||
name="Polaris IQ Home / Rusclimate (MQTT)",
|
||||
verified="2026-10-03 @ samoswall/polaris-mqtt main (0db4fa1)",
|
||||
source=(
|
||||
"HACS polaris (polaris-mqtt: Polaris IQ Home and Rusclimate — Ballu / Electrolux / Royal "
|
||||
"Thermo / Zanussi — appliances over MQTT). sensor.py PolarisSensor: entity_id "
|
||||
"sensor.<class>_<model>_<description.name>, has_entity_name; const.py SENSORS_* carry a "
|
||||
"translation_key; device model = '<class> - <model>'. The 'expendables' payload '[x,y]' "
|
||||
"feeds the '… retain' sensors (= remaining, ru 'Остаток …'): tk 'filter_retain' = x — "
|
||||
"HOURS on humidifiers (SENSORS_HUMIDIFIER; the vendor app's expendable_max 4392 h filter / "
|
||||
"168 h tank), Polaris air cleaners (SENSORS_AIRCLEANER) and the Electrolux EPVS/ERVX "
|
||||
"ventilation unit (SENSORS_VENTILATION, model 'ventilation - …' → the ventilation filter, "
|
||||
"a week's lead), PERCENTAGE on the Ballu ONEAIR ASP-100/200 breezers (SENSORS_CLIMATE / "
|
||||
"_200) and the Electrolux EAP purifier — the unit routes each entity to its direction; tk "
|
||||
"'clean_retain' = y (HOURS, humidifiers except type 835: water-tank cleaning); tk "
|
||||
"'pre_filter_retain' = y (PERCENTAGE, ASP-200); tk 'anode_retain' = x (UnitOfTime.DAYS, "
|
||||
"water heaters 844/876/877). button.py: humidifiers (not 835/881) 'button_reset_filter' "
|
||||
"(payload [0,0], 'Reset time filter') and 'button_reset_tank' ([1,0], 'Reset time water "
|
||||
"tank'), Polaris air cleaners (not PAW-0804) 'button_reset_filter' ([0]) → wired on the "
|
||||
"hour duties. The Ballu breezers' resets stay unwired: the ASP-100 sends [100], the "
|
||||
"ASP-200 'button_reset_filter' [0,100] and 'button_reset_prefilter' [100,0] while "
|
||||
"sensor.py reads the filter from index 0 and the pre-filter from index 1 — which counter "
|
||||
"each ASP-200 button resets is not established. No anode reset exists. Skipped: the "
|
||||
"vacuums' 'expendables' brush/filter/mop hours (used vs left not established)."
|
||||
),
|
||||
tasks=(
|
||||
ConsumableSignature(
|
||||
("filter_retain",),
|
||||
"Replace Filter",
|
||||
"duration_left",
|
||||
models_exclude=("ventilation",),
|
||||
resets=(("filter_retain", "button_reset_filter"),),
|
||||
),
|
||||
ConsumableSignature(("filter_retain",), "Replace Filter", "percent_left"),
|
||||
ConsumableSignature(
|
||||
("filter_retain",),
|
||||
"Replace Ventilation Filter",
|
||||
"duration_left",
|
||||
below_hours=168,
|
||||
models=("ventilation",),
|
||||
),
|
||||
ConsumableSignature(
|
||||
("clean_retain",),
|
||||
"Clean Tank",
|
||||
"duration_left",
|
||||
resets=(("clean_retain", "button_reset_tank"),),
|
||||
),
|
||||
ConsumableSignature(("pre_filter_retain",), "Clean Pre-Filter", "percent_left"),
|
||||
ConsumableSignature(("anode_retain",), "Anode Rod Inspection", "duration_left", below_hours=336),
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -59,22 +59,34 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
"litterrobot": IntegrationSignature(
|
||||
name="Litter-Robot",
|
||||
verified="2026-07-18 @ home-assistant/core dev; re-checked 2026-09-27 @ home-assistant/core 2026.9",
|
||||
verified="2026-07-18 @ home-assistant/core dev; re-checked 2026-09-27 @ home-assistant/core 2026.9; filter due date 2026-10-03 @ home-assistant/core 2026.10.0b0",
|
||||
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). "
|
||||
"2026-09 re-check, skipped: LR5 tk 'next_filter_replacement' (TIMESTAMP, "
|
||||
"pylitterbot nextFilterReplacementDate — no catalog direction reads a due "
|
||||
"date; its 'change_filter' button has no counter duty to reset), LR5 binary "
|
||||
"'laser_dirty' (device_class problem → problem-sensor adoption), button "
|
||||
"'reset' (LR4/LR5 robot reset, not a counter reset)."
|
||||
"2026-10-03 (identical in 2026.9): LR5 tk 'next_filter_replacement' "
|
||||
"(device_class TIMESTAMP, DIAGNOSTIC, enabled; pylitterbot LitterRobot5."
|
||||
"next_filter_replacement_date = the cloud's nextFilterReplacementDate) → "
|
||||
"due_date, now that the catalog reads due dates; button.py LR5 tk "
|
||||
"'change_filter' (CONFIG; pylitterbot change_filter() 'Reset the filter "
|
||||
"replacement counter', command CHANGE_FILTER) moves that date forward → the "
|
||||
"duty's reset. Skipped: LR5 binary 'laser_dirty' (device_class problem → "
|
||||
"problem-sensor adoption), button 'reset' (LR4/LR5 robot reset, not a counter "
|
||||
"reset)."
|
||||
),
|
||||
tasks=(
|
||||
ConsumableSignature(("waste_drawer",), "Empty Waste Drawer", "alert_above", delta_units=90, resets=(("waste_drawer", "reset_waste_drawer"),)),
|
||||
ConsumableSignature(("litter_level",), "Refill Litter", "percent_left"),
|
||||
ConsumableSignature(("total_cycles",), "Wash Litter Box", "usage_delta", delta_units=150),
|
||||
# LR5 filter: a week's lead time to order one (as for Vitesy's filter).
|
||||
ConsumableSignature(
|
||||
("next_filter_replacement",),
|
||||
"Replace Filter",
|
||||
"due_date",
|
||||
days_before=7,
|
||||
resets=(("next_filter_replacement", "change_filter"),),
|
||||
),
|
||||
),
|
||||
),
|
||||
"petlibro": IntegrationSignature(
|
||||
@@ -159,4 +171,30 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
),
|
||||
),
|
||||
# --- Round 16 (2026-10-03) --------------------------------------------
|
||||
"neakasa": IntegrationSignature(
|
||||
name="Neakasa litter box",
|
||||
verified="2026-10-03 @ tabascoz/hass-neakasa main (0052c0a)",
|
||||
source=(
|
||||
"HACS neakasa (custom repository; Neakasa M1) — every entity class sets "
|
||||
"_attr_translation_key: sensor/sand_percent.py tk 'sand_percent' ('Cat litter level', "
|
||||
"PERCENTAGE = the device's sand level percent) → Refill Litter at the household floor (the "
|
||||
"Litter-Robot precedent); binary_sensor/bin_full.py tk 'bin_full' ('Garbage can full' = "
|
||||
"property binFullWaitReset == 1, NO device_class → not adoptable as a problem sensor) → "
|
||||
"latch on 'on', cleared once the bin is emptied. Skipped: the 'bin_state' / 'sand_state' "
|
||||
"ENUMs (the same two signals as text) and the clean / level buttons (actions, not "
|
||||
"counter resets)."
|
||||
),
|
||||
translation_keys_authoritative=True,
|
||||
tasks=(
|
||||
ConsumableSignature(("sand_percent",), "Refill Litter", "percent_left"),
|
||||
ConsumableSignature(
|
||||
("bin_full",),
|
||||
"Empty Waste Drawer",
|
||||
"event_present",
|
||||
entity_domain="binary_sensor",
|
||||
on_states=("on",),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -238,7 +238,7 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
"brother": IntegrationSignature(
|
||||
name="Brother printer",
|
||||
verified="2026-09-25 @ home-assistant/core dev",
|
||||
verified="2026-09-25 @ home-assistant/core dev; skips re-checked 2026-10-03 @ home-assistant/core 2026.10.0b0",
|
||||
source=(
|
||||
"home-assistant/core homeassistant/components/brother/sensor.py "
|
||||
"(*_toner_remaining / *_ink_remaining / *_remaining_life, PERCENTAGE). "
|
||||
@@ -246,7 +246,11 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"ink_capture_box_remaining_life (dev, 2026-09; strings.json 'Ink capture "
|
||||
"box remaining lifetime') = the waste-ink box → the maintenance-box duty. "
|
||||
"laser_remaining_life, pf_kit_1_remaining_life, pf_kit_mp_remaining_life "
|
||||
"(paper-feed kits, tray 1 / multipurpose tray)."
|
||||
"(paper-feed kits, tray 1 / multipurpose tray). 2026-10-03 skips: the unitless "
|
||||
"'*drum_remaining_pages' / '*drum_page_counter' page counts describe the same "
|
||||
"drums as the % duty (one signal per part); binary_sensor.py (new in 2026.10: "
|
||||
"low/no toner or ink, overdue_prevent_maint, jams, trays, door) is device_class "
|
||||
"problem throughout → problem-sensor adoption."
|
||||
),
|
||||
tasks=(
|
||||
ConsumableSignature(
|
||||
|
||||
@@ -92,7 +92,12 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"filter is reset on the purifier) → usage_above at 4,320 h = the "
|
||||
"cluster's default filter_life_time of 259,200 min (the "
|
||||
"DIRIGERA/STARKVIND duty). The 'replace_filter' binary is "
|
||||
"device_class problem (adoption path); no reset button."
|
||||
"device_class problem (adoption path); no reset button. "
|
||||
"Re-checked 2026-10-03 @ core 2026.10.0b0 / zha 2.3.0: unchanged; "
|
||||
"IkeaDeviceRunTime 'device_run_time' is the purifier's own "
|
||||
"lifetime, not the filter's, and the zhaquirks 'lifetime' sensor "
|
||||
"(Schneider W599001 smoke alarm, half-years, meaning undocumented) "
|
||||
"is not signed."
|
||||
),
|
||||
tasks=(
|
||||
LOCK_CYLINDER_CYCLES,
|
||||
@@ -148,4 +153,9 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
LOCK_CYLINDER_CYCLES,
|
||||
),
|
||||
),
|
||||
# Reviewed 2026-10-03 @ core 2026.10.0b0 (round 16), not signed: homee —
|
||||
# 'replace_filter' (AttributeType.REPLACE_FILTER_ALARM) is a problem-class
|
||||
# binary (adoption path); 'operating_hours' (DURATION, TOTAL_INCREASING)
|
||||
# sits on whatever node the hub bridges, so no duty fits it without a
|
||||
# device-type gate; the 'reset_meter' buttons reset energy meters.
|
||||
}
|
||||
@@ -230,7 +230,7 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
"tuya": IntegrationSignature(
|
||||
name="Tuya vacuum",
|
||||
verified="2026-09-25 @ home-assistant/core dev",
|
||||
verified="2026-09-25 @ home-assistant/core dev; skips re-checked 2026-10-03 @ home-assistant/core 2026.10.0b0",
|
||||
source=(
|
||||
"home-assistant/core homeassistant/components/tuya/vacuum.py (vacuum "
|
||||
"platform) — the ENGINE accumulates cleaning time, entity_domain-gated so "
|
||||
@@ -248,7 +248,11 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"used-vs-remaining undetermined) and CWYSJ fountain 'filter_duration' (DP "
|
||||
"filter_life 'hours', direction undetermined). The runtime duties stay: "
|
||||
"washing the filter / de-tangling the brush is a separate duty from "
|
||||
"replacing the part at end of life."
|
||||
"replacing the part at end of life. 2026-10-03 skips: CWYSJ 'uv_runtime' "
|
||||
"(DURATION, disabled by default, device-defined unit, direction undocumented; "
|
||||
"the fountain's filter/pump/water resets are SWITCHES, not buttons, and UV has "
|
||||
"none) and the binary 'filter_cleaning' (a device_class problem fault bit → "
|
||||
"problem-sensor adoption)."
|
||||
),
|
||||
tasks=(
|
||||
# 2.95: each counter has its reset_* button (button.py category SD).
|
||||
@@ -275,7 +279,7 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
"smartthings": IntegrationSignature(
|
||||
name="SmartThings",
|
||||
verified="2026-07-18 (vacuum) / 2026-07-20 (filters) @ home-assistant/core dev; dust bag 2026-09-27 @ home-assistant/core 2026.9",
|
||||
verified="2026-07-18 (vacuum) / 2026-07-20 (filters) @ home-assistant/core dev; dust bag 2026-09-27 @ home-assistant/core 2026.9; skips re-checked 2026-10-03 @ home-assistant/core 2026.10.0b0",
|
||||
source=(
|
||||
"home-assistant/core homeassistant/components/smartthings/vacuum.py "
|
||||
"(vacuum platform verified present; no consumable sensors) — the "
|
||||
@@ -291,7 +295,9 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
"/ 'filter_status' (device_class problem → problem-sensor adoption), "
|
||||
"'stick_cleaner_dust_bag_usage' (unitless TOTAL_INCREASING bag cycles, reset "
|
||||
"semantics not established) and button 'reset_hepa_filter' (custom.hepaFilter — "
|
||||
"no sensor exposes that filter, core 2026.9 nor dev, so there is no duty to wire)."
|
||||
"no sensor exposes that filter, core 2026.9 nor dev, so there is no duty to wire). "
|
||||
"2026-10-03: the washer's 'microfiber_filter_job_state' / "
|
||||
"'microfiber_filter_operating_state' ENUMs are operating status — skipped."
|
||||
),
|
||||
tasks=(
|
||||
VACUUM_FILTER_CLEANING_RUNTIME,
|
||||
|
||||
@@ -133,4 +133,18 @@ SIGNATURES: dict[str, IntegrationSignature] = {
|
||||
),
|
||||
tasks=(CHARGER_CABLE_ETO,),
|
||||
),
|
||||
# --- Round 16 (2026-10-03) --------------------------------------------
|
||||
"wattpilot": IntegrationSignature(
|
||||
name="Fronius Wattpilot",
|
||||
verified="2026-10-03 @ mk-maddin/wattpilot-HA master (507a066)",
|
||||
source=(
|
||||
"HACS wattpilot (custom repository) sensor.yaml: property 'eto' ('Totally Charged' — "
|
||||
"'Totally charged power', device_class energy, state_class total, unit Wh; the go-e API "
|
||||
"lifetime energy the catalogued go-e entries use); entities.py names it "
|
||||
"f'{charger} Totally Charged' (no has_entity_name, no translation_key) → suffix "
|
||||
"_totally_charged. The unit map converts the 5,000 kWh target into Wh. 'wh' ('Connection "
|
||||
"Charged', energy since the car was connected) is per session — not used."
|
||||
),
|
||||
tasks=(ConsumableSignature(("totally_charged",), "Inspect Cable and Plug", "usage_delta", delta_units=5000),),
|
||||
),
|
||||
}
|
||||
Reference in new issue
Block a user