"""Support for scene platform for Hue scenes (V2 only).""" import logging from typing import Any, override from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.scenes import ScenesController from aiohue.v2.models.scene import Scene as HueScene, ScenePut as HueScenePut from aiohue.v2.models.smart_scene import SmartScene as HueSmartScene, SmartSceneState import voluptuous as vol from homeassistant.components.scene import ATTR_TRANSITION, Scene as SceneEntity from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, async_get_current_platform, ) from .bridge import HueBridge, HueConfigEntry from .const import DOMAIN from .v2.entity import HueBaseEntity from .v2.helpers import normalize_hue_brightness, normalize_hue_transition SERVICE_ACTIVATE_SCENE = "activate_scene" ATTR_DYNAMIC = "dynamic" ATTR_SPEED = "speed" ATTR_BRIGHTNESS = "brightness" LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up scene platform from Hue group scenes.""" bridge = config_entry.runtime_data api: HueBridgeV2 = bridge.api if bridge.api_version == 1: # should not happen, but just in case raise NotImplementedError("Scene support is only available for V2 bridges") # add entities for all scenes @callback def async_add_entity( event_type: EventType, resource: HueScene | HueSmartScene ) -> None: """Add entity from Hue resource.""" # Catch creation errors to continue adding other scenes even if one fails try: entity: HueSceneEntityBase if isinstance(resource, HueSmartScene): entity = HueSmartSceneEntity(bridge, api.scenes, resource) else: entity = HueSceneEntity(bridge, api.scenes, resource) except KeyError, StopIteration: LOGGER.exception("Unable to create Hue scene entity for %s", resource.id) return async_add_entities([entity]) # add all current items in controller for item in api.scenes: async_add_entity(EventType.RESOURCE_ADDED, item) # register listener for new items only config_entry.async_on_unload( api.scenes.subscribe(async_add_entity, event_filter=EventType.RESOURCE_ADDED) ) # add platform service to turn_on/activate scene with advanced options platform = async_get_current_platform() platform.async_register_entity_service( SERVICE_ACTIVATE_SCENE, { vol.Optional(ATTR_DYNAMIC): vol.Coerce(bool), vol.Optional(ATTR_SPEED): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ), vol.Optional(ATTR_TRANSITION): vol.All( vol.Coerce(float), vol.Range(min=0, max=3600) ), vol.Optional(ATTR_BRIGHTNESS): vol.All( vol.Coerce(int), vol.Range(min=1, max=255) ), }, "_async_activate", ) class HueSceneEntityBase(HueBaseEntity, SceneEntity): """Base Representation of a Scene entity from Hue Scenes.""" _attr_has_entity_name = True def __init__( self, bridge: HueBridge, controller: ScenesController, resource: HueScene | HueSmartScene, ) -> None: """Initialize the entity.""" super().__init__(bridge, controller, resource) self.resource = resource self.controller = controller if (hue_group := self.controller.get_group(self.resource.id)) is None: raise KeyError(self.resource.group.rid) self.hue_group = hue_group # we create a virtual service/device for Hue zones/rooms # so we have a parent for grouped lights and scenes self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.hue_group.id)}, ) @override async def async_added_to_hass(self) -> None: """Call when entity is added.""" await super().async_added_to_hass() # Add value_changed callback for group to catch name changes. self.async_on_remove( self.bridge.api.groups.subscribe( self._handle_event, self.hue_group.id, (EventType.RESOURCE_UPDATED), ) ) @property @override def name(self) -> str: """Return name of the scene.""" return self.resource.metadata.name class HueSceneEntity(HueSceneEntityBase): """Representation of a Scene entity from Hue Scenes.""" @property def is_dynamic(self) -> bool: """Return if this scene has a dynamic color palette.""" if ( self.resource.palette and self.resource.palette.color and len(self.resource.palette.color) > 1 ): return True if ( self.resource.palette and self.resource.palette.color_temperature and len(self.resource.palette.color_temperature) > 1 ): return True return False @override async def async_activate(self, **kwargs: Any) -> None: """Activate Hue scene.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) # the options below are advanced only # as we're not allowed to override the default scene turn_on service # we've implemented a `activate_scene` entity service dynamic = kwargs.get(ATTR_DYNAMIC, False) speed = kwargs.get(ATTR_SPEED) brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) if speed is not None: await self.bridge.async_request_call( self.controller.scene.update, self.resource.id, HueScenePut(speed=speed / 100), ) await self.bridge.async_request_call( self.controller.scene.recall, self.resource.id, dynamic=dynamic, duration=transition, brightness=brightness, ) @property @override def extra_state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes.""" brightness = None if palette := self.resource.palette: if palette.dimming: brightness = palette.dimming[0].brightness if brightness is None: # get brightness from actions for action in self.resource.actions: if action.action.dimming: brightness = action.action.dimming.brightness break if brightness is not None: # Hue uses a range of [0, 100] to control brightness. brightness = round((brightness / 100) * 255) return { "group_name": self.hue_group.metadata.name, "group_type": self.hue_group.type.value, "name": self.resource.metadata.name, "speed": self.resource.speed, "brightness": brightness, "is_dynamic": self.is_dynamic, } class HueSmartSceneEntity(HueSceneEntityBase): """Representation of a Smart Scene entity from Hue Scenes.""" @property def is_active(self) -> bool: """Return if this smart scene is currently active.""" return self.resource.state == SmartSceneState.ACTIVE @override async def async_activate(self, **kwargs: Any) -> None: """Activate Hue Smart scene.""" await self.bridge.async_request_call( self.controller.smart_scene.recall, self.resource.id, ) @property @override def extra_state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes.""" res = { "group_name": self.hue_group.metadata.name, "group_type": self.hue_group.type.value, "name": self.resource.metadata.name, "is_active": self.is_active, } if self.is_active and self.resource.active_timeslot: res["active_timeslot_id"] = self.resource.active_timeslot.timeslot_id res["active_timeslot_name"] = self.resource.active_timeslot.weekday.value # lookup active scene in timeslot active_scene = None count = 0 for day_timeslot in self.resource.week_timeslots: for timeslot in day_timeslot.timeslots: if count != self.resource.active_timeslot.timeslot_id: count += 1 continue active_scene = self.controller.get(timeslot.target.rid) break if active_scene is not None: res["active_scene"] = active_scene.metadata.name return res