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 14:29 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-25 14:29 +0000
1from __future__ import annotations
3import logging
4from typing import TYPE_CHECKING, Any
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
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
37if TYPE_CHECKING:
38 from custom_components.supernotify.envelope import Envelope
39 from custom_components.supernotify.hass_api import HomeAssistantAPI
41_LOGGER = logging.getLogger(__name__)
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"
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"
57class AlexaDevicesTransport(Transport):
58 """Notify via Home Assistant's built-in Alexa Devices integration
60 options:
61 message_usage: standard | use_title | combine_title
63 """
65 name = TRANSPORT_ALEXA
67 def __init__(self, *args: Any, **kwargs: Any) -> None:
68 super().__init__(*args, **kwargs)
70 @property
71 def supported_features(self) -> TransportFeature:
72 return TransportFeature.MESSAGE | TransportFeature.SPOKEN
74 @property
75 def inclusion_mode(self) -> list[str]:
76 # Notify Entity based
77 return [INCLUSION_DEFAULT]
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
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 ]
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
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
143 async def deliver(self, envelope: Envelope, debug_trace: DebugTrace | None = None) -> bool:
144 _LOGGER.debug("SUPERNOTIFY notify_alexa_devices: %s", envelope.message)
146 targets = envelope.target.entity_ids or []
148 if not targets:
149 _LOGGER.debug("SUPERNOTIFY Skipping alexa devices, no targets")
150 return False
152 action_data: dict[str, Any] = {ATTR_MESSAGE: envelope.message or ""}
153 target_data: dict[str, Any] = {ATTR_ENTITY_ID: targets}
155 return await self.call_action(envelope, action_data=action_data, target_data=target_data)