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

1"""LaMetric native transport for SuperNotify. 

2 

3Sends messages and charts to LaMetric smart displays using the 

4HA lametric integration services (lametric.message, lametric.chart). 

5 

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. 

9 

10Target: NEVER — LaMetric is a fixed device, not person-routed. 

11The device_id must be specified in delivery config data: 

12 

13 deliveries: 

14 - name: lametric 

15 transport: lametric 

16 data: 

17 device_id: "49b6e2186ef37e164818aacb9cea1f53" 

18 

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. 

49 

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""" 

57 

58from __future__ import annotations 

59 

60import logging 

61from typing import TYPE_CHECKING, Any 

62 

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 

72 

73if TYPE_CHECKING: 

74 from custom_components.supernotify.envelope import Envelope 

75 

76_LOGGER = logging.getLogger(__name__) 

77 

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} 

86 

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} 

95 

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} 

104 

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} 

113 

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} 

122 

123 

124class LaMetricTransport(Transport): 

125 """LaMetric smart display transport for SuperNotify. 

126 

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 """ 

131 

132 name = TRANSPORT_LAMETRIC 

133 

134 def __init__(self, *args: Any, **kwargs: Any) -> None: 

135 super().__init__(*args, **kwargs) 

136 

137 @property 

138 def supported_features(self) -> TransportFeature: 

139 return TransportFeature.MESSAGE | TransportFeature.TITLE 

140 

141 @property 

142 def default_config(self) -> TransportConfig: 

143 config = TransportConfig() 

144 config.delivery_defaults.target_required = TargetRequired.NEVER 

145 return config 

146 

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 

150 

151 async def deliver(self, envelope: Envelope, debug_trace: DebugTrace | None = None) -> bool: # noqa: ARG002 

152 _LOGGER.debug("SUPERNOTIFY lametric %s", envelope.message) 

153 

154 # 1. Extract raw data (flat dict — rule #6) 

155 raw_data: dict[str, Any] = dict(envelope.data) if envelope.data else {} 

156 

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 

165 

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) 

173 

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) 

181 

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 

184 

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 

193 

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 

203 

204 return await self.call_action( 

205 envelope, 

206 qualified_action="lametric.chart", 

207 action_data=action_data, 

208 ) 

209 

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 

222 

223 return await self.call_action( 

224 envelope, 

225 qualified_action="lametric.message", 

226 action_data=action_data, 

227 )