187 files
This commit is contained in:
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)
|
||||
|
||||
Reference in new issue
Block a user