diff --git a/custom_components/taskmate/coord_assignments.py b/custom_components/taskmate/coord_assignments.py
index fe304e78..893289db 100644
--- a/custom_components/taskmate/coord_assignments.py
+++ b/custom_components/taskmate/coord_assignments.py
@@ -53,6 +53,14 @@ def _refresh_tracked_availability_entities(self) -> None:
visibility.add(chore.weather_entity)
self._tracked_visibility_entities = visibility
+ # A child's photo is read from their picture entity, so a photo changed
+ # there has to reach the sensors without waiting for a TaskMate edit.
+ # Trimmed the same way child_avatar_image reads it, so a stray space in
+ # a stored id can't leave the photo untracked.
+ self._tracked_picture_entities = {
+ entity for c in self.storage.get_children() if (entity := (getattr(c, "picture_entity", "") or "").strip())
+ }
+
@callback
def _availability_state_changed(self, event: Any) -> None:
"""Cheap bus-filter: react only when a tracked entity changes.
@@ -66,6 +74,9 @@ def _availability_state_changed(self, event: Any) -> None:
if not entity_id:
return
+ if entity_id in getattr(self, "_tracked_picture_entities", set()):
+ self._picture_entity_changed(data)
+
is_availability = entity_id in getattr(self, "_tracked_availability_entities", set())
is_visibility = entity_id in getattr(self, "_tracked_visibility_entities", set())
if not (is_availability or is_visibility):
@@ -79,6 +90,22 @@ def _availability_state_changed(self, event: Any) -> None:
if is_availability:
self.hass.async_create_task(self._async_reevaluate_availability())
+ def _picture_entity_changed(self, data: dict) -> None:
+ """Push a child's new photo out as soon as the entity's picture changes.
+
+ A person's state flips between home and away all day without touching
+ its picture, so only a real change of ``entity_picture`` (or the entity
+ appearing or vanishing) is worth invalidating the sensors for.
+ """
+
+ def picture(state: Any) -> str:
+ return str((getattr(state, "attributes", None) or {}).get("entity_picture") or "") if state else ""
+
+ if picture(data.get("old_state")) == picture(data.get("new_state")):
+ return
+ self.external_state_version += 1
+ self.async_update_listeners()
+
async def _async_reevaluate_availability(self) -> None:
"""Re-run assignment for require_availability chores when availability flips.
diff --git a/custom_components/taskmate/coord_avatars.py b/custom_components/taskmate/coord_avatars.py
index ff0be77b..589d09e7 100644
--- a/custom_components/taskmate/coord_avatars.py
+++ b/custom_components/taskmate/coord_avatars.py
@@ -10,6 +10,10 @@
import logging
+from homeassistant.helpers import entity_registry as er
+
+from .const import DOMAIN
+
_LOGGER = logging.getLogger(__name__)
# Shipped defaults so the feature is useful out of the box. Parents can replace
@@ -36,6 +40,31 @@ def avatar_catalog(self) -> list[dict]:
return [a for a in raw if isinstance(a, dict) and a.get("icon")]
return list(DEFAULT_AVATAR_CATALOG)
+ def child_avatar_image(self, child) -> str:
+ """The picture of the entity chosen for a child's face, or ``""``.
+
+ Read from that entity on every update rather than stored, so a photo
+ changed in HA appears here with nothing to re-save, and a deleted
+ entity simply falls back to the MDI avatar instead of a broken image.
+
+ Empty when no entity is chosen, it no longer exists, or it has no
+ picture — every call site then shows the MDI avatar, so a child is
+ never left with a blank face.
+
+ TaskMate's own child sensors are refused: they show this very photo,
+ so choosing one would make the face its own source and leave it stale.
+ """
+ entity_id = (getattr(child, "picture_entity", "") or "").strip()
+ if not entity_id:
+ return ""
+ entry = er.async_get(self.hass).async_get(entity_id)
+ if entry is not None and entry.platform == DOMAIN:
+ return ""
+ state = self.hass.states.get(entity_id)
+ if state is None:
+ return ""
+ return str(state.attributes.get("entity_picture") or "")
+
def _avatar_unlocked(self, entry: dict, child) -> bool:
kind = entry.get("unlock_type", "free")
try:
diff --git a/custom_components/taskmate/coordinator.py b/custom_components/taskmate/coordinator.py
index b9deff90..616b4049 100644
--- a/custom_components/taskmate/coordinator.py
+++ b/custom_components/taskmate/coordinator.py
@@ -986,6 +986,7 @@ async def async_add_child(
unavailability_entity: str = "",
pause_streak_when_unavailable: bool = False,
linked_user_id: str = "",
+ picture_entity: str = "",
birthday: str = "",
presence_entity: str = "",
age_group: str = "",
@@ -1004,6 +1005,7 @@ async def async_add_child(
unavailability_entity=unavailability_entity,
pause_streak_when_unavailable=pause_streak_when_unavailable,
linked_user_id=linked_user_id,
+ picture_entity=picture_entity,
birthday=birthday,
presence_entity=presence_entity,
age_group=age_group,
diff --git a/custom_components/taskmate/models.py b/custom_components/taskmate/models.py
index b620ccb9..01d01f43 100644
--- a/custom_components/taskmate/models.py
+++ b/custom_components/taskmate/models.py
@@ -235,6 +235,12 @@ class Child:
total_penalties_received: int = 0
notify_service: str | None = None
linked_user_id: str = "" # HA user id; when set, only that user (or an admin) may self-serve as this child
+ # Entity whose picture replaces the MDI avatar — normally the child's
+ # person.* entity. Deliberately independent of linked_user_id: a young
+ # child often has a person (with a photo) but no HA login of their own.
+ # Only the entity id is stored; the picture is read from it on every
+ # update, so a new photo set in HA shows up with nothing to re-save here.
+ picture_entity: str = ""
quiet_hours_start: str = "" # "HH:MM" — start of do-not-disturb window; empty = no quiet hours
quiet_hours_end: str = "" # "HH:MM" — end of do-not-disturb window; start>end means overnight
presence_entity: str = "" # person./device_tracker.; while not home, reminders wait for arrival (#926)
@@ -282,6 +288,7 @@ def from_dict(cls, data: dict[str, Any]) -> Child:
total_penalties_received=data.get("total_penalties_received", 0),
notify_service=data.get("notify_service"),
linked_user_id=data.get("linked_user_id", ""),
+ picture_entity=str(data.get("picture_entity", "") or ""),
quiet_hours_start=data.get("quiet_hours_start", ""),
quiet_hours_end=data.get("quiet_hours_end", ""),
presence_entity=str(data.get("presence_entity", "") or ""),
@@ -320,6 +327,7 @@ def to_dict(self) -> dict[str, Any]:
"total_penalties_received": self.total_penalties_received,
"notify_service": self.notify_service,
"linked_user_id": self.linked_user_id,
+ "picture_entity": self.picture_entity,
"quiet_hours_start": self.quiet_hours_start,
"quiet_hours_end": self.quiet_hours_end,
"presence_entity": self.presence_entity,
diff --git a/custom_components/taskmate/sensor.py b/custom_components/taskmate/sensor.py
index 40d51760..d40ae026 100644
--- a/custom_components/taskmate/sensor.py
+++ b/custom_components/taskmate/sensor.py
@@ -68,8 +68,11 @@ def _compute_common(coordinator: TaskMateCoordinator) -> dict:
"""
data = coordinator.data
data_id = id(data)
+ # External entities TaskMate reads (a child's photo among them) are not part
+ # of ``data``, so their version is part of the key.
+ external = getattr(coordinator, "external_state_version", 0)
cached = getattr(coordinator, _COMMON_CACHE_ATTR, None)
- if cached and cached.get("data_id") == data_id:
+ if cached and cached.get("data_id") == data_id and cached.get("external") == external:
return cached["common"]
children = data.get("children", [])
@@ -142,10 +145,13 @@ def _compute_common(coordinator: TaskMateCoordinator) -> dict:
"pool_by_child_reward": pool_by_child_reward,
"pool_total_by_reward": pool_total_by_reward,
"total_allocated_by_child": total_allocated_by_child,
+ # Each child's photo (from their chosen picture entity), resolved once
+ # per update: every call site that draws a child's face reads this.
+ "child_avatar_images": {c.id: coordinator.child_avatar_image(c) for c in children},
# Sent back to redo by an inspection (#981): not "done" on the card.
"inspection_redo_ids": _inspection_redo_ids(coordinator),
}
- setattr(coordinator, _COMMON_CACHE_ATTR, {"data_id": data_id, "common": common})
+ setattr(coordinator, _COMMON_CACHE_ATTR, {"data_id": data_id, "external": external, "common": common})
return common
@@ -217,6 +223,8 @@ def _build_children_summary(coordinator: TaskMateCoordinator, common: dict) -> l
"total_points_earned": getattr(c, "total_points_earned", 0) or 0,
"total_chores_completed": getattr(c, "total_chores_completed", 0) or 0,
"avatar": getattr(c, "avatar", "mdi:account-circle") or "mdi:account-circle",
+ # The child's photo, or "" — cards fall back to `avatar`.
+ "avatar_image": common.get("child_avatar_images", {}).get(c.id, ""),
"last_completion_date": getattr(c, "last_completion_date", None),
"streak_paused": getattr(c, "streak_paused", False),
"on_vacation": coordinator._is_child_on_vacation(c),
@@ -698,6 +706,9 @@ def _build_pending_reward_claims(common: dict) -> list[dict]:
"reward_id": rc.reward_id,
"child_id": rc.child_id,
"child_name": child.name,
+ # No avatar_image here on purpose: the cards resolve a claim's
+ # face from the overview sensor's children[], and a copy per
+ # claim pushed this sensor past the 16KB recorder limit.
"child_avatar": getattr(child, "avatar", "mdi:account-circle") or "mdi:account-circle",
"reward_name": reward.name,
"reward_icon": reward.icon or "mdi:gift",
@@ -1575,6 +1586,7 @@ def extra_state_attributes(self) -> dict:
"child_id": child.id,
"child_name": child.name,
"avatar": child.avatar,
+ "avatar_image": self.coordinator.child_avatar_image(child),
"total_points_earned": child.total_points_earned,
"total_chores_completed": child.total_chores_completed,
"current_streak": child.current_streak,
@@ -1615,6 +1627,18 @@ def icon(self) -> str:
child = self.coordinator.get_child(self.child_id)
return child.avatar if child else "mdi:account-circle"
+ @property
+ def entity_picture(self) -> str | None:
+ """The child's photo, so HA's own UI shows their face too.
+
+ HA prefers a picture over ``icon`` when both are set, so returning
+ None (the default) leaves the MDI avatar above in charge.
+ """
+ child = self.coordinator.get_child(self.child_id)
+ if not child:
+ return None
+ return self.coordinator.child_avatar_image(child) or None
+
@property
def extra_state_attributes(self) -> dict:
"""Return additional attributes."""
@@ -1650,6 +1674,7 @@ def _included(c):
"child_id": child.id,
"child_name": child.name,
"avatar": child.avatar,
+ "avatar_image": self.coordinator.child_avatar_image(child),
"points": child.points,
"total_points_earned": child.total_points_earned,
"total_chores_completed": child.total_chores_completed,
@@ -1869,6 +1894,10 @@ def extra_state_attributes(self) -> dict:
earned.sort(key=lambda e: e.get("earned_at") or "", reverse=True)
return {
+ # Who the badges belong to, so the card can put a face to them.
+ "child_name": child.name,
+ "child_avatar": getattr(child, "avatar", "mdi:account-circle") or "mdi:account-circle",
+ "avatar_image": self.coordinator.child_avatar_image(child),
"earned": earned,
"available": available,
"total_badges": len(applicable),
diff --git a/custom_components/taskmate/websocket.py b/custom_components/taskmate/websocket.py
index c591a7b7..897aa296 100644
--- a/custom_components/taskmate/websocket.py
+++ b/custom_components/taskmate/websocket.py
@@ -490,6 +490,7 @@ async def _ws_get_state(hass, connection, msg, coordinator):
vol.Optional("unavailability_entity", default=""): str,
vol.Optional("pause_streak_when_unavailable", default=False): bool,
vol.Optional("linked_user_id", default=""): str,
+ vol.Optional("picture_entity", default=""): str,
vol.Optional("birthday", default=""): vol.All(str, vol.Length(max=10)),
vol.Optional("presence_entity", default=""): _validate_presence_entity,
vol.Optional("age_group", default=""): vol.In(("", *AGE_GROUPS)),
@@ -511,6 +512,7 @@ async def _ws_add_child(hass, connection, msg, coordinator):
unavailability_entity=_opt_str(msg.get("unavailability_entity")),
pause_streak_when_unavailable=bool(msg.get("pause_streak_when_unavailable", False)),
linked_user_id=_opt_str(msg.get("linked_user_id")),
+ picture_entity=_opt_str(msg.get("picture_entity")),
birthday=birthday,
presence_entity=msg.get("presence_entity", ""),
age_group=msg.get("age_group", ""),
@@ -529,6 +531,7 @@ async def _ws_add_child(hass, connection, msg, coordinator):
vol.Optional("unavailability_entity"): str,
vol.Optional("pause_streak_when_unavailable"): bool,
vol.Optional("linked_user_id"): str,
+ vol.Optional("picture_entity"): str,
vol.Optional("presence_entity"): _validate_presence_entity,
vol.Optional("is_guest"): bool,
vol.Optional("guest_expires_on"): str,
@@ -557,6 +560,8 @@ async def _ws_update_child(hass, connection, msg, coordinator):
existing.pause_streak_when_unavailable = bool(msg["pause_streak_when_unavailable"])
if "linked_user_id" in msg:
existing.linked_user_id = _opt_str(msg["linked_user_id"])
+ if "picture_entity" in msg:
+ existing.picture_entity = _opt_str(msg["picture_entity"])
if "birthday" in msg:
try:
existing.birthday = normalize_birthday(msg["birthday"])
diff --git a/custom_components/taskmate/www/locales/da.json b/custom_components/taskmate/www/locales/da.json
index 5f54fb73..dd164c76 100644
--- a/custom_components/taskmate/www/locales/da.json
+++ b/custom_components/taskmate/www/locales/da.json
@@ -778,6 +778,8 @@
"panel.child_inverted": "(omvendt)",
"panel.child_link_user_label": "Knyt til Home Assistant-bruger (valgfrit)",
"panel.child_link_user_hint": "Når det er sat, kan kun denne Home Assistant-bruger (eller en admin) fuldføre pligter, indløse belønninger eller køre timere som dette barn via service-kald. Vælg \"Alle\" til en fælles familie-tablet.",
+ "panel.child_picture_entity_label": "Foto fra",
+ "panel.child_picture_entity_hint": "Vælg barnets person for at vise deres foto i stedet for ikonet ovenfor. Fotoet indstilles under Indstillinger › Personer; uden foto vises ikonet.",
"panel.child_kiosk_pin_label": "Kiosk-PIN-kode",
"panel.child_kiosk_pin_hint": "Valgfri. 4 cifre, som barnet taster på en delt kiosk-tablet. Gemmes som hash og kan ikke læses igen — lad feltet stå tomt for at beholde den nuværende kode.",
"panel.child_kiosk_pin_set": "Der er sat en PIN-kode.",
diff --git a/custom_components/taskmate/www/locales/de.json b/custom_components/taskmate/www/locales/de.json
index 9445097b..5549e071 100644
--- a/custom_components/taskmate/www/locales/de.json
+++ b/custom_components/taskmate/www/locales/de.json
@@ -1980,6 +1980,8 @@
"panel.btn_clone_chore": "Duplizieren",
"panel.child_link_user_label": "Mit Home-Assistant-Benutzer verknüpfen (optional)",
"panel.child_link_user_hint": "Wenn gesetzt, kann nur dieser Home-Assistant-Benutzer (oder ein Admin) per Dienstaufruf Aufgaben erledigen, Belohnungen einlösen oder Timer für dieses Kind starten. Für ein gemeinsames Familien-Tablet auf \"Beliebig\" lassen.",
+ "panel.child_picture_entity_label": "Foto von",
+ "panel.child_picture_entity_hint": "Wähle die Person des Kindes, um ihr Foto anstelle des Symbols oben zu zeigen. Das Foto wird unter Einstellungen › Personen gesetzt; ohne Foto wird das Symbol angezeigt.",
"panel.child_kiosk_pin_label": "Kiosk-PIN",
"panel.child_kiosk_pin_hint": "Optional. 4 Ziffern, die das Kind auf einem gemeinsamen Kiosk-Tablet eintippt. Wird als Hash gespeichert und kann nicht ausgelesen werden — leer lassen, um die aktuelle PIN zu behalten.",
"panel.child_kiosk_pin_set": "Eine PIN ist gesetzt.",
diff --git a/custom_components/taskmate/www/locales/en-GB.json b/custom_components/taskmate/www/locales/en-GB.json
index 4fd7dc37..4ea97d9e 100644
--- a/custom_components/taskmate/www/locales/en-GB.json
+++ b/custom_components/taskmate/www/locales/en-GB.json
@@ -778,6 +778,8 @@
"panel.child_inverted": "(inverted)",
"panel.child_link_user_label": "Link to Home Assistant user (optional)",
"panel.child_link_user_hint": "When set, only this Home Assistant user (or an admin) can complete chores, claim rewards, or run timers as this child via service calls. Leave as \"Anyone\" for a shared family tablet.",
+ "panel.child_picture_entity_label": "Photo from",
+ "panel.child_picture_entity_hint": "Pick the child's person to show their photo instead of the icon above. Set the photo under Settings › People; with none set, the icon is shown.",
"panel.child_kiosk_pin_label": "Kiosk PIN",
"panel.child_kiosk_pin_hint": "Optional. 4 digits this child types on a shared kiosk tablet. Stored hashed, so it can't be read back — leave blank to keep the current PIN.",
"panel.child_kiosk_pin_set": "A PIN is set.",
diff --git a/custom_components/taskmate/www/locales/en.json b/custom_components/taskmate/www/locales/en.json
index 4fd7dc37..ec557728 100644
--- a/custom_components/taskmate/www/locales/en.json
+++ b/custom_components/taskmate/www/locales/en.json
@@ -778,6 +778,8 @@
"panel.child_inverted": "(inverted)",
"panel.child_link_user_label": "Link to Home Assistant user (optional)",
"panel.child_link_user_hint": "When set, only this Home Assistant user (or an admin) can complete chores, claim rewards, or run timers as this child via service calls. Leave as \"Anyone\" for a shared family tablet.",
+ "panel.child_picture_entity_label": "Photo from",
+ "panel.child_picture_entity_hint": "Pick any entity with a picture, usually the child's person, to show it instead of the icon above. A person's photo is set under Settings › People; with no picture, the icon is shown.",
"panel.child_kiosk_pin_label": "Kiosk PIN",
"panel.child_kiosk_pin_hint": "Optional. 4 digits this child types on a shared kiosk tablet. Stored hashed, so it can't be read back — leave blank to keep the current PIN.",
"panel.child_kiosk_pin_set": "A PIN is set.",
diff --git a/custom_components/taskmate/www/locales/fr.json b/custom_components/taskmate/www/locales/fr.json
index 8d8af266..41edcb47 100644
--- a/custom_components/taskmate/www/locales/fr.json
+++ b/custom_components/taskmate/www/locales/fr.json
@@ -1980,6 +1980,8 @@
"panel.btn_clone_chore": "Dupliquer",
"panel.child_link_user_label": "Lier à un utilisateur Home Assistant (facultatif)",
"panel.child_link_user_hint": "Si défini, seul cet utilisateur Home Assistant (ou un admin) peut terminer des tâches, échanger des récompenses ou lancer des minuteurs pour cet enfant via les services. Laissez sur « N'importe qui » pour une tablette familiale partagée.",
+ "panel.child_picture_entity_label": "Photo de",
+ "panel.child_picture_entity_hint": "Choisissez la personne de l'enfant pour afficher sa photo à la place de l'icône ci-dessus. La photo se définit dans Paramètres › Personnes ; sans photo, l'icône est affichée.",
"panel.child_kiosk_pin_label": "Code PIN du kiosque",
"panel.child_kiosk_pin_hint": "Facultatif. 4 chiffres que l'enfant tape sur une tablette kiosque partagée. Stocké haché, il ne peut pas être relu — laissez vide pour garder le code actuel.",
"panel.child_kiosk_pin_set": "Un code PIN est défini.",
diff --git a/custom_components/taskmate/www/locales/nb.json b/custom_components/taskmate/www/locales/nb.json
index e739d6f8..2adf998c 100644
--- a/custom_components/taskmate/www/locales/nb.json
+++ b/custom_components/taskmate/www/locales/nb.json
@@ -1980,6 +1980,8 @@
"panel.btn_clone_chore": "Dupliser",
"panel.child_link_user_label": "Koble til Home Assistant-bruker (valgfritt)",
"panel.child_link_user_hint": "Når satt, kan bare denne Home Assistant-brukeren (eller en admin) fullføre gjøremål, løse inn belønninger eller starte tidtakere for dette barnet via tjenestekall. La stå på «Hvem som helst» for et delt familiebrett.",
+ "panel.child_picture_entity_label": "Bilde fra",
+ "panel.child_picture_entity_hint": "Velg barnets person for å vise bildet i stedet for ikonet over. Bildet settes under Innstillinger › Personer; uten bilde vises ikonet.",
"panel.child_kiosk_pin_label": "Kiosk-PIN",
"panel.child_kiosk_pin_hint": "Valgfritt. 4 sifre barnet taster på et delt kiosk-nettbrett. Lagres som hash og kan ikke leses tilbake — la feltet stå tomt for å beholde gjeldende kode.",
"panel.child_kiosk_pin_set": "En PIN-kode er satt.",
diff --git a/custom_components/taskmate/www/locales/nn.json b/custom_components/taskmate/www/locales/nn.json
index 56a1b693..2a900994 100644
--- a/custom_components/taskmate/www/locales/nn.json
+++ b/custom_components/taskmate/www/locales/nn.json
@@ -1980,6 +1980,8 @@
"panel.btn_clone_chore": "Dupliser",
"panel.child_link_user_label": "Kople til Home Assistant-brukar (valfritt)",
"panel.child_link_user_hint": "Når sett, kan berre denne Home Assistant-brukaren (eller ein admin) fullføre gjeremål, løyse inn løningar eller starte tidtakarar for dette barnet via tenestekall. Lat stå på «Kven som helst» for eit delt familiebrett.",
+ "panel.child_picture_entity_label": "Bilete frå",
+ "panel.child_picture_entity_hint": "Vel personen til barnet for å vise biletet i staden for ikonet over. Biletet vert sett under Innstillingar › Personar; utan bilete vert ikonet vist.",
"panel.child_kiosk_pin_label": "Kiosk-PIN",
"panel.child_kiosk_pin_hint": "Valfritt. 4 siffer barnet tastar på eit delt kiosk-nettbrett. Vert lagra som hash og kan ikkje lesast tilbake — la feltet stå tomt for å halde på gjeldande kode.",
"panel.child_kiosk_pin_set": "Ein PIN-kode er sett.",
diff --git a/custom_components/taskmate/www/locales/pl.json b/custom_components/taskmate/www/locales/pl.json
index 8ef97537..f8eca63e 100644
--- a/custom_components/taskmate/www/locales/pl.json
+++ b/custom_components/taskmate/www/locales/pl.json
@@ -778,6 +778,8 @@
"panel.child_inverted": "(odwrócone)",
"panel.child_link_user_label": "Powiąż z użytkownikiem Home Assistant (opcjonalnie)",
"panel.child_link_user_hint": "Po ustawieniu tylko ten użytkownik Home Assistant (lub administrator) może wykonywać obowiązki, odbierać nagrody i uruchamiać stopery jako to dziecko przez wywołania usług. Pozostaw „Każdy” dla wspólnego tabletu rodzinnego.",
+ "panel.child_picture_entity_label": "Zdjęcie z",
+ "panel.child_picture_entity_hint": "Wybierz osobę dziecka, aby pokazać jego zdjęcie zamiast ikony powyżej. Zdjęcie ustawisz w Ustawienia › Osoby; bez zdjęcia pokazywana jest ikona.",
"panel.child_kiosk_pin_label": "PIN kiosku",
"panel.child_kiosk_pin_hint": "Opcjonalny. 4 cyfry, które dziecko wpisuje na wspólnym tablecie w trybie kiosku. Przechowywany jako hash, więc nie da się go odczytać — pozostaw puste, aby zachować obecny PIN.",
"panel.child_kiosk_pin_set": "PIN jest ustawiony.",
diff --git a/custom_components/taskmate/www/locales/pt-BR.json b/custom_components/taskmate/www/locales/pt-BR.json
index 8053106b..93fa3269 100644
--- a/custom_components/taskmate/www/locales/pt-BR.json
+++ b/custom_components/taskmate/www/locales/pt-BR.json
@@ -1980,6 +1980,8 @@
"panel.btn_clone_chore": "Duplicar",
"panel.child_link_user_label": "Vincular a um usuário do Home Assistant (opcional)",
"panel.child_link_user_hint": "Quando definido, apenas este usuário do Home Assistant (ou um admin) pode concluir tarefas, resgatar recompensas ou iniciar timers por esta criança via serviços. Deixe em \"Qualquer pessoa\" para um tablet familiar compartilhado.",
+ "panel.child_picture_entity_label": "Foto de",
+ "panel.child_picture_entity_hint": "Escolha a pessoa da criança para mostrar a foto em vez do ícone acima. A foto é definida em Configurações › Pessoas; sem foto, o ícone é exibido.",
"panel.child_kiosk_pin_label": "PIN do quiosque",
"panel.child_kiosk_pin_hint": "Opcional. 4 dígitos que a criança digita em um tablet de quiosque compartilhado. Salvo com hash, não pode ser lido de volta — deixe em branco para manter o PIN atual.",
"panel.child_kiosk_pin_set": "Há um PIN definido.",
diff --git a/custom_components/taskmate/www/locales/pt.json b/custom_components/taskmate/www/locales/pt.json
index 8be54547..b9326ed8 100644
--- a/custom_components/taskmate/www/locales/pt.json
+++ b/custom_components/taskmate/www/locales/pt.json
@@ -1980,6 +1980,8 @@
"panel.btn_clone_chore": "Duplicar",
"panel.child_link_user_label": "Associar a um utilizador do Home Assistant (opcional)",
"panel.child_link_user_hint": "Quando definido, apenas este utilizador do Home Assistant (ou um admin) pode concluir tarefas, resgatar recompensas ou iniciar temporizadores por esta criança via serviços. Deixe em \"Qualquer pessoa\" para um tablet familiar partilhado.",
+ "panel.child_picture_entity_label": "Fotografia de",
+ "panel.child_picture_entity_hint": "Escolhe a pessoa da criança para mostrar a fotografia em vez do ícone acima. A fotografia define-se em Definições › Pessoas; sem fotografia, é mostrado o ícone.",
"panel.child_kiosk_pin_label": "PIN do quiosque",
"panel.child_kiosk_pin_hint": "Opcional. 4 dígitos que a criança escreve num tablet de quiosque partilhado. Guardado com hash, não pode ser lido de volta — deixe em branco para manter o PIN atual.",
"panel.child_kiosk_pin_set": "Há um PIN definido.",
diff --git a/custom_components/taskmate/www/taskmate-approvals-card.js b/custom_components/taskmate/www/taskmate-approvals-card.js
index c451ab6a..849a9f51 100644
--- a/custom_components/taskmate/www/taskmate-approvals-card.js
+++ b/custom_components/taskmate/www/taskmate-approvals-card.js
@@ -789,23 +789,26 @@ class TaskMateApprovalsCard extends LitElement {
_designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; }
- _av(name, avatar, tone, size) {
- const a = avatar || "";
- const inner = a.startsWith("mdi:")
- ? html``
- : a
- ? html``
+ // `source` is whatever _childAvatar returned: a child object, or a bare
+ // avatar string from an older payload. The shared helper accepts either.
+ _av(name, source, tone, size) {
+ const face = window.__taskmate_child_visual(source);
+ const inner = face.kind === "icon"
+ ? html``
+ : face.kind === "image"
+ ? html``
: (name || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
return html`
${inner}
`;
}
- // Resolve a child's avatar from the overview sensor's children list (the
- // completion/claim payloads don't carry an avatar field).
+ // Resolve a child's face from the overview sensor's children list (the
+ // completion/claim payloads don't carry one). Returns the whole child so the
+ // child's photo travels with the avatar, not just the icon.
_childAvatar(childId) {
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity))
|| this.hass?.states?.[this.config.entity]?.attributes || {};
const child = (attrs.children || []).find(c => c.id === childId);
- return child ? child.avatar : null;
+ return child || null;
}
_pointsAttrs() {
diff --git a/custom_components/taskmate/www/taskmate-auction-card.js b/custom_components/taskmate/www/taskmate-auction-card.js
index d8fe4aa3..75e1c529 100644
--- a/custom_components/taskmate/www/taskmate-auction-card.js
+++ b/custom_components/taskmate/www/taskmate-auction-card.js
@@ -183,7 +183,11 @@ class TaskMateAuctionCard extends LitElement {
const idx = Math.max(0, children.findIndex(c => String(c.id) === String(childId)));
const name = this._childName(childId);
const colour = `var(--tmd-c${(idx % 6) + 1}, ${CHILD_COLOURS[idx % CHILD_COLOURS.length]})`;
- return html`${name.charAt(0).toUpperCase()}`;
+ // The child's photo when they have one (the shared face helper), else their initial.
+ const child = children.find(c => String(c.id) === String(childId));
+ const face = child && window.__taskmate_child_visual ? window.__taskmate_child_visual(child) : null;
+ const inner = face && face.kind === "image" ? html`` : name.charAt(0).toUpperCase();
+ return html`${inner}`;
}
/**
@@ -623,8 +627,9 @@ class TaskMateAuctionCard extends LitElement {
background: var(--ac-ac, #9b59b6); color: #fff;
display: inline-grid; place-items: center;
font-weight: 700; font-size: calc(var(--ac-s, 30px) * .42); line-height: 1;
- box-sizing: border-box;
+ box-sizing: border-box; overflow: hidden;
}
+ .ac-av img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ac-btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
diff --git a/custom_components/taskmate/www/taskmate-badges-card.js b/custom_components/taskmate/www/taskmate-badges-card.js
index 55b8184c..613804d4 100644
--- a/custom_components/taskmate/www/taskmate-badges-card.js
+++ b/custom_components/taskmate/www/taskmate-badges-card.js
@@ -145,6 +145,8 @@ class TaskMateBadgesCard extends LitElement {
--mdc-icon-size: 26px;
color: white;
}
+ .child-avatar { overflow: hidden; }
+ .child-avatar img { width: 100%; height: 100%; object-fit: cover; }
.badges-title {
font-size: 1.1rem;
@@ -479,6 +481,17 @@ class TaskMateBadgesCard extends LitElement {
}
}
+ // The child's photo when they have one (the shared face helper), else the
+ // avatar icon the sensor sends.
+ _face(attrs, avatar) {
+ const face = window.__taskmate_child_visual
+ ? window.__taskmate_child_visual({ avatar, avatar_image: attrs.avatar_image })
+ : { kind: "icon", icon: avatar };
+ return face.kind === "image"
+ ? html``
+ : html``;
+ }
+
render() {
if (!this.hass || !this.config) return html``;
@@ -528,9 +541,7 @@ class TaskMateBadgesCard extends LitElement {