198 files
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user