Coverage for custom_components/supernotify/transports/lametric.py: 100%
68 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-25 14:29 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-25 14:29 +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 homeassistant.helpers.typing import ConfigType
65from custom_components.supernotify.common import boolify
66from custom_components.supernotify.const import TRANSPORT_LAMETRIC
67from custom_components.supernotify.model import (
68 DebugTrace,
69 TargetRequired,
70 TransportConfig,
71 TransportFeature,
72)
73from custom_components.supernotify.transport import Transport
75if TYPE_CHECKING:
76 from custom_components.supernotify.envelope import Envelope
77 from custom_components.supernotify.hass_api import HomeAssistantAPI
79_LOGGER = logging.getLogger(__name__)
81HA_LAMETRIC_DOMAIN = "lametric"
83# Priority mapping: SuperNotify string → LaMetric priority string
84_PRIORITY_MAP: dict[str, str] = {
85 "critical": "critical",
86 "high": "warning",
87 "medium": "info",
88 "low": "info",
89 "minimum": "info",
90}
92# Default display cycles per priority (0 = permanent until dismissed)
93_CYCLES_MAP: dict[str, int] = {
94 "critical": 0, # stays on display until manually dismissed
95 "high": 2,
96 "medium": 1,
97 "low": 1,
98 "minimum": 1,
99}
101# Icon type per priority ("alert" = red flashing, "info" = blue, "none" = no highlight)
102_ICON_TYPE_MAP: dict[str, str] = {
103 "critical": "alert",
104 "high": "alert",
105 "medium": "info",
106 "low": "none",
107 "minimum": "none",
108}
110# Default sound per priority (None = silent)
111_SOUND_MAP: dict[str, str | None] = {
112 "critical": "alarm1",
113 "high": "knock-knock",
114 "medium": "notification",
115 "low": None,
116 "minimum": None,
117}
119# Default icon ID per priority (None = no icon)
120_ICON_MAP: dict[str, str | None] = {
121 "critical": "a1784", # animated alert icon (red)
122 "high": "i140", # exclamation mark
123 "medium": "i2867", # bell icon (already used by Lollo)
124 "low": "i2867", # bell icon
125 "minimum": None, # text only
126}
129class LaMetricTransport(Transport):
130 """LaMetric smart display transport for SuperNotify.
132 Delivers notifications to LaMetric TIME devices via the HA lametric
133 integration. Supports text messages and bar charts with full priority
134 mapping (sound, icon, cycles, icon_type auto-selected per priority level).
135 """
137 name = TRANSPORT_LAMETRIC
139 def __init__(self, *args: Any, **kwargs: Any) -> None:
140 super().__init__(*args, **kwargs)
142 @property
143 def supported_features(self) -> TransportFeature:
144 return TransportFeature.MESSAGE | TransportFeature.TITLE
146 @property
147 def default_config(self) -> TransportConfig:
148 config = TransportConfig()
149 config.delivery_defaults.target_required = TargetRequired.NEVER
150 config.delivery_defaults.inclusion = self.inclusion_mode
151 return config
153 def is_viable(self, hass_api: HomeAssistantAPI) -> bool:
154 return hass_api.find_config_entry_data(HA_LAMETRIC_DOMAIN) is not None
156 def build_standard_deliveries(self, hass_api: HomeAssistantAPI) -> dict[str, ConfigType]:
157 return {self.name: {}}
159 def validate_action(self, action: str | None) -> bool:
160 # No external action required - transport uses lametric.message / lametric.chart directly
161 return True
163 async def deliver(self, envelope: Envelope, debug_trace: DebugTrace | None = None) -> bool:
164 _LOGGER.debug("SUPERNOTIFY lametric %s", envelope.message)
166 # 1. Extract raw data (flat dict — rule #6)
167 raw_data: dict[str, Any] = dict(envelope.data) if envelope.data else {}
169 # 2. Pop device_id (required — configured in delivery data)
170 device_id: str | None = raw_data.pop("device_id", None)
171 if not device_id:
172 _LOGGER.debug(
173 "SUPERNOTIFY lametric: device_id missing from delivery data, skipping. "
174 "Add 'device_id: <uuid>' under data: in your lametric delivery config."
175 )
176 return False
178 # 3. Pop all lametric-specific keys (must NOT reach the HA service)
179 lametric_sound = raw_data.pop("lametric_sound", None)
180 lametric_icon = raw_data.pop("lametric_icon", None)
181 lametric_cycles = raw_data.pop("lametric_cycles", None)
182 lametric_icon_type = raw_data.pop("lametric_icon_type", None)
183 lametric_chart_data = raw_data.pop("lametric_chart_data", None)
184 lametric_simplify = boolify(raw_data.pop("lametric_simplify", False), default=False)
186 # 4. Resolve priority → default values
187 sn_priority = envelope.priority or "medium"
188 final_priority = _PRIORITY_MAP.get(sn_priority, "info")
189 final_cycles = lametric_cycles if lametric_cycles is not None else _CYCLES_MAP.get(sn_priority, 1)
190 final_icon_type = lametric_icon_type if lametric_icon_type is not None else _ICON_TYPE_MAP.get(sn_priority, "none")
191 final_sound = lametric_sound if lametric_sound is not None else _SOUND_MAP.get(sn_priority)
192 final_icon = lametric_icon if lametric_icon is not None else _ICON_MAP.get(sn_priority)
194 # 5. Optionally simplify message text for small physical display
195 message = self.simplify(envelope.message, strip_urls=True) if lametric_simplify else envelope.message
197 # 6A. CHART path — if lametric_chart_data is provided
198 if lametric_chart_data is not None:
199 if not isinstance(lametric_chart_data, list):
200 _LOGGER.debug(
201 "SUPERNOTIFY lametric: lametric_chart_data must be a list of ints, got %s",
202 type(lametric_chart_data).__name__,
203 )
204 return False
206 action_data: dict[str, Any] = {
207 "device_id": device_id,
208 "data": lametric_chart_data, # field name is "data" for lametric.chart
209 "cycles": final_cycles,
210 "priority": final_priority,
211 "icon_type": final_icon_type,
212 }
213 if final_sound:
214 action_data["sound"] = final_sound
216 return await self.call_action(
217 envelope,
218 qualified_action="lametric.chart",
219 action_data=action_data,
220 )
222 # 6B. MESSAGE path (default)
223 action_data = {
224 "device_id": device_id,
225 "message": message,
226 "cycles": final_cycles,
227 "priority": final_priority,
228 "icon_type": final_icon_type,
229 }
230 if final_icon:
231 action_data["icon"] = final_icon
232 if final_sound:
233 action_data["sound"] = final_sound
235 return await self.call_action(
236 envelope,
237 qualified_action="lametric.message",
238 action_data=action_data,
239 )