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
23 changes: 17 additions & 6 deletions custom_components/taskmate/coord_auctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util

from . import notify_strings
from .const import (
AUCTION_POINTS_MAX,
AUCTION_REMINDER_MINUTES,
Expand Down Expand Up @@ -354,13 +355,15 @@ async def _async_tell_bidders_called_off(self, auction: Auction, bidders: list[s
"""The "called off" push, to the children who bid on ``auction``."""
if not bidders:
return
await notify_strings.async_load(self.hass)
await self.notifications.fire(
NOTIF_TYPE_AUCTION_RESULT,
{
"chore_name": auction.chore_name,
"date": self._auction_date_label(auction),
"result_text": f"the auction for '{auction.chore_name}' was called off. "
"It goes back to the normal rota.",
"result_text": notify_strings.render(
self.hass, "auction_called_off", {"chore_name": auction.chore_name}
),
},
only_recipients={f"child:{c}" for c in bidders},
)
Expand Down Expand Up @@ -471,13 +474,21 @@ async def _async_after_auction_closed(self, auction: Auction) -> None:
"""Announce a settled auction: bus event and the results push."""
winner = self.get_child(auction.winner_id) if auction.winner_id else None
self._fire_auction_event("taskmate_auction_closed", auction, auction.winner_id)
await notify_strings.async_load(self.hass)
if winner is not None:
result = (
f"{winner.name} won '{auction.chore_name}' on {self._auction_date_label(auction)} "
f"for {auction.price} {self.storage.get_points_name()}."
result = notify_strings.render(
self.hass,
"auction_won",
{
"child_name": winner.name,
"chore_name": auction.chore_name,
"date": self._auction_date_label(auction),
"points": auction.price,
"points_name": self.storage.get_points_name(),
},
)
else:
result = f"no bids for '{auction.chore_name}', so it goes back to the normal rota."
result = notify_strings.render(self.hass, "auction_no_bids", {"chore_name": auction.chore_name})
recipients = {f"child:{c}" for c in auction.eligible_child_ids}
recipients |= {p.id for p in self.storage.get_parent_recipients()}
await self.notifications.fire(
Expand Down
77 changes: 21 additions & 56 deletions custom_components/taskmate/coord_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_change

from . import authz
from . import authz, notify_strings
from .const import (
DEFAULT_NOTIFICATION_GROUP,
DEFAULT_NOTIFICATION_NAV_URL,
Expand Down Expand Up @@ -64,24 +64,21 @@

DEFAULT_MORNING_NOTIFY_TIME = "08:00"

# Appended to actionable notifications sent to non-mobile_app backends, which
# silently ignore tap actions. Gives those recipients a way to act.
_APPROVE_IN_PANEL_HINT = "Open the TaskMate panel to approve or reject."

# Mobile action id prefix for "approve with an N-star quality rating" (#927):
# TASKMATE_RATE_<n>_<completion id>. Distinct from TASKMATE_APPROVE_ so the
# original Approve/Reject ids keep working unchanged when ratings are off.
_RATE_ACTION_PREFIX = "TASKMATE_RATE_"


def _reject_action(entry_id: str) -> dict[str, str]:
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)."""
reject = notify_strings.text(hass, "action_reject")
return {
"action": f"TASKMATE_REJECT_{entry_id}",
"title": "Reject",
"title": reject,
"behavior": "textInput",
"textInputButtonTitle": "Reject",
"textInputPlaceholder": "Reason (optional)",
"textInputButtonTitle": reject,
"textInputPlaceholder": notify_strings.text(hass, "action_reject_placeholder"),
}


Expand Down Expand Up @@ -255,11 +252,7 @@
_is_within_quiet_hours = is_within_window


class _SafeDict(dict):
"""str.format_map dict that leaves missing keys as `{key}` literal."""

def __missing__(self, key: str) -> str:
return "{" + key + "}"
_SafeDict = notify_strings.SafeDict


class NotificationCoordinator:
Expand Down Expand Up @@ -300,6 +293,7 @@
return

recipients_fired: list[str] = []
await notify_strings.async_load(self.hass)
message = self._render_template(meta, context)
nav_url = self._resolve_nav_url(cfg)
group = self._resolve_group(cfg)
Expand Down Expand Up @@ -448,6 +442,7 @@
"note_text": ": Brilliant job!",
"points_name": self.storage.get_points_name(),
}
await notify_strings.async_load(self.hass)
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 @@ -506,47 +501,17 @@
return ""

def _render_template(self, meta: "NotificationTypeMeta", context: dict[str, Any]) -> str:
# Built-in types use a baked-in default; will be replaced by translations
# in a later task. For now use a safe English fallback so dispatch works.
templates = {
NOTIF_TYPE_BEDTIME_REMINDER: "{child_name}, you still have chores to do before bedtime.",
NOTIF_TYPE_STREAK_AT_RISK: "{child_name}, complete a chore today to keep your {streak}-day streak!",
NOTIF_TYPE_ALL_CHORES_DONE: "{child_name} finished every chore today!",
NOTIF_TYPE_BADGE_EARNED: "{child_name} earned the {badge_name} badge!",
NOTIF_TYPE_PENDING_CHORE_APPROVAL: "{child_name} completed '{chore_name}' (+{points} {points_name}) — awaiting approval.",
NOTIF_TYPE_PENDING_REWARD_CLAIM: "{child_name} claimed '{reward_name}' ({cost} {points_name}) — awaiting approval.",
NOTIF_TYPE_STREAK_MILESTONE: "{child_name} hit a {days}-day streak — +{points} {points_name}!",
NOTIF_TYPE_LEVEL_UP: "{child_name} reached level {level}! 🎉",
NOTIF_TYPE_WEEKLY_DIGEST: "TaskMate weekly digest:\n{summary}",
NOTIF_TYPE_CELEBRATION: "🎉 {message}",
NOTIF_TYPE_MANDATORY_REMINDER: "{child_name}, you still need to do '{chore_name}'.",
NOTIF_TYPE_MANDATORY_PARENT_ALERT: "{child_name} still hasn't done the mandatory chore '{chore_name}'.",
NOTIF_TYPE_MONTHLY_REPORT: "TaskMate {month} report:\n{summary}",
NOTIF_TYPE_SEASON_CHAMPION: "🏆 {child_name} won the {month} leaderboard with {points} {points_name}!",
NOTIF_TYPE_FAMILY_GOAL_REACHED: "🎉 Family goal reached: {goal_name}! Time for {goal_reward}.",
NOTIF_TYPE_BIRTHDAY: "🎂 Happy birthday, {child_name}! Every chore pays {multiplier}× today.",
NOTIF_TYPE_STREAK_FREEZE_USED: "❄️ A streak freeze saved {child_name}'s {streak}-day streak ({freezes_left} left).",
NOTIF_TYPE_PRESENCE_ARRIVAL: "🏠 You're home, {child_name} — {count} chores left today.",
NOTIF_TYPE_WISH_REQUESTED: "{child_name} wished for '{wish_name}' ({target} {points_name}) — waiting for your approval.",
NOTIF_TYPE_WISH_PLEDGED: "💝 {pledger} added {points} {points_name} to your wish '{wish_name}'!",
NOTIF_TYPE_BOUNTY_POSTED: "🏁 New bounty: {bounty_name} — {points} {points_name}. First to claim it gets it!",
NOTIF_TYPE_BOUNTY_CLAIM_LAPSING: "⏳ {child_name}, {minutes} minutes left to finish '{bounty_name}' before it goes back on the board.",
NOTIF_TYPE_RECAP_READY: "✨ Your {period} recap is ready, {child_name}! Tap to watch it.",
NOTIF_TYPE_ITEM_REJECTED: "↩️ {child_name}, '{item_name}' was sent back{reason_text}",
NOTIF_TYPE_AUCTION_OPENED: "🔨 New auction: {chore_name} on {date}, up to {max_points} {points_name}. Lowest bid wins!",
NOTIF_TYPE_AUCTION_CLOSING: "⏳ {child_name}, bidding on '{chore_name}' closes in {minutes} minutes.",
NOTIF_TYPE_AUCTION_RESULT: "🔨 Auction closed: {result_text}",
NOTIF_TYPE_INSPECTION_STARTED: "🔍 {child_name}, a grown-up is coming to inspect '{chore_name}' before {until}. Keep it looking great for +{bonus} {points_name}!",
NOTIF_TYPE_INSPECTION_PASSED: "🌟 Inspection passed, {child_name}! '{chore_name}' looked great: +{bonus} {points_name}{note_text}",
NOTIF_TYPE_INSPECTION_REMINDER: "🔍 {child_name}'s '{chore_name}' inspection closes in {minutes} minutes — pass or fail it before {until}.",
}
tpl = context.get("message_template") or templates.get(meta.id, "")
# Built-in types read their wording from the locale catalogue in HA's
# language (#1037); a custom message_template is the user's own text.
custom = context.get("message_template")
if not custom:
return notify_strings.render(self.hass, meta.id, context)
try:
return tpl.format_map(_SafeDict(context))
return custom.format_map(_SafeDict(context))
except (ValueError, IndexError, KeyError):
# Malformed user template (e.g. stray '{') — fall back to raw text
_LOGGER.warning("Malformed notification template %r, sending raw", tpl)
return tpl
_LOGGER.warning("Malformed notification template %r, sending raw", custom)
return custom

async def _send_to(
self,
Expand Down Expand Up @@ -587,7 +552,7 @@
push["tag"] = _approval_tag(entry_id)
push["actions"] = self._approval_actions(meta.id, entry_id)
else:
data["message"] = f"{message} {_APPROVE_IN_PANEL_HINT}"
data["message"] = f"{message} {notify_strings.text(self.hass, 'approve_in_panel_hint')}"

if attach_photo:
# "image" renders inline on Android; iOS needs it under
Expand Down Expand Up @@ -647,11 +612,11 @@
if rated:
return [
*({"action": f"{_RATE_ACTION_PREFIX}{n}_{entry_id}", "title": "★" * n} for n in QUALITY_RATINGS),
_reject_action(entry_id),
_reject_action(self.hass, entry_id),
]
return [
{"action": f"TASKMATE_APPROVE_{entry_id}", "title": "Approve"},
_reject_action(entry_id),
{"action": f"TASKMATE_APPROVE_{entry_id}", "title": notify_strings.text(self.hass, "action_approve")},
_reject_action(self.hass, entry_id),
]

async def clear_approval(self, type_id: str, entry_id: str) -> None:
Expand Down Expand Up @@ -712,7 +677,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 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 335, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 335, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 335, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 335, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 335, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 335, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 335, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 335, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 335, in fire self._fire_bus_event(type_id, context, recipients_fired) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1180, in _mock_call return self._execute_mock_call(*args, **kwargs)

Check warning on line 680 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.7/x64/lib/python3.14/asyncio/runners.py", line 128, in run return self._loop.run_until_complete(task) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 707, in run_until_complete self.run_forever() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 678, in run_forever self._run_once() File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/asyncio/base_events.py", line 2049, in _run_once handle._run() File "/opt/hostedtoolcache/Python/3.14.7/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 292, in fire self._fire_bus_event(type_id, context, recipients=[]) File "/home/runner/work/taskmate/taskmate/custom_components/taskmate/coord_notifications.py", line 680, in _fire_bus_event self.hass.bus.async_fire(notification_event_name(type_id), payload) File "/opt/hostedtoolcache/Python/3.14.7/x64/lib/python3.14/unittest/mock.py", line 1176, in __call__ return self._mock_call(*args, **kwargs) File "/opt/hostedtoolcache/Python/3.14.7/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
27 changes: 23 additions & 4 deletions custom_components/taskmate/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util

from . import photos
from . import notify_strings, photos
from .const import DOMAIN
from .coord_assignments import AssignmentsMixin
from .coord_auctions import AuctionsMixin
Expand Down Expand Up @@ -470,6 +470,7 @@ def _async_weekly_digest_check(self, now: datetime) -> None:

async def _async_send_weekly_digest(self) -> None:
"""Build and send the weekly digest to parents (opt-in)."""
await notify_strings.async_load(self.hass)
summary = self._build_weekly_digest()
if not summary:
return
Expand All @@ -492,14 +493,22 @@ def _build_weekly_digest(self) -> str:
done[comp.child_id] = done.get(comp.child_id, 0) + 1
earned[comp.child_id] = earned.get(comp.child_id, 0) + (comp.points_awarded or 0)
pts = self.storage.get_points_name()
lines = [f"• {c.name}: {done.get(c.id, 0)} chores, {earned.get(c.id, 0)} {pts} earned" for c in children]
lines = [
notify_strings.render(
self.hass,
"weekly_digest_line",
{"child_name": c.name, "chores": done.get(c.id, 0), "points": earned.get(c.id, 0), "points_name": pts},
)
for c in children
]
return "\n".join(lines)

async def _async_send_monthly_report(self) -> None:
"""Build and send the previous calendar month's per-child recap (FEAT-14)."""
today = dt_util.now().date()
month_end = today.replace(day=1) - timedelta(days=1)
month_start = month_end.replace(day=1)
await notify_strings.async_load(self.hass)
summary = self._build_monthly_report(month_start, month_end)
if not summary:
return
Expand Down Expand Up @@ -528,8 +537,18 @@ def _build_monthly_report(self, month_start: date, month_end: date) -> str:
earned[comp.child_id] = earned.get(comp.child_id, 0) + (comp.points_awarded or 0)
pts = self.storage.get_points_name()
lines = [
f"• {c.name}: {done.get(c.id, 0)} chores, {earned.get(c.id, 0)} {pts}, "
f"level {getattr(c, 'level', 1)}, best streak {getattr(c, 'best_streak', 0)}"
notify_strings.render(
self.hass,
"monthly_report_line",
{
"child_name": c.name,
"chores": done.get(c.id, 0),
"points": earned.get(c.id, 0),
"points_name": pts,
"level": getattr(c, "level", 1),
"streak": getattr(c, "best_streak", 0),
},
)
for c in children
]
return "\n".join(lines)
Expand Down
Loading
Loading