Coverage for custom_components/supernotify/transports/lametric.py: 100%

68 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-09-25 21:14 +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 homeassistant.helpers.typing import ConfigType 

64 

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 

74 

75if TYPE_CHECKING: 

76 from custom_components.supernotify.envelope import Envelope 

77 from custom_components.supernotify.hass_api import HomeAssistantAPI 

78 

79_LOGGER = logging.getLogger(__name__) 

80 

81HA_LAMETRIC_DOMAIN = "lametric" 

82 

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} 

91 

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} 

100 

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} 

109 

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} 

118 

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} 

127 

128 

129class LaMetricTransport(Transport): 

130 """LaMetric smart display transport for SuperNotify. 

131 

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

136 

137 name = TRANSPORT_LAMETRIC 

138 

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

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

141 

142 @property 

143 def supported_features(self) -> TransportFeature: 

144 return TransportFeature.MESSAGE | TransportFeature.TITLE 

145 

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 

152 

153 def is_viable(self, hass_api: HomeAssistantAPI) -> bool: 

154 return hass_api.find_config_entry_data(HA_LAMETRIC_DOMAIN) is not None 

155 

156 def build_standard_deliveries(self, hass_api: HomeAssistantAPI) -> dict[str, ConfigType]: 

157 return {self.name: {}} 

158 

159 def validate_action(self, action: str | None) -> bool: 

160 # No external action required - transport uses lametric.message / lametric.chart directly 

161 return True 

162 

163 async def deliver(self, envelope: Envelope, debug_trace: DebugTrace | None = None) -> bool: 

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

165 

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

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

168 

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 

177 

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) 

185 

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) 

193 

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 

196 

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 

205 

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 

215 

216 return await self.call_action( 

217 envelope, 

218 qualified_action="lametric.chart", 

219 action_data=action_data, 

220 ) 

221 

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 

234 

235 return await self.call_action( 

236 envelope, 

237 qualified_action="lametric.message", 

238 action_data=action_data, 

239 )