1 """Support for local control of entities by emulating a Philips Hue bridge."""
3 from __future__
import annotations
5 from functools
import cache
21 EventStateChangedData,
29 async_track_state_added_domain,
30 async_track_state_removed_domain,
47 TYPE_GOOGLE =
"google_home"
50 NUMBERS_FILE =
"emulated_hue_ids.json"
51 DATA_KEY =
"emulated_hue.ids"
55 CONF_ADVERTISE_IP =
"advertise_ip"
56 CONF_ADVERTISE_PORT =
"advertise_port"
57 CONF_ENTITY_HIDDEN =
"hidden"
58 CONF_ENTITY_NAME =
"name"
59 CONF_EXPOSE_BY_DEFAULT =
"expose_by_default"
60 CONF_EXPOSED_DOMAINS =
"exposed_domains"
61 CONF_HOST_IP =
"host_ip"
62 CONF_LIGHTS_ALL_DIMMABLE =
"lights_all_dimmable"
63 CONF_LISTEN_PORT =
"listen_port"
64 CONF_OFF_MAPS_TO_ON_DOMAINS =
"off_maps_to_on_domains"
65 CONF_UPNP_BIND_MULTICAST =
"upnp_bind_multicast"
68 DEFAULT_LIGHTS_ALL_DIMMABLE =
False
69 DEFAULT_LISTEN_PORT = 8300
70 DEFAULT_UPNP_BIND_MULTICAST =
True
71 DEFAULT_OFF_MAPS_TO_ON_DOMAINS = {
"script",
"scene"}
72 DEFAULT_EXPOSE_BY_DEFAULT =
True
73 DEFAULT_EXPOSED_DOMAINS = [
81 DEFAULT_TYPE = TYPE_GOOGLE
83 ATTR_EMULATED_HUE_NAME =
"emulated_hue_name"
86 _LOGGER = logging.getLogger(__name__)
90 """Hold configuration variables for the emulated hue bridge."""
92 def __init__(self, hass: HomeAssistant, conf: ConfigType, local_ip: str) ->
None:
93 """Initialize the instance."""
95 self.
typetype = conf.get(CONF_TYPE)
96 self.
numbersnumbers: dict[str, str] = {}
97 self.
storestore: storage.Store |
None =
None
98 self.cached_states: dict[str, list] = {}
99 self._exposed_cache: dict[str, bool] = {}
101 if self.
typetype == TYPE_ALEXA:
103 "Emulated Hue running in legacy mode because type has been "
104 "specified. More info at https://goo.gl/M6tgz8"
108 self.host_ip_addr: str = conf.get(CONF_HOST_IP)
or local_ip
111 self.listen_port: int = conf.get(CONF_LISTEN_PORT)
or DEFAULT_LISTEN_PORT
115 self.upnp_bind_multicast: bool = conf.get(
116 CONF_UPNP_BIND_MULTICAST, DEFAULT_UPNP_BIND_MULTICAST
122 off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS)
123 if isinstance(off_maps_to_on_domains, list):
130 self.expose_by_default: bool = conf.get(
131 CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT
137 conf.get(CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS)
141 self.advertise_ip: str = conf.get(CONF_ADVERTISE_IP)
or self.host_ip_addr
143 self.advertise_port: int = conf.get(CONF_ADVERTISE_PORT)
or self.listen_port
145 self.entities: dict[str, dict[str, str]] = conf.get(CONF_ENTITIES, {})
148 for entity_id
in self.entities:
149 hidden_value = self.entities[entity_id].
get(CONF_ENTITY_HIDDEN)
150 if hidden_value
is not None:
155 self.lights_all_dimmable: bool = conf.get(CONF_LIGHTS_ALL_DIMMABLE)
or False
157 if self.expose_by_default:
165 """Set up tracking and migrate to storage."""
167 self.
storestore = storage.Store(hass, DATA_VERSION, DATA_KEY)
168 numbers_path = hass.config.path(NUMBERS_FILE)
170 await storage.async_migrator(hass, numbers_path, self.
storestore)
or {}
181 """Get a unique number for the entity id."""
182 if self.
typetype == TYPE_ALEXA:
186 for number, ent_id
in self.
numbersnumbers.items():
187 if entity_id == ent_id:
193 self.
numbersnumbers[number] = entity_id
194 assert self.
storestore
is not None
199 """Convert unique number to entity id."""
200 if self.
typetype == TYPE_ALEXA:
207 """Get the name of an entity."""
209 state.entity_id
in self.entities
210 and CONF_ENTITY_NAME
in self.entities[state.entity_id]
212 return self.entities[state.entity_id][CONF_ENTITY_NAME]
214 return state.attributes.get(ATTR_EMULATED_HUE_NAME, state.name)
218 """Return a list of exposed states."""
219 state_machine = self.
hasshass.states
220 if self.expose_by_default:
223 for state
in state_machine.async_all()
228 for entity_id
in self.entities
229 if (state := state_machine.get(entity_id))
and self.
is_state_exposedis_state_exposed(state)
234 """Clear the cache of exposed entity ids."""
238 """Cache determine if an entity should be exposed on the emulated bridge."""
239 if (exposed := self._exposed_cache.
get(state.entity_id))
is not None:
242 self._exposed_cache[state.entity_id] = exposed
246 """Determine if an entity state should be exposed on the emulated bridge.
250 if state.attributes.get(
"view")
is not None:
257 if not self.expose_by_default:
str get_entity_name(self, State state)
str|None number_to_entity_id(self, str number)
None __init__(self, HomeAssistant hass, ConfigType conf, str local_ip)
str entity_id_to_number(self, str entity_id)
list[str] get_exposed_entity_ids(self)
None _clear_exposed_cache(self, Event[EventStateChangedData] event)
bool is_state_exposed(self, State state)
_entities_with_hidden_attr_in_config
bool _is_state_exposed(self, State state)
web.Response get(self, web.Request request, str config_key)
tuple[str, str] split_entity_id(str entity_id)
CALLBACK_TYPE async_track_state_added_domain(HomeAssistant hass, str|Iterable[str] domains, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
CALLBACK_TYPE async_track_state_removed_domain(HomeAssistant hass, str|Iterable[str] domains, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)