39 files
This commit is contained in:
@@ -158,6 +158,7 @@ class NotificationCoordinator:
|
||||
|
||||
recipients_fired: list[str] = []
|
||||
message = self._render_template(meta, context)
|
||||
nav_url = self._resolve_nav_url(cfg)
|
||||
|
||||
# Multi-parent routing (#687): thin the PARENT recipients down per the
|
||||
# configured policy. Child routes are never touched — a reminder for a
|
||||
@@ -179,7 +180,7 @@ class NotificationCoordinator:
|
||||
notify_service = self._resolve_notify_service(recipient_id)
|
||||
if not notify_service:
|
||||
continue
|
||||
await self._send_to(notify_service, message, meta, context)
|
||||
await self._send_to(notify_service, message, meta, context, nav_url)
|
||||
recipients_fired.append(recipient_id)
|
||||
|
||||
# NOTE: deliberately no unconditional persistent_notification here.
|
||||
@@ -283,6 +284,7 @@ class NotificationCoordinator:
|
||||
}
|
||||
message = "[TEST] " + self._render_template(meta, ctx)
|
||||
cfg = self.storage.get_notification_config(type_id)
|
||||
nav_url = self._resolve_nav_url(cfg)
|
||||
sent: list[str] = []
|
||||
for recipient_id, route in cfg.routes.items():
|
||||
if not route.enabled:
|
||||
@@ -290,7 +292,7 @@ class NotificationCoordinator:
|
||||
notify_service = self._resolve_notify_service(recipient_id)
|
||||
if not notify_service:
|
||||
continue
|
||||
await self._send_to(notify_service, message, meta, ctx)
|
||||
await self._send_to(notify_service, message, meta, ctx, nav_url)
|
||||
sent.append(recipient_id)
|
||||
await self._fire_persistent_notification(type_id, message)
|
||||
return sent
|
||||
@@ -308,6 +310,13 @@ class NotificationCoordinator:
|
||||
child.quiet_hours_start, child.quiet_hours_end, dt_util.now()
|
||||
)
|
||||
|
||||
def _resolve_nav_url(self, cfg) -> str:
|
||||
"""Tap target for this notification: per-type override, else global default."""
|
||||
per_type = (getattr(cfg, "nav_url", "") or "").strip()
|
||||
if per_type:
|
||||
return per_type
|
||||
return str(self.storage.get_setting("notification_nav_url", "/taskmate") or "").strip()
|
||||
|
||||
def _resolve_notify_service(self, recipient_id: str) -> str:
|
||||
if recipient_id.startswith("child:"):
|
||||
child_id = recipient_id.split(":", 1)[1]
|
||||
@@ -349,7 +358,7 @@ class NotificationCoordinator:
|
||||
|
||||
async def _send_to(
|
||||
self, notify_service: str, message: str,
|
||||
meta: "NotificationTypeMeta", context: dict[str, Any],
|
||||
meta: "NotificationTypeMeta", context: dict[str, Any], nav_url: str = "",
|
||||
) -> None:
|
||||
domain, service = (
|
||||
notify_service.split(".", 1) if "." in notify_service
|
||||
@@ -395,6 +404,13 @@ class NotificationCoordinator:
|
||||
push["image"] = photo_url
|
||||
push["attachment"] = {"url": photo_url, "content-type": "jpeg"}
|
||||
|
||||
# Tap target (#734): open a chosen place when the notification body is
|
||||
# tapped. clickAction=Android, url=iOS — the same value works on both.
|
||||
# Only the mobile app honours these; other backends ignore data, so skip.
|
||||
if nav_url and service.startswith("mobile_app"):
|
||||
push["clickAction"] = nav_url
|
||||
push["url"] = nav_url
|
||||
|
||||
if push:
|
||||
data["data"] = push
|
||||
|
||||
@@ -693,6 +709,15 @@ class NotificationCoordinator:
|
||||
await self.storage.async_save()
|
||||
await self.async_setup_schedules()
|
||||
|
||||
async def set_nav_url(self, type_id: str | None, nav_url: str) -> None:
|
||||
"""Set the tap target — global (type_id falsy) or per notification type."""
|
||||
nav_url = (nav_url or "").strip()
|
||||
if type_id:
|
||||
self.storage.set_notification_nav_url(type_id, nav_url)
|
||||
else:
|
||||
self.storage.set_setting("notification_nav_url", nav_url)
|
||||
await self.storage.async_save()
|
||||
|
||||
def _has_outstanding_chores_today(self, child_id: str) -> bool:
|
||||
"""Returns True if the child has at least one chore assigned today
|
||||
that has no approved/pending completion yet."""
|
||||
|
||||
@@ -17,5 +17,5 @@
|
||||
"iot_class": "calculated",
|
||||
"issue_tracker": "https://github.com/tempus2016/taskmate/issues",
|
||||
"requirements": [],
|
||||
"version": "5.0.1"
|
||||
"version": "5.0.2"
|
||||
}
|
||||
|
||||
@@ -1194,6 +1194,7 @@ class NotificationConfig:
|
||||
type_id: str
|
||||
master_enabled: bool = False
|
||||
routes: dict[str, NotificationRoute] = field(default_factory=dict)
|
||||
nav_url: str = "" # tap target; "" = inherit global default
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> NotificationConfig:
|
||||
@@ -1205,14 +1206,18 @@ class NotificationConfig:
|
||||
rid: NotificationRoute.from_dict(rdata)
|
||||
for rid, rdata in raw_routes.items()
|
||||
},
|
||||
nav_url=data.get("nav_url", "") or "",
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
d: dict[str, Any] = {
|
||||
"type_id": self.type_id,
|
||||
"master_enabled": self.master_enabled,
|
||||
"routes": {rid: r.to_dict() for rid, r in self.routes.items()},
|
||||
}
|
||||
if self.nav_url:
|
||||
d["nav_url"] = self.nav_url
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -728,6 +728,11 @@ class TaskMateStorage:
|
||||
cfg.master_enabled = enabled
|
||||
self._data.setdefault("notification_config", {})[type_id] = cfg.to_dict()
|
||||
|
||||
def set_notification_nav_url(self, type_id: str, nav_url: str) -> None:
|
||||
cfg = self.get_notification_config(type_id)
|
||||
cfg.nav_url = nav_url
|
||||
self._data.setdefault("notification_config", {})[type_id] = cfg.to_dict()
|
||||
|
||||
def set_notification_route(
|
||||
self, type_id: str, recipient_id: str, route: NotificationRoute
|
||||
) -> None:
|
||||
|
||||
@@ -154,6 +154,7 @@ WS_NOTIF_LIST_NOTIFY: Final = "taskmate/notifications/list_notify_service
|
||||
WS_NOTIF_SET_STREAK_CUTOFF: Final = "taskmate/notifications/set_streak_cutoff"
|
||||
WS_NOTIF_SET_ESCALATION: Final = "taskmate/notifications/set_escalation"
|
||||
WS_NOTIF_SEND_TEST: Final = "taskmate/notifications/send_test"
|
||||
WS_NOTIF_SET_NAV_URL: Final = "taskmate/notifications/set_nav_url"
|
||||
|
||||
# Calendar ICS feed (FEAT-10)
|
||||
WS_CAL_GET_URL: Final = "taskmate/calendar/get_ics_url"
|
||||
@@ -1909,6 +1910,7 @@ async def ws_notif_get_state(hass, connection, msg, coordinator):
|
||||
"streak_at_risk_cutoff_time": c.storage.get_streak_at_risk_cutoff(),
|
||||
"mandatory_escalation_reminder_minutes": c.storage.get_escalation_reminder_minutes(),
|
||||
"mandatory_escalation_parent_minutes": c.storage.get_escalation_parent_minutes(),
|
||||
"notification_nav_url": c.storage.get_setting("notification_nav_url", "/taskmate"),
|
||||
},
|
||||
}
|
||||
connection.send_result(msg["id"], state)
|
||||
@@ -2135,6 +2137,18 @@ async def ws_notif_send_test(hass, connection, msg, coordinator):
|
||||
connection.send_result(msg["id"], {"sent": sent})
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
vol.Required("type"): WS_NOTIF_SET_NAV_URL,
|
||||
vol.Optional("type_id"): vol.Any(str, None),
|
||||
vol.Required("nav_url"): vol.All(str, vol.Length(max=200)),
|
||||
})
|
||||
@websocket_api.async_response
|
||||
@_admin_only
|
||||
async def ws_notif_set_nav_url(hass, connection, msg, coordinator):
|
||||
await coordinator.notifications.set_nav_url(msg.get("type_id"), msg["nav_url"])
|
||||
connection.send_result(msg["id"], {"ok": True})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar ICS feed (FEAT-10)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -2323,7 +2337,7 @@ _COMMANDS = (
|
||||
ws_notif_upsert_parent, ws_notif_delete_parent,
|
||||
ws_notif_upsert_custom, ws_notif_delete_custom,
|
||||
ws_notif_list_notify, ws_notif_set_streak_cutoff, ws_notif_send_test,
|
||||
ws_notif_set_escalation,
|
||||
ws_notif_set_escalation, ws_notif_set_nav_url,
|
||||
ws_cal_get_url, ws_cal_regen_token,
|
||||
)
|
||||
|
||||
|
||||
@@ -866,6 +866,9 @@
|
||||
"panel.notif_section_recipients": "Empfänger",
|
||||
"panel.notif_section_recipients_desc": "Weise jedem Kind und Elternteil einen Home Assistant Benachrichtigungsdienst zu. Leer lassen, um Benachrichtigungen für diese Person zu überspringen.",
|
||||
"panel.notif_streak_cutoff_label": "Frist",
|
||||
"panel.notif_nav_url_global_label": "Beim Tippen öffnen",
|
||||
"panel.notif_nav_url_global_hint": "Wohin eine angetippte Benachrichtigung führt: ein Pfad wie /taskmate oder /lovelace/parents, eine vollständige URL oder noAction. Gilt für alle Benachrichtigungen, sofern unten nicht pro Typ überschrieben.",
|
||||
"panel.notif_nav_url_row_placeholder": "Nutzt globalen Standard",
|
||||
"panel.notif_tab_title": "Benachrichtigungen",
|
||||
"panel.template_assignment_mode_label": "Zuweisungsmodus",
|
||||
"panel.template_builtin": "Integriert",
|
||||
|
||||
@@ -916,6 +916,9 @@
|
||||
"panel.notif_section_recipients": "Recipients",
|
||||
"panel.notif_section_recipients_desc": "Map each child and parent to a Home Assistant notify service. Leave blank to skip notifications for that person.",
|
||||
"panel.notif_streak_cutoff_label": "Cutoff",
|
||||
"panel.notif_nav_url_global_label": "When tapped, open",
|
||||
"panel.notif_nav_url_global_hint": "Where a tapped notification opens: a path like /taskmate or /lovelace/parents, a full URL, or noAction. Applies to all notifications unless overridden per type below.",
|
||||
"panel.notif_nav_url_row_placeholder": "Uses global default",
|
||||
"panel.notif_send_test": "Send test",
|
||||
"panel.notif_test_sent": "Test sent to {count} recipient(s) + persistent notification",
|
||||
"panel.notif_tab_title": "Notifications",
|
||||
|
||||
@@ -916,6 +916,9 @@
|
||||
"panel.notif_section_recipients": "Recipients",
|
||||
"panel.notif_section_recipients_desc": "Map each child and parent to a Home Assistant notify service. Leave blank to skip notifications for that person.",
|
||||
"panel.notif_streak_cutoff_label": "Cutoff",
|
||||
"panel.notif_nav_url_global_label": "When tapped, open",
|
||||
"panel.notif_nav_url_global_hint": "Where a tapped notification opens: a path like /taskmate or /lovelace/parents, a full URL, or noAction. Applies to all notifications unless overridden per type below.",
|
||||
"panel.notif_nav_url_row_placeholder": "Uses global default",
|
||||
"panel.notif_send_test": "Send test",
|
||||
"panel.notif_test_sent": "Test sent to {count} recipient(s) + persistent notification",
|
||||
"panel.notif_tab_title": "Notifications",
|
||||
|
||||
@@ -866,6 +866,9 @@
|
||||
"panel.notif_section_recipients": "Destinataires",
|
||||
"panel.notif_section_recipients_desc": "Associez chaque enfant et parent à un service de notification Home Assistant. Laisser vide pour ignorer les notifications pour cette personne.",
|
||||
"panel.notif_streak_cutoff_label": "Limite",
|
||||
"panel.notif_nav_url_global_label": "Au toucher, ouvrir",
|
||||
"panel.notif_nav_url_global_hint": "Où mène une notification touchée : un chemin comme /taskmate ou /lovelace/parents, une URL complète ou noAction. S'applique à toutes les notifications sauf remplacement par type ci-dessous.",
|
||||
"panel.notif_nav_url_row_placeholder": "Utilise la valeur globale",
|
||||
"panel.notif_tab_title": "Notifications",
|
||||
"panel.template_assignment_mode_label": "Mode d'assignation",
|
||||
"panel.template_builtin": "Intégré",
|
||||
|
||||
@@ -866,6 +866,9 @@
|
||||
"panel.notif_section_recipients": "Mottakere",
|
||||
"panel.notif_section_recipients_desc": "Koble hvert barn og foresatt til en Home Assistant-varslingstjeneste. La stå tomt for å hoppe over varsler for den personen.",
|
||||
"panel.notif_streak_cutoff_label": "Grense",
|
||||
"panel.notif_nav_url_global_label": "Ved trykk, åpne",
|
||||
"panel.notif_nav_url_global_hint": "Hvor et trykk på et varsel åpner: en sti som /taskmate eller /lovelace/parents, en full URL, eller noAction. Gjelder alle varsler med mindre det overstyres per type nedenfor.",
|
||||
"panel.notif_nav_url_row_placeholder": "Bruker global standard",
|
||||
"panel.notif_tab_title": "Varsler",
|
||||
"panel.template_assignment_mode_label": "Tildelingsmodus",
|
||||
"panel.template_builtin": "Innebygd",
|
||||
|
||||
@@ -866,6 +866,9 @@
|
||||
"panel.notif_section_recipients": "Mottakarar",
|
||||
"panel.notif_section_recipients_desc": "Kople kvart born og kvar føresett til ein Home Assistant-varslingsteneste. La stå tomt for å hoppa over varsel for den personen.",
|
||||
"panel.notif_streak_cutoff_label": "Grense",
|
||||
"panel.notif_nav_url_global_label": "Ved trykk, opne",
|
||||
"panel.notif_nav_url_global_hint": "Kvar eit trykk på eit varsel opnar: ein sti som /taskmate eller /lovelace/parents, ein full URL, eller noAction. Gjeld alle varsel med mindre det vert overstyrt per type nedanfor.",
|
||||
"panel.notif_nav_url_row_placeholder": "Brukar global standard",
|
||||
"panel.notif_tab_title": "Varsel",
|
||||
"panel.template_assignment_mode_label": "Tildelingsmodus",
|
||||
"panel.template_builtin": "Innebygd",
|
||||
|
||||
@@ -866,6 +866,9 @@
|
||||
"panel.notif_section_recipients": "Destinatários",
|
||||
"panel.notif_section_recipients_desc": "Associe cada criança e responsável a um serviço de notificação do Home Assistant. Deixe em branco para pular notificações para essa pessoa.",
|
||||
"panel.notif_streak_cutoff_label": "Limite",
|
||||
"panel.notif_nav_url_global_label": "Ao tocar, abrir",
|
||||
"panel.notif_nav_url_global_hint": "Para onde uma notificação tocada abre: um caminho como /taskmate ou /lovelace/parents, uma URL completa ou noAction. Aplica-se a todas as notificações, exceto se substituída por tipo abaixo.",
|
||||
"panel.notif_nav_url_row_placeholder": "Usa o padrão global",
|
||||
"panel.notif_tab_title": "Notificações",
|
||||
"panel.template_assignment_mode_label": "Modo de atribuição",
|
||||
"panel.template_builtin": "Integrado",
|
||||
|
||||
@@ -871,6 +871,9 @@
|
||||
"panel.notif_section_recipients": "Destinatários",
|
||||
"panel.notif_section_recipients_desc": "Associe cada criança e encarregado a um serviço de notificação do Home Assistant. Deixe em branco para ignorar notificações para essa pessoa.",
|
||||
"panel.notif_streak_cutoff_label": "Limite",
|
||||
"panel.notif_nav_url_global_label": "Ao tocar, abrir",
|
||||
"panel.notif_nav_url_global_hint": "Para onde uma notificação tocada abre: um caminho como /taskmate ou /lovelace/parents, um URL completo ou noAction. Aplica-se a todas as notificações, salvo substituição por tipo abaixo.",
|
||||
"panel.notif_nav_url_row_placeholder": "Usa o padrão global",
|
||||
"panel.notif_tab_title": "Notificações",
|
||||
"panel.template_assignment_mode_label": "Modo de atribuição",
|
||||
"panel.template_builtin": "Incorporado",
|
||||
|
||||
@@ -676,6 +676,8 @@ class TaskMatePanel extends HTMLElement {
|
||||
if (act === "notif-set-parent-notify"){ /* handled in _onChange */ return; }
|
||||
if (act === "notif-rename-parent") { /* handled in _onChange */ return; }
|
||||
if (act === "notif-set-streak-cutoff"){ /* handled in _onChange */ return; }
|
||||
if (act === "notif-set-nav-url-global"){ /* handled in _onChange */ return; }
|
||||
if (act === "notif-set-nav-url-type") { /* handled in _onChange */ return; }
|
||||
if (act === "notif-add-parent") { this._notifAddParent(); return; }
|
||||
if (act === "notif-delete-parent") { this._notifDeleteParent(t.dataset.parentId); return; }
|
||||
if (act === "notif-add-custom") { this._notifAddCustom(); return; }
|
||||
@@ -915,6 +917,14 @@ class TaskMatePanel extends HTMLElement {
|
||||
this._notifSetStreakCutoff(t.value);
|
||||
return;
|
||||
}
|
||||
if (t.dataset.act === "notif-set-nav-url-global") {
|
||||
this._notifSetNavUrl(null, t.value);
|
||||
return;
|
||||
}
|
||||
if (t.dataset.act === "notif-set-nav-url-type") {
|
||||
this._notifSetNavUrl(t.dataset.typeId, t.value);
|
||||
return;
|
||||
}
|
||||
if (t.dataset.act === "notif-set-escalation") {
|
||||
this._notifSetEscalation(t.dataset.escField, t.value);
|
||||
return;
|
||||
@@ -2100,6 +2110,13 @@ class TaskMatePanel extends HTMLElement {
|
||||
await this._fetchState();
|
||||
}
|
||||
|
||||
async _notifSetNavUrl(typeId, navUrl) {
|
||||
const payload = { type: "taskmate/notifications/set_nav_url", nav_url: navUrl || "" };
|
||||
if (typeId) payload.type_id = typeId;
|
||||
await this._callWS(payload);
|
||||
await this._fetchState();
|
||||
}
|
||||
|
||||
async _notifSetEscalation(field, value) {
|
||||
const s = (this._notifState && this._notifState.settings) || {};
|
||||
const cur = {
|
||||
@@ -4578,6 +4595,13 @@ class TaskMatePanel extends HTMLElement {
|
||||
<div class="tm-card" style="margin-bottom:16px">
|
||||
<h3>${this._t("panel.notif_section_matrix")}</h3>
|
||||
<p class="tm-meta">${this._t("panel.notif_section_matrix_desc")}</p>
|
||||
<div class="tm-meta" style="margin:4px 0 12px;display:flex;flex-wrap:wrap;align-items:center;gap:8px">
|
||||
<label style="font-weight:500">${this._t("panel.notif_nav_url_global_label")}</label>
|
||||
<input type="text" class="tm-input" style="flex:1;min-width:180px"
|
||||
value="${this._esc((ns.settings && ns.settings.notification_nav_url) || "")}"
|
||||
data-act="notif-set-nav-url-global" placeholder="/taskmate">
|
||||
<div class="tm-meta" style="flex-basis:100%">${this._t("panel.notif_nav_url_global_hint")}</div>
|
||||
</div>
|
||||
<div class="tm-table-wrap">
|
||||
<table class="tm-table">
|
||||
<thead>
|
||||
@@ -4630,6 +4654,12 @@ class TaskMatePanel extends HTMLElement {
|
||||
<ha-icon icon="mdi:send" style="--mdc-icon-size:14px"></ha-icon> ${this._t("panel.notif_send_test")}
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-top:6px;display:flex;align-items:center;gap:8px">
|
||||
<input type="text" class="tm-notif-time-input" style="width:150px"
|
||||
value="${this._esc(c.nav_url || "")}"
|
||||
data-act="notif-set-nav-url-type" data-type-id="${this._esc(t.id)}"
|
||||
placeholder="${this._t("panel.notif_nav_url_row_placeholder")}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user