278 files

This commit is contained in:
Home Assistant Version Control
2026-09-13 20:38:58 +00:00
parent d7a0a08372
commit 1b4f5f68c6
278 changed files with 36892 additions and 7150 deletions
+38 -4
View File
@@ -18,6 +18,7 @@ from homeassistant.util import dt as dt_util
from . import images
from .const import DOMAIN
from .coord_rewards import reward_is_time_locked
from .coordinator import TaskMateCoordinator
from .entity import taskmate_device_info
from .models import Child
@@ -309,6 +310,12 @@ def _build_chores_list(coordinator: TaskMateCoordinator, common: dict) -> list[d
# `icon` above, to keep records under the 16KB recorder limit.
image_url = getattr(c, "image_url", "")
if image_url:
# Same trade-off as custom sounds, and unlike evidence photos
# (which deliberately emit a bare path for the card to sign per
# viewer): a chore picture is decorative and renders on first
# paint, so signing it here keeps that render synchronous. It does
# mean the signed URL is readable in this attribute — fine for a
# chore picture, not fine for anything personal.
record["image_url"] = images.sign_image_url(common["hass"], image_url)
completion_sound = getattr(c, "completion_sound", "coin")
if completion_sound and completion_sound != "coin":
@@ -453,7 +460,8 @@ def _build_rewards_list(common: dict) -> list[dict]:
children = common["children"]
pool_by_child_reward = common["pool_by_child_reward"]
pool_total_by_reward = common["pool_total_by_reward"]
today = dt_util.now().date()
now = dt_util.now()
today = now.date()
out = []
for r in rewards:
assigned = r.assigned_to if isinstance(r.assigned_to, list) and r.assigned_to else [c.id for c in children]
@@ -472,6 +480,7 @@ def _build_rewards_list(common: dict) -> list[dict]:
days_until_expiry = (deadline - today).days
except (TypeError, ValueError):
pass
is_time_locked = reward_is_time_locked(r, now)
out.append(
{
"id": r.id,
@@ -489,10 +498,21 @@ def _build_rewards_list(common: dict) -> list[dict]:
"expires_at": expires_at,
"is_sold_out": is_sold_out,
"is_expired": is_expired,
"is_available": not (is_sold_out or is_expired),
"is_available": not (is_sold_out or is_expired or is_time_locked),
"days_until_expiry": days_until_expiry,
}
)
# Time lock (#857): only carried for rewards that actually use it. This
# attribute slice is capped at the recorder's 16 KB limit, so unused
# feature fields must not cost every reward bytes.
if getattr(r, "time_lock_enabled", False):
out[-1]["time_lock"] = {
"days": list(getattr(r, "available_days", []) or []),
"from": getattr(r, "available_from", "") or "",
"until": getattr(r, "available_until", "") or "",
}
if is_time_locked:
out[-1]["is_time_locked"] = True
return out
@@ -849,7 +869,15 @@ class TaskMateOverallStatsSensor(_CachedAttrsSensor):
"points_icon": data.get("points_icon", "mdi:star"),
# Global default card-design style; cards read this when no per-card override (#design).
"card_design": settings.get("card_design", "classic"),
# Non-admin parent role (#661): cards unlock parent controls for these HA users.
# Non-admin parent role (#661): cards unlock parent controls for
# these HA users. Deliberately still published in the clear, and
# deliberately never trusted: every privileged path re-resolves the
# role server-side (`_async_require_parent` / `authz`), so this list
# only decides which buttons a card draws. Any authenticated user
# can read it, which tells them which accounts hold the role — worth
# knowing, but it grants nothing, and the alternatives (digests, or
# a per-connection lookup) make the check asynchronous and race the
# first paint in every card that gates controls on it.
"parent_user_ids": self.coordinator.storage.get_parent_user_ids(),
"children": _build_children_summary(self.coordinator, common),
"time_boundaries": time_boundaries,
@@ -876,7 +904,11 @@ class TaskMateOverallStatsSensor(_CachedAttrsSensor):
class TaskMateChoresSensor(_CachedAttrsSensor):
"""Chores catalog + today's completions."""
_unrecorded_attributes = frozenset({"chores", "todays_completions", "task_groups", "active_timed_sessions"})
# custom_sounds carries a freshly signed URL per sound, so it changes on every
# refresh — recording it would churn the DB for no history value (#856).
_unrecorded_attributes = frozenset(
{"chores", "todays_completions", "task_groups", "active_timed_sessions", "custom_sounds"}
)
def __init__(
self,
@@ -910,6 +942,8 @@ class TaskMateChoresSensor(_CachedAttrsSensor):
return {
"chores": _build_chores_list(self.coordinator, common),
"todays_completions": _build_todays_completions(common),
# Lookup table for chores whose completion_sound is "custom:<file>" (#856).
"custom_sounds": self.coordinator.custom_sounds_state(),
"task_groups": task_groups,
"active_timed_sessions": _build_active_timed_sessions(self.coordinator),
"vacation_active": active_vacation is not None,