331 files

This commit is contained in:
Home Assistant Version Control
2026-10-04 05:26:42 +00:00
parent 22ba8b4648
commit c4d601bfba
331 changed files with 10227 additions and 50159 deletions
+109 -3
View File
@@ -11,7 +11,8 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -24,7 +25,7 @@ from .coord_rewards import reward_is_time_locked
from .coordinator import TaskMateCoordinator
from .entity import taskmate_device_info
from .models import Child
from .watch import DEFAULT_STATE_STRINGS, watch_state, watch_summary
from .watch import DEFAULT_STATE_STRINGS, today_board, watch_state, watch_summary
_LOGGER = logging.getLogger(__name__)
@@ -922,6 +923,7 @@ async def async_setup_entry(
entities.append(TaskMateIncentivesSensor(coordinator, entry))
entities.append(TaskMateBountiesSensor(coordinator, entry))
entities.append(TaskMateAuctionsSensor(coordinator, entry))
entities.append(TaskMateChoreBoardSensor(coordinator, entry))
# Add sensors for each child
for child in coordinator.data.get("children", []):
@@ -1002,6 +1004,22 @@ class _CachedAttrsSensor(TaskMateBaseSensor):
raise NotImplementedError
# The singleton sensors the cards merge into the overview's attributes, by
# unique_id suffix. The suffix doubles as the key in `companion_entities` and
# matches the default entity id (sensor.taskmate_<suffix>) the cards fall back to.
COMPANION_SUFFIXES = (
"chores",
"chore_availability",
"rewards",
"activity",
"incentives",
"pending_approvals",
"bounties",
"auctions",
"chore_board",
)
class TaskMateOverallStatsSensor(_CachedAttrsSensor):
"""Overview sensor — scalars plus the compact per-child summary.
@@ -1011,7 +1029,9 @@ class TaskMateOverallStatsSensor(_CachedAttrsSensor):
# The per-child summary grows with the family; the scalars stay recorded
# so history/statistics on them keep working (#817).
_unrecorded_attributes = frozenset({"children", "vacation_periods", "season_champions"})
_unrecorded_attributes = frozenset(
{"children", "vacation_periods", "season_champions", "companion_entities", "badge_entities"}
)
def __init__(
self,
@@ -1021,6 +1041,44 @@ class TaskMateOverallStatsSensor(_CachedAttrsSensor):
super().__init__(coordinator, entry)
self._attr_unique_id = f"{entry.entry_id}_overall_stats"
self._attr_name = "TaskMate Overview"
self._published_entity_map: dict | None = None
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
self.async_on_remove(
self.hass.bus.async_listen(er.EVENT_ENTITY_REGISTRY_UPDATED, self._async_entity_registry_updated)
)
@callback
def _async_entity_registry_updated(self, _event: Event) -> None:
"""Republish when a companion sensor is created, removed or renamed."""
if self._entity_map() != self._published_entity_map:
self.async_write_ha_state()
def _entity_map(self) -> dict:
"""The entity ids the companion and per-child badges sensors really
have (#1018). Home Assistant puts the device's area in front of a new
entity's id (2026.6+), and users rename entities, so the cards can't
rely on the sensor.taskmate_<suffix> defaults."""
if getattr(self, "hass", None) is None:
return {"companion_entities": {}, "badge_entities": {}}
registry = er.async_get(self.hass)
prefix = self._entry.entry_id
def lookup(suffix: str) -> str | None:
return registry.async_get_entity_id("sensor", DOMAIN, f"{prefix}_{suffix}")
companions = {s: eid for s in COMPANION_SUFFIXES if (eid := lookup(s))}
badges = {c.id: eid for c in self.coordinator.storage.get_children() if (eid := lookup(f"{c.id}_badges"))}
return {"companion_entities": companions, "badge_entities": badges}
@property
def extra_state_attributes(self) -> dict:
# Kept out of the cached build: a rename doesn't touch the coordinator
# data the cache is keyed on.
entity_map = self._entity_map()
self._published_entity_map = entity_map
return {**super().extra_state_attributes, **entity_map}
@property
def native_value(self) -> int:
@@ -1423,6 +1481,54 @@ class TaskMateAuctionsSensor(_CachedAttrsSensor):
return {"auctions": self.coordinator.auctions_public_state()}
class TaskMateChoreBoardSensor(TaskMateBaseSensor):
"""The Today page's chore board, for the chore board card (#1017).
The state is how many chores are still to do today across every child.
The ``chore_board`` attribute is what the admin panel's Today page draws:
today's status of each child's chores, plus the last seven days of
done/total per child for the week view. The panel reads it over the
admin-only WebSocket; this puts it where a non-admin dashboard can.
Keyed on the date as well as the coordinator snapshot, like the watch
sensors, so a quiet midnight still rolls the board over.
"""
_unrecorded_attributes = frozenset({"chore_board"})
_attr_icon = "mdi:view-grid-outline"
def __init__(
self,
coordinator: TaskMateCoordinator,
entry: ConfigEntry,
) -> None:
super().__init__(coordinator, entry)
self._attr_unique_id = f"{entry.entry_id}_chore_board"
self._attr_name = "TaskMate Chore Board"
self._board_key: tuple | None = None
self._board: dict | None = None
def _current_board(self) -> dict:
key = (
id(self.coordinator.data),
getattr(self.coordinator, "external_state_version", 0),
dt_util.as_local(dt_util.now()).date(),
)
if key != self._board_key or self._board is None:
self._board = self.coordinator.daily_progress_state(board=today_board(self.coordinator))
self._board_key = key
return self._board
@property
def native_value(self) -> int:
children = self._current_board()["board"]["children"]
return sum(1 for e in children for c in e["chores"] if c["status"] in ("todo", "missed"))
@property
def extra_state_attributes(self) -> dict:
return {"chore_board": self._current_board()}
class ChildPointsSensor(TaskMateBaseSensor):
"""Sensor for a child's points."""