Home Assistant Unofficial Reference 2024.12.1
entities.py
Go to the documentation of this file.
1 """Alexa entity adapters."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Generator, Iterable
6 import logging
7 from typing import TYPE_CHECKING, Any
8 
9 from homeassistant.components import (
10  alarm_control_panel,
11  alert,
12  automation,
13  binary_sensor,
14  button,
15  camera,
16  climate,
17  cover,
18  event,
19  fan,
20  group,
21  humidifier,
22  image_processing,
23  input_boolean,
24  input_button,
25  input_number,
26  light,
27  lock,
28  media_player,
29  number,
30  remote,
31  scene,
32  script,
33  sensor,
34  switch,
35  timer,
36  vacuum,
37  valve,
38  water_heater,
39 )
40 from homeassistant.const import (
41  ATTR_DEVICE_CLASS,
42  ATTR_SUPPORTED_FEATURES,
43  ATTR_UNIT_OF_MEASUREMENT,
44  CLOUD_NEVER_EXPOSED_ENTITIES,
45  CONF_DESCRIPTION,
46  CONF_NAME,
47  UnitOfTemperature,
48  __version__,
49 )
50 from homeassistant.core import HomeAssistant, State, callback
51 from homeassistant.helpers import network
52 from homeassistant.helpers.entity import entity_sources
53 from homeassistant.util.decorator import Registry
54 
55 from .capabilities import (
56  Alexa,
57  AlexaBrightnessController,
58  AlexaCameraStreamController,
59  AlexaCapability,
60  AlexaChannelController,
61  AlexaColorController,
62  AlexaColorTemperatureController,
63  AlexaContactSensor,
64  AlexaDoorbellEventSource,
65  AlexaEndpointHealth,
66  AlexaEqualizerController,
67  AlexaEventDetectionSensor,
68  AlexaInputController,
69  AlexaLockController,
70  AlexaModeController,
71  AlexaMotionSensor,
72  AlexaPlaybackController,
73  AlexaPlaybackStateReporter,
74  AlexaPowerController,
75  AlexaRangeController,
76  AlexaSceneController,
77  AlexaSecurityPanelController,
78  AlexaSeekController,
79  AlexaSpeaker,
80  AlexaStepSpeaker,
81  AlexaTemperatureSensor,
82  AlexaThermostatController,
83  AlexaTimeHoldController,
84  AlexaToggleController,
85 )
86 from .const import CONF_DISPLAY_CATEGORIES
87 
88 if TYPE_CHECKING:
89  from .config import AbstractConfig
90 
91 _LOGGER = logging.getLogger(__name__)
92 
93 ENTITY_ADAPTERS: Registry[str, type[AlexaEntity]] = Registry()
94 
95 TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None)
96 
97 
99  """Possible display categories for Discovery response.
100 
101  https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
102  """
103 
104  # Describes a combination of devices set to a specific state, when the
105  # state change must occur in a specific order. For example, a "watch
106  # Netflix" scene might require the: 1. TV to be powered on & 2. Input set
107  # to HDMI1. Applies to Scenes
108  ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
109 
110  # Indicates a device that cools the air in interior spaces.
111  AIR_CONDITIONER = "AIR_CONDITIONER"
112 
113  # Indicates a device that emits pleasant odors and masks unpleasant
114  # odors in interior spaces.
115  AIR_FRESHENER = "AIR_FRESHENER"
116 
117  # Indicates a device that improves the quality of air in interior spaces.
118  AIR_PURIFIER = "AIR_PURIFIER"
119 
120  # Indicates a smart device in an automobile, such as a dash camera.
121  AUTO_ACCESSORY = "AUTO_ACCESSORY"
122 
123  # Indicates a security device with video or photo functionality.
124  CAMERA = "CAMERA"
125 
126  # Indicates a religious holiday decoration that often contains lights.
127  CHRISTMAS_TREE = "CHRISTMAS_TREE"
128 
129  # Indicates a device that makes coffee.
130  COFFEE_MAKER = "COFFEE_MAKER"
131 
132  # Indicates a non-mobile computer, such as a desktop computer.
133  COMPUTER = "COMPUTER"
134 
135  # Indicates an endpoint that detects and reports contact.
136  CONTACT_SENSOR = "CONTACT_SENSOR"
137 
138  # Indicates a door.
139  DOOR = "DOOR"
140 
141  # Indicates a doorbell.
142  DOORBELL = "DOORBELL"
143 
144  # Indicates a window covering on the outside of a structure.
145  EXTERIOR_BLIND = "EXTERIOR_BLIND"
146 
147  # Indicates a fan.
148  FAN = "FAN"
149 
150  # Indicates a game console, such as Microsoft Xbox or Nintendo Switch
151  GAME_CONSOLE = "GAME_CONSOLE"
152 
153  # Indicates a garage door.
154  # Garage doors must implement the ModeController interface to
155  # open and close the door.
156  GARAGE_DOOR = "GARAGE_DOOR"
157 
158  # Indicates a wearable device that transmits audio directly into the ear.
159  HEADPHONES = "HEADPHONES"
160 
161  # Indicates a smart-home hub.
162  HUB = "HUB"
163 
164  # Indicates a window covering on the inside of a structure.
165  INTERIOR_BLIND = "INTERIOR_BLIND"
166 
167  # Indicates a laptop or other mobile computer.
168  LAPTOP = "LAPTOP"
169 
170  # Indicates light sources or fixtures.
171  LIGHT = "LIGHT"
172 
173  # Indicates a microwave oven.
174  MICROWAVE = "MICROWAVE"
175 
176  # Indicates a mobile phone.
177  MOBILE_PHONE = "MOBILE_PHONE"
178 
179  # Indicates an endpoint that detects and reports motion.
180  MOTION_SENSOR = "MOTION_SENSOR"
181 
182  # Indicates a network-connected music system.
183  MUSIC_SYSTEM = "MUSIC_SYSTEM"
184 
185  # Indicates a network router.
186  NETWORK_HARDWARE = "NETWORK_HARDWARE"
187 
188  # An endpoint that cannot be described in on of the other categories.
189  OTHER = "OTHER"
190 
191  # Indicates an oven cooking appliance.
192  OVEN = "OVEN"
193 
194  # Indicates a non-mobile phone, such as landline or an IP phone.
195  PHONE = "PHONE"
196 
197  # Indicates a device that prints.
198  PRINTER = "PRINTER"
199 
200  # Indicates a decive that support stateless events,
201  # such as remote switches and smart buttons.
202  REMOTE = "REMOTE"
203 
204  # Indicates a network router.
205  ROUTER = "ROUTER"
206 
207  # Describes a combination of devices set to a specific state, when the
208  # order of the state change is not important. For example a bedtime scene
209  # might include turning off lights and lowering the thermostat, but the
210  # order is unimportant. Applies to Scenes
211  SCENE_TRIGGER = "SCENE_TRIGGER"
212 
213  # Indicates a projector screen.
214  SCREEN = "SCREEN"
215 
216  # Indicates a security panel.
217  SECURITY_PANEL = "SECURITY_PANEL"
218 
219  # Indicates a security system.
220  SECURITY_SYSTEM = "SECURITY_SYSTEM"
221 
222  # Indicates an electric cooking device that sits on a countertop,
223  # cooks at low temperatures, and is often shaped like a cooking pot.
224  SLOW_COOKER = "SLOW_COOKER"
225 
226  # Indicates an endpoint that locks.
227  SMARTLOCK = "SMARTLOCK"
228 
229  # Indicates modules that are plugged into an existing electrical outlet.
230  # Can control a variety of devices.
231  SMARTPLUG = "SMARTPLUG"
232 
233  # Indicates the endpoint is a speaker or speaker system.
234  SPEAKER = "SPEAKER"
235 
236  # Indicates a streaming device such as Apple TV, Chromecast, or Roku.
237  STREAMING_DEVICE = "STREAMING_DEVICE"
238 
239  # Indicates in-wall switches wired to the electrical system. Can control a
240  # variety of devices.
241  SWITCH = "SWITCH"
242 
243  # Indicates a tablet computer.
244  TABLET = "TABLET"
245 
246  # Indicates endpoints that report the temperature only.
247  TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR"
248 
249  # Indicates endpoints that control temperature, stand-alone air
250  # conditioners, or heaters with direct temperature control.
251  THERMOSTAT = "THERMOSTAT"
252 
253  # Indicates the endpoint is a television.
254  TV = "TV"
255 
256  # Indicates a vacuum cleaner.
257  VACUUM_CLEANER = "VACUUM_CLEANER"
258 
259  # Indicates a water heater.
260  WATER_HEATER = "WATER_HEATER"
261 
262  # Indicates a network-connected wearable device, such as an Apple Watch,
263  # Fitbit, or Samsung Gear.
264  WEARABLE = "WEARABLE"
265 
266 
268  """An adaptation of an entity, expressed in Alexa's terms.
269 
270  The API handlers should manipulate entities only through this interface.
271  """
272 
273  def __init__(
274  self, hass: HomeAssistant, config: AbstractConfig, entity: State
275  ) -> None:
276  """Initialize Alexa Entity."""
277  self.hasshass = hass
278  self.configconfig = config
279  self.entityentity = entity
280  self.entity_confentity_conf = config.entity_config.get(entity.entity_id, {})
281 
282  @property
283  def entity_id(self) -> str:
284  """Return the Entity ID."""
285  return self.entityentity.entity_id
286 
287  def friendly_name(self) -> str:
288  """Return the Alexa API friendly name."""
289  friendly_name: str = self.entity_confentity_conf.get(
290  CONF_NAME, self.entityentity.name
291  ).translate(TRANSLATION_TABLE)
292  return friendly_name
293 
294  def description(self) -> str:
295  """Return the Alexa API description."""
296  description = self.entity_confentity_conf.get(CONF_DESCRIPTION) or self.entity_identity_id
297  return f"{description} via Home Assistant".translate(TRANSLATION_TABLE)
298 
299  def alexa_id(self) -> str:
300  """Return the Alexa API entity id."""
301  return self.configconfig.generate_alexa_id(self.entityentity.entity_id)
302 
303  def display_categories(self) -> list[str] | None:
304  """Return a list of display categories."""
305  entity_conf = self.configconfig.entity_config.get(self.entityentity.entity_id, {})
306  if CONF_DISPLAY_CATEGORIES in entity_conf:
307  return [entity_conf[CONF_DISPLAY_CATEGORIES]]
308  return self.default_display_categoriesdefault_display_categories()
309 
310  def default_display_categories(self) -> list[str] | None:
311  """Return a list of default display categories.
312 
313  This can be overridden by the user in the Home Assistant configuration.
314 
315  See also DisplayCategory.
316  """
317  raise NotImplementedError
318 
319  def interfaces(self) -> Iterable[AlexaCapability]:
320  """Return a list of supported interfaces.
321 
322  Used for discovery. The list should contain AlexaInterface instances.
323  If the list is empty, this entity will not be discovered.
324  """
325  raise NotImplementedError
326 
327  def serialize_properties(self) -> Generator[dict[str, Any]]:
328  """Yield each supported property in API format."""
329  for interface in self.interfacesinterfaces():
330  if not interface.properties_proactively_reported():
331  continue
332 
333  yield from interface.serialize_properties()
334 
335  def serialize_discovery(self) -> dict[str, Any]:
336  """Serialize the entity for discovery."""
337  result: dict[str, Any] = {
338  "displayCategories": self.display_categoriesdisplay_categories(),
339  "cookie": {},
340  "endpointId": self.alexa_idalexa_id(),
341  "friendlyName": self.friendly_namefriendly_name(),
342  "description": self.descriptiondescription(),
343  "manufacturerName": "Home Assistant",
344  "additionalAttributes": {
345  "manufacturer": "Home Assistant",
346  "model": self.entityentity.domain,
347  "softwareVersion": __version__,
348  "customIdentifier": f"{self.config.user_identifier()}-{self.entity_id}",
349  },
350  }
351 
352  locale = self.configconfig.locale
353  capabilities = []
354 
355  for i in self.interfacesinterfaces():
356  if locale not in i.supported_locales:
357  continue
358 
359  try:
360  capabilities.append(i.serialize_discovery())
361  except Exception:
362  _LOGGER.exception(
363  "Error serializing %s discovery for %s", i.name(), self.entityentity
364  )
365 
366  result["capabilities"] = capabilities
367 
368  return result
369 
370 
371 @callback
373  hass: HomeAssistant, config: AbstractConfig
374 ) -> list[AlexaEntity]:
375  """Return all entities that are supported by Alexa."""
376  entities: list[AlexaEntity] = []
377  for state in hass.states.async_all():
378  if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
379  continue
380 
381  if state.domain not in ENTITY_ADAPTERS:
382  continue
383 
384  try:
385  alexa_entity = ENTITY_ADAPTERS[state.domain](hass, config, state)
386  interfaces = list(alexa_entity.interfaces())
387  except Exception:
388  _LOGGER.exception("Unable to serialize %s for discovery", state.entity_id)
389  else:
390  if not interfaces:
391  continue
392  entities.append(alexa_entity)
393 
394  return entities
395 
396 
397 @ENTITY_ADAPTERS.register(alert.DOMAIN)
398 @ENTITY_ADAPTERS.register(automation.DOMAIN)
399 @ENTITY_ADAPTERS.register(group.DOMAIN)
401  """A generic, on/off device.
402 
403  The choice of last resort.
404  """
405 
406  def default_display_categories(self) -> list[str]:
407  """Return the display categories for this entity."""
408  if self.entityentity.domain == automation.DOMAIN:
409  return [DisplayCategory.ACTIVITY_TRIGGER]
410 
411  return [DisplayCategory.OTHER]
412 
413  def interfaces(self) -> Generator[AlexaCapability]:
414  """Yield the supported interfaces."""
415  yield AlexaPowerController(self.entityentity)
416  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
417  yield Alexa(self.entityentity)
418 
419 
420 @ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
421 @ENTITY_ADAPTERS.register(switch.DOMAIN)
423  """Class to represent Switch capabilities."""
424 
425  def default_display_categories(self) -> list[str]:
426  """Return the display categories for this entity."""
427  if self.entityentity.domain == input_boolean.DOMAIN:
428  return [DisplayCategory.OTHER]
429 
430  device_class = self.entityentity.attributes.get(ATTR_DEVICE_CLASS)
431  if device_class == switch.SwitchDeviceClass.OUTLET:
432  return [DisplayCategory.SMARTPLUG]
433 
434  return [DisplayCategory.SWITCH]
435 
436  def interfaces(self) -> Generator[AlexaCapability]:
437  """Yield the supported interfaces."""
438  yield AlexaPowerController(self.entityentity)
439  yield AlexaContactSensor(self.hasshass, self.entityentity)
440  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
441  yield Alexa(self.entityentity)
442 
443 
444 @ENTITY_ADAPTERS.register(button.DOMAIN)
445 @ENTITY_ADAPTERS.register(input_button.DOMAIN)
447  """Class to represent Button capabilities."""
448 
449  def default_display_categories(self) -> list[str]:
450  """Return the display categories for this entity."""
451  return [DisplayCategory.ACTIVITY_TRIGGER]
452 
453  def interfaces(self) -> Generator[AlexaCapability]:
454  """Yield the supported interfaces."""
455  yield AlexaSceneController(self.entityentity, supports_deactivation=False)
456  yield AlexaEventDetectionSensor(self.hasshass, self.entityentity)
457  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
458  yield Alexa(self.entityentity)
459 
460 
461 @ENTITY_ADAPTERS.register(climate.DOMAIN)
462 @ENTITY_ADAPTERS.register(water_heater.DOMAIN)
464  """Class to represent Climate capabilities."""
465 
466  def default_display_categories(self) -> list[str]:
467  """Return the display categories for this entity."""
468  if self.entityentity.domain == water_heater.DOMAIN:
469  return [DisplayCategory.WATER_HEATER]
470  return [DisplayCategory.THERMOSTAT]
471 
472  def interfaces(self) -> Generator[AlexaCapability]:
473  """Yield the supported interfaces."""
474  # If we support two modes, one being off, we allow turning on too.
475  supported_features = self.entityentity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
476  if (
477  self.entityentity.domain == climate.DOMAIN
478  and climate.HVACMode.OFF
479  in (self.entityentity.attributes.get(climate.ATTR_HVAC_MODES) or [])
480  or self.entityentity.domain == climate.DOMAIN
481  and (
482  supported_features
483  & (
484  climate.ClimateEntityFeature.TURN_ON
485  | climate.ClimateEntityFeature.TURN_OFF
486  )
487  )
488  or self.entityentity.domain == water_heater.DOMAIN
489  and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF)
490  ):
491  yield AlexaPowerController(self.entityentity)
492 
493  if (
494  self.entityentity.domain == climate.DOMAIN
495  or self.entityentity.domain == water_heater.DOMAIN
496  and (
497  supported_features
498  & water_heater.WaterHeaterEntityFeature.OPERATION_MODE
499  )
500  ):
501  yield AlexaThermostatController(self.hasshass, self.entityentity)
502  yield AlexaTemperatureSensor(self.hasshass, self.entityentity)
503  if self.entityentity.domain == water_heater.DOMAIN and (
504  supported_features & water_heater.WaterHeaterEntityFeature.OPERATION_MODE
505  ):
506  yield AlexaModeController(
507  self.entityentity,
508  instance=f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}",
509  )
510  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
511  yield Alexa(self.entityentity)
512 
513 
514 @ENTITY_ADAPTERS.register(cover.DOMAIN)
516  """Class to represent Cover capabilities."""
517 
518  def default_display_categories(self) -> list[str]:
519  """Return the display categories for this entity."""
520  device_class = self.entityentity.attributes.get(ATTR_DEVICE_CLASS)
521  if device_class in (cover.CoverDeviceClass.GARAGE, cover.CoverDeviceClass.GATE):
522  return [DisplayCategory.GARAGE_DOOR]
523  if device_class == cover.CoverDeviceClass.DOOR:
524  return [DisplayCategory.DOOR]
525  if device_class in (
526  cover.CoverDeviceClass.BLIND,
527  cover.CoverDeviceClass.SHADE,
528  cover.CoverDeviceClass.CURTAIN,
529  ):
530  return [DisplayCategory.INTERIOR_BLIND]
531  if device_class in (
532  cover.CoverDeviceClass.WINDOW,
533  cover.CoverDeviceClass.AWNING,
534  cover.CoverDeviceClass.SHUTTER,
535  ):
536  return [DisplayCategory.EXTERIOR_BLIND]
537 
538  return [DisplayCategory.OTHER]
539 
540  def interfaces(self) -> Generator[AlexaCapability]:
541  """Yield the supported interfaces."""
542  device_class = self.entityentity.attributes.get(ATTR_DEVICE_CLASS)
543  if device_class not in (
544  cover.CoverDeviceClass.GARAGE,
545  cover.CoverDeviceClass.GATE,
546  ):
547  yield AlexaPowerController(self.entityentity)
548 
549  supported = self.entityentity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
550  if supported & cover.CoverEntityFeature.SET_POSITION:
551  yield AlexaRangeController(
552  self.entityentity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
553  )
554  elif supported & (
555  cover.CoverEntityFeature.CLOSE | cover.CoverEntityFeature.OPEN
556  ):
557  yield AlexaModeController(
558  self.entityentity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
559  )
560  if supported & cover.CoverEntityFeature.SET_TILT_POSITION:
561  yield AlexaRangeController(self.entityentity, instance=f"{cover.DOMAIN}.tilt")
562  if supported & (
563  cover.CoverEntityFeature.STOP | cover.CoverEntityFeature.STOP_TILT
564  ):
565  yield AlexaPlaybackController(self.entityentity, instance=f"{cover.DOMAIN}.stop")
566  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
567  yield Alexa(self.entityentity)
568 
569 
570 @ENTITY_ADAPTERS.register(event.DOMAIN)
572  """Class to represent doorbel event capabilities."""
573 
574  def default_display_categories(self) -> list[str] | None:
575  """Return the display categories for this entity."""
576  attrs = self.entityentity.attributes
577  device_class: event.EventDeviceClass | None = attrs.get(ATTR_DEVICE_CLASS)
578  if device_class == event.EventDeviceClass.DOORBELL:
579  return [DisplayCategory.DOORBELL]
580  return None
581 
582  def interfaces(self) -> Generator[AlexaCapability]:
583  """Yield the supported interfaces."""
584  if self.default_display_categoriesdefault_display_categoriesdefault_display_categories() is not None:
585  yield AlexaDoorbellEventSource(self.entityentity)
586  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
587  yield Alexa(self.entityentity)
588 
589 
590 @ENTITY_ADAPTERS.register(light.DOMAIN)
592  """Class to represent Light capabilities."""
593 
594  def default_display_categories(self) -> list[str]:
595  """Return the display categories for this entity."""
596  return [DisplayCategory.LIGHT]
597 
598  def interfaces(self) -> Generator[AlexaCapability]:
599  """Yield the supported interfaces."""
600  yield AlexaPowerController(self.entityentity)
601 
602  color_modes = self.entityentity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES)
603  if light.brightness_supported(color_modes):
604  yield AlexaBrightnessController(self.entityentity)
605  if light.color_supported(color_modes):
606  yield AlexaColorController(self.entityentity)
607  if light.color_temp_supported(color_modes):
608  yield AlexaColorTemperatureController(self.entityentity)
609 
610  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
611  yield Alexa(self.entityentity)
612 
613 
614 @ENTITY_ADAPTERS.register(fan.DOMAIN)
616  """Class to represent Fan capabilities."""
617 
618  def default_display_categories(self) -> list[str]:
619  """Return the display categories for this entity."""
620  return [DisplayCategory.FAN]
621 
622  def interfaces(self) -> Generator[AlexaCapability]:
623  """Yield the supported interfaces."""
624  yield AlexaPowerController(self.entityentity)
625  force_range_controller = True
626  supported = self.entityentity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
627  if supported & fan.FanEntityFeature.OSCILLATE:
628  yield AlexaToggleController(
629  self.entityentity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
630  )
631  force_range_controller = False
632  if supported & fan.FanEntityFeature.PRESET_MODE:
633  yield AlexaModeController(
634  self.entityentity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
635  )
636  force_range_controller = False
637  if supported & fan.FanEntityFeature.DIRECTION:
638  yield AlexaModeController(
639  self.entityentity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"
640  )
641  force_range_controller = False
642 
643  # AlexaRangeController controls the Fan Speed Percentage.
644  # For fans which only support on/off, no controller is added. This makes
645  # the fan impossible to turn on or off through Alexa, most likely due
646  # to a bug in Alexa. As a workaround, we add a range controller which
647  # can only be set to 0% or 100%.
648  if force_range_controller or supported & fan.FanEntityFeature.SET_SPEED:
649  yield AlexaRangeController(
650  self.entityentity, instance=f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}"
651  )
652 
653  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
654  yield Alexa(self.entityentity)
655 
656 
657 @ENTITY_ADAPTERS.register(remote.DOMAIN)
659  """Class to represent Remote capabilities."""
660 
661  def default_display_categories(self) -> list[str]:
662  """Return the display categories for this entity."""
663  return [DisplayCategory.REMOTE]
664 
665  def interfaces(self) -> Generator[AlexaCapability]:
666  """Yield the supported interfaces."""
667  yield AlexaPowerController(self.entityentity)
668  supported = self.entityentity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
669  activities = self.entityentity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
670  if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
671  yield AlexaModeController(
672  self.entityentity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
673  )
674  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
675  yield Alexa(self.entityentity)
676 
677 
678 @ENTITY_ADAPTERS.register(humidifier.DOMAIN)
680  """Class to represent Humidifier capabilities."""
681 
682  def default_display_categories(self) -> list[str]:
683  """Return the display categories for this entity."""
684  return [DisplayCategory.OTHER]
685 
686  def interfaces(self) -> Generator[AlexaCapability]:
687  """Yield the supported interfaces."""
688  yield AlexaPowerController(self.entityentity)
689  supported = self.entityentity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
690  if supported & humidifier.HumidifierEntityFeature.MODES:
691  yield AlexaModeController(
692  self.entityentity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}"
693  )
694  yield AlexaRangeController(
695  self.entityentity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}"
696  )
697 
698  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
699  yield Alexa(self.entityentity)
700 
701 
702 @ENTITY_ADAPTERS.register(lock.DOMAIN)
704  """Class to represent Lock capabilities."""
705 
706  def default_display_categories(self) -> list[str]:
707  """Return the display categories for this entity."""
708  return [DisplayCategory.SMARTLOCK]
709 
710  def interfaces(self) -> Generator[AlexaCapability]:
711  """Yield the supported interfaces."""
712  yield AlexaLockController(self.entityentity)
713  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
714  yield Alexa(self.entityentity)
715 
716 
717 @ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
719  """Class to represent MediaPlayer capabilities."""
720 
721  def default_display_categories(self) -> list[str]:
722  """Return the display categories for this entity."""
723  device_class = self.entityentity.attributes.get(ATTR_DEVICE_CLASS)
724  if device_class == media_player.MediaPlayerDeviceClass.SPEAKER:
725  return [DisplayCategory.SPEAKER]
726 
727  return [DisplayCategory.TV]
728 
729  def interfaces(self) -> Generator[AlexaCapability]:
730  """Yield the supported interfaces."""
731  yield AlexaPowerController(self.entityentity)
732 
733  supported = self.entityentity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
734  if supported & media_player.MediaPlayerEntityFeature.VOLUME_SET:
735  yield AlexaSpeaker(self.entityentity)
736  elif supported & media_player.MediaPlayerEntityFeature.VOLUME_STEP:
737  yield AlexaStepSpeaker(self.entityentity)
738 
739  playback_features = (
740  media_player.MediaPlayerEntityFeature.PLAY
741  | media_player.MediaPlayerEntityFeature.PAUSE
742  | media_player.MediaPlayerEntityFeature.STOP
743  | media_player.MediaPlayerEntityFeature.NEXT_TRACK
744  | media_player.MediaPlayerEntityFeature.PREVIOUS_TRACK
745  )
746  if supported & playback_features:
747  yield AlexaPlaybackController(self.entityentity)
748  yield AlexaPlaybackStateReporter(self.entityentity)
749 
750  if supported & media_player.MediaPlayerEntityFeature.SEEK:
751  yield AlexaSeekController(self.entityentity)
752 
753  if supported & media_player.MediaPlayerEntityFeature.SELECT_SOURCE:
754  inputs = AlexaInputController.get_valid_inputs(
755  self.entityentity.attributes.get(
756  media_player.const.ATTR_INPUT_SOURCE_LIST, []
757  )
758  )
759  if len(inputs) > 0:
760  yield AlexaInputController(self.entityentity)
761 
762  if supported & media_player.MediaPlayerEntityFeature.PLAY_MEDIA:
763  yield AlexaChannelController(self.entityentity)
764 
765  # AlexaEqualizerController is disabled for denonavr
766  # since it blocks alexa from discovering any devices.
767  entity_info = entity_sources(self.hasshass).get(self.entity_identity_id)
768  domain = entity_info["domain"] if entity_info else None
769  if (
770  supported & media_player.MediaPlayerEntityFeature.SELECT_SOUND_MODE
771  and domain != "denonavr"
772  ):
773  inputs = AlexaEqualizerController.get_valid_inputs(
774  self.entityentity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
775  or []
776  )
777  if len(inputs) > 0:
778  yield AlexaEqualizerController(self.entityentity)
779 
780  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
781  yield Alexa(self.entityentity)
782 
783 
784 @ENTITY_ADAPTERS.register(scene.DOMAIN)
786  """Class to represent Scene capabilities."""
787 
788  def description(self) -> str:
789  """Return the Alexa API description."""
790  description = AlexaEntity.description(self)
791  if "scene" not in description.casefold():
792  return f"{description} (Scene)"
793  return description
794 
795  def default_display_categories(self) -> list[str]:
796  """Return the display categories for this entity."""
797  return [DisplayCategory.SCENE_TRIGGER]
798 
799  def interfaces(self) -> Generator[AlexaCapability]:
800  """Yield the supported interfaces."""
801  yield AlexaSceneController(self.entityentity, supports_deactivation=False)
802  yield Alexa(self.entityentity)
803 
804 
805 @ENTITY_ADAPTERS.register(script.DOMAIN)
807  """Class to represent Script capabilities."""
808 
809  def default_display_categories(self) -> list[str]:
810  """Return the display categories for this entity."""
811  return [DisplayCategory.ACTIVITY_TRIGGER]
812 
813  def interfaces(self) -> Generator[AlexaCapability]:
814  """Yield the supported interfaces."""
815  yield AlexaSceneController(self.entityentity, supports_deactivation=True)
816  yield Alexa(self.entityentity)
817 
818 
819 @ENTITY_ADAPTERS.register(sensor.DOMAIN)
821  """Class to represent Sensor capabilities."""
822 
823  def default_display_categories(self) -> list[str]:
824  """Return the display categories for this entity."""
825  # although there are other kinds of sensors, all but temperature
826  # sensors are currently ignored.
827  return [DisplayCategory.TEMPERATURE_SENSOR]
828 
829  def interfaces(self) -> Generator[AlexaCapability]:
830  """Yield the supported interfaces."""
831  attrs = self.entityentity.attributes
832  if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in {
833  UnitOfTemperature.FAHRENHEIT,
834  UnitOfTemperature.CELSIUS,
835  }:
836  yield AlexaTemperatureSensor(self.hasshass, self.entityentity)
837  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
838  yield Alexa(self.entityentity)
839 
840 
841 @ENTITY_ADAPTERS.register(binary_sensor.DOMAIN)
843  """Class to represent BinarySensor capabilities."""
844 
845  TYPE_CONTACT = "contact"
846  TYPE_MOTION = "motion"
847  TYPE_PRESENCE = "presence"
848 
849  def default_display_categories(self) -> list[str] | None:
850  """Return the display categories for this entity."""
851  sensor_type = self.get_typeget_type()
852  if sensor_type is self.TYPE_CONTACTTYPE_CONTACT:
853  return [DisplayCategory.CONTACT_SENSOR]
854  if sensor_type is self.TYPE_MOTIONTYPE_MOTION:
855  return [DisplayCategory.MOTION_SENSOR]
856  if sensor_type is self.TYPE_PRESENCETYPE_PRESENCE:
857  return [DisplayCategory.CAMERA]
858  return None
859 
860  def interfaces(self) -> Generator[AlexaCapability]:
861  """Yield the supported interfaces."""
862  sensor_type = self.get_typeget_type()
863  if sensor_type is self.TYPE_CONTACTTYPE_CONTACT:
864  yield AlexaContactSensor(self.hasshass, self.entityentity)
865  elif sensor_type is self.TYPE_MOTIONTYPE_MOTION:
866  yield AlexaMotionSensor(self.hasshass, self.entityentity)
867  elif sensor_type is self.TYPE_PRESENCETYPE_PRESENCE:
868  yield AlexaEventDetectionSensor(self.hasshass, self.entityentity)
869 
870  # yield additional interfaces based on specified display category in config.
871  entity_conf = self.configconfig.entity_config.get(self.entityentity.entity_id, {})
872  if CONF_DISPLAY_CATEGORIES in entity_conf:
873  if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL:
874  yield AlexaDoorbellEventSource(self.entityentity)
875  elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CONTACT_SENSOR:
876  yield AlexaContactSensor(self.hasshass, self.entityentity)
877  elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR:
878  yield AlexaMotionSensor(self.hasshass, self.entityentity)
879  elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CAMERA:
880  yield AlexaEventDetectionSensor(self.hasshass, self.entityentity)
881 
882  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
883  yield Alexa(self.entityentity)
884 
885  def get_type(self) -> str | None:
886  """Return the type of binary sensor."""
887  attrs = self.entityentity.attributes
888  if attrs.get(ATTR_DEVICE_CLASS) in (
889  binary_sensor.BinarySensorDeviceClass.DOOR,
890  binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR,
891  binary_sensor.BinarySensorDeviceClass.OPENING,
892  binary_sensor.BinarySensorDeviceClass.WINDOW,
893  ):
894  return self.TYPE_CONTACTTYPE_CONTACT
895 
896  if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.BinarySensorDeviceClass.MOTION:
897  return self.TYPE_MOTIONTYPE_MOTION
898 
899  if (
900  attrs.get(ATTR_DEVICE_CLASS)
901  == binary_sensor.BinarySensorDeviceClass.PRESENCE
902  ):
903  return self.TYPE_PRESENCETYPE_PRESENCE
904 
905  return None
906 
907 
908 @ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN)
910  """Class to represent Alarm capabilities."""
911 
912  def default_display_categories(self) -> list[str]:
913  """Return the display categories for this entity."""
914  return [DisplayCategory.SECURITY_PANEL]
915 
916  def interfaces(self) -> Generator[AlexaCapability]:
917  """Yield the supported interfaces."""
918  if not self.entityentity.attributes.get("code_arm_required"):
919  yield AlexaSecurityPanelController(self.hasshass, self.entityentity)
920  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
921  yield Alexa(self.entityentity)
922 
923 
924 @ENTITY_ADAPTERS.register(image_processing.DOMAIN)
926  """Class to represent image_processing capabilities."""
927 
928  def default_display_categories(self) -> list[str]:
929  """Return the display categories for this entity."""
930  return [DisplayCategory.CAMERA]
931 
932  def interfaces(self) -> Generator[AlexaCapability]:
933  """Yield the supported interfaces."""
934  yield AlexaEventDetectionSensor(self.hasshass, self.entityentity)
935  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
936  yield Alexa(self.entityentity)
937 
938 
939 @ENTITY_ADAPTERS.register(input_number.DOMAIN)
940 @ENTITY_ADAPTERS.register(number.DOMAIN)
942  """Class to represent number and input_number capabilities."""
943 
944  def default_display_categories(self) -> list[str]:
945  """Return the display categories for this entity."""
946  return [DisplayCategory.OTHER]
947 
948  def interfaces(self) -> Generator[AlexaCapability]:
949  """Yield the supported interfaces."""
950  domain = self.entityentity.domain
951  yield AlexaRangeController(self.entityentity, instance=f"{domain}.value")
952  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
953  yield Alexa(self.entityentity)
954 
955 
956 @ENTITY_ADAPTERS.register(timer.DOMAIN)
958  """Class to represent Timer capabilities."""
959 
960  def default_display_categories(self) -> list[str]:
961  """Return the display categories for this entity."""
962  return [DisplayCategory.OTHER]
963 
964  def interfaces(self) -> Generator[AlexaCapability]:
965  """Yield the supported interfaces."""
966  yield AlexaTimeHoldController(self.entityentity, allow_remote_resume=True)
967  yield AlexaPowerController(self.entityentity)
968  yield Alexa(self.entityentity)
969 
970 
971 @ENTITY_ADAPTERS.register(vacuum.DOMAIN)
973  """Class to represent vacuum capabilities."""
974 
975  def default_display_categories(self) -> list[str]:
976  """Return the display categories for this entity."""
977  return [DisplayCategory.VACUUM_CLEANER]
978 
979  def interfaces(self) -> Generator[AlexaCapability]:
980  """Yield the supported interfaces."""
981  supported = self.entityentity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
982  if (
983  (supported & vacuum.VacuumEntityFeature.TURN_ON)
984  or (supported & vacuum.VacuumEntityFeature.START)
985  ) and (
986  (supported & vacuum.VacuumEntityFeature.TURN_OFF)
987  or (supported & vacuum.VacuumEntityFeature.RETURN_HOME)
988  ):
989  yield AlexaPowerController(self.entityentity)
990 
991  if supported & vacuum.VacuumEntityFeature.FAN_SPEED:
992  yield AlexaRangeController(
993  self.entityentity, instance=f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}"
994  )
995 
996  if supported & vacuum.VacuumEntityFeature.PAUSE:
997  support_resume = bool(supported & vacuum.VacuumEntityFeature.START)
999  self.entityentity, allow_remote_resume=support_resume
1000  )
1001 
1002  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
1003  yield Alexa(self.entityentity)
1004 
1005 
1006 @ENTITY_ADAPTERS.register(valve.DOMAIN)
1008  """Class to represent Valve capabilities."""
1009 
1010  def default_display_categories(self) -> list[str]:
1011  """Return the display categories for this entity."""
1012  return [DisplayCategory.OTHER]
1013 
1014  def interfaces(self) -> Generator[AlexaCapability]:
1015  """Yield the supported interfaces."""
1016  supported = self.entityentity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
1017  if supported & valve.ValveEntityFeature.SET_POSITION:
1018  yield AlexaRangeController(
1019  self.entityentity, instance=f"{valve.DOMAIN}.{valve.ATTR_POSITION}"
1020  )
1021  elif supported & (
1022  valve.ValveEntityFeature.CLOSE | valve.ValveEntityFeature.OPEN
1023  ):
1024  yield AlexaModeController(self.entityentity, instance=f"{valve.DOMAIN}.state")
1025  if supported & valve.ValveEntityFeature.STOP:
1026  yield AlexaToggleController(self.entityentity, instance=f"{valve.DOMAIN}.stop")
1027  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
1028  yield Alexa(self.entityentity)
1029 
1030 
1031 @ENTITY_ADAPTERS.register(camera.DOMAIN)
1033  """Class to represent Camera capabilities."""
1034 
1035  def default_display_categories(self) -> list[str]:
1036  """Return the display categories for this entity."""
1037  return [DisplayCategory.CAMERA]
1038 
1039  def interfaces(self) -> Generator[AlexaCapability]:
1040  """Yield the supported interfaces."""
1041  if self._check_requirements_check_requirements():
1042  supported = self.entityentity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
1043  if supported & camera.CameraEntityFeature.STREAM:
1044  yield AlexaCameraStreamController(self.entityentity)
1045 
1046  yield AlexaEndpointHealth(self.hasshass, self.entityentity)
1047  yield Alexa(self.entityentity)
1048 
1049  def _check_requirements(self) -> bool:
1050  """Check the hass URL for HTTPS scheme."""
1051  if "stream" not in self.hasshass.config.components:
1052  _LOGGER.debug(
1053  "%s requires stream component for AlexaCameraStreamController",
1054  self.entity_identity_id,
1055  )
1056  return False
1057 
1058  try:
1059  network.get_url(
1060  self.hasshass,
1061  allow_internal=False,
1062  allow_ip=False,
1063  require_ssl=True,
1064  require_standard_port=True,
1065  )
1066  except network.NoURLAvailableError:
1067  _LOGGER.debug(
1068  "%s requires HTTPS for AlexaCameraStreamController", self.entity_identity_id
1069  )
1070  return False
1071 
1072  return True
Iterable[AlexaCapability] interfaces(self)
Definition: entities.py:319
None __init__(self, HomeAssistant hass, AbstractConfig config, State entity)
Definition: entities.py:275
Generator[dict[str, Any]] serialize_properties(self)
Definition: entities.py:327
Generator[AlexaCapability] interfaces(self)
Definition: entities.py:622
Generator[AlexaCapability] interfaces(self)
Definition: entities.py:710
list[AlexaEntity] async_get_entities(HomeAssistant hass, AbstractConfig config)
Definition: entities.py:374
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
dict[str, EntityInfo] entity_sources(HomeAssistant hass)
Definition: entity.py:98