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

63 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-08 17:42 +0000

1from __future__ import annotations 

2 

3import logging 

4from typing import TYPE_CHECKING, Any 

5 

6from homeassistant.components.group.const import DOMAIN as HA_GROUP_DOMAIN 

7from homeassistant.components.notify.const import ATTR_MESSAGE, ATTR_TITLE 

8from homeassistant.const import ATTR_ENTITY_ID, CONF_TARGET 

9from homeassistant.helpers.typing import ConfigType 

10 

11from custom_components.supernotify.const import ( 

12 CONF_INCLUSION, 

13 INCLUSION_EXPLICIT, 

14 RE_NOTIFY_ENTITY_ID, 

15 TRANSPORT_ALEXA, 

16) 

17from custom_components.supernotify.model import ( 

18 DebugTrace, 

19 MessageOnlyPolicy, 

20 TransportConfig, 

21 TransportFeature, 

22) 

23from custom_components.supernotify.options import ( 

24 OPTION_MESSAGE_USAGE, 

25 OPTION_SIMPLIFY_TEXT, 

26 OPTION_STRIP_URLS, 

27 OPTION_TARGET_SELECT, 

28 SELECT_EXCLUDE, 

29) 

30from custom_components.supernotify.target import TargetEntityCategory 

31from custom_components.supernotify.transport import Transport 

32 

33if TYPE_CHECKING: 

34 from custom_components.supernotify.envelope import Envelope 

35 from custom_components.supernotify.hass_api import HomeAssistantAPI 

36 

37_LOGGER = logging.getLogger(__name__) 

38 

39HA_ALEXA_DEVICES_DOMAIN = "alexa_devices" 

40# the entity registry platform for notify entities the integration itself creates - 

41# the same name as the config entry/integration domain above 

42HA_ALEXA_DEVICES_PLATFORM = "alexa_devices" 

43 

44# extra standard deliveries grouping notify entities by naming convention - see 

45# build_standard_deliveries() below 

46STANDARD_DELIVERY_SPEAK_ALL = f"{TRANSPORT_ALEXA}_speak_all" 

47STANDARD_DELIVERY_ANNOUNCE_ALL = f"{TRANSPORT_ALEXA}_announce_all" 

48 

49 

50class AlexaDevicesTransport(Transport): 

51 """Notify via Home Assistant's built-in Alexa Devices integration 

52 

53 options: 

54 message_usage: standard | use_title | combine_title | message_only 

55 

56 """ 

57 

58 name = TRANSPORT_ALEXA 

59 integration_domain = HA_ALEXA_DEVICES_DOMAIN 

60 

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

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

63 

64 @property 

65 def supported_features(self) -> TransportFeature: 

66 return TransportFeature.MESSAGE | TransportFeature.TITLE | TransportFeature.SPOKEN 

67 

68 @property 

69 def default_config(self) -> TransportConfig: 

70 config = super().default_config 

71 config.delivery_defaults.action = "notify.send_message" 

72 config.delivery_defaults.options = { 

73 OPTION_SIMPLIFY_TEXT: True, 

74 OPTION_STRIP_URLS: True, 

75 OPTION_MESSAGE_USAGE: MessageOnlyPolicy.STANDARD, 

76 # an HA group (not owned by any platform) or one of this integration's own 

77 # notify entities (identified by platform, not just its entity_id shape) 

78 OPTION_TARGET_SELECT: [r"group\.[a-z0-9_]+", RE_NOTIFY_ENTITY_ID], 

79 } 

80 return config 

81 

82 @property 

83 def unique_target_categories(self) -> list[str | TargetEntityCategory]: 

84 return [ 

85 TargetEntityCategory(domain="notify", platform=HA_ALEXA_DEVICES_PLATFORM), 

86 # an HA group isn't owned by any platform - membership/expansion isn't handled 

87 # here yet (only chime.py does that), so it's accepted at face value 

88 TargetEntityCategory(domain=HA_GROUP_DOMAIN), 

89 ] 

90 

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

92 if hass_api.find_config_entry_data(HA_ALEXA_DEVICES_DOMAIN) is None: 

93 _LOGGER.debug("SUPERNOTIFY No config entry data found for %s", HA_ALEXA_DEVICES_DOMAIN) 

94 return False 

95 # integration installed but no Alexa device has registered a notify entity yet 

96 if hass_api.entity_ids_for_platform("notify", HA_ALEXA_DEVICES_PLATFORM): 

97 return True 

98 _LOGGER.debug("SUPERNOTIFY No notify entities found for %s", HA_ALEXA_DEVICES_PLATFORM) 

99 return False 

100 

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

102 """Its own default, plus "..._speak_all"/"..._announce_all" - explicit-only 

103 groupings of notify entities whose entity_id follows the Alexa Devices 

104 integration's own "_speak"/"_announce" naming convention for spoken-only vs. full 

105 announcement chime+speech. Each extra is only built if at least one matching 

106 entity exists.""" 

107 deliveries: dict[str, ConfigType] = {self.name: {}} 

108 # speaker groups get their own "_announce"/"_speak" notify entities alongside their 

109 # member devices, but can't be expanded to their members here (see target_categories 

110 # below), so they're excluded to avoid double notification of the same group members 

111 entity_ids = hass_api.entity_ids_for_platform( 

112 "notify", HA_ALEXA_DEVICES_PLATFORM, device_model_select={SELECT_EXCLUDE: ["Speaker Group"]} 

113 ) 

114 speak_entities = [e for e in entity_ids if "_speak" in e] 

115 if speak_entities: 

116 deliveries[STANDARD_DELIVERY_SPEAK_ALL] = { 

117 CONF_TARGET: {ATTR_ENTITY_ID: speak_entities}, 

118 CONF_INCLUSION: [INCLUSION_EXPLICIT], 

119 } 

120 announce_entities = [e for e in entity_ids if "_announce" in e] 

121 if announce_entities: 

122 deliveries[STANDARD_DELIVERY_ANNOUNCE_ALL] = { 

123 CONF_TARGET: {ATTR_ENTITY_ID: announce_entities}, 

124 CONF_INCLUSION: [INCLUSION_EXPLICIT], 

125 } 

126 return deliveries 

127 

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

129 _LOGGER.debug("SUPERNOTIFY notify_alexa_devices: %s", envelope.message) 

130 

131 targets = envelope.target.entity_ids or [] 

132 

133 if not targets: 

134 _LOGGER.debug("SUPERNOTIFY Skipping alexa devices, no targets") 

135 return False 

136 

137 action_data: dict[str, Any] = {ATTR_MESSAGE: envelope.message or ""} 

138 if envelope.title: 

139 # shown on an Echo's screen by its `announce` entity, from Home Assistant 2026.11 - Home 

140 # Assistant only passes it on to an entity that supports a title, so not to `speak` 

141 action_data[ATTR_TITLE] = envelope.title 

142 target_data: dict[str, Any] = {ATTR_ENTITY_ID: targets} 

143 

144 return await self.call_action(envelope, action_data=action_data, target_data=target_data)