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
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-08 17:42 +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, ATTR_TITLE
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_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
33if TYPE_CHECKING:
34 from custom_components.supernotify.envelope import Envelope
35 from custom_components.supernotify.hass_api import HomeAssistantAPI
37_LOGGER = logging.getLogger(__name__)
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"
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"
50class AlexaDevicesTransport(Transport):
51 """Notify via Home Assistant's built-in Alexa Devices integration
53 options:
54 message_usage: standard | use_title | combine_title | message_only
56 """
58 name = TRANSPORT_ALEXA
59 integration_domain = HA_ALEXA_DEVICES_DOMAIN
61 def __init__(self, *args: Any, **kwargs: Any) -> None:
62 super().__init__(*args, **kwargs)
64 @property
65 def supported_features(self) -> TransportFeature:
66 return TransportFeature.MESSAGE | TransportFeature.TITLE | TransportFeature.SPOKEN
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
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 ]
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
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
128 async def deliver(self, envelope: Envelope, debug_trace: DebugTrace | None = None) -> bool:
129 _LOGGER.debug("SUPERNOTIFY notify_alexa_devices: %s", envelope.message)
131 targets = envelope.target.entity_ids or []
133 if not targets:
134 _LOGGER.debug("SUPERNOTIFY Skipping alexa devices, no targets")
135 return False
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}
144 return await self.call_action(envelope, action_data=action_data, target_data=target_data)