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

68 statements  

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

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_DEFAULT, 

14 INCLUSION_EXPLICIT, 

15 RE_NOTIFY_ENTITY_ID, 

16 TRANSPORT_ALEXA, 

17) 

18from custom_components.supernotify.model import ( 

19 DebugTrace, 

20 MessageOnlyPolicy, 

21 TargetRequired, 

22 TransportConfig, 

23 TransportFeature, 

24) 

25from custom_components.supernotify.options import ( 

26 OPTION_MESSAGE_USAGE, 

27 OPTION_SIMPLIFY_TEXT, 

28 OPTION_STRIP_URLS, 

29 OPTION_TARGET_SELECT, 

30 OPTION_UNIQUE_TARGETS, 

31 SELECT_EXCLUDE, 

32) 

33from custom_components.supernotify.schema import SelectionRank 

34from custom_components.supernotify.target import TargetEntityCategory 

35from custom_components.supernotify.transport import Transport 

36 

37if TYPE_CHECKING: 

38 from custom_components.supernotify.envelope import Envelope 

39 from custom_components.supernotify.hass_api import HomeAssistantAPI 

40 

41_LOGGER = logging.getLogger(__name__) 

42 

43HA_ALEXA_DEVICES_DOMAIN = "alexa_devices" 

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

45# singular, unlike the (plural) config entry/integration domain above 

46HA_ALEXA_DEVICES_PLATFORM = "alexa_devices" 

47# alandtse/alexa_media_player HACS integration's notify platform module - kept in sync 

48# with the constant of the same name in alexa_media_player.py 

49HA_ALEXA_MEDIA_PLAYER_MODULE = "custom_components.alexa_media.notify" 

50 

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

52# build_standard_deliveries() below 

53STANDARD_DELIVERY_SPEAK_ALL = f"{TRANSPORT_ALEXA}_speak_all" 

54STANDARD_DELIVERY_ANNOUNCE_ALL = f"{TRANSPORT_ALEXA}_announce_all" 

55 

56 

57class AlexaDevicesTransport(Transport): 

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

59 

60 options: 

61 message_usage: standard | use_title | combine_title 

62 

63 """ 

64 

65 name = TRANSPORT_ALEXA 

66 

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

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

69 

70 @property 

71 def supported_features(self) -> TransportFeature: 

72 return TransportFeature.MESSAGE | TransportFeature.SPOKEN 

73 

74 @property 

75 def inclusion_mode(self) -> list[str]: 

76 # Notify Entity based 

77 return [INCLUSION_DEFAULT] 

78 

79 @property 

80 def default_config(self) -> TransportConfig: 

81 config = TransportConfig() 

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

83 config.delivery_defaults.target_required = TargetRequired.ALWAYS 

84 config.delivery_defaults.selection_rank = SelectionRank.FIRST 

85 config.delivery_defaults.inclusion = self.inclusion_mode 

86 config.delivery_defaults.options = { 

87 OPTION_SIMPLIFY_TEXT: True, 

88 OPTION_STRIP_URLS: True, 

89 OPTION_MESSAGE_USAGE: MessageOnlyPolicy.STANDARD, 

90 OPTION_UNIQUE_TARGETS: True, 

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

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

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

94 } 

95 return config 

96 

97 @property 

98 def target_categories(self) -> list[str | TargetEntityCategory]: 

99 return [ 

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

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

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

103 TargetEntityCategory(domain=HA_GROUP_DOMAIN), 

104 ] 

105 

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

107 if hass_api.find_config_entry_data(HA_ALEXA_DEVICES_DOMAIN) is None: 

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

109 return False 

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

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

112 return True 

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

114 return False 

115 

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

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

118 groupings of notify entities whose entity_id follows the Alexa Devices 

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

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

121 entity exists.""" 

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

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

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

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

126 entity_ids = hass_api.entity_ids_for_platform( 

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

128 ) 

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

130 if speak_entities: 

131 deliveries[STANDARD_DELIVERY_SPEAK_ALL] = { 

132 CONF_TARGET: {ATTR_ENTITY_ID: speak_entities}, 

133 CONF_INCLUSION: [INCLUSION_EXPLICIT], 

134 } 

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

136 if announce_entities: 

137 deliveries[STANDARD_DELIVERY_ANNOUNCE_ALL] = { 

138 CONF_TARGET: {ATTR_ENTITY_ID: announce_entities}, 

139 CONF_INCLUSION: [INCLUSION_EXPLICIT], 

140 } 

141 return deliveries 

142 

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

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

145 

146 targets = envelope.target.entity_ids or [] 

147 

148 if not targets: 

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

150 return False 

151 

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

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

154 

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