187 files

This commit is contained in:
Home Assistant Version Control committed 2026-09-03 17:54:35 +00:00
1 parent 16c9b4d7f4
commit d6413e19b5
187 files changed
+15542 -2765

No files matched your search

@@ -10,6 +10,7 @@ threshold trigger. No per-battery task.
from __future__ import annotations
import logging
from collections.abc import Callable
from typing import Any
from uuid import uuid4
@@ -30,6 +31,9 @@ from ..const import (
DOMAIN,
)
from .battery_fleet import _norm_type, discover_battery_types, lifetime_months, read_batteries
from .trigger_fallback import threshold_limits_overlap
_LOGGER = logging.getLogger(__name__)
# The global aggregate sensor the fleet task triggers on (fixed entity_id).
LOW_COUNT_ENTITY_ID = "sensor.maintenance_supporter_batteries_to_replace"
@@ -521,6 +525,34 @@ def _heal_fleet_trigger_recovery_flag(hass: HomeAssistant, entry: ConfigEntry) -
return True
def _warn_fleet_trigger_overlap(entry: ConfigEntry) -> bool:
"""#156: a hand-edited fleet trigger with ``trigger_below`` above
``trigger_above`` is always tripped (the limits are OR-ed), so the task
never recovers and never auto-completes. Newer editors refuse the
combination; stored configs from before that only get this log line —
the user's edit is not silently rewritten. Returns True when warned.
"""
found = find_fleet_task(entry)
if found is None:
return False
_task_id, task_data = found
tc = task_data.get("trigger_config") or {}
above, below = tc.get("trigger_above"), tc.get("trigger_below")
if not (isinstance(above, (int, float)) and isinstance(below, (int, float))):
return False
if not threshold_limits_overlap(float(above), float(below)):
return False
_LOGGER.warning(
"Battery fleet task '%s' has 'trigger below' (%s) higher than 'trigger above' (%s): "
"the limits are OR-ed, so the task is always triggered and can never auto-complete. "
"Clear 'Trigger below' in the task editor (issue #156).",
task_data.get("name", _task_id),
below,
above,
)
return True
async def reconcile_fleet_parts_at_start(hass: HomeAssistant, entry: ConfigEntry, lang: str) -> dict[str, Any]:
"""Boot-time reconcile of the fleet's type-parts (issue #148).
@@ -535,6 +567,7 @@ async def reconcile_fleet_parts_at_start(hass: HomeAssistant, entry: ConfigEntry
is theirs and stays.
"""
trigger_healed = _heal_fleet_trigger_recovery_flag(hass, entry)
_warn_fleet_trigger_overlap(entry)
types = discover_battery_types(hass)
added = _reconcile_type_parts(hass, entry, types, lang)
@@ -165,11 +165,11 @@ SIGNATURES: dict[str, IntegrationSignature] = {
),
"winix": IntegrationSignature(
name="Winix",
verified="2026-07-19 @ iprak/winix master sensor.py (HACS default)",
verified="2026-09-02 @ iprak/winix master sensor.py (HACS default)",
source=(
"HACS winix: tk 'filter_life' (PERCENTAGE) — % remaining derived "
"device-side from filter hours vs the model's "
"filter_alarm_duration."
"from filter hours vs the model's max filter life "
"(wrapper.filter_max_life; was filter_alarm_duration before 2026-09)."
),
tasks=(ConsumableSignature(("filter_life",), "Replace Filter", "percent_left"),),
),
@@ -38,13 +38,25 @@ SIGNATURES: dict[str, IntegrationSignature] = {
),
"dreame_vacuum": IntegrationSignature(
name="Dreame Vacuum",
verified="2026-07-16 @ Tasshack/dreame-vacuum master",
source="Tasshack/dreame-vacuum custom_components/dreame_vacuum/sensor.py (property keys *_left, percent)",
verified="2026-09-02 @ Tasshack/dreame-vacuum master (ae8422f2)",
source=(
"Tasshack/dreame-vacuum custom_components/dreame_vacuum/sensor.py "
"(property_key *_LEFT, UNIT_PERCENT) + dreame/const.py PROPERTY_TO_NAME "
"(translation_key = first element, e.g. mop_pad_left); every *_left "
"sensor is entity_registry_enabled_default=False — the user enables "
"the ones their model has (#150, station consumables)."
),
tasks=(
ConsumableSignature(("main_brush_left",), "Replace Main Brush", "percent_left"),
ConsumableSignature(("side_brush_left",), "Replace Side Brush", "percent_left"),
ConsumableSignature(("filter_left",), "Replace Filter", "percent_left"),
ConsumableSignature(("sensor_dirty_left",), "Clean Sensors", "percent_left"),
# Self-wash base stations (L10s/L20/X30/X40 …) add four more wear
# counters — same percent scale, same *_left key convention.
ConsumableSignature(("mop_pad_left",), "Replace Mop Pads", "percent_left"),
ConsumableSignature(("detergent_left",), "Refill Detergent", "percent_left"),
ConsumableSignature(("secondary_filter_left",), "Replace Secondary Filter", "percent_left"),
ConsumableSignature(("silver_ion_left",), "Replace Silver-ion Module", "percent_left"),
),
),
"ecovacs": IntegrationSignature(
@@ -71,6 +71,18 @@ def threshold_exceeds(
return not_equals is not None and not math.isclose(value, not_equals, rel_tol=1e-9, abs_tol=1e-9)
def threshold_limits_overlap(above: float | None, below: float | None) -> bool:
"""Whether ``above``/``below`` together cover every possible reading.
The limits are OR-ed by :func:`threshold_exceeds`, so ``below > above``
makes any value a hit — the trigger can never clear and
``auto_complete_on_recovery`` never fires (#156). ``below == above`` is
left alone on purpose: on a count sensor 0/0 reads as "anything but 0"
and does recover.
"""
return above is not None and below is not None and below > above
def _numeric_entity_value(get_state: StateGetter, entity_id: str, attribute: str | None) -> float | None:
"""Read a numeric value from an entity state/attribute (None when unusable)."""
state = get_state(entity_id)