Coverage for custom_components/supernotify/transports/chime.py: 97%

265 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-09-25 14:29 +0000

1from __future__ import annotations 

2 

3import logging 

4from abc import abstractmethod 

5from dataclasses import dataclass, field 

6from typing import TYPE_CHECKING, Any, ClassVar, cast 

7 

8import voluptuous as vol 

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

10from homeassistant.const import ( # ATTR_VARIABLES from script.const has import issues 

11 ATTR_DEVICE_ID, 

12 ATTR_ENTITY_ID, 

13 CONF_ALIAS, 

14 CONF_DOMAIN, 

15 CONF_TARGET, 

16) 

17from homeassistant.exceptions import NoEntitySpecifiedError 

18from homeassistant.helpers import config_validation as cv 

19from voluptuous.humanize import humanize_error 

20 

21from custom_components.supernotify.const import ( 

22 ATTR_DATA, 

23 ATTR_MEDIA, 

24 ATTR_PRIORITY, 

25 CONF_DATA, 

26 CONF_DURATION, 

27 CONF_INCLUSION, 

28 CONF_TUNE, 

29 CONF_VOLUME, 

30 INCLUSION_EXPLICIT, 

31 OPTIONS_CHIME_DOMAINS, 

32 RE_DEVICE_ID, 

33 TRANSPORT_CHIME, 

34) 

35from custom_components.supernotify.model import ( 

36 DebugTrace, 

37 SelectionRule, 

38 TargetRequired, 

39 TransportConfig, 

40 TransportFeature, 

41) 

42from custom_components.supernotify.options import ( 

43 OPTION_DEVICE_DISCOVERY, 

44 OPTION_DEVICE_DOMAIN, 

45 OPTION_DEVICE_MODEL_SELECT, 

46 OPTION_TARGET_SELECT, 

47 SELECT_EXCLUDE, 

48 DeliveryOption, 

49) 

50from custom_components.supernotify.schema import DATA_SCHEMA, TARGET_SCHEMA 

51from custom_components.supernotify.target import Target, TargetEntityCategory 

52from custom_components.supernotify.transport import Transport 

53 

54if TYPE_CHECKING: 

55 from homeassistant.helpers.typing import ConfigType 

56 

57 from custom_components.supernotify.envelope import Envelope 

58 from custom_components.supernotify.hass_api import HomeAssistantAPI 

59 

60# kept in sync with RE_VALID_CHIME below and this transport's target_categories property 

61CHIME_ENTITY_DOMAINS = ["switch", "script", "group", "rest_command", "siren", "media_player"] 

62RE_VALID_CHIME = r"(switch|script|group|rest_command|siren|media_player)\.[A-Za-z0-9_]+" 

63 

64# extra standard delivery grouping every siren - see build_standard_deliveries() below 

65STANDARD_DELIVERY_SIREN_ALL = f"{TRANSPORT_CHIME}_siren_all" 

66 

67_LOGGER = logging.getLogger(__name__) 

68 

69# device-registry scan (see HomeAssistantAPI.discover_devices), independent of whether 

70# AlexaDevicesTransport itself is currently viable/loaded - HA doesn't reliably prune 

71# device registry entries when a config entry is unloaded, so chime can keep finding and 

72# targeting alexa_devices devices after that transport has gone away 

73DEVICE_DOMAINS = ["alexa_devices"] 

74 

75OPTION_CHIME_ALIASES = "chime_aliases" 

76CHIME_ALIASES_SCHEMA = vol.Schema({ 

77 vol.Required(OPTION_CHIME_ALIASES, default=dict): vol.Schema({ 

78 cv.string: vol.Schema({ 

79 cv.string: vol.Any( 

80 vol.Any(None, cv.string, vol.In(OPTIONS_CHIME_DOMAINS)), 

81 vol.Schema({ 

82 vol.Optional(CONF_ALIAS): cv.string, 

83 vol.Optional(CONF_DOMAIN): cv.string, 

84 vol.Optional(CONF_TUNE): cv.string, 

85 vol.Optional(CONF_DATA): DATA_SCHEMA, 

86 vol.Optional(CONF_VOLUME): float, 

87 vol.Optional(CONF_TARGET): TARGET_SCHEMA, 

88 vol.Optional(CONF_DURATION): cv.positive_int, 

89 }), 

90 ) 

91 }) 

92 }) 

93}) 

94 

95 

96@dataclass 

97class ActionCall: 

98 domain: str 

99 service: str 

100 action_data: dict[str, Any] | None = field(default_factory=dict) 

101 target_data: dict[str, Any] | None = field(default_factory=dict) 

102 

103 

104class ChimeTargetConfig: 

105 def __init__( 

106 self, 

107 entity_id: str | None = None, 

108 device_id: str | None = None, 

109 tune: str | None = None, 

110 duration: int | None = None, 

111 volume: float | None = None, 

112 data: dict[str, Any] | None = None, 

113 domain: str | None = None, 

114 **kwargs: Any, 

115 ) -> None: 

116 self.entity_id: str | None = entity_id 

117 self.device_id: str | None = device_id 

118 self.domain: str | None = None 

119 self.entity_name: str | None = None 

120 if self.entity_id and "." in self.entity_id: 

121 self.domain, self.entity_name = self.entity_id.split(".", 1) 

122 elif self.device_id: 

123 self.domain = domain 

124 else: 

125 _LOGGER.warning( 

126 "SUPERNOTIFY Invalid chime target, entity_id: %s, device_id %s, tune:%s", entity_id, device_id, tune 

127 ) 

128 raise NoEntitySpecifiedError("ChimeTargetConfig target must be entity_id or device_id") 

129 if kwargs: 

130 _LOGGER.warning("SUPERNOTIFY ChimeTargetConfig ignoring unexpected args: %s", kwargs) 

131 self.volume: float | None = volume 

132 self.tune: str | None = tune 

133 self.duration: int | None = duration 

134 self.data: dict[str, Any] | None = data or {} 

135 

136 def as_dict(self, **kwargs: Any) -> dict[str, Any]: 

137 return { 

138 "entity_id": self.entity_id, 

139 "device_id": self.device_id, 

140 "domain": self.domain, 

141 "tune": self.tune, 

142 "duration": self.duration, 

143 "volume": self.volume, 

144 "data": self.data, 

145 } 

146 

147 def __repr__(self) -> str: 

148 """Return a developer-oriented string representation of this ChimeTargetConfig""" 

149 if self.device_id is not None: 

150 return f"ChimeTargetConfig(device_id={self.device_id})" 

151 return f"ChimeTargetConfig(entity_id={self.entity_id})" 

152 

153 

154class MiniChimeTransport: 

155 domain: str 

156 

157 @abstractmethod 

158 def build( 

159 self, 

160 target_config: ChimeTargetConfig, 

161 action_data: dict[str, Any] | None = None, 

162 entity_name: str | None = None, 

163 envelope: Envelope | None = None, 

164 **_kwargs: Any, 

165 ) -> ActionCall | None: 

166 raise NotImplementedError() 

167 

168 

169class RestCommandChimeTransport(MiniChimeTransport): 

170 domain = "rest_command" 

171 

172 def build( 

173 self, 

174 target_config: ChimeTargetConfig, 

175 action_data: dict[str, Any] | None = None, 

176 entity_name: str | None = None, 

177 envelope: Envelope | None = None, 

178 **_kwargs: Any, 

179 ) -> ActionCall | None: 

180 if entity_name is None: 

181 _LOGGER.warning("SUPERNOTIFY rest_command chime target requires entity") 

182 return None 

183 output_data = dict(target_config.data) if target_config.data else {} 

184 return ActionCall(self.domain, entity_name, action_data=output_data) 

185 

186 

187class SwitchChimeTransport(MiniChimeTransport): 

188 domain = "switch" 

189 

190 def build( 

191 self, 

192 target_config: ChimeTargetConfig, 

193 action_data: dict[str, Any] | None = None, 

194 entity_name: str | None = None, 

195 envelope: Envelope | None = None, 

196 **_kwargs: Any, 

197 ) -> ActionCall | None: 

198 return ActionCall(self.domain, "turn_on", target_data={ATTR_ENTITY_ID: target_config.entity_id}) 

199 

200 

201class SirenChimeTransport(MiniChimeTransport): 

202 domain = "siren" 

203 

204 def build( 

205 self, 

206 target_config: ChimeTargetConfig, 

207 action_data: dict[str, Any] | None = None, 

208 entity_name: str | None = None, 

209 envelope: Envelope | None = None, 

210 **_kwargs: Any, 

211 ) -> ActionCall | None: 

212 output_data: dict[str, Any] = {ATTR_DATA: {}} 

213 if target_config.tune: 

214 output_data[ATTR_DATA]["tone"] = target_config.tune 

215 if target_config.duration is not None: 

216 output_data[ATTR_DATA]["duration"] = target_config.duration 

217 if target_config.volume is not None: 

218 output_data[ATTR_DATA]["volume_level"] = target_config.volume 

219 return ActionCall( 

220 self.domain, "turn_on", action_data=output_data, target_data={ATTR_ENTITY_ID: target_config.entity_id} 

221 ) 

222 

223 

224class ScriptChimeTransport(MiniChimeTransport): 

225 domain = "script" 

226 

227 def build( 

228 self, 

229 target_config: ChimeTargetConfig, 

230 action_data: dict[str, Any] | None = None, 

231 entity_name: str | None = None, 

232 envelope: Envelope | None = None, 

233 **_kwargs: Any, 

234 ) -> ActionCall | None: 

235 if entity_name is None: 

236 _LOGGER.warning("SUPERNOTIFY Script chime target requires entity") 

237 return None 

238 if envelope is None: 

239 _LOGGER.warning("SUPERNOTIFY Script chime target requires envelope") 

240 return None 

241 variables: dict[str, Any] = target_config.data or {} 

242 variables[ATTR_MESSAGE] = envelope.message 

243 variables[ATTR_TITLE] = envelope.title 

244 variables[ATTR_PRIORITY] = envelope.priority 

245 variables["chime_tune"] = target_config.tune 

246 variables["chime_volume"] = target_config.volume 

247 variables["chime_duration"] = target_config.duration 

248 output_data: dict[str, Any] = {"variables": variables} 

249 if envelope.delivery.debug: 

250 output_data["wait"] = envelope.delivery.debug 

251 # use `turn_on` rather than direct call to run script in background 

252 return ActionCall( 

253 self.domain, "turn_on", action_data=output_data, target_data={ATTR_ENTITY_ID: target_config.entity_id} 

254 ) 

255 

256 

257class AlexaDevicesChimeTransport(MiniChimeTransport): 

258 domain = "alexa_devices" 

259 

260 def build( 

261 self, 

262 target_config: ChimeTargetConfig, 

263 action_data: dict[str, Any] | None = None, 

264 entity_name: str | None = None, 

265 envelope: Envelope | None = None, 

266 **_kwargs: Any, 

267 ) -> ActionCall | None: 

268 output_data: dict[str, Any] = { 

269 "device_id": target_config.device_id, 

270 "sound": target_config.tune, 

271 } 

272 return ActionCall(self.domain, "send_sound", action_data=output_data) 

273 

274 

275class MediaPlayerChimeTransport(MiniChimeTransport): 

276 domain = "media_player" 

277 

278 def build( 

279 self, 

280 target_config: ChimeTargetConfig, 

281 action_data: dict[str, Any] | None = None, 

282 entity_name: str | None = None, 

283 envelope: Envelope | None = None, 

284 **_kwargs: Any, 

285 ) -> ActionCall | None: 

286 input_data = target_config.data or {} 

287 if action_data: 

288 input_data.update(action_data) 

289 output_data: dict[str, Any] = { 

290 "media": { 

291 "media_content_type": input_data.get(ATTR_MEDIA, {"media_content_type": "sound"}).get( 

292 "media_content_type", "sound" 

293 ), 

294 "media_content_id": target_config.tune, 

295 } 

296 } 

297 if input_data.get("enqueue") is not None: 

298 output_data["enqueue"] = input_data.get("enqueue") 

299 if input_data.get("announce") is not None: 

300 output_data["announce"] = input_data.get("announce") 

301 

302 return ActionCall( 

303 self.domain, "play_media", action_data=output_data, target_data={ATTR_ENTITY_ID: target_config.entity_id} 

304 ) 

305 

306 

307class ChimeTransport(Transport): 

308 name = TRANSPORT_CHIME 

309 declared_options: ClassVar[list[DeliveryOption]] = [ 

310 DeliveryOption(OPTION_CHIME_ALIASES, "Custom chime device aliases and their per-domain tuning"), 

311 ] 

312 

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

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

315 self.mini_transports: dict[str, MiniChimeTransport] = { 

316 t.domain: t 

317 for t in [ 

318 RestCommandChimeTransport(), 

319 SwitchChimeTransport(), 

320 SirenChimeTransport(), 

321 ScriptChimeTransport(), 

322 AlexaDevicesChimeTransport(), 

323 MediaPlayerChimeTransport(), 

324 ] 

325 } 

326 

327 def setup_delivery_options(self, options: dict[str, Any], delivery_name: str) -> dict[str, Any]: 

328 if OPTION_CHIME_ALIASES in options: 

329 chime_aliases: ConfigType = build_aliases(options[OPTION_CHIME_ALIASES]) 

330 if chime_aliases: 

331 _LOGGER.info("SUPERNOTIFY Set up %s chime aliases for %s", len(chime_aliases), delivery_name) 

332 else: 

333 _LOGGER.warning("SUPERNOTIFY Chime aliases for %s configured but not recognized", delivery_name) 

334 else: 

335 chime_aliases = {} 

336 _LOGGER.debug("SUPERNOTIFY No chime aliases configured for %s", delivery_name) 

337 return {"chime_aliases": chime_aliases} 

338 

339 @property 

340 def supported_features(self) -> TransportFeature: 

341 return TransportFeature.SOUND 

342 

343 def extra_attributes(self) -> dict[str, Any]: 

344 return {"mini_transports": [t.domain for t in self.mini_transports.values()]} 

345 

346 @property 

347 def default_config(self) -> TransportConfig: 

348 config = TransportConfig() 

349 config.delivery_defaults.target_required = TargetRequired.OPTIONAL 

350 config.delivery_defaults.inclusion = self.inclusion_mode 

351 config.delivery_defaults.options = { 

352 OPTION_TARGET_SELECT: [RE_VALID_CHIME, RE_DEVICE_ID], 

353 OPTION_DEVICE_DISCOVERY: True, 

354 OPTION_DEVICE_DOMAIN: DEVICE_DOMAINS, 

355 OPTION_DEVICE_MODEL_SELECT: {SELECT_EXCLUDE: ["Speaker Group"]}, 

356 } 

357 return config 

358 

359 @property 

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

361 return [TargetEntityCategory(domain=CHIME_ENTITY_DOMAINS), ATTR_DEVICE_ID] 

362 

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

364 return action is None 

365 

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

367 # an explicit delivery can supply its own chime_aliases regardless of whether the 

368 # transport-level default is configured - is_viable() can't see delivery-level 

369 # config, so it can't rule that out; DeliveryRegistry prunes this transport 

370 # entirely once it's confirmed no delivery (explicit or auto) actually uses it 

371 return True 

372 

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

374 """Its own default (only if chime_aliases is configured - with none, there's 

375 nothing to map a tune/priority to a target), plus "..._siren_all" - all siren.* 

376 entities, regardless of chime_aliases: unlike the other chime domains, 

377 SirenChimeTransport.build() doesn't need a tune/alias mapping to call 

378 siren.turn_on, so this extra doesn't share the alias requirement.""" 

379 deliveries: dict[str, ConfigType] = {} 

380 if OPTION_CHIME_ALIASES in self.delivery_defaults.options: 

381 deliveries[self.name] = {} 

382 siren_entity_ids = hass_api.entity_ids_for_domain("siren") 

383 if siren_entity_ids: 

384 deliveries[STANDARD_DELIVERY_SIREN_ALL] = { 

385 CONF_TARGET: {ATTR_ENTITY_ID: siren_entity_ids}, 

386 CONF_INCLUSION: [INCLUSION_EXPLICIT], 

387 } 

388 

389 return deliveries 

390 

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

392 data: dict[str, Any] = {} 

393 data.update(envelope.delivery.data) 

394 data.update(envelope.data or {}) 

395 target: Target = envelope.target 

396 

397 # chime_repeat = data.pop("chime_repeat", 1) 

398 chime_tune: str | None = data.pop("chime_tune", None) 

399 chime_volume: float | None = data.pop("chime_volume", None) 

400 chime_duration: int | None = data.pop("chime_duration", None) 

401 

402 _LOGGER.debug( 

403 "SUPERNOTIFY notify_chime: %s -> %s (delivery: %s, env_data:%s, dlv_data:%s)", 

404 chime_tune, 

405 target.entity_ids, 

406 envelope.delivery_name, 

407 envelope.data, 

408 envelope.delivery.data, 

409 ) 

410 # expand groups 

411 expanded_targets = { 

412 e: ChimeTargetConfig(tune=chime_tune, volume=chime_volume, duration=chime_duration, entity_id=e) 

413 for e in self.hass_api.expand_group(target.entity_ids) 

414 } 

415 model_filter = SelectionRule(envelope.delivery.options.get(OPTION_DEVICE_MODEL_SELECT)) 

416 

417 expanded_targets.update({ 

418 d: ChimeTargetConfig(tune=chime_tune, volume=chime_volume, duration=chime_duration, device_id=d) 

419 for d in target.device_ids 

420 if not (dev_entry := self.hass_api.find_device(d)) 

421 or model_filter.match(dev_entry.model if isinstance(dev_entry.model, str) else None) 

422 }) 

423 # resolve and include chime aliases 

424 expanded_targets.update( 

425 self.resolve_tune(chime_tune, envelope.delivery.transport_data.get("chime_aliases", {}), target) 

426 ) # overwrite and extend 

427 

428 chimes = 0 

429 if not expanded_targets: 

430 _LOGGER.info("SUPERNOTIFY Skipping chime, no targets") 

431 return False 

432 if debug_trace: 

433 debug_trace.record_delivery_artefact(envelope.delivery.name, "expanded_targets", expanded_targets) 

434 

435 for chime_entity_config in expanded_targets.values(): 

436 _LOGGER.debug("SUPERNOTIFY Chime %s: %s", chime_entity_config.entity_id, chime_entity_config.tune) 

437 try: 

438 action_call: ActionCall | None = self.analyze_target(chime_entity_config, data, envelope) 

439 if action_call is not None: 

440 if await self.call_action( 

441 envelope, 

442 qualified_action=f"{action_call.domain}.{action_call.service}", 

443 action_data=action_call.action_data, 

444 target_data=action_call.target_data, 

445 ): 

446 chimes += 1 

447 else: 

448 _LOGGER.debug("SUPERNOTIFY Chime skipping incomplete service for %s", chime_entity_config.entity_id) 

449 except Exception as e: 

450 _LOGGER.error( 

451 "SUPERNOTIFY Failed to chime %s: %s", 

452 chime_entity_config.entity_id, 

453 e, 

454 ) 

455 if debug_trace: 

456 debug_trace.record_delivery_exception(envelope.delivery.name, "chime_target", e) 

457 return chimes > 0 

458 

459 def analyze_target(self, target_config: ChimeTargetConfig, data: dict[str, Any], envelope: Envelope) -> ActionCall | None: 

460 

461 if not target_config.entity_id and not target_config.device_id: 

462 _LOGGER.warning("SUPERNOTIFY Empty chime target") 

463 return None 

464 

465 domain: str | None = None 

466 name: str | None = None 

467 

468 # Alexa Devices use device_id not entity_id for sounds 

469 # TODO: use method or delivery config vs fixed local constant for domains 

470 if target_config.device_id is not None and DEVICE_DOMAINS: 

471 if target_config.domain is not None and target_config.domain in DEVICE_DOMAINS: 

472 _LOGGER.debug(f"SUPERNOTIFY Chime selected target {domain} for {target_config.domain}") 

473 domain = target_config.domain 

474 else: 

475 domain = self.hass_api.domain_for_device(target_config.device_id, DEVICE_DOMAINS) 

476 _LOGGER.debug(f"SUPERNOTIFY Chime selected device {domain} for {target_config.device_id}") 

477 

478 elif target_config.entity_id and "." in target_config.entity_id: 

479 domain, name = target_config.entity_id.split(".", 1) 

480 if not domain: 

481 _LOGGER.warning("SUPERNOTIFY Unknown domain: %s", target_config) 

482 return None 

483 mini_transport: MiniChimeTransport | None = self.mini_transports.get(domain) 

484 if mini_transport is None: 

485 _LOGGER.warning( 

486 "SUPERNOTIFY No matching chime domain/tune: %s, target: %s, tune: %s", 

487 domain, 

488 target_config.entity_id, 

489 target_config.tune, 

490 ) 

491 return None 

492 

493 action_call: ActionCall | None = mini_transport.build( 

494 envelope=envelope, entity_name=name, action_data=data, target_config=target_config 

495 ) 

496 _LOGGER.debug("SUPERNOTIFY analyze_chime->%s", action_call) 

497 

498 return action_call 

499 

500 def resolve_tune( 

501 self, tune_or_alias: str | None, chime_config: dict[str, Any], target: Target | None = None 

502 ) -> dict[str, ChimeTargetConfig]: 

503 target_configs: dict[str, ChimeTargetConfig] = {} 

504 if tune_or_alias is not None: 

505 for alias_config in chime_config.get(tune_or_alias, {}).values(): 

506 alias_target: Target | None = alias_config.get(CONF_TARGET, None) 

507 alias_kwargs: dict[str, Any] = {k: v for k, v in alias_config.items() if k != CONF_TARGET} 

508 # pass through variables or data if present 

509 if alias_target is not None: 

510 target_configs.update({t: ChimeTargetConfig(entity_id=t, **alias_kwargs) for t in alias_target.entity_ids}) 

511 target_configs.update({t: ChimeTargetConfig(device_id=t, **alias_kwargs) for t in alias_target.device_ids}) 

512 elif alias_config[CONF_DOMAIN] in DEVICE_DOMAINS and target is not None: 

513 # bulk apply to all known target devices of this domain 

514 bulk_apply = { 

515 dev: ChimeTargetConfig(device_id=dev, **alias_kwargs) 

516 for dev in target.device_ids 

517 if dev not in target_configs # don't overwrite existing specific targets 

518 and ATTR_DEVICE_ID not in alias_config 

519 } 

520 # TODO: Constrain to device domain 

521 target_configs.update(bulk_apply) 

522 elif target is not None: 

523 # bulk apply to all known target entities of this domain 

524 bulk_apply = { 

525 ent: ChimeTargetConfig(entity_id=ent, **alias_kwargs) 

526 for ent in target.entity_ids 

527 if ent.startswith(f"{alias_config[CONF_DOMAIN]}.") 

528 and ent not in target_configs # don't overwrite existing specific targets 

529 and ATTR_ENTITY_ID not in alias_config 

530 } 

531 target_configs.update(bulk_apply) 

532 _LOGGER.debug("SUPERNOTIFY transport_chime: Resolved tune %s to %s", tune_or_alias, target_configs) 

533 return target_configs 

534 

535 

536def build_aliases(src_config: ConfigType) -> ConfigType: 

537 dest_config: dict[str, Any] = {} 

538 try: 

539 validated: ConfigType = CHIME_ALIASES_SCHEMA({OPTION_CHIME_ALIASES: src_config}) 

540 for alias, alias_config in validated[OPTION_CHIME_ALIASES].items(): 

541 alias_config = alias_config or {} 

542 for domain_or_label, domain_config in alias_config.items(): 

543 domain_config = domain_config or {} 

544 if isinstance(domain_config, str): 

545 domain_config = {CONF_TUNE: domain_config} 

546 domain_config = cast("dict[str, Any]", domain_config) 

547 domain_config.setdefault(CONF_TUNE, alias) 

548 if domain_or_label in OPTIONS_CHIME_DOMAINS: 

549 domain_config.setdefault(CONF_DOMAIN, domain_or_label) 

550 

551 try: 

552 if domain_config.get(CONF_TARGET): 

553 domain_config[CONF_TARGET] = Target(domain_config[CONF_TARGET]) 

554 if not domain_config[CONF_TARGET].has_targets(): 

555 _LOGGER.warning("SUPERNOTIFY Chime alias %s has empty target", alias) 

556 elif domain_config[CONF_TARGET].has_unknown_targets(): 

557 _LOGGER.warning("SUPERNOTIFY Chime alias %s has unknown targets", alias) 

558 dest_config.setdefault(alias, {}) 

559 dest_config[alias][domain_or_label] = domain_config 

560 except Exception: 

561 _LOGGER.exception("SUPERNOTIFY Chime alias %s has invalid target", alias) 

562 

563 except vol.Invalid as ve: 

564 _LOGGER.error("SUPERNOTIFY Chime alias configuration error: %s", ve) 

565 _LOGGER.error("SUPERNOTIFY %s", humanize_error(src_config, ve)) 

566 except Exception: 

567 _LOGGER.exception("SUPERNOTIFY Chime alias unexpected error") 

568 return dest_config