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
This commit is contained in:
J. Nick Koston 2022-06-04 21:32:59 -10:00 committed by GitHub
parent aad3253ed1
commit 4c11cc3dbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 66 additions and 55 deletions

View File

@ -156,49 +156,49 @@ class Config:
# Google Home # Google Home
return self.numbers.get(number) 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.""" """Get the name of an entity."""
if ( if (
entity.entity_id in self.entities state.entity_id in self.entities
and CONF_ENTITY_NAME in self.entities[entity.entity_id] 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.""" """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 return exposed
exposed = self._is_entity_exposed(entity) exposed = self._is_state_exposed(state)
self._exposed_cache[entity.entity_id] = exposed self._exposed_cache[state.entity_id] = exposed
return 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.""" """Filter a list of all states down to exposed entities."""
exposed: list[State] = [ 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 return exposed
def _is_entity_exposed(self, entity: State) -> bool: def _is_state_exposed(self, state: State) -> bool:
"""Determine if an entity should be exposed on the emulated bridge. """Determine if an entity state should be exposed on the emulated bridge.
Async friendly. Async friendly.
""" """
if entity.attributes.get("view") is not None: if state.attributes.get("view") is not None:
# Ignore entities that are views # Ignore entities that are views
return False return False
if entity.entity_id in self._entities_with_hidden_attr_in_config: if state.entity_id in self._entities_with_hidden_attr_in_config:
return not self._entities_with_hidden_attr_in_config[entity.entity_id] return not self._entities_with_hidden_attr_in_config[state.entity_id]
if not self.expose_by_default: if not self.expose_by_default:
return False return False
# Expose an entity if the entity's domain is exposed by default and # Expose an entity if the entity's domain is exposed by default and
# the configuration doesn't explicitly exclude it from being # the configuration doesn't explicitly exclude it from being
# exposed, or if the entity is explicitly exposed # 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 True
return False return False

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from functools import lru_cache
import hashlib import hashlib
from http import HTTPStatus from http import HTTPStatus
from ipaddress import ip_address from ipaddress import ip_address
@ -302,15 +303,15 @@ class HueOneLightStateView(HomeAssistantView):
) )
return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) 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) _LOGGER.error("Entity not found: %s", hass_entity_id)
return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) 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) _LOGGER.error("Entity not exposed: %s", entity_id)
return self.json_message("Entity not exposed", HTTPStatus.UNAUTHORIZED) 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) return self.json(json_response)
@ -346,7 +347,7 @@ class HueOneLightChangeView(HomeAssistantView):
_LOGGER.error("Entity not found: %s", entity_id) _LOGGER.error("Entity not found: %s", entity_id)
return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) 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) _LOGGER.error("Entity not exposed: %s", entity_id)
return self.json_message("Entity not exposed", HTTPStatus.UNAUTHORIZED) return self.json_message("Entity not exposed", HTTPStatus.UNAUTHORIZED)
@ -614,7 +615,7 @@ class HueOneLightChangeView(HomeAssistantView):
return self.json(json_response) 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.""" """Retrieve and convert state and brightness values for an entity."""
cached_state_entry = config.cached_states.get(entity.entity_id, None) cached_state_entry = config.cached_states.get(entity.entity_id, None)
cached_state = None cached_state = None
@ -718,22 +719,32 @@ def get_entity_state(config: Config, entity: State) -> dict[str, Any]:
return data 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.""" """Convert an entity to its Hue bridge JSON representation."""
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) entity_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) color_modes = state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, [])
unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest() unique_id = _entity_unique_id(state.entity_id)
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]}" state_dict = get_entity_state_dict(config, state)
state = get_entity_state(config, entity) json_state: dict[str, str | bool | int] = {
HUE_API_STATE_ON: state_dict[STATE_ON],
retval: dict[str, Any] = { "reachable": state.state != STATE_UNAVAILABLE,
"state": { "mode": "homeautomation",
HUE_API_STATE_ON: state[STATE_ON], }
"reachable": entity.state != STATE_UNAVAILABLE, retval: dict[str, str | dict[str, str | bool | int]] = {
"mode": "homeautomation", "state": json_state,
}, "name": config.get_entity_name(state),
"name": config.get_entity_name(entity),
"uniqueid": unique_id, "uniqueid": unique_id,
"manufacturername": "Home Assistant", "manufacturername": "Home Assistant",
"swversion": "123", "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 # Same as Color light, but which supports additional setting of color temperature
retval["type"] = "Extended color light" retval["type"] = "Extended color light"
retval["modelid"] = "HASS231" retval["modelid"] = "HASS231"
retval["state"].update( json_state.update(
{ {
HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS],
HUE_API_STATE_HUE: state[STATE_HUE], HUE_API_STATE_HUE: state_dict[STATE_HUE],
HUE_API_STATE_SAT: state[STATE_SATURATION], HUE_API_STATE_SAT: state_dict[STATE_SATURATION],
HUE_API_STATE_CT: state[STATE_COLOR_TEMP], HUE_API_STATE_CT: state_dict[STATE_COLOR_TEMP],
HUE_API_STATE_EFFECT: "none", HUE_API_STATE_EFFECT: "none",
} }
) )
if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0: if state_dict[STATE_HUE] > 0 or state_dict[STATE_SATURATION] > 0:
retval["state"][HUE_API_STATE_COLORMODE] = "hs" json_state[HUE_API_STATE_COLORMODE] = "hs"
else: else:
retval["state"][HUE_API_STATE_COLORMODE] = "ct" json_state[HUE_API_STATE_COLORMODE] = "ct"
elif light.color_supported(color_modes): elif light.color_supported(color_modes):
# Color light (Zigbee Device ID: 0x0200) # Color light (Zigbee Device ID: 0x0200)
# Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY)
retval["type"] = "Color light" retval["type"] = "Color light"
retval["modelid"] = "HASS213" 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_COLORMODE: "hs",
HUE_API_STATE_HUE: state[STATE_HUE], HUE_API_STATE_HUE: state_dict[STATE_HUE],
HUE_API_STATE_SAT: state[STATE_SATURATION], HUE_API_STATE_SAT: state_dict[STATE_SATURATION],
HUE_API_STATE_EFFECT: "none", 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 # Supports groups, scenes, on/off, dimming, and setting of a color temperature
retval["type"] = "Color temperature light" retval["type"] = "Color temperature light"
retval["modelid"] = "HASS312" retval["modelid"] = "HASS312"
retval["state"].update( json_state.update(
{ {
HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_COLORMODE: "ct",
HUE_API_STATE_CT: state[STATE_COLOR_TEMP], HUE_API_STATE_CT: state_dict[STATE_COLOR_TEMP],
HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS],
} }
) )
elif entity_features & ( 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 # Supports groups, scenes, on/off and dimming
retval["type"] = "Dimmable light" retval["type"] = "Dimmable light"
retval["modelid"] = "HASS123" 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: elif not config.lights_all_dimmable:
# On/Off light (ZigBee Device ID: 0x0000) # On/Off light (ZigBee Device ID: 0x0000)
# Supports groups, scenes and on/off control # 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. # Reports fixed brightness for compatibility with Alexa.
retval["type"] = "Dimmable light" retval["type"] = "Dimmable light"
retval["modelid"] = "HASS123" 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 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.""" """Create a list of all entities."""
hass: core.HomeAssistant = request.app["hass"] hass: core.HomeAssistant = request.app["hass"]
json_response: dict[str, Any] = { json_response: dict[str, Any] = {
config.entity_id_to_number(entity.entity_id): entity_to_json(config, entity) config.entity_id_to_number(entity.entity_id): state_to_json(config, entity)
for entity in config.filter_exposed_entities(hass.states.async_all()) for entity in config.filter_exposed_states(hass.states.async_all())
} }
return json_response return json_response