This commit is contained in:
Home Assistant Version Control
2026-08-25 14:19:39 +00:00
parent 45bd102a6a
commit e8c2498cf8
17 changed files with 194 additions and 22 deletions
+6
View File
@@ -228,3 +228,9 @@ NOTIF_TYPE_FAMILY_GOAL_REACHED: Final = "family_goal_reached"
# Default notification tap target. Must match PANEL_URL_PATH in panel.py —
# a bare /taskmate is the static-files prefix and returns 403, not the panel.
DEFAULT_NOTIFICATION_NAV_URL: Final = "/taskmate-admin"
# Default notification group (#811). The HA companion app stacks notifications
# that share this key, so TaskMate's alerts collapse into one bundle instead of
# scattering through the rest of the phone's HA notifications. Applied by
# default — set it to "" in the panel to turn grouping off.
DEFAULT_NOTIFICATION_GROUP: Final = "taskmate"
@@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_track_time_change
from .const import (
DEFAULT_NOTIFICATION_GROUP,
DEFAULT_NOTIFICATION_NAV_URL,
NOTIF_TYPE_ALL_CHORES_DONE,
NOTIF_TYPE_BADGE_EARNED,
@@ -106,6 +107,25 @@ def _validate_nav_url(value: str) -> str:
raise ValueError("nav_url must be a /path, an http(s) URL, or noAction")
def _validate_group(value: str) -> str:
"""Normalise and vet a notification group key.
The value is an opaque bundling key for the companion app, so anything
printable will do — but control characters would land inside the JSON
payload the app parses, and an unbounded string is just storage bloat.
"""
value = (value or "").strip()
if not value:
return ""
if " " in value:
raise ValueError("group must not contain spaces (try family-chores)")
if any(ord(c) <= 32 or ord(c) == 127 for c in value):
raise ValueError("group must not contain control characters")
if len(value) > 64:
raise ValueError("group must be 64 characters or fewer")
return value
def _parse_hhmm(value: str) -> tuple[int, int] | None:
"""Parse "HH:MM" into (hour, minute); None if blank/malformed."""
if not value:
@@ -180,6 +200,7 @@ class NotificationCoordinator:
recipients_fired: list[str] = []
message = self._render_template(meta, context)
nav_url = self._resolve_nav_url(cfg)
group = self._resolve_group(cfg)
# Multi-parent routing (#687): thin the PARENT recipients down per the
# configured policy. Child routes are never touched — a reminder for a
@@ -201,7 +222,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, nav_url)
await self._send_to(notify_service, message, meta, context, nav_url, group)
recipients_fired.append(recipient_id)
# NOTE: deliberately no unconditional persistent_notification here.
@@ -310,6 +331,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)
group = self._resolve_group(cfg)
sent: list[str] = []
for recipient_id, route in cfg.routes.items():
if not route.enabled:
@@ -317,7 +339,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, nav_url)
await self._send_to(notify_service, message, meta, ctx, nav_url, group)
sent.append(recipient_id)
await self._fire_persistent_notification(type_id, message)
return sent
@@ -341,6 +363,17 @@ class NotificationCoordinator:
return per_type
return str(self.storage.get_setting("notification_nav_url", DEFAULT_NOTIFICATION_NAV_URL) or "").strip()
def _resolve_group(self, cfg=None) -> str:
"""Group key for this notification: per-type override, else global default.
Called with no cfg for dispatch paths that have no notification type of
their own (custom reminders), which always use the global value.
"""
per_type = (getattr(cfg, "group", "") or "").strip()
if per_type:
return per_type
return str(self.storage.get_setting("notification_group", DEFAULT_NOTIFICATION_GROUP) or "").strip()
def _resolve_notify_service(self, recipient_id: str) -> str:
if recipient_id.startswith("child:"):
child_id = recipient_id.split(":", 1)[1]
@@ -387,6 +420,7 @@ class NotificationCoordinator:
meta: "NotificationTypeMeta",
context: dict[str, Any],
nav_url: str = "",
group: str = "",
) -> None:
domain, service = notify_service.split(".", 1) if "." in notify_service else ("notify", notify_service)
if domain != "notify":
@@ -436,6 +470,14 @@ class NotificationCoordinator:
push["clickAction"] = nav_url
push["url"] = nav_url
# Grouping (#811): stack TaskMate's notifications into one bundle so
# they don't scatter through the phone's other HA alerts. Android reads
# data.group; iOS threads on push.thread-id, so send both — same value,
# and each platform ignores the other's key.
if group and service.startswith("mobile_app"):
push["group"] = group
push["push"] = {"thread-id": group}
if push:
data["data"] = push
@@ -665,10 +707,17 @@ class NotificationCoordinator:
)
message = n.message_template
service_name = notify_service.split(".", 1)[1] if "." in notify_service else notify_service
payload: dict[str, Any] = {"title": "TaskMate", "message": message}
# Custom reminders bypass _send_to, so apply the group here too
# (#811) — otherwise they'd be the one kind of TaskMate alert
# that still lands outside the bundle.
group = self._resolve_group()
if group and service_name.startswith("mobile_app"):
payload["data"] = {"group": group, "push": {"thread-id": group}}
await self.hass.services.async_call(
"notify",
service_name,
{"title": "TaskMate", "message": message},
payload,
blocking=False,
)
self.hass.bus.async_fire(
@@ -753,6 +802,17 @@ class NotificationCoordinator:
self.storage.set_setting("notification_nav_url", nav_url)
await self.storage.async_save()
async def set_group(self, type_id: str | None, group: str) -> None:
"""Set the group key — global (type_id falsy) or per notification type."""
group = _validate_group(group)
if type_id:
if type_id not in NOTIFICATION_TYPES_BY_ID:
raise ValueError(f"Unknown notification type {type_id}")
self.storage.set_notification_group(type_id, group)
else:
self.storage.set_setting("notification_group", group)
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."""
+1 -1
View File
@@ -17,5 +17,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/tempus2016/taskmate/issues",
"requirements": [],
"version": "5.2.0"
"version": "5.3.0"
}
+9 -1
View File
@@ -1219,15 +1219,21 @@ class NotificationConfig:
master_enabled: bool = False
routes: dict[str, NotificationRoute] = field(default_factory=dict)
nav_url: str = "" # tap target; "" = inherit global default
group: str = "" # notification group/thread; "" = inherit global default
@classmethod
def from_dict(cls, data: dict[str, Any]) -> NotificationConfig:
raw_routes = data.get("routes", {}) or {}
# Imported backups aren't field-validated, so non-string values must
# fall back to "" here or dispatch crashes on .strip() later.
raw_nav_url = data.get("nav_url")
raw_group = data.get("group")
return cls(
type_id=data.get("type_id", ""),
master_enabled=bool(data.get("master_enabled", False)),
routes={rid: NotificationRoute.from_dict(rdata) for rid, rdata in raw_routes.items()},
nav_url=data.get("nav_url", "") or "",
nav_url=raw_nav_url if isinstance(raw_nav_url, str) else "",
group=raw_group if isinstance(raw_group, str) else "",
)
def to_dict(self) -> dict[str, Any]:
@@ -1238,6 +1244,8 @@ class NotificationConfig:
}
if self.nav_url:
d["nav_url"] = self.nav_url
if self.group:
d["group"] = self.group
return d
+20
View File
@@ -773,6 +773,10 @@ class TaskMateOverallStatsSensor(_CachedAttrsSensor):
sensors so this entity stays well under the 16KB recorder limit.
"""
# The per-child summary grows with the family; the scalars stay recorded
# so history/statistics on them keep working (#817).
_unrecorded_attributes = frozenset({"children", "vacation_periods", "season_champions"})
def __init__(
self,
coordinator: TaskMateCoordinator,
@@ -869,6 +873,8 @@ class TaskMateOverallStatsSensor(_CachedAttrsSensor):
class TaskMateChoresSensor(_CachedAttrsSensor):
"""Chores catalog + today's completions."""
_unrecorded_attributes = frozenset({"chores", "todays_completions", "task_groups", "active_timed_sessions"})
def __init__(
self,
coordinator: TaskMateCoordinator,
@@ -917,6 +923,8 @@ class TaskMateChoreAvailabilitySensor(_CachedAttrsSensor):
The map is `{chore_id: {child_id: bool}}`.
"""
_unrecorded_attributes = frozenset({"chore_availability"})
def __init__(
self,
coordinator: TaskMateCoordinator,
@@ -949,6 +957,8 @@ class TaskMateChoreAvailabilitySensor(_CachedAttrsSensor):
class TaskMateRewardsSensor(_CachedAttrsSensor):
"""Rewards catalog + pending claims + pool allocations."""
_unrecorded_attributes = frozenset({"rewards", "pending_reward_claims", "pool_allocations"})
def __init__(
self,
coordinator: TaskMateCoordinator,
@@ -978,6 +988,10 @@ class TaskMateRewardsSensor(_CachedAttrsSensor):
class TaskMateActivitySensor(_CachedAttrsSensor):
"""Recent completions + recent points/reward transactions."""
_unrecorded_attributes = frozenset(
{"recent_completions", "recent_transactions", "career_score_history", "photo_gallery"}
)
def __init__(
self,
coordinator: TaskMateCoordinator,
@@ -1012,6 +1026,8 @@ class TaskMateActivitySensor(_CachedAttrsSensor):
class TaskMateIncentivesSensor(_CachedAttrsSensor):
"""Penalties + bonuses catalogue."""
_unrecorded_attributes = frozenset({"penalties", "bonuses"})
def __init__(
self,
coordinator: TaskMateCoordinator,
@@ -1094,6 +1110,7 @@ class ChildStatsSensor(TaskMateBaseSensor):
"""Sensor for a child's statistics."""
_attr_translation_key = "child_stats"
_unrecorded_attributes = frozenset({"assigned_chores", "chore_order"})
def __init__(
self,
@@ -1171,6 +1188,7 @@ class ChildBadgesSensor(TaskMateBaseSensor):
_attr_icon = "mdi:trophy-award"
_attr_translation_key = "child_badges"
_unrecorded_attributes = frozenset({"earned", "available"})
def __init__(
self,
@@ -1261,6 +1279,8 @@ class ChildBadgesSensor(TaskMateBaseSensor):
class PendingApprovalsSensor(TaskMateBaseSensor):
"""Sensor for pending approvals."""
_unrecorded_attributes = frozenset({"chore_completions", "reward_claims", "mandatory_misses"})
def __init__(
self,
coordinator: TaskMateCoordinator,
+5
View File
@@ -727,6 +727,11 @@ class TaskMateStorage:
cfg.nav_url = nav_url
self._data.setdefault("notification_config", {})[type_id] = cfg.to_dict()
def set_notification_group(self, type_id: str, group: str) -> None:
cfg = self.get_notification_config(type_id)
cfg.group = group
self._data.setdefault("notification_config", {})[type_id] = cfg.to_dict()
def set_notification_route(self, type_id: str, recipient_id: str, route: NotificationRoute) -> None:
cfg = self.get_notification_config(type_id)
cfg.routes[recipient_id] = route
+18
View File
@@ -60,6 +60,7 @@ from homeassistant.core import HomeAssistant
from . import images, photos
from .const import (
ASSIGNMENT_MODES,
DEFAULT_NOTIFICATION_GROUP,
DEFAULT_NOTIFICATION_NAV_URL,
DEFAULT_TIME_PERIODS,
DIFFICULTY_TIERS,
@@ -165,6 +166,7 @@ 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"
WS_NOTIF_SET_GROUP: Final = "taskmate/notifications/set_group"
# Calendar ICS feed (FEAT-10)
WS_CAL_GET_URL: Final = "taskmate/calendar/get_ics_url"
@@ -2178,6 +2180,7 @@ async def ws_notif_get_state(hass, connection, msg, coordinator):
"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", DEFAULT_NOTIFICATION_NAV_URL),
"notification_group": c.storage.get_setting("notification_group", DEFAULT_NOTIFICATION_GROUP),
},
}
connection.send_result(msg["id"], state)
@@ -2438,6 +2441,20 @@ async def ws_notif_set_nav_url(hass, connection, msg, coordinator):
connection.send_result(msg["id"], {"ok": True})
@websocket_api.websocket_command(
{
vol.Required("type"): WS_NOTIF_SET_GROUP,
vol.Optional("type_id"): vol.Any(str, None),
vol.Required("group"): vol.All(str, vol.Length(max=64)),
}
)
@websocket_api.async_response
@_admin_only
async def ws_notif_set_group(hass, connection, msg, coordinator):
await coordinator.notifications.set_group(msg.get("type_id"), msg["group"])
connection.send_result(msg["id"], {"ok": True})
# ---------------------------------------------------------------------------
# Calendar ICS feed (FEAT-10)
# ---------------------------------------------------------------------------
@@ -2701,6 +2718,7 @@ _COMMANDS = (
ws_notif_send_test,
ws_notif_set_escalation,
ws_notif_set_nav_url,
ws_notif_set_group,
ws_cal_get_url,
ws_cal_regen_token,
)
@@ -881,6 +881,9 @@
"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-admin 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_group_global_label": "Benachrichtigungen gruppieren als",
"panel.notif_group_global_hint": "Benachrichtigungen mit demselben Namen werden auf dem Telefon zusammengefasst, statt sich unter die anderen Home-Assistant-Meldungen zu mischen. Gilt für alle Benachrichtigungen, sofern unten nicht pro Typ überschrieben. Leer lassen, um die Gruppierung zu deaktivieren.",
"panel.notif_group_row_placeholder": "Gruppe: globaler Standard",
"panel.notif_tab_title": "Benachrichtigungen",
"panel.template_assignment_mode_label": "Zuweisungsmodus",
"panel.template_builtin": "Integriert",
@@ -931,6 +931,9 @@
"panel.notif_nav_url_global_label": "When tapped, open",
"panel.notif_nav_url_global_hint": "Where a tapped notification opens: a path like /taskmate-admin 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_group_global_label": "Group notifications as",
"panel.notif_group_global_hint": "Notifications sharing this name stack together on the phone instead of scattering through your other Home Assistant alerts. Applies to all notifications unless overridden per type below. Leave blank to turn grouping off.",
"panel.notif_group_row_placeholder": "Group: 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",
@@ -931,6 +931,9 @@
"panel.notif_nav_url_global_label": "When tapped, open",
"panel.notif_nav_url_global_hint": "Where a tapped notification opens: a path like /taskmate-admin 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_group_global_label": "Group notifications as",
"panel.notif_group_global_hint": "Notifications sharing this name stack together on the phone instead of scattering through your other Home Assistant alerts. Applies to all notifications unless overridden per type below. Leave blank to turn grouping off.",
"panel.notif_group_row_placeholder": "Group: 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",
@@ -881,6 +881,9 @@
"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-admin 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_group_global_label": "Grouper les notifications sous",
"panel.notif_group_global_hint": "Les notifications portant ce nom sont regroupées sur le téléphone au lieu de se disperser parmi vos autres alertes Home Assistant. S'applique à toutes les notifications sauf remplacement par type ci-dessous. Laisser vide pour désactiver le regroupement.",
"panel.notif_group_row_placeholder": "Groupe : valeur globale",
"panel.notif_tab_title": "Notifications",
"panel.template_assignment_mode_label": "Mode d'assignation",
"panel.template_builtin": "Intégré",
@@ -881,6 +881,9 @@
"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-admin 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_group_global_label": "Grupper varsler som",
"panel.notif_group_global_hint": "Varsler med samme navn samles på telefonen i stedet for å spres blant de andre Home Assistant-varslene dine. Gjelder alle varsler med mindre det overstyres per type nedenfor. La stå tomt for å slå av gruppering.",
"panel.notif_group_row_placeholder": "Gruppe: global standard",
"panel.notif_tab_title": "Varsler",
"panel.template_assignment_mode_label": "Tildelingsmodus",
"panel.template_builtin": "Innebygd",
@@ -881,6 +881,9 @@
"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-admin 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_group_global_label": "Grupper varsel som",
"panel.notif_group_global_hint": "Varsel med same namn vert samla på telefonen i staden for å spreie seg blant dei andre Home Assistant-varsla dine. Gjeld alle varsel med mindre det vert overstyrt per type nedanfor. La stå tomt for å slå av grupperinga.",
"panel.notif_group_row_placeholder": "Gruppe: global standard",
"panel.notif_tab_title": "Varsel",
"panel.template_assignment_mode_label": "Tildelingsmodus",
"panel.template_builtin": "Innebygd",
@@ -881,6 +881,9 @@
"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-admin 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_group_global_label": "Agrupar notificações como",
"panel.notif_group_global_hint": "As notificações com este nome são agrupadas no celular em vez de se espalharem pelos outros alertas do Home Assistant. Aplica-se a todas as notificações, exceto se substituída por tipo abaixo. Deixe em branco para desativar o agrupamento.",
"panel.notif_group_row_placeholder": "Grupo: padrão global",
"panel.notif_tab_title": "Notificações",
"panel.template_assignment_mode_label": "Modo de atribuição",
"panel.template_builtin": "Integrado",
@@ -886,6 +886,9 @@
"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-admin 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_group_global_label": "Agrupar notificações como",
"panel.notif_group_global_hint": "As notificações com este nome são agrupadas no telemóvel em vez de se dispersarem pelos outros alertas do Home Assistant. Aplica-se a todas as notificações, salvo substituição por tipo abaixo. Deixe em branco para desativar o agrupamento.",
"panel.notif_group_row_placeholder": "Grupo: padrão global",
"panel.notif_tab_title": "Notificações",
"panel.template_assignment_mode_label": "Modo de atribuição",
"panel.template_builtin": "Incorporado",
@@ -698,6 +698,8 @@ class TaskMatePanel extends HTMLElement {
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-set-group-global") { /* handled in _onChange */ return; }
if (act === "notif-set-group-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; }
@@ -945,6 +947,14 @@ class TaskMatePanel extends HTMLElement {
this._notifSetNavUrl(t.dataset.typeId, t.value);
return;
}
if (t.dataset.act === "notif-set-group-global") {
this._notifSetGroup(null, t.value);
return;
}
if (t.dataset.act === "notif-set-group-type") {
this._notifSetGroup(t.dataset.typeId, t.value);
return;
}
if (t.dataset.act === "notif-set-escalation") {
this._notifSetEscalation(t.dataset.escField, t.value);
return;
@@ -2236,7 +2246,16 @@ class TaskMatePanel extends HTMLElement {
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);
const { ok, err } = await this._callWS(payload);
if (!ok) this._showToast("err", this._t("panel.toast_save_failed", { error: err }));
await this._fetchState();
}
async _notifSetGroup(typeId, group) {
const payload = { type: "taskmate/notifications/set_group", group: group || "" };
if (typeId) payload.type_id = typeId;
const { ok, err } = await this._callWS(payload);
if (!ok) this._showToast("err", this._t("panel.toast_save_failed", { error: err }));
await this._fetchState();
}
@@ -4762,6 +4781,13 @@ class TaskMatePanel extends HTMLElement {
data-act="notif-set-nav-url-global" placeholder="/taskmate-admin">
<div class="tm-meta" style="flex-basis:100%">${this._t("panel.notif_nav_url_global_hint")}</div>
</div>
<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_group_global_label")}</label>
<input type="text" class="tm-input" style="flex:1;min-width:180px" maxlength="64"
value="${this._esc((ns.settings && ns.settings.notification_group) || "")}"
data-act="notif-set-group-global" placeholder="taskmate">
<div class="tm-meta" style="flex-basis:100%">${this._t("panel.notif_group_global_hint")}</div>
</div>
<div class="tm-table-wrap">
<table class="tm-table">
<thead>
@@ -4818,7 +4844,12 @@ class TaskMatePanel extends HTMLElement {
<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")}">
placeholder="${this._esc(this._t("panel.notif_nav_url_row_placeholder"))}">
<input type="text" class="tm-notif-time-input" style="width:150px" maxlength="64"
value="${this._esc(c.group || "")}"
data-act="notif-set-group-type" data-type-id="${this._esc(t.id)}"
title="${this._esc(this._t("panel.notif_group_global_label"))}"
placeholder="${this._esc(this._t("panel.notif_group_row_placeholder"))}">
</div>
</div>
</div>
+15 -15
View File
@@ -4,7 +4,7 @@
"state": "ON",
"led_brightness": 100,
"countdown_to_turn_off": 0,
"voltage": 122.2,
"voltage": 121.7,
"countdown_to_turn_on": 0,
"ac_frequency": 60,
"power_factor": 0.14,
@@ -27,7 +27,7 @@
"led_brightness": 100,
"countdown_to_turn_off": 0,
"countdown_to_turn_on": 0,
"power": 2,
"power": 1.9,
"current": 0.1,
"energy": 32.7,
"power_factor": 0.17,
@@ -47,7 +47,7 @@
"countdown_to_turn_off": 0,
"voltage": 121.5,
"countdown_to_turn_on": 0,
"energy": 66.04,
"energy": 66.05,
"power_factor": 0.88,
"ac_frequency": 60,
"update": {
@@ -58,7 +58,7 @@
"latest_release_notes": null
},
"linkquality": 138,
"power": 86.1,
"power": 82.8,
"current": 0.82,
"power_on_behavior": "on"
},
@@ -74,13 +74,13 @@
"led_brightness": 100,
"countdown_to_turn_off": 0,
"countdown_to_turn_on": 0,
"voltage": 120.5,
"voltage": 121,
"state": "ON",
"ac_frequency": 60,
"energy": 133.62,
"power": 96.6,
"current": 0.85,
"power_factor": 0.94,
"energy": 133.64,
"power": 0.8,
"current": 0.02,
"power_factor": 0.35,
"update": {
"state": "idle",
"installed_version": 268513381,
@@ -95,13 +95,13 @@
"led_brightness": 100,
"countdown_to_turn_off": 0,
"countdown_to_turn_on": 0,
"voltage": 122,
"energy": 63.98,
"voltage": 121.5,
"energy": 63.99,
"state": "ON",
"power": 19.9,
"current": 0.33,
"power": 18,
"current": 0.31,
"ac_frequency": 60,
"power_factor": 0.59,
"power_factor": 0.53,
"update": {
"state": "idle",
"installed_version": 268513381,
@@ -119,7 +119,7 @@
"energy": 8.8,
"current": 0.01,
"power": 0.2,
"power_factor": 0.22,
"power_factor": 0.11,
"linkquality": 109,
"update": {
"state": "idle",