198 files

This commit is contained in:
Home Assistant Version Control
2026-10-11 23:51:59 +00:00
parent 4a64770282
commit 669d229519
198 changed files with 4891 additions and 2815 deletions
@@ -41,6 +41,7 @@ from .entity.entity_base import MaintenanceEntity
from .entity.summary_coordinator import MaintenanceSummaryCoordinator
from .entity.triggers import BaseTrigger, create_triggers, normalize_entity_ids
from .helpers.aggregate import object_slug as aggregate_object_slug
from .helpers.assignees import person_of_user
from .helpers.coordinator_data import has_coordinator_data
from .helpers.dates import parse_hhmm
from .helpers.global_options import is_schedule_time_enabled
@@ -248,6 +249,11 @@ class MaintenanceSensor(MaintenanceEntity, SensorEntity):
# (state_attr(..., 'priority') == 'high'). Always present — tasks
# without an explicit value are "normal", the model default.
"priority": task.get("priority") or DEFAULT_TASK_PRIORITY,
# #210: who does it, as the user id and as the person entity (the
# entity update_task takes as responsible_user), so an automation
# can pick one person's tasks. None while nobody is responsible.
"responsible_user_id": task.get("responsible_user_id") or None,
"responsible_user": person_of_user(self.hass, task.get("responsible_user_id")),
}
# Recorded readings (#83 / #161 phase 2): the newest completion's
@@ -741,6 +747,12 @@ class PartStockSensor(MaintenanceEntity, SensorEntity):
store = getattr(rd, "store", None) if rd else None
return store.get_part_stock(self._part_id) if store else None
@property
def _restoring(self) -> float:
rd = getattr(self.coordinator.entry, "runtime_data", None)
store = getattr(rd, "store", None) if rd else None
return store.get_part_restoring(self._part_id) if store else 0
@property
def available(self) -> bool:
"""Catalog-only parts (no tracked stock) read unavailable."""
@@ -756,15 +768,21 @@ class PartStockSensor(MaintenanceEntity, SensorEntity):
@property
def extra_state_attributes(self) -> dict[str, Any]:
from .helpers.parts import part_is_low
from .helpers.parts import on_hand, part_is_low
part = self._part
return {
attrs: dict[str, Any] = {
"part_name": part.get("name"),
"reorder_threshold": part.get("reorder_threshold"),
"storage_location": part.get("storage_location") or None,
"is_low": part_is_low(part, self._stock),
"is_low": part_is_low(part, on_hand(part, self._stock, self._restoring)),
}
# #208: a reusable part says what readies a used one (charge, sharpen,
# ...) and how many wait for it; the state stays the ready stock.
if part.get("reuse"):
attrs["reuse"] = part["reuse"]
attrs["restoring"] = self._restoring
return attrs
class PartsToReorderSensor(SensorEntity):
@@ -809,7 +827,7 @@ class PartsToReorderSensor(SensorEntity):
@property
def native_value(self) -> int:
from .const import CONF_PARTS
from .helpers.parts import part_is_low
from .helpers.parts import on_hand, part_is_low
count = 0
for entry in self.hass.config_entries.async_entries(DOMAIN):
@@ -820,7 +838,7 @@ class PartsToReorderSensor(SensorEntity):
if store is None:
continue
for part in (entry.data.get(CONF_PARTS) or {}).values():
if part_is_low(part, store.get_part_stock(part["id"])):
if part_is_low(part, on_hand(part, store.get_part_stock(part["id"]), store.get_part_restoring(part["id"]))):
count += 1
return count
@@ -914,6 +932,18 @@ class BatteryFleetLowSensor(SensorEntity):
sticky.pop(eid, None) # gone from the fleet
continue
level = current.get("level")
# #205: a threshold lowered since this battery went low is a
# decision, not a hover — judged against the new threshold,
# without the band (the latch releases it the same way).
now_threshold = current.get("low_threshold")
if (
isinstance(now_threshold, (int, float))
and now_threshold < sticky[eid]
and level is not None
and float(level) > float(now_threshold)
):
sticky.pop(eid, None)
continue
# A sensorless note (D#162) has no level to hover — it leaves
# `low` only when its forecast re-anchors (or the option is
# switched off), and that is the real all-clear.