17 files
This commit is contained in:
@@ -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 —
|
# 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.
|
# a bare /taskmate is the static-files prefix and returns 403, not the panel.
|
||||||
DEFAULT_NOTIFICATION_NAV_URL: Final = "/taskmate-admin"
|
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 homeassistant.helpers.event import async_track_time_change
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
DEFAULT_NOTIFICATION_GROUP,
|
||||||
DEFAULT_NOTIFICATION_NAV_URL,
|
DEFAULT_NOTIFICATION_NAV_URL,
|
||||||
NOTIF_TYPE_ALL_CHORES_DONE,
|
NOTIF_TYPE_ALL_CHORES_DONE,
|
||||||
NOTIF_TYPE_BADGE_EARNED,
|
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")
|
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:
|
def _parse_hhmm(value: str) -> tuple[int, int] | None:
|
||||||
"""Parse "HH:MM" into (hour, minute); None if blank/malformed."""
|
"""Parse "HH:MM" into (hour, minute); None if blank/malformed."""
|
||||||
if not value:
|
if not value:
|
||||||
@@ -180,6 +200,7 @@ class NotificationCoordinator:
|
|||||||
recipients_fired: list[str] = []
|
recipients_fired: list[str] = []
|
||||||
message = self._render_template(meta, context)
|
message = self._render_template(meta, context)
|
||||||
nav_url = self._resolve_nav_url(cfg)
|
nav_url = self._resolve_nav_url(cfg)
|
||||||
|
group = self._resolve_group(cfg)
|
||||||
|
|
||||||
# Multi-parent routing (#687): thin the PARENT recipients down per the
|
# Multi-parent routing (#687): thin the PARENT recipients down per the
|
||||||
# configured policy. Child routes are never touched — a reminder for a
|
# 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)
|
notify_service = self._resolve_notify_service(recipient_id)
|
||||||
if not notify_service:
|
if not notify_service:
|
||||||
continue
|
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)
|
recipients_fired.append(recipient_id)
|
||||||
|
|
||||||
# NOTE: deliberately no unconditional persistent_notification here.
|
# NOTE: deliberately no unconditional persistent_notification here.
|
||||||
@@ -310,6 +331,7 @@ class NotificationCoordinator:
|
|||||||
message = "[TEST] " + self._render_template(meta, ctx)
|
message = "[TEST] " + self._render_template(meta, ctx)
|
||||||
cfg = self.storage.get_notification_config(type_id)
|
cfg = self.storage.get_notification_config(type_id)
|
||||||
nav_url = self._resolve_nav_url(cfg)
|
nav_url = self._resolve_nav_url(cfg)
|
||||||
|
group = self._resolve_group(cfg)
|
||||||
sent: list[str] = []
|
sent: list[str] = []
|
||||||
for recipient_id, route in cfg.routes.items():
|
for recipient_id, route in cfg.routes.items():
|
||||||
if not route.enabled:
|
if not route.enabled:
|
||||||
@@ -317,7 +339,7 @@ class NotificationCoordinator:
|
|||||||
notify_service = self._resolve_notify_service(recipient_id)
|
notify_service = self._resolve_notify_service(recipient_id)
|
||||||
if not notify_service:
|
if not notify_service:
|
||||||
continue
|
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)
|
sent.append(recipient_id)
|
||||||
await self._fire_persistent_notification(type_id, message)
|
await self._fire_persistent_notification(type_id, message)
|
||||||
return sent
|
return sent
|
||||||
@@ -341,6 +363,17 @@ class NotificationCoordinator:
|
|||||||
return per_type
|
return per_type
|
||||||
return str(self.storage.get_setting("notification_nav_url", DEFAULT_NOTIFICATION_NAV_URL) or "").strip()
|
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:
|
def _resolve_notify_service(self, recipient_id: str) -> str:
|
||||||
if recipient_id.startswith("child:"):
|
if recipient_id.startswith("child:"):
|
||||||
child_id = recipient_id.split(":", 1)[1]
|
child_id = recipient_id.split(":", 1)[1]
|
||||||
@@ -387,6 +420,7 @@ class NotificationCoordinator:
|
|||||||
meta: "NotificationTypeMeta",
|
meta: "NotificationTypeMeta",
|
||||||
context: dict[str, Any],
|
context: dict[str, Any],
|
||||||
nav_url: str = "",
|
nav_url: str = "",
|
||||||
|
group: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
domain, service = notify_service.split(".", 1) if "." in notify_service else ("notify", notify_service)
|
domain, service = notify_service.split(".", 1) if "." in notify_service else ("notify", notify_service)
|
||||||
if domain != "notify":
|
if domain != "notify":
|
||||||
@@ -436,6 +470,14 @@ class NotificationCoordinator:
|
|||||||
push["clickAction"] = nav_url
|
push["clickAction"] = nav_url
|
||||||
push["url"] = 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:
|
if push:
|
||||||
data["data"] = push
|
data["data"] = push
|
||||||
|
|
||||||
@@ -665,10 +707,17 @@ class NotificationCoordinator:
|
|||||||
)
|
)
|
||||||
message = n.message_template
|
message = n.message_template
|
||||||
service_name = notify_service.split(".", 1)[1] if "." in notify_service else notify_service
|
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(
|
await self.hass.services.async_call(
|
||||||
"notify",
|
"notify",
|
||||||
service_name,
|
service_name,
|
||||||
{"title": "TaskMate", "message": message},
|
payload,
|
||||||
blocking=False,
|
blocking=False,
|
||||||
)
|
)
|
||||||
self.hass.bus.async_fire(
|
self.hass.bus.async_fire(
|
||||||
@@ -753,6 +802,17 @@ class NotificationCoordinator:
|
|||||||
self.storage.set_setting("notification_nav_url", nav_url)
|
self.storage.set_setting("notification_nav_url", nav_url)
|
||||||
await self.storage.async_save()
|
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:
|
def _has_outstanding_chores_today(self, child_id: str) -> bool:
|
||||||
"""Returns True if the child has at least one chore assigned today
|
"""Returns True if the child has at least one chore assigned today
|
||||||
that has no approved/pending completion yet."""
|
that has no approved/pending completion yet."""
|
||||||
|
|||||||
@@ -17,5 +17,5 @@
|
|||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"issue_tracker": "https://github.com/tempus2016/taskmate/issues",
|
"issue_tracker": "https://github.com/tempus2016/taskmate/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"version": "5.2.0"
|
"version": "5.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1219,15 +1219,21 @@ class NotificationConfig:
|
|||||||
master_enabled: bool = False
|
master_enabled: bool = False
|
||||||
routes: dict[str, NotificationRoute] = field(default_factory=dict)
|
routes: dict[str, NotificationRoute] = field(default_factory=dict)
|
||||||
nav_url: str = "" # tap target; "" = inherit global default
|
nav_url: str = "" # tap target; "" = inherit global default
|
||||||
|
group: str = "" # notification group/thread; "" = inherit global default
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> NotificationConfig:
|
def from_dict(cls, data: dict[str, Any]) -> NotificationConfig:
|
||||||
raw_routes = data.get("routes", {}) or {}
|
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(
|
return cls(
|
||||||
type_id=data.get("type_id", ""),
|
type_id=data.get("type_id", ""),
|
||||||
master_enabled=bool(data.get("master_enabled", False)),
|
master_enabled=bool(data.get("master_enabled", False)),
|
||||||
routes={rid: NotificationRoute.from_dict(rdata) for rid, rdata in raw_routes.items()},
|
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]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
@@ -1238,6 +1244,8 @@ class NotificationConfig:
|
|||||||
}
|
}
|
||||||
if self.nav_url:
|
if self.nav_url:
|
||||||
d["nav_url"] = self.nav_url
|
d["nav_url"] = self.nav_url
|
||||||
|
if self.group:
|
||||||
|
d["group"] = self.group
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -773,6 +773,10 @@ class TaskMateOverallStatsSensor(_CachedAttrsSensor):
|
|||||||
sensors so this entity stays well under the 16KB recorder limit.
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: TaskMateCoordinator,
|
coordinator: TaskMateCoordinator,
|
||||||
@@ -869,6 +873,8 @@ class TaskMateOverallStatsSensor(_CachedAttrsSensor):
|
|||||||
class TaskMateChoresSensor(_CachedAttrsSensor):
|
class TaskMateChoresSensor(_CachedAttrsSensor):
|
||||||
"""Chores catalog + today's completions."""
|
"""Chores catalog + today's completions."""
|
||||||
|
|
||||||
|
_unrecorded_attributes = frozenset({"chores", "todays_completions", "task_groups", "active_timed_sessions"})
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: TaskMateCoordinator,
|
coordinator: TaskMateCoordinator,
|
||||||
@@ -917,6 +923,8 @@ class TaskMateChoreAvailabilitySensor(_CachedAttrsSensor):
|
|||||||
The map is `{chore_id: {child_id: bool}}`.
|
The map is `{chore_id: {child_id: bool}}`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_unrecorded_attributes = frozenset({"chore_availability"})
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: TaskMateCoordinator,
|
coordinator: TaskMateCoordinator,
|
||||||
@@ -949,6 +957,8 @@ class TaskMateChoreAvailabilitySensor(_CachedAttrsSensor):
|
|||||||
class TaskMateRewardsSensor(_CachedAttrsSensor):
|
class TaskMateRewardsSensor(_CachedAttrsSensor):
|
||||||
"""Rewards catalog + pending claims + pool allocations."""
|
"""Rewards catalog + pending claims + pool allocations."""
|
||||||
|
|
||||||
|
_unrecorded_attributes = frozenset({"rewards", "pending_reward_claims", "pool_allocations"})
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: TaskMateCoordinator,
|
coordinator: TaskMateCoordinator,
|
||||||
@@ -978,6 +988,10 @@ class TaskMateRewardsSensor(_CachedAttrsSensor):
|
|||||||
class TaskMateActivitySensor(_CachedAttrsSensor):
|
class TaskMateActivitySensor(_CachedAttrsSensor):
|
||||||
"""Recent completions + recent points/reward transactions."""
|
"""Recent completions + recent points/reward transactions."""
|
||||||
|
|
||||||
|
_unrecorded_attributes = frozenset(
|
||||||
|
{"recent_completions", "recent_transactions", "career_score_history", "photo_gallery"}
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: TaskMateCoordinator,
|
coordinator: TaskMateCoordinator,
|
||||||
@@ -1012,6 +1026,8 @@ class TaskMateActivitySensor(_CachedAttrsSensor):
|
|||||||
class TaskMateIncentivesSensor(_CachedAttrsSensor):
|
class TaskMateIncentivesSensor(_CachedAttrsSensor):
|
||||||
"""Penalties + bonuses catalogue."""
|
"""Penalties + bonuses catalogue."""
|
||||||
|
|
||||||
|
_unrecorded_attributes = frozenset({"penalties", "bonuses"})
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: TaskMateCoordinator,
|
coordinator: TaskMateCoordinator,
|
||||||
@@ -1094,6 +1110,7 @@ class ChildStatsSensor(TaskMateBaseSensor):
|
|||||||
"""Sensor for a child's statistics."""
|
"""Sensor for a child's statistics."""
|
||||||
|
|
||||||
_attr_translation_key = "child_stats"
|
_attr_translation_key = "child_stats"
|
||||||
|
_unrecorded_attributes = frozenset({"assigned_chores", "chore_order"})
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -1171,6 +1188,7 @@ class ChildBadgesSensor(TaskMateBaseSensor):
|
|||||||
|
|
||||||
_attr_icon = "mdi:trophy-award"
|
_attr_icon = "mdi:trophy-award"
|
||||||
_attr_translation_key = "child_badges"
|
_attr_translation_key = "child_badges"
|
||||||
|
_unrecorded_attributes = frozenset({"earned", "available"})
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -1261,6 +1279,8 @@ class ChildBadgesSensor(TaskMateBaseSensor):
|
|||||||
class PendingApprovalsSensor(TaskMateBaseSensor):
|
class PendingApprovalsSensor(TaskMateBaseSensor):
|
||||||
"""Sensor for pending approvals."""
|
"""Sensor for pending approvals."""
|
||||||
|
|
||||||
|
_unrecorded_attributes = frozenset({"chore_completions", "reward_claims", "mandatory_misses"})
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: TaskMateCoordinator,
|
coordinator: TaskMateCoordinator,
|
||||||
|
|||||||
@@ -727,6 +727,11 @@ class TaskMateStorage:
|
|||||||
cfg.nav_url = nav_url
|
cfg.nav_url = nav_url
|
||||||
self._data.setdefault("notification_config", {})[type_id] = cfg.to_dict()
|
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:
|
def set_notification_route(self, type_id: str, recipient_id: str, route: NotificationRoute) -> None:
|
||||||
cfg = self.get_notification_config(type_id)
|
cfg = self.get_notification_config(type_id)
|
||||||
cfg.routes[recipient_id] = route
|
cfg.routes[recipient_id] = route
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from . import images, photos
|
from . import images, photos
|
||||||
from .const import (
|
from .const import (
|
||||||
ASSIGNMENT_MODES,
|
ASSIGNMENT_MODES,
|
||||||
|
DEFAULT_NOTIFICATION_GROUP,
|
||||||
DEFAULT_NOTIFICATION_NAV_URL,
|
DEFAULT_NOTIFICATION_NAV_URL,
|
||||||
DEFAULT_TIME_PERIODS,
|
DEFAULT_TIME_PERIODS,
|
||||||
DIFFICULTY_TIERS,
|
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_SET_ESCALATION: Final = "taskmate/notifications/set_escalation"
|
||||||
WS_NOTIF_SEND_TEST: Final = "taskmate/notifications/send_test"
|
WS_NOTIF_SEND_TEST: Final = "taskmate/notifications/send_test"
|
||||||
WS_NOTIF_SET_NAV_URL: Final = "taskmate/notifications/set_nav_url"
|
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)
|
# Calendar ICS feed (FEAT-10)
|
||||||
WS_CAL_GET_URL: Final = "taskmate/calendar/get_ics_url"
|
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_reminder_minutes": c.storage.get_escalation_reminder_minutes(),
|
||||||
"mandatory_escalation_parent_minutes": c.storage.get_escalation_parent_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_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)
|
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})
|
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)
|
# Calendar ICS feed (FEAT-10)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -2701,6 +2718,7 @@ _COMMANDS = (
|
|||||||
ws_notif_send_test,
|
ws_notif_send_test,
|
||||||
ws_notif_set_escalation,
|
ws_notif_set_escalation,
|
||||||
ws_notif_set_nav_url,
|
ws_notif_set_nav_url,
|
||||||
|
ws_notif_set_group,
|
||||||
ws_cal_get_url,
|
ws_cal_get_url,
|
||||||
ws_cal_regen_token,
|
ws_cal_regen_token,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -881,6 +881,9 @@
|
|||||||
"panel.notif_nav_url_global_label": "Beim Tippen öffnen",
|
"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_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_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.notif_tab_title": "Benachrichtigungen",
|
||||||
"panel.template_assignment_mode_label": "Zuweisungsmodus",
|
"panel.template_assignment_mode_label": "Zuweisungsmodus",
|
||||||
"panel.template_builtin": "Integriert",
|
"panel.template_builtin": "Integriert",
|
||||||
|
|||||||
@@ -931,6 +931,9 @@
|
|||||||
"panel.notif_nav_url_global_label": "When tapped, open",
|
"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_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_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_send_test": "Send test",
|
||||||
"panel.notif_test_sent": "Test sent to {count} recipient(s) + persistent notification",
|
"panel.notif_test_sent": "Test sent to {count} recipient(s) + persistent notification",
|
||||||
"panel.notif_tab_title": "Notifications",
|
"panel.notif_tab_title": "Notifications",
|
||||||
|
|||||||
@@ -931,6 +931,9 @@
|
|||||||
"panel.notif_nav_url_global_label": "When tapped, open",
|
"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_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_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_send_test": "Send test",
|
||||||
"panel.notif_test_sent": "Test sent to {count} recipient(s) + persistent notification",
|
"panel.notif_test_sent": "Test sent to {count} recipient(s) + persistent notification",
|
||||||
"panel.notif_tab_title": "Notifications",
|
"panel.notif_tab_title": "Notifications",
|
||||||
|
|||||||
@@ -881,6 +881,9 @@
|
|||||||
"panel.notif_nav_url_global_label": "Au toucher, ouvrir",
|
"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_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_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.notif_tab_title": "Notifications",
|
||||||
"panel.template_assignment_mode_label": "Mode d'assignation",
|
"panel.template_assignment_mode_label": "Mode d'assignation",
|
||||||
"panel.template_builtin": "Intégré",
|
"panel.template_builtin": "Intégré",
|
||||||
|
|||||||
@@ -881,6 +881,9 @@
|
|||||||
"panel.notif_nav_url_global_label": "Ved trykk, åpne",
|
"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_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_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.notif_tab_title": "Varsler",
|
||||||
"panel.template_assignment_mode_label": "Tildelingsmodus",
|
"panel.template_assignment_mode_label": "Tildelingsmodus",
|
||||||
"panel.template_builtin": "Innebygd",
|
"panel.template_builtin": "Innebygd",
|
||||||
|
|||||||
@@ -881,6 +881,9 @@
|
|||||||
"panel.notif_nav_url_global_label": "Ved trykk, opne",
|
"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_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_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.notif_tab_title": "Varsel",
|
||||||
"panel.template_assignment_mode_label": "Tildelingsmodus",
|
"panel.template_assignment_mode_label": "Tildelingsmodus",
|
||||||
"panel.template_builtin": "Innebygd",
|
"panel.template_builtin": "Innebygd",
|
||||||
|
|||||||
@@ -881,6 +881,9 @@
|
|||||||
"panel.notif_nav_url_global_label": "Ao tocar, abrir",
|
"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_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_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.notif_tab_title": "Notificações",
|
||||||
"panel.template_assignment_mode_label": "Modo de atribuição",
|
"panel.template_assignment_mode_label": "Modo de atribuição",
|
||||||
"panel.template_builtin": "Integrado",
|
"panel.template_builtin": "Integrado",
|
||||||
|
|||||||
@@ -886,6 +886,9 @@
|
|||||||
"panel.notif_nav_url_global_label": "Ao tocar, abrir",
|
"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_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_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.notif_tab_title": "Notificações",
|
||||||
"panel.template_assignment_mode_label": "Modo de atribuição",
|
"panel.template_assignment_mode_label": "Modo de atribuição",
|
||||||
"panel.template_builtin": "Incorporado",
|
"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-streak-cutoff"){ /* handled in _onChange */ return; }
|
||||||
if (act === "notif-set-nav-url-global"){ /* 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-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-add-parent") { this._notifAddParent(); return; }
|
||||||
if (act === "notif-delete-parent") { this._notifDeleteParent(t.dataset.parentId); return; }
|
if (act === "notif-delete-parent") { this._notifDeleteParent(t.dataset.parentId); return; }
|
||||||
if (act === "notif-add-custom") { this._notifAddCustom(); return; }
|
if (act === "notif-add-custom") { this._notifAddCustom(); return; }
|
||||||
@@ -945,6 +947,14 @@ class TaskMatePanel extends HTMLElement {
|
|||||||
this._notifSetNavUrl(t.dataset.typeId, t.value);
|
this._notifSetNavUrl(t.dataset.typeId, t.value);
|
||||||
return;
|
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") {
|
if (t.dataset.act === "notif-set-escalation") {
|
||||||
this._notifSetEscalation(t.dataset.escField, t.value);
|
this._notifSetEscalation(t.dataset.escField, t.value);
|
||||||
return;
|
return;
|
||||||
@@ -2236,7 +2246,16 @@ class TaskMatePanel extends HTMLElement {
|
|||||||
async _notifSetNavUrl(typeId, navUrl) {
|
async _notifSetNavUrl(typeId, navUrl) {
|
||||||
const payload = { type: "taskmate/notifications/set_nav_url", nav_url: navUrl || "" };
|
const payload = { type: "taskmate/notifications/set_nav_url", nav_url: navUrl || "" };
|
||||||
if (typeId) payload.type_id = typeId;
|
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();
|
await this._fetchState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4762,6 +4781,13 @@ class TaskMatePanel extends HTMLElement {
|
|||||||
data-act="notif-set-nav-url-global" placeholder="/taskmate-admin">
|
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 class="tm-meta" style="flex-basis:100%">${this._t("panel.notif_nav_url_global_hint")}</div>
|
||||||
</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">
|
<div class="tm-table-wrap">
|
||||||
<table class="tm-table">
|
<table class="tm-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -4818,7 +4844,12 @@ class TaskMatePanel extends HTMLElement {
|
|||||||
<input type="text" class="tm-notif-time-input" style="width:150px"
|
<input type="text" class="tm-notif-time-input" style="width:150px"
|
||||||
value="${this._esc(c.nav_url || "")}"
|
value="${this._esc(c.nav_url || "")}"
|
||||||
data-act="notif-set-nav-url-type" data-type-id="${this._esc(t.id)}"
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+15
-15
@@ -4,7 +4,7 @@
|
|||||||
"state": "ON",
|
"state": "ON",
|
||||||
"led_brightness": 100,
|
"led_brightness": 100,
|
||||||
"countdown_to_turn_off": 0,
|
"countdown_to_turn_off": 0,
|
||||||
"voltage": 122.2,
|
"voltage": 121.7,
|
||||||
"countdown_to_turn_on": 0,
|
"countdown_to_turn_on": 0,
|
||||||
"ac_frequency": 60,
|
"ac_frequency": 60,
|
||||||
"power_factor": 0.14,
|
"power_factor": 0.14,
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"led_brightness": 100,
|
"led_brightness": 100,
|
||||||
"countdown_to_turn_off": 0,
|
"countdown_to_turn_off": 0,
|
||||||
"countdown_to_turn_on": 0,
|
"countdown_to_turn_on": 0,
|
||||||
"power": 2,
|
"power": 1.9,
|
||||||
"current": 0.1,
|
"current": 0.1,
|
||||||
"energy": 32.7,
|
"energy": 32.7,
|
||||||
"power_factor": 0.17,
|
"power_factor": 0.17,
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"countdown_to_turn_off": 0,
|
"countdown_to_turn_off": 0,
|
||||||
"voltage": 121.5,
|
"voltage": 121.5,
|
||||||
"countdown_to_turn_on": 0,
|
"countdown_to_turn_on": 0,
|
||||||
"energy": 66.04,
|
"energy": 66.05,
|
||||||
"power_factor": 0.88,
|
"power_factor": 0.88,
|
||||||
"ac_frequency": 60,
|
"ac_frequency": 60,
|
||||||
"update": {
|
"update": {
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"latest_release_notes": null
|
"latest_release_notes": null
|
||||||
},
|
},
|
||||||
"linkquality": 138,
|
"linkquality": 138,
|
||||||
"power": 86.1,
|
"power": 82.8,
|
||||||
"current": 0.82,
|
"current": 0.82,
|
||||||
"power_on_behavior": "on"
|
"power_on_behavior": "on"
|
||||||
},
|
},
|
||||||
@@ -74,13 +74,13 @@
|
|||||||
"led_brightness": 100,
|
"led_brightness": 100,
|
||||||
"countdown_to_turn_off": 0,
|
"countdown_to_turn_off": 0,
|
||||||
"countdown_to_turn_on": 0,
|
"countdown_to_turn_on": 0,
|
||||||
"voltage": 120.5,
|
"voltage": 121,
|
||||||
"state": "ON",
|
"state": "ON",
|
||||||
"ac_frequency": 60,
|
"ac_frequency": 60,
|
||||||
"energy": 133.62,
|
"energy": 133.64,
|
||||||
"power": 96.6,
|
"power": 0.8,
|
||||||
"current": 0.85,
|
"current": 0.02,
|
||||||
"power_factor": 0.94,
|
"power_factor": 0.35,
|
||||||
"update": {
|
"update": {
|
||||||
"state": "idle",
|
"state": "idle",
|
||||||
"installed_version": 268513381,
|
"installed_version": 268513381,
|
||||||
@@ -95,13 +95,13 @@
|
|||||||
"led_brightness": 100,
|
"led_brightness": 100,
|
||||||
"countdown_to_turn_off": 0,
|
"countdown_to_turn_off": 0,
|
||||||
"countdown_to_turn_on": 0,
|
"countdown_to_turn_on": 0,
|
||||||
"voltage": 122,
|
"voltage": 121.5,
|
||||||
"energy": 63.98,
|
"energy": 63.99,
|
||||||
"state": "ON",
|
"state": "ON",
|
||||||
"power": 19.9,
|
"power": 18,
|
||||||
"current": 0.33,
|
"current": 0.31,
|
||||||
"ac_frequency": 60,
|
"ac_frequency": 60,
|
||||||
"power_factor": 0.59,
|
"power_factor": 0.53,
|
||||||
"update": {
|
"update": {
|
||||||
"state": "idle",
|
"state": "idle",
|
||||||
"installed_version": 268513381,
|
"installed_version": 268513381,
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"energy": 8.8,
|
"energy": 8.8,
|
||||||
"current": 0.01,
|
"current": 0.01,
|
||||||
"power": 0.2,
|
"power": 0.2,
|
||||||
"power_factor": 0.22,
|
"power_factor": 0.11,
|
||||||
"linkquality": 109,
|
"linkquality": 109,
|
||||||
"update": {
|
"update": {
|
||||||
"state": "idle",
|
"state": "idle",
|
||||||
|
|||||||
Reference in New Issue
Block a user