From 4c11cc3dbbcae294d3df8acf3c72193fd5ff9eca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 21:32:59 -1000 Subject: [PATCH] Additional cleanups for emulated_hue (#73004) * Additional cleanups for emulated_hue Followup to https://github.com/home-assistant/core/pull/72663#discussion_r884268731 * split long lines --- .../components/emulated_hue/config.py | 34 ++++---- .../components/emulated_hue/hue_api.py | 87 +++++++++++-------- 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index fce521eee55..c2cf67b43f4 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -156,49 +156,49 @@ class Config: # Google Home return self.numbers.get(number) - def get_entity_name(self, entity: State) -> str: + def get_entity_name(self, state: State) -> str: """Get the name of an entity.""" if ( - entity.entity_id in self.entities - and CONF_ENTITY_NAME in self.entities[entity.entity_id] + state.entity_id in self.entities + and CONF_ENTITY_NAME in self.entities[state.entity_id] ): - return self.entities[entity.entity_id][CONF_ENTITY_NAME] + return self.entities[state.entity_id][CONF_ENTITY_NAME] - return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) + return state.attributes.get(ATTR_EMULATED_HUE_NAME, state.name) - def is_entity_exposed(self, entity: State) -> bool: + def is_state_exposed(self, state: State) -> bool: """Cache determine if an entity should be exposed on the emulated bridge.""" - if (exposed := self._exposed_cache.get(entity.entity_id)) is not None: + if (exposed := self._exposed_cache.get(state.entity_id)) is not None: return exposed - exposed = self._is_entity_exposed(entity) - self._exposed_cache[entity.entity_id] = exposed + exposed = self._is_state_exposed(state) + self._exposed_cache[state.entity_id] = exposed return exposed - def filter_exposed_entities(self, states: Iterable[State]) -> list[State]: + def filter_exposed_states(self, states: Iterable[State]) -> list[State]: """Filter a list of all states down to exposed entities.""" exposed: list[State] = [ - state for state in states if self.is_entity_exposed(state) + state for state in states if self.is_state_exposed(state) ] return exposed - def _is_entity_exposed(self, entity: State) -> bool: - """Determine if an entity should be exposed on the emulated bridge. + def _is_state_exposed(self, state: State) -> bool: + """Determine if an entity state should be exposed on the emulated bridge. Async friendly. """ - if entity.attributes.get("view") is not None: + if state.attributes.get("view") is not None: # Ignore entities that are views return False - if entity.entity_id in self._entities_with_hidden_attr_in_config: - return not self._entities_with_hidden_attr_in_config[entity.entity_id] + if state.entity_id in self._entities_with_hidden_attr_in_config: + return not self._entities_with_hidden_attr_in_config[state.entity_id] if not self.expose_by_default: return False # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being # exposed, or if the entity is explicitly exposed - if entity.domain in self.exposed_domains: + if state.domain in self.exposed_domains: return True return False diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index d6ac67b6984..2a9022f909d 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from functools import lru_cache import hashlib from http import HTTPStatus from ipaddress import ip_address @@ -302,15 +303,15 @@ class HueOneLightStateView(HomeAssistantView): ) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) - if (entity := hass.states.get(hass_entity_id)) is None: + if (state := hass.states.get(hass_entity_id)) is None: _LOGGER.error("Entity not found: %s", hass_entity_id) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) - if not self.config.is_entity_exposed(entity): + if not self.config.is_state_exposed(state): _LOGGER.error("Entity not exposed: %s", entity_id) return self.json_message("Entity not exposed", HTTPStatus.UNAUTHORIZED) - json_response = entity_to_json(self.config, entity) + json_response = state_to_json(self.config, state) return self.json(json_response) @@ -346,7 +347,7 @@ class HueOneLightChangeView(HomeAssistantView): _LOGGER.error("Entity not found: %s", entity_id) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) - if not config.is_entity_exposed(entity): + if not config.is_state_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) return self.json_message("Entity not exposed", HTTPStatus.UNAUTHORIZED) @@ -614,7 +615,7 @@ class HueOneLightChangeView(HomeAssistantView): return self.json(json_response) -def get_entity_state(config: Config, entity: State) -> dict[str, Any]: +def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: """Retrieve and convert state and brightness values for an entity.""" cached_state_entry = config.cached_states.get(entity.entity_id, None) cached_state = None @@ -718,22 +719,32 @@ def get_entity_state(config: Config, entity: State) -> dict[str, Any]: return data -def entity_to_json(config: Config, entity: State) -> dict[str, Any]: +@lru_cache(maxsize=1024) +def _entity_unique_id(entity_id: str) -> str: + """Return the emulated_hue unique id for the entity_id.""" + unique_id = hashlib.md5(entity_id.encode()).hexdigest() + return ( + f"00:{unique_id[0:2]}:{unique_id[2:4]}:" + f"{unique_id[4:6]}:{unique_id[6:8]}:{unique_id[8:10]}:" + f"{unique_id[10:12]}:{unique_id[12:14]}-{unique_id[14:16]}" + ) + + +def state_to_json(config: Config, state: State) -> dict[str, Any]: """Convert an entity to its Hue bridge JSON representation.""" - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) - unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest() - unique_id = f"00:{unique_id[0:2]}:{unique_id[2:4]}:{unique_id[4:6]}:{unique_id[6:8]}:{unique_id[8:10]}:{unique_id[10:12]}:{unique_id[12:14]}-{unique_id[14:16]}" + entity_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + color_modes = state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) + unique_id = _entity_unique_id(state.entity_id) + state_dict = get_entity_state_dict(config, state) - state = get_entity_state(config, entity) - - retval: dict[str, Any] = { - "state": { - HUE_API_STATE_ON: state[STATE_ON], - "reachable": entity.state != STATE_UNAVAILABLE, - "mode": "homeautomation", - }, - "name": config.get_entity_name(entity), + json_state: dict[str, str | bool | int] = { + HUE_API_STATE_ON: state_dict[STATE_ON], + "reachable": state.state != STATE_UNAVAILABLE, + "mode": "homeautomation", + } + retval: dict[str, str | dict[str, str | bool | int]] = { + "state": json_state, + "name": config.get_entity_name(state), "uniqueid": unique_id, "manufacturername": "Home Assistant", "swversion": "123", @@ -744,30 +755,30 @@ def entity_to_json(config: Config, entity: State) -> dict[str, Any]: # Same as Color light, but which supports additional setting of color temperature retval["type"] = "Extended color light" retval["modelid"] = "HASS231" - retval["state"].update( + json_state.update( { - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - HUE_API_STATE_CT: state[STATE_COLOR_TEMP], + HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state_dict[STATE_HUE], + HUE_API_STATE_SAT: state_dict[STATE_SATURATION], + HUE_API_STATE_CT: state_dict[STATE_COLOR_TEMP], HUE_API_STATE_EFFECT: "none", } ) - if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0: - retval["state"][HUE_API_STATE_COLORMODE] = "hs" + if state_dict[STATE_HUE] > 0 or state_dict[STATE_SATURATION] > 0: + json_state[HUE_API_STATE_COLORMODE] = "hs" else: - retval["state"][HUE_API_STATE_COLORMODE] = "ct" + json_state[HUE_API_STATE_COLORMODE] = "ct" elif light.color_supported(color_modes): # Color light (Zigbee Device ID: 0x0200) # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) retval["type"] = "Color light" retval["modelid"] = "HASS213" - retval["state"].update( + json_state.update( { - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], HUE_API_STATE_COLORMODE: "hs", - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], + HUE_API_STATE_HUE: state_dict[STATE_HUE], + HUE_API_STATE_SAT: state_dict[STATE_SATURATION], HUE_API_STATE_EFFECT: "none", } ) @@ -776,11 +787,11 @@ def entity_to_json(config: Config, entity: State) -> dict[str, Any]: # Supports groups, scenes, on/off, dimming, and setting of a color temperature retval["type"] = "Color temperature light" retval["modelid"] = "HASS312" - retval["state"].update( + json_state.update( { HUE_API_STATE_COLORMODE: "ct", - HUE_API_STATE_CT: state[STATE_COLOR_TEMP], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_CT: state_dict[STATE_COLOR_TEMP], + HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], } ) elif entity_features & ( @@ -793,7 +804,7 @@ def entity_to_json(config: Config, entity: State) -> dict[str, Any]: # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" - retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) + json_state.update({HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS]}) elif not config.lights_all_dimmable: # On/Off light (ZigBee Device ID: 0x0000) # Supports groups, scenes and on/off control @@ -806,7 +817,7 @@ def entity_to_json(config: Config, entity: State) -> dict[str, Any]: # Reports fixed brightness for compatibility with Alexa. retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" - retval["state"].update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX}) + json_state.update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX}) return retval @@ -835,8 +846,8 @@ def create_list_of_entities(config: Config, request: web.Request) -> dict[str, A """Create a list of all entities.""" hass: core.HomeAssistant = request.app["hass"] json_response: dict[str, Any] = { - config.entity_id_to_number(entity.entity_id): entity_to_json(config, entity) - for entity in config.filter_exposed_entities(hass.states.async_all()) + config.entity_id_to_number(entity.entity_id): state_to_json(config, entity) + for entity in config.filter_exposed_states(hass.states.async_all()) } return json_response