Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
3. **Parents approve** — chores marked "requires approval" land in a pending queue
4. **Kids claim rewards** — points are only deducted once a parent approves the claim

Everything is managed from the **TaskMate panel** in the Home Assistant sidebar, and surfaced on your dashboards through 27 Lovelace cards.
Everything is managed from the **TaskMate panel** in the Home Assistant sidebar, and surfaced on your dashboards through 28 Lovelace cards.

All data stays inside your Home Assistant instance. Nothing is sent to any external service.

Expand Down Expand Up @@ -89,7 +89,7 @@ Lovelace resources register automatically on startup. Edit your dashboard → **

| For kids | For parents |
|----------|-------------|
| Child · Rewards · Reward Progress · Points Display · Routine · Kiosk · Streak · Badges · Bounty Board · Wishlist · Recap · Chore Auction | Approvals · Parent Dashboard · Overview · Chore Board · Activity · Points · Reorder · Penalties · Bonuses · Weekly · Graph · Leaderboard · Calendar · Family Goal · Photo Gallery |
| Child · Sticker Chart · Rewards · Reward Progress · Points Display · Routine · Kiosk · Streak · Badges · Bounty Board · Wishlist · Recap · Chore Auction | Approvals · Parent Dashboard · Overview · Chore Board · Activity · Points · Reorder · Penalties · Bonuses · Weekly · Graph · Leaderboard · Calendar · Family Goal · Photo Gallery |

Every card takes a `header_color` and a `card_design` option. See [Dashboard Cards](https://github.com/tempus2016/taskmate/wiki/Dashboard-Cards) for each card's full configuration.

Expand All @@ -108,7 +108,7 @@ Every card takes a `header_color` and a `card_design` option. See [Dashboard Car
| [Getting Started](https://github.com/tempus2016/taskmate/wiki/Getting-Started) | First-time setup walkthrough |
| [Admin Panel](https://github.com/tempus2016/taskmate/wiki/Admin-Panel) | The management hub in the HA sidebar |
| [Settings](https://github.com/tempus2016/taskmate/wiki/Settings) | Every configurable setting in one place |
| [Dashboard Cards](https://github.com/tempus2016/taskmate/wiki/Dashboard-Cards) | All 27 Lovelace cards and their options |
| [Dashboard Cards](https://github.com/tempus2016/taskmate/wiki/Dashboard-Cards) | All 28 Lovelace cards and their options |
| [Services](https://github.com/tempus2016/taskmate/wiki/Services) | Every callable service, with examples |
| [Sensor Reference](https://github.com/tempus2016/taskmate/wiki/Sensor-Reference) | All entities and their attributes |
| [Automations](https://github.com/tempus2016/taskmate/wiki/Automations) | Bus events, blueprints, and examples |
Expand All @@ -134,4 +134,4 @@ Bug reports and feature requests are welcome in [Issues](https://github.com/temp

MIT — see [LICENSE](LICENSE).

> Originally created by [vinnybad/choremander](https://github.com/vinnybad/choremander). This fork adds 27 Lovelace cards, an admin panel, a bonus points system, streak tracking, reward approval flow, a penalty system, and much more.
> Originally created by [vinnybad/choremander](https://github.com/vinnybad/choremander). This fork adds 28 Lovelace cards, an admin panel, a bonus points system, streak tracking, reward approval flow, a penalty system, and much more.
11 changes: 11 additions & 0 deletions custom_components/taskmate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,16 @@ async def handle_complete_chore(call: ServiceCall) -> None:
chore_id = call.data[ATTR_CHORE_ID]
child_id = call.data[ATTR_CHILD_ID]
as_parent = call.data.get(ATTR_AS_PARENT, False)
completed_date = call.data.get("completed_date")
if completed_date is not None:
# Logging a job for an earlier day is a grown-up's correction, never
# a child's: it is always parent-gated and always approved.
await _async_require_parent(hass, call)
try:
await coordinator.async_complete_chore_on_date(chore_id, child_id, completed_date)
except ValueError as err:
raise ServiceValidationError(str(err)) from err
return
if as_parent:
# Completing on behalf of a child (auto-approve + instant award) is a
# parent privilege. Enforce it on the backend, not just by hiding the
Expand Down Expand Up @@ -1302,6 +1312,7 @@ async def handle_rebuild_badges(call: ServiceCall) -> None:
vol.Required(ATTR_CHILD_ID): cv.string,
vol.Optional(ATTR_AS_PARENT, default=False): cv.boolean,
vol.Optional("photo_url"): cv.string,
vol.Optional("completed_date"): cv.date,
vol.Optional("note"): cv.string,
vol.Optional("suggested_points"): vol.All(
vol.Coerce(int), vol.Range(min=0, max=CHORE_SUGGESTED_POINTS_MAX)
Expand Down
27 changes: 27 additions & 0 deletions custom_components/taskmate/coord_assignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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):
Expand All @@ -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.

Expand Down
29 changes: 29 additions & 0 deletions custom_components/taskmate/coord_avatars.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
Loading
Loading