Coverage for custom_components / supernotify / transports / lametric.py: 100%
61 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-11 22:18 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-11 22:18 +0000
1"""LaMetric native transport for SuperNotify.
3Sends messages and charts to LaMetric smart displays using the
4HA lametric integration services (lametric.message, lametric.chart).
6Requires: Home Assistant lametric integration (core, auto-discovered via mDNS/SSDP).
7No action: required in delivery config — uses lametric.message or lametric.chart
8based on presence of lametric_chart_data in envelope data.
10Target: NEVER — LaMetric is a fixed device, not person-routed.
11The device_id must be specified in delivery config data:
13 deliveries:
14 - name: lametric
15 transport: lametric
16 data:
17 device_id: "49b6e2186ef37e164818aacb9cea1f53"
19New data keys (all optional unless noted):
20 device_id str REQUIRED. LaMetric device UUID from HA device registry.
21 Obtain from: HA Settings → Devices → LaMetric → device ID.
22 Example: "49b6e2186ef37e164818aacb9cea1f53"
23 lametric_sound str Built-in sound name. Overrides priority default.
24 Built-in sounds: alarm1, alarm2, ..., alarm13,
25 bicycle, car, cash, cat, dog, dog2, energy,
26 knock-knock, letter_email, lose1, lose2,
27 negative1, negative2, negative3, negative4, negative5,
28 notification, notification2, notification3, notification4,
29 open_door, positive1, ..., positive6,
30 statistic, thunder, water1, water2,
31 win, win2, wind, wind_short.
32 Omit or set None for silent notification.
33 lametric_icon str Icon ID override (e.g. "i2867", "a1784").
34 Overrides priority-based default icon.
35 Full icon list: https://developer.lametric.com/icons
36 lametric_cycles int Display cycles override.
37 0 = permanent (stays until dismissed manually).
38 1+ = number of scroll cycles, then auto-dismiss.
39 Overrides priority default.
40 lametric_icon_type str Icon style: "none", "info", "alert".
41 "alert" produces a red flashing icon.
42 Overrides priority default.
43 lametric_chart_data list[int] If present, sends lametric.chart instead of lametric.message.
44 List of integers representing bar chart values.
45 Example: [10, 30, 50, 80, 60, 20]
46 lametric_simplify bool If True, apply simplify() to message text
47 (strips URLs, shortens for small physical display).
48 Default: False.
50Priority defaults (auto-applied when keys not specified):
51 critical → priority=critical, cycles=0 (permanent), icon_type=alert, sound=alarm1, icon=a1784
52 high → priority=warning, cycles=2, icon_type=alert, sound=knock-knock, icon=i140
53 medium → priority=info, cycles=1, icon_type=info, sound=notification, icon=i2867
54 low → priority=info, cycles=1, icon_type=none, sound=None (silent), icon=i2867
55 minimum → priority=info, cycles=1, icon_type=none, sound=None (silent), icon=None
56"""
58from __future__ import annotations
60import logging
61from typing import TYPE_CHECKING, Any
63from custom_components.supernotify.common import boolify
64from custom_components.supernotify.const import TRANSPORT_LAMETRIC
65from custom_components.supernotify.model import (
66 DebugTrace,
67 TargetRequired,
68 TransportConfig,
69 TransportFeature,
70)
71from custom_components.supernotify.transport import Transport
73if TYPE_CHECKING:
74 from custom_components.supernotify.envelope import Envelope
76_LOGGER = logging.getLogger(__name__)
78# Priority mapping: SuperNotify string → LaMetric priority string
79_PRIORITY_MAP: dict[str, str] = {
80 "critical": "critical",
81 "high": "warning",
82 "medium": "info",
83 "low": "info",
84 "minimum": "info",
85}
87# Default display cycles per priority (0 = permanent until dismissed)
88_CYCLES_MAP: dict[str, int] = {
89 "critical": 0, # stays on display until manually dismissed
90 "high": 2,
91 "medium": 1,
92 "low": 1,
93 "minimum": 1,
94}
96# Icon type per priority ("alert" = red flashing, "info" = blue, "none" = no highlight)
97_ICON_TYPE_MAP: dict[str, str] = {
98 "critical": "alert",
99 "high": "alert",
100 "medium": "info",
101 "low": "none",
102 "minimum": "none",
103}
105# Default sound per priority (None = silent)
106_SOUND_MAP: dict[str, str | None] = {
107 "critical": "alarm1",
108 "high": "knock-knock",
109 "medium": "notification",
110 "low": None,
111 "minimum": None,
112}
114# Default icon ID per priority (None = no icon)
115_ICON_MAP: dict[str, str | None] = {
116 "critical": "a1784", # animated alert icon (red)
117 "high": "i140", # exclamation mark
118 "medium": "i2867", # bell icon (already used by Lollo)
119 "low": "i2867", # bell icon
120 "minimum": None, # text only
121}
124class LaMetricTransport(Transport):
125 """LaMetric smart display transport for SuperNotify.
127 Delivers notifications to LaMetric TIME devices via the HA lametric
128 integration. Supports text messages and bar charts with full priority
129 mapping (sound, icon, cycles, icon_type auto-selected per priority level).
130 """
132 name = TRANSPORT_LAMETRIC
134 def __init__(self, *args: Any, **kwargs: Any) -> None:
135 super().__init__(*args, **kwargs)
137 @property
138 def supported_features(self) -> TransportFeature:
139 return TransportFeature.MESSAGE | TransportFeature.TITLE
141 @property
142 def default_config(self) -> TransportConfig:
143 config = TransportConfig()
144 config.delivery_defaults.target_required = TargetRequired.NEVER
145 return config
147 def validate_action(self, action: str | None) -> bool: # noqa: ARG002
148 # No external action required - transport uses lametric.message / lametric.chart directly
149 return True
151 async def deliver(self, envelope: Envelope, debug_trace: DebugTrace | None = None) -> bool: # noqa: ARG002
152 _LOGGER.debug("SUPERNOTIFY lametric %s", envelope.message)
154 # 1. Extract raw data (flat dict — rule #6)
155 raw_data: dict[str, Any] = dict(envelope.data) if envelope.data else {}
157 # 2. Pop device_id (required — configured in delivery data)
158 device_id: str | None = raw_data.pop("device_id", None)
159 if not device_id:
160 _LOGGER.debug(
161 "SUPERNOTIFY lametric: device_id missing from delivery data, skipping. "
162 "Add 'device_id: <uuid>' under data: in your lametric delivery config."
163 )
164 return False
166 # 3. Pop all lametric-specific keys (must NOT reach the HA service)
167 lametric_sound = raw_data.pop("lametric_sound", None)
168 lametric_icon = raw_data.pop("lametric_icon", None)
169 lametric_cycles = raw_data.pop("lametric_cycles", None)
170 lametric_icon_type = raw_data.pop("lametric_icon_type", None)
171 lametric_chart_data = raw_data.pop("lametric_chart_data", None)
172 lametric_simplify = boolify(raw_data.pop("lametric_simplify", False), default=False)
174 # 4. Resolve priority → default values
175 sn_priority = envelope.priority or "medium"
176 final_priority = _PRIORITY_MAP.get(sn_priority, "info")
177 final_cycles = lametric_cycles if lametric_cycles is not None else _CYCLES_MAP.get(sn_priority, 1)
178 final_icon_type = lametric_icon_type if lametric_icon_type is not None else _ICON_TYPE_MAP.get(sn_priority, "none")
179 final_sound = lametric_sound if lametric_sound is not None else _SOUND_MAP.get(sn_priority)
180 final_icon = lametric_icon if lametric_icon is not None else _ICON_MAP.get(sn_priority)
182 # 5. Optionally simplify message text for small physical display
183 message = self.simplify(envelope.message, strip_urls=True) if lametric_simplify else envelope.message
185 # 6A. CHART path — if lametric_chart_data is provided
186 if lametric_chart_data is not None:
187 if not isinstance(lametric_chart_data, list):
188 _LOGGER.debug(
189 "SUPERNOTIFY lametric: lametric_chart_data must be a list of ints, got %s",
190 type(lametric_chart_data).__name__,
191 )
192 return False
194 action_data: dict[str, Any] = {
195 "device_id": device_id,
196 "data": lametric_chart_data, # field name is "data" for lametric.chart
197 "cycles": final_cycles,
198 "priority": final_priority,
199 "icon_type": final_icon_type,
200 }
201 if final_sound:
202 action_data["sound"] = final_sound
204 return await self.call_action(
205 envelope,
206 qualified_action="lametric.chart",
207 action_data=action_data,
208 )
210 # 6B. MESSAGE path (default)
211 action_data = {
212 "device_id": device_id,
213 "message": message,
214 "cycles": final_cycles,
215 "priority": final_priority,
216 "icon_type": final_icon_type,
217 }
218 if final_icon:
219 action_data["icon"] = final_icon
220 if final_sound:
221 action_data["sound"] = final_sound
223 return await self.call_action(
224 envelope,
225 qualified_action="lametric.message",
226 action_data=action_data,
227 )