187 files
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
@@ -26,10 +27,12 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.translation import async_get_cached_translations
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
STATE_RUNNING,
|
||||
CYCLE_IN_PROGRESS_STATES,
|
||||
SIGNAL_WASHER_UPDATE,
|
||||
CONF_EXPOSE_DEBUG_ENTITIES,
|
||||
)
|
||||
@@ -44,7 +47,10 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the binary sensor."""
|
||||
manager: WashDataManager = hass.data[DOMAIN][entry.entry_id]
|
||||
entities = [WasherRunningBinarySensor(manager, entry)]
|
||||
entities = [
|
||||
WasherRunningBinarySensor(manager, entry),
|
||||
WasherMaintenanceDueBinarySensor(manager, entry),
|
||||
]
|
||||
|
||||
if entry.options.get(CONF_EXPOSE_DEBUG_ENTITIES):
|
||||
entities.append(WasherAmbiguitySensor(manager, entry))
|
||||
@@ -56,6 +62,8 @@ async def async_setup_entry(
|
||||
class WasherRunningBinarySensor(BinarySensorEntity):
|
||||
"""Binary sensor indicating if washer is running."""
|
||||
|
||||
_attr_should_poll = False # pushed by the manager's update signal (PERF-02)
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
_attr_translation_key = "running"
|
||||
@@ -74,7 +82,9 @@ class WasherRunningBinarySensor(BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._manager.check_state() == STATE_RUNNING
|
||||
# Any in-progress state, not only `running`: a soak, a pause or the end
|
||||
# wait is not "done" (audit PLATFORM-06).
|
||||
return self._manager.check_state() in CYCLE_IN_PROGRESS_STATES
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
@@ -114,3 +124,102 @@ class WasherAmbiguitySensor(WasherRunningBinarySensor):
|
||||
"""Return ambiguous candidate info."""
|
||||
details = self._manager.last_match_details
|
||||
return {"margin": details.get("ambiguity_margin", 0.0) if details else 0.0}
|
||||
|
||||
|
||||
# How often a day-based reminder is re-checked when nothing else happens. The cycle
|
||||
# and log paths refresh the sensor through the update signal; only the passing of
|
||||
# time needs a clock, and hourly is plenty for an interval counted in days.
|
||||
_MAINTENANCE_RECHECK = timedelta(hours=1)
|
||||
|
||||
|
||||
class WasherMaintenanceDueBinarySensor(BinarySensorEntity):
|
||||
"""On while any maintenance task is due (#461): build your own notification.
|
||||
|
||||
WashData itself never notifies about maintenance (no new notification type);
|
||||
this entity is the hook for a user's automation. Attributes list the due tasks
|
||||
with how far past their interval each is. Built-in task names are translated
|
||||
into Home Assistant's language; custom task names are the user's own text.
|
||||
"""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "maintenance_due"
|
||||
_attr_icon = "mdi:wrench-clock"
|
||||
|
||||
def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self._manager = manager
|
||||
self._entry = entry
|
||||
self._attr_unique_id = f"{entry.entry_id}_maintenance_due"
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, entry.entry_id)},
|
||||
"name": entry.title,
|
||||
"manufacturer": "WashData",
|
||||
}
|
||||
self._attr_is_on = False
|
||||
self._attr_extra_state_attributes = {"due_task_ids": [], "due_tasks": []}
|
||||
|
||||
def _builtin_names(self) -> dict[str, str]:
|
||||
"""Home Assistant's loaded entity strings for this integration ({} if none)."""
|
||||
try:
|
||||
return async_get_cached_translations(
|
||||
self.hass, self.hass.config.language, "entity", DOMAIN
|
||||
)
|
||||
except Exception: # noqa: BLE001 - a missing translation is not an error
|
||||
return {}
|
||||
|
||||
def _refresh(self) -> bool:
|
||||
"""Recompute state and attributes; return whether either changed.
|
||||
|
||||
The update signal fires on every power reading, so the state is written
|
||||
only when it changes (audit PERF-08 counts entity writes per reading).
|
||||
"""
|
||||
due = [row for row in self._manager.maintenance_status if row.get("due")]
|
||||
strings = self._builtin_names() if any(not r.get("custom") for r in due) else {}
|
||||
prefix = (
|
||||
f"component.{DOMAIN}.entity.binary_sensor.maintenance_due"
|
||||
".state_attributes.due_tasks.state."
|
||||
)
|
||||
tasks = [
|
||||
{
|
||||
"id": row["id"],
|
||||
"name": (
|
||||
row["name"] if row.get("custom")
|
||||
else strings.get(prefix + row["id"]) or row["id"]
|
||||
),
|
||||
"cycles_since": row.get("cycles_since"),
|
||||
"cycles_interval": row.get("cycles_interval"),
|
||||
"days_since": row.get("days_since"),
|
||||
"days_interval": row.get("days_interval"),
|
||||
}
|
||||
for row in due
|
||||
]
|
||||
attrs = {"due_task_ids": [t["id"] for t in tasks], "due_tasks": tasks}
|
||||
is_on = bool(tasks)
|
||||
if is_on == self._attr_is_on and attrs == self._attr_extra_state_attributes:
|
||||
return False
|
||||
self._attr_is_on = is_on
|
||||
self._attr_extra_state_attributes = attrs
|
||||
return True
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks and compute the first state."""
|
||||
self._refresh()
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_WASHER_UPDATE.format(self._entry.entry_id),
|
||||
self._update_callback,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_track_time_interval(
|
||||
self.hass, self._update_callback, _MAINTENANCE_RECHECK
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_callback(self, *_args: Any) -> None:
|
||||
"""Write the state only when it changed."""
|
||||
if self._refresh():
|
||||
self.async_write_ha_state()
|
||||
|
||||
Reference in New Issue
Block a user