Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 2 additions & 7 deletions custom_components/taskmate/coord_auctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,6 @@
_OCCURRENCE_LIMIT = 6


def _short_date(day: date) -> str:
"""A short date like "Sat 4 Oct", for push messages (rendered server-side)."""
return f"{day.strftime('%a')} {day.day} {day.strftime('%b')}"


def _as_int(value, default: int = 0) -> int:
try:
return int(value)
Expand Down Expand Up @@ -303,7 +298,7 @@ async def async_start_auction(
NOTIF_TYPE_AUCTION_OPENED,
{
"chore_name": auction.chore_name,
"date": _short_date(day),
"date": notify_strings.short_date(self.hass, day),
"max_points": auction.max_points,
"points_name": self.storage.get_points_name(),
},
Expand Down Expand Up @@ -670,7 +665,7 @@ async def async_remove_orphaned_auctions(self) -> None:

def _auction_date_label(self, auction: Auction) -> str:
try:
return _short_date(date.fromisoformat(auction.occurrence))
return notify_strings.short_date(self.hass, date.fromisoformat(auction.occurrence))
except ValueError:
return auction.occurrence

Expand Down
4 changes: 3 additions & 1 deletion custom_components/taskmate/coord_birthdays.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

from homeassistant.util import dt as dt_util

from . import notify_strings

_LOGGER = logging.getLogger(__name__)

DEFAULT_BIRTHDAY_MULTIPLIER = 2.0
Expand Down Expand Up @@ -154,7 +156,7 @@ async def async_check_birthdays(self, refresh: bool = True) -> list[str]:
await self._celebrate(
child,
"birthday",
f"Happy birthday, {child.name}!",
notify_strings.render(self.hass, "celebrate_birthday", {"child_name": child.name}),
tier=3,
extra={"multiplier": self.birthday_multiplier(), **({"age": age} if age is not None else {})},
)
Expand Down
7 changes: 6 additions & 1 deletion custom_components/taskmate/coord_challenges.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from homeassistant.util import dt as dt_util

from . import notify_strings
from .models import Challenge, PointsTransaction

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -173,7 +174,11 @@ async def _award_challenge(self, challenge: Challenge, child) -> None:
await self._celebrate(
child,
"challenge_completed",
f"{child.name} completed the challenge '{challenge.name}'!",
notify_strings.render(
self.hass,
"celebrate_challenge_completed",
{"child_name": child.name, "challenge_name": challenge.name},
),
tier=2,
extra={"challenge_id": challenge.id, "bonus": bonus},
)
Expand Down
4 changes: 2 additions & 2 deletions custom_components/taskmate/coord_chores.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from homeassistant.util import dt as dt_util

from . import images, photos
from . import images, notify_strings, photos
from .chore_undo import child_can_undo, undo_window_seconds
from .const import (
CHORE_NOTE_MAX_LENGTH,
Expand Down Expand Up @@ -1416,7 +1416,7 @@ async def async_approve_chore(
await self._celebrate(
child,
"all_chores_done",
f"{child.name} finished every chore today!",
notify_strings.render(self.hass, "celebrate_all_chores_done", {"child_name": child.name}),
tier=1,
)
else:
Expand Down
7 changes: 6 additions & 1 deletion custom_components/taskmate/coord_inspections.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from homeassistant.helpers.event import async_track_point_in_time, async_track_time_change
from homeassistant.util import dt as dt_util

from . import notify_strings
from .const import (
NOTIF_TYPE_INSPECTION_PASSED,
NOTIF_TYPE_INSPECTION_REMINDER,
Expand Down Expand Up @@ -306,7 +307,11 @@ async def async_pass_inspection(self, inspection_id: str, *, bonus: int | None =
await self._celebrate(
child,
"inspection_passed",
f"{child.name}'s {record['chore_name']} passed inspection!",
notify_strings.render(
self.hass,
"celebrate_inspection_passed",
{"child_name": child.name, "chore_name": record["chore_name"]},
),
tier=1,
extra={"inspection_id": record["id"], "bonus": record["bonus"]},
)
Expand Down
56 changes: 37 additions & 19 deletions custom_components/taskmate/coord_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import contextlib
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import date, datetime, timedelta
from typing import Any

from homeassistant.core import HomeAssistant, callback
Expand Down Expand Up @@ -69,6 +69,9 @@
# original Approve/Reject ids keep working unchanged when ratings are off.
_RATE_ACTION_PREFIX = "TASKMATE_RATE_"

# Stand-in names for a test push, translated like the rest (notify.sample_*).
_SAMPLE_KEYS = ("chore", "reward", "badge", "goal", "goal_reward", "wish", "pledger", "bounty", "reason", "note")


def _reject_action(hass: HomeAssistant, entry_id: str) -> dict[str, str]:
"""The Reject button of an approval push, with a reply box for the reason (#976)."""
Expand Down Expand Up @@ -410,39 +413,54 @@
meta = NOTIFICATION_TYPES_BY_ID.get(type_id)
if meta is None:
raise ValueError(f"Unknown notification type {type_id}")
await notify_strings.async_load(self.hass)
points_name = self.storage.get_points_name()
sample_month = date(2026, 1, 1)
sample = {key: notify_strings.text(self.hass, f"sample_{key}") for key in _SAMPLE_KEYS}
ctx = {
"child_name": "Alex",
"chore_name": "Tidy room",
"reward_name": "Movie night",
"badge_name": "Star Helper",
"chore_name": sample["chore"],
"reward_name": sample["reward"],
"badge_name": sample["badge"],
"points": 10,
"cost": 50,
"streak": 7,
"days": 7,
"level": 5,
"tier": 3,
"message": "Alex reached level 5!",
"summary": "• Alex: 5 chores, 50 Stars earned",
"month": "January 2026",
"goal_name": "Movie night fund",
"goal_reward": "a family movie night",
"message": notify_strings.render(self.hass, "celebrate_level_up", {"child_name": "Alex", "level": 5}),
"summary": notify_strings.render(
self.hass,
"weekly_digest_line",
{"child_name": "Alex", "chores": 5, "points": 50, "points_name": points_name},
),
"month": notify_strings.month_year(self.hass, sample_month),
"goal_name": sample["goal"],
"goal_reward": sample["goal_reward"],
"multiplier": "2",
"count": 3,
"wish_name": "Lego set",
"freezes_left": 2,
"wish_name": sample["wish"],
"target": 600,
"pledger": "Grandma",
"bounty_name": "Wash the car",
"pledger": sample["pledger"],
"bounty_name": sample["bounty"],
"minutes": 15,
"period": "January",
"item_name": "Tidy room",
"reason": "Not finished",
"reason_text": ": Not finished",
"period": notify_strings.month_name(self.hass, sample_month.month),
"item_name": sample["chore"],
"reason": sample["reason"],
"reason_text": f": {sample['reason']}",
"date": notify_strings.short_date(self.hass, sample_month),
"max_points": 20,
"until": "18:20",
"bonus": 10,
"note_text": ": Brilliant job!",
"points_name": self.storage.get_points_name(),
"note_text": f": {sample['note']}",
"points_name": points_name,
}
await notify_strings.async_load(self.hass)
ctx["result_text"] = notify_strings.render(
self.hass,
"auction_won",
{**ctx, "points": 12},
)
message = "[TEST] " + self._render_template(meta, ctx)
cfg = self.storage.get_notification_config(type_id)
nav_url = self._resolve_nav_url(cfg)
Expand Down Expand Up @@ -677,7 +695,7 @@
def _fire_bus_event(self, type_id: str, context: dict[str, Any], recipients: list[str]) -> None:
payload = dict(context)
payload["recipients"] = recipients
self.hass.bus.async_fire(notification_event_name(type_id), payload)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 296, in test_nav_url_only_for_mobile_app await coord.fire("badge_earned", {"child_name": "M", "badge_name": "Star"}) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 338, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 282, in test_nav_url_empty_emits_no_keys await coord.fire("badge_earned", {"child_name": "M", "badge_name": "Star"}) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 338, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 267, in test_nav_url_per_type_overrides_global await coord.fire("badge_earned", {"child_name": "M", "badge_name": "Star"}) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 338, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 252, in test_nav_url_global_default_applied await coord.fire("badge_earned", {"child_name": "M", "badge_name": "Star"}) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 338, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 152, in test_pending_approval_push_carries_clear_tag await coord.fire( File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 338, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 115, in test_pending_chore_approval_dispatches_actionable await coord.fire( File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 338, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 82, in test_fire_emits_bus_event_with_recipients await coord.fire("badge_earned", {"child_name": "M", "badge_name": "Star"}) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 338, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 65, in test_fire_renders_template_safely_with_missing_key await coord.fire("badge_earned", {"child_name": "M"}) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 338, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 48, in test_fire_routes_only_to_enabled_recipients await coord.fire("badge_earned", {"child_name": "M", "badge_name": "Star"}) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 338, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 698 in custom_components/taskmate/coord_notifications.py

View workflow job for this annotation

GitHub Actions / tests (py3.14, HA 2026.7.4)

coroutine 'AsyncMockMixin._execute_mock_call' was never awaited Coroutine created at (most recent call last) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/base_events.py", line 2054, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/asyncio/events.py", line 94, in _run self._context.run(self._callback, *self._args) File "/home/runner/work/taskmate/taskmate/tests/test_notifications_dispatch.py", line 27, in test_fire_skips_when_master_disabled await coord.fire("badge_earned", {"child_name": "Maria", "badge_name": "Star"}) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 295, in fire self._fire_bus_event(type_id, context, recipients=[]) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 698, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

async def handle_mobile_action(self, event) -> None:
"""Route TASKMATE_APPROVE_<id> / TASKMATE_REJECT_<id> mobile actions."""
Expand Down
14 changes: 10 additions & 4 deletions custom_components/taskmate/coord_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from homeassistant.util import dt as dt_util

from . import photos
from . import notify_strings, photos
from .models import Bonus, Child, Penalty, PointsTransaction, generate_id

if TYPE_CHECKING:
Expand Down Expand Up @@ -143,7 +143,11 @@ def _required_dates(child_id: str) -> set[str]:
await self._celebrate(
child,
"perfect_week",
f"{child.name} earned a perfect week — +{perfect_week_bonus}!",
notify_strings.render(
self.hass,
"celebrate_perfect_week",
{"child_name": child.name, "bonus": perfect_week_bonus},
),
tier=3,
extra={"bonus": perfect_week_bonus},
)
Expand Down Expand Up @@ -657,7 +661,7 @@ async def _maybe_level_up(self, child) -> None:
await self._celebrate(
child,
"level_up",
f"{child.name} reached level {lvl}!",
notify_strings.render(self.hass, "celebrate_level_up", {"child_name": child.name, "level": lvl}),
tier=3 if lvl % 5 == 0 else 2,
extra={"level": lvl},
)
Expand Down Expand Up @@ -1130,7 +1134,9 @@ async def _award_points(
await self._celebrate(
child,
"streak_milestone",
f"{child.name} hit a {days}-day streak!",
notify_strings.render(
self.hass, "celebrate_streak_milestone", {"child_name": child.name, "days": days}
),
tier=3 if days >= 30 else 2,
extra={"days": days},
)
Expand Down
7 changes: 6 additions & 1 deletion custom_components/taskmate/coord_quests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from homeassistant.util import dt as dt_util

from . import notify_strings
from .models import PointsTransaction, Quest

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -223,7 +224,11 @@ async def _complete_quest(self, quest: Quest, child, prog: dict) -> None:
await self._celebrate(
child,
"quest_completed",
f"{child.name} completed the quest '{quest.name}'!",
notify_strings.render(
self.hass,
"celebrate_quest_completed",
{"child_name": child.name, "quest_name": quest.name},
),
tier=3,
extra={"quest_id": quest.id, "bonus": bonus},
)
Expand Down
51 changes: 23 additions & 28 deletions custom_components/taskmate/coord_recaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from homeassistant.helpers.event import async_track_time_change
from homeassistant.util import dt as dt_util

from . import notify_strings
from .models import generate_id
from .timewindow import parse_hhmm

Expand Down Expand Up @@ -86,22 +87,6 @@
)
_SATURDAY_FIRST = frozenset("AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY".split())

_MONTH_ABBR = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
_MONTH_NAME = (
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
)


def _add_months(day: date, months: int) -> date:
"""The first of the month ``months`` after ``day``'s month (may be negative)."""
Expand Down Expand Up @@ -143,15 +128,22 @@ def completed_periods(freq: str, today: date, week_start: int = 0, limit: int =
day = start - timedelta(days=1)


def period_label(freq: str, start: date, end: date) -> str:
"""Short English label for a period, used in notification text."""
def period_label(freq: str, start: date, end: date, hass: Any = None) -> str:
"""Short label for a period, used in notification text (HA's language)."""
if freq == "weekly":
return "weekly"
return notify_strings.text(hass, "period_weekly")
if freq == "monthly":
return _MONTH_NAME[start.month - 1]
return notify_strings.month_name(hass, start.month)
if freq == "yearly":
return str(start.year)
return f"{_MONTH_ABBR[start.month - 1]}–{_MONTH_ABBR[end.month - 1]}"
return notify_strings.render(
hass,
"period_range",
{
"start": notify_strings.text(hass, f"month_short_{start.month}"),
"end": notify_strings.text(hass, f"month_short_{end.month}"),
},
)


def _valid_frequencies(raw: Any) -> list[str]:
Expand Down Expand Up @@ -754,12 +746,14 @@ async def async_send_recap_notifications(self) -> int:
continue
first = recaps[0]
label = period_label(
first["frequency"], date.fromisoformat(first["start"]), date.fromisoformat(first["end"])
first["frequency"], date.fromisoformat(first["start"]), date.fromisoformat(first["end"]), self.hass
)
if len(recaps) == 1:
message = f"✨ Your {label} recap is ready, {child.name}! Tap to watch it."
message = notify_strings.render(self.hass, "recap_ready", {"period": label, "child_name": child.name})
else:
message = f"✨ {len(recaps)} new recaps are ready, {child.name}! Tap to watch them."
message = notify_strings.render(
self.hass, "recap_ready_many", {"count": len(recaps), "child_name": child.name}
)
per_child.append(
{
"child_id": child_id,
Expand All @@ -775,13 +769,14 @@ async def async_send_recap_notifications(self) -> int:
if not per_child:
return 0

names = [c["child_name"] for c in per_child]
joined = names[0] if len(names) == 1 else ", ".join(names[:-1]) + f" and {names[-1]}"
joined = notify_strings.join_names(self.hass, [c["child_name"] for c in per_child])
labels = {c["label"] for c in per_child}
if len(labels) == 1:
parent_message = f"✨ {per_child[0]['label'].capitalize()} recaps are ready for {joined}. Tap to see them."
parent_message = notify_strings.render(
self.hass, "recap_parent_one", {"period": per_child[0]["label"], "names": joined}
)
else:
parent_message = f"✨ New recaps are ready for {joined}. Tap to see them."
parent_message = notify_strings.render(self.hass, "recap_parent_many", {"names": joined})

await self.notifications.send_recap_ready(
per_child,
Expand Down
Loading
Loading