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