1 """Support for a Hue API to control Home Assistant."""
3 from __future__
import annotations
6 from collections.abc
import Iterable
7 from functools
import lru_cache
9 from http
import HTTPStatus
10 from ipaddress
import ip_address
13 from typing
import Any
15 from aiohttp
import web
17 from homeassistant
import core
29 SERVICE_SET_TEMPERATURE,
33 ATTR_CURRENT_POSITION,
50 ATTR_MEDIA_VOLUME_LEVEL,
51 MediaPlayerEntityFeature,
55 ATTR_SUPPORTED_FEATURES,
59 SERVICE_SET_COVER_POSITION,
73 from .config
import Config
75 _LOGGER = logging.getLogger(__name__)
76 _OFF_STATES: dict[str, str] = {cover.DOMAIN: STATE_CLOSED}
79 STATE_CHANGE_WAIT_TIMEOUT = 5.0
81 STATE_CACHED_TIMEOUT = 2.0
83 STATE_BRIGHTNESS =
"bri"
84 STATE_COLORMODE =
"colormode"
86 STATE_SATURATION =
"sat"
87 STATE_COLOR_TEMP =
"ct"
88 STATE_TRANSITION =
"tt"
92 HUE_API_STATE_ON =
"on"
93 HUE_API_STATE_BRI =
"bri"
94 HUE_API_STATE_COLORMODE =
"colormode"
95 HUE_API_STATE_HUE =
"hue"
96 HUE_API_STATE_SAT =
"sat"
97 HUE_API_STATE_CT =
"ct"
98 HUE_API_STATE_XY =
"xy"
99 HUE_API_STATE_EFFECT =
"effect"
100 HUE_API_STATE_TRANSITION =
"transitiontime"
103 HUE_API_STATE_BRI_MIN = 1
104 HUE_API_STATE_BRI_MAX = 254
105 HUE_API_STATE_HUE_MIN = 0
106 HUE_API_STATE_HUE_MAX = 65535
107 HUE_API_STATE_SAT_MIN = 0
108 HUE_API_STATE_SAT_MAX = 254
109 HUE_API_STATE_CT_MIN = 153
110 HUE_API_STATE_CT_MAX = 500
112 HUE_API_USERNAME =
"nouser"
113 UNAUTHORIZED_USER = [
114 {
"error": {
"address":
"/",
"description":
"unauthorized user",
"type":
"1"}}
117 DIMMABLE_SUPPORTED_FEATURES_BY_DOMAIN = {
118 cover.DOMAIN: CoverEntityFeature.SET_POSITION,
119 fan.DOMAIN: FanEntityFeature.SET_SPEED,
120 media_player.DOMAIN: MediaPlayerEntityFeature.VOLUME_SET,
121 climate.DOMAIN: ClimateEntityFeature.TARGET_TEMPERATURE,
124 ENTITY_FEATURES_BY_DOMAIN = {
125 cover.DOMAIN: CoverEntityFeature,
126 fan.DOMAIN: FanEntityFeature,
127 media_player.DOMAIN: MediaPlayerEntityFeature,
128 climate.DOMAIN: ClimateEntityFeature,
132 @lru_cache(maxsize=32)
134 """Check if remote address is allowed."""
135 return is_local(ip_address(address))
139 """Handle requests to find the emulated hue bridge."""
142 name =
"emulated_hue:api:unauthorized_user"
143 extra_urls = [
"/api/"]
144 requires_auth =
False
146 async
def get(self, request: web.Request) -> web.Response:
147 """Handle a GET request."""
148 return self.json(UNAUTHORIZED_USER)
152 """Handle requests to create a username for the emulated hue bridge."""
155 name =
"emulated_hue:api:create_username"
156 extra_urls = [
"/api/"]
157 requires_auth =
False
159 async
def post(self, request: web.Request) -> web.Response:
160 """Handle a POST request."""
161 assert request.remote
is not None
163 return self.json_message(
"Only local IPs allowed", HTTPStatus.UNAUTHORIZED)
166 data = await request.json(loads=json_loads)
168 return self.json_message(
"Invalid JSON", HTTPStatus.BAD_REQUEST)
170 if "devicetype" not in data:
171 return self.json_message(
"devicetype not specified", HTTPStatus.BAD_REQUEST)
173 return self.json([{
"success": {
"username": HUE_API_USERNAME}}])
177 """Handle requests for getting info about entity groups."""
179 url =
"/api/{username}/groups"
180 name =
"emulated_hue:all_groups:state"
181 requires_auth =
False
184 """Initialize the instance of the view."""
188 def get(self, request: web.Request, username: str) -> web.Response:
189 """Process a request to make the Brilliant Lightpad work."""
190 assert request.remote
is not None
192 return self.json_message(
"Only local IPs allowed", HTTPStatus.UNAUTHORIZED)
198 """Group handler to get Logitech Pop working."""
200 url =
"/api/{username}/groups/0/action"
201 name =
"emulated_hue:groups:state"
202 requires_auth =
False
205 """Initialize the instance of the view."""
209 def put(self, request: web.Request, username: str) -> web.Response:
210 """Process a request to make the Logitech Pop working."""
211 assert request.remote
is not None
213 return self.json_message(
"Only local IPs allowed", HTTPStatus.UNAUTHORIZED)
219 "address":
"/groups/0/action/scene",
221 "description":
"invalid value, dummy for parameter, scene",
229 """Handle requests for getting info about all entities."""
231 url =
"/api/{username}/lights"
232 name =
"emulated_hue:lights:state"
233 requires_auth =
False
236 """Initialize the instance of the view."""
240 def get(self, request: web.Request, username: str) -> web.Response:
241 """Process a request to get the list of available lights."""
242 assert request.remote
is not None
244 return self.json_message(
"Only local IPs allowed", HTTPStatus.UNAUTHORIZED)
250 """Return full state view of emulated hue."""
252 url =
"/api/{username}"
253 name =
"emulated_hue:username:state"
254 requires_auth =
False
257 """Initialize the instance of the view."""
261 def get(self, request: web.Request, username: str) -> web.Response:
262 """Process a request to get the list of available lights."""
263 assert request.remote
is not None
265 return self.json_message(
"only local IPs allowed", HTTPStatus.UNAUTHORIZED)
266 if username != HUE_API_USERNAME:
267 return self.json(UNAUTHORIZED_USER)
274 return self.json(json_response)
278 """Return config view of emulated hue."""
280 url =
"/api/{username}/config"
281 extra_urls = [
"/api/config"]
282 name =
"emulated_hue:username:config"
283 requires_auth =
False
286 """Initialize the instance of the view."""
290 def get(self, request: web.Request, username: str =
"") -> web.Response:
291 """Process a request to get the configuration."""
292 assert request.remote
is not None
294 return self.json_message(
"only local IPs allowed", HTTPStatus.UNAUTHORIZED)
298 return self.json(json_response)
302 """Handle requests for getting info about a single entity."""
304 url =
"/api/{username}/lights/{entity_id}"
305 name =
"emulated_hue:light:state"
306 requires_auth =
False
309 """Initialize the instance of the view."""
313 def get(self, request: web.Request, username: str, entity_id: str) -> web.Response:
314 """Process a request to get the state of an individual light."""
315 assert request.remote
is not None
317 return self.json_message(
"Only local IPs allowed", HTTPStatus.UNAUTHORIZED)
319 hass = request.app[KEY_HASS]
320 hass_entity_id = self.
configconfig.number_to_entity_id(entity_id)
322 if hass_entity_id
is None:
324 "Unknown entity number: %s not found in emulated_hue_ids.json",
327 return self.json_message(
"Entity not found", HTTPStatus.NOT_FOUND)
329 if (state := hass.states.get(hass_entity_id))
is None:
330 _LOGGER.error(
"Entity not found: %s", hass_entity_id)
331 return self.json_message(
"Entity not found", HTTPStatus.NOT_FOUND)
333 if not self.
configconfig.is_state_exposed(state):
334 _LOGGER.error(
"Entity not exposed: %s", entity_id)
335 return self.json_message(
"Entity not exposed", HTTPStatus.UNAUTHORIZED)
339 return self.json(json_response)
343 """Handle requests for setting info about entities."""
345 url =
"/api/{username}/lights/{entity_number}/state"
346 name =
"emulated_hue:light:state"
347 requires_auth =
False
350 """Initialize the instance of the view."""
354 self, request: web.Request, username: str, entity_number: str
356 """Process a request to set the state of an individual light."""
357 assert request.remote
is not None
359 return self.json_message(
"Only local IPs allowed", HTTPStatus.UNAUTHORIZED)
361 config = self.
configconfig
362 hass = request.app[KEY_HASS]
363 entity_id = config.number_to_entity_id(entity_number)
365 if entity_id
is None:
366 _LOGGER.error(
"Unknown entity number: %s", entity_number)
367 return self.json_message(
"Entity not found", HTTPStatus.NOT_FOUND)
369 if (entity := hass.states.get(entity_id))
is None:
370 _LOGGER.error(
"Entity not found: %s", entity_id)
371 return self.json_message(
"Entity not found", HTTPStatus.NOT_FOUND)
373 if not config.is_state_exposed(entity):
374 _LOGGER.error(
"Entity not exposed: %s", entity_id)
375 return self.json_message(
"Entity not exposed", HTTPStatus.UNAUTHORIZED)
378 request_json = await request.json()
380 _LOGGER.error(
"Received invalid json")
381 return self.json_message(
"Invalid JSON", HTTPStatus.BAD_REQUEST)
384 entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
385 if entity.domain == light.DOMAIN:
386 color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES)
or []
389 parsed: dict[str, Any] = {
391 STATE_BRIGHTNESS:
None,
393 STATE_SATURATION:
None,
394 STATE_COLOR_TEMP:
None,
396 STATE_TRANSITION:
None,
399 if HUE_API_STATE_ON
in request_json:
400 if not isinstance(request_json[HUE_API_STATE_ON], bool):
401 _LOGGER.error(
"Unable to parse data: %s", request_json)
402 return self.json_message(
"Bad request", HTTPStatus.BAD_REQUEST)
403 parsed[STATE_ON] = request_json[HUE_API_STATE_ON]
408 (HUE_API_STATE_BRI, STATE_BRIGHTNESS),
409 (HUE_API_STATE_HUE, STATE_HUE),
410 (HUE_API_STATE_SAT, STATE_SATURATION),
411 (HUE_API_STATE_CT, STATE_COLOR_TEMP),
412 (HUE_API_STATE_TRANSITION, STATE_TRANSITION),
414 if key
in request_json:
416 parsed[attr] =
int(request_json[key])
418 _LOGGER.error(
"Unable to parse data (2): %s", request_json)
419 return self.json_message(
"Bad request", HTTPStatus.BAD_REQUEST)
420 if HUE_API_STATE_XY
in request_json:
423 float(request_json[HUE_API_STATE_XY][0]),
424 float(request_json[HUE_API_STATE_XY][1]),
427 _LOGGER.error(
"Unable to parse data (2): %s", request_json)
428 return self.json_message(
"Bad request", HTTPStatus.BAD_REQUEST)
430 if HUE_API_STATE_BRI
in request_json:
431 if entity.domain == light.DOMAIN:
432 if light.brightness_supported(color_modes):
433 parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0
435 parsed[STATE_BRIGHTNESS] =
None
437 elif entity.domain == scene.DOMAIN:
438 parsed[STATE_BRIGHTNESS] =
None
439 parsed[STATE_ON] =
True
441 elif entity.domain
in [
450 level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100
451 parsed[STATE_BRIGHTNESS] = round(level)
452 parsed[STATE_ON] =
True
458 turn_on_needed =
False
461 service: str |
None = SERVICE_TURN_ON
if parsed[STATE_ON]
else SERVICE_TURN_OFF
464 data: dict[str, Any] = {ATTR_ENTITY_ID: entity_id}
468 if entity.domain == light.DOMAIN:
471 light.brightness_supported(color_modes)
472 and parsed[STATE_BRIGHTNESS]
is not None
475 parsed[STATE_BRIGHTNESS]
478 if light.color_supported(color_modes):
479 if any((parsed[STATE_HUE], parsed[STATE_SATURATION])):
480 if parsed[STATE_HUE]
is not None:
481 hue = parsed[STATE_HUE]
485 if parsed[STATE_SATURATION]
is not None:
486 sat = parsed[STATE_SATURATION]
491 hue =
int((hue / HUE_API_STATE_HUE_MAX) * 360)
492 sat =
int((sat / HUE_API_STATE_SAT_MAX) * 100)
494 data[ATTR_HS_COLOR] = (hue, sat)
496 if parsed[STATE_XY]
is not None:
497 data[ATTR_XY_COLOR] = parsed[STATE_XY]
500 light.color_temp_supported(color_modes)
501 and parsed[STATE_COLOR_TEMP]
is not None
503 data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP]
506 entity_features & LightEntityFeature.TRANSITION
507 and parsed[STATE_TRANSITION]
is not None
509 data[ATTR_TRANSITION] = parsed[STATE_TRANSITION] / 10
512 elif entity.domain == script.DOMAIN:
513 data[
"variables"] = {
514 "requested_state": STATE_ON
if parsed[STATE_ON]
else STATE_OFF
517 if parsed[STATE_BRIGHTNESS]
is not None:
518 data[
"variables"][
"requested_level"] = parsed[STATE_BRIGHTNESS]
521 elif entity.domain == climate.DOMAIN:
527 entity_features & ClimateEntityFeature.TARGET_TEMPERATURE
528 and parsed[STATE_BRIGHTNESS]
is not None
530 domain = entity.domain
531 service = SERVICE_SET_TEMPERATURE
532 data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS]
535 elif entity.domain == humidifier.DOMAIN:
536 if parsed[STATE_BRIGHTNESS]
is not None:
537 turn_on_needed =
True
538 domain = entity.domain
539 service = SERVICE_SET_HUMIDITY
540 data[ATTR_HUMIDITY] = parsed[STATE_BRIGHTNESS]
543 elif entity.domain == media_player.DOMAIN:
545 entity_features & MediaPlayerEntityFeature.VOLUME_SET
546 and parsed[STATE_BRIGHTNESS]
is not None
548 turn_on_needed =
True
549 domain = entity.domain
550 service = SERVICE_VOLUME_SET
552 data[ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0
555 elif entity.domain == cover.DOMAIN:
556 domain = entity.domain
557 if service == SERVICE_TURN_ON:
558 service = SERVICE_OPEN_COVER
560 service = SERVICE_CLOSE_COVER
563 entity_features & CoverEntityFeature.SET_POSITION
564 and parsed[STATE_BRIGHTNESS]
is not None
566 domain = entity.domain
567 service = SERVICE_SET_COVER_POSITION
568 data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS]
572 entity.domain == fan.DOMAIN
573 and entity_features & FanEntityFeature.SET_SPEED
574 and parsed[STATE_BRIGHTNESS]
is not None
576 domain = entity.domain
578 data[ATTR_PERCENTAGE] = parsed[STATE_BRIGHTNESS]
581 if entity.domain
in config.off_maps_to_on_domains:
582 service = SERVICE_TURN_ON
586 await hass.services.async_call(
589 {ATTR_ENTITY_ID: entity_id},
593 if service
is not None:
596 await hass.services.async_call(domain, service, data, blocking=
False)
598 if state_will_change:
601 hass, entity_id, STATE_CACHED_TIMEOUT
607 entity_number, HUE_API_STATE_ON, parsed[STATE_ON]
612 (STATE_BRIGHTNESS, HUE_API_STATE_BRI),
613 (STATE_HUE, HUE_API_STATE_HUE),
614 (STATE_SATURATION, HUE_API_STATE_SAT),
615 (STATE_COLOR_TEMP, HUE_API_STATE_CT),
616 (STATE_XY, HUE_API_STATE_XY),
617 (STATE_TRANSITION, HUE_API_STATE_TRANSITION),
619 if parsed[key]
is not None:
620 json_response.append(
624 if entity.domain
in config.off_maps_to_on_domains:
630 config.cached_states[entity_id] = [parsed,
None]
632 config.cached_states[entity_id] = [parsed, time.time()]
634 return self.json(json_response)
638 """Retrieve and convert state and brightness values for an entity."""
639 cached_state_entry = config.cached_states.get(entity.entity_id,
None)
643 if cached_state_entry
is not None:
644 entry_state, entry_time = cached_state_entry
645 if entry_time
is None:
647 cached_state = entry_state
648 elif time.time() - entry_time < STATE_CACHED_TIMEOUT
and entry_state[
653 cached_state = entry_state
656 config.cached_states.pop(entity.entity_id)
658 if cached_state
is None:
661 data: dict[str, Any] = cached_state
663 if data[STATE_BRIGHTNESS]
is None:
664 data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX
if data[STATE_ON]
else 0
667 if (data[STATE_HUE]
is None)
or (data[STATE_SATURATION]
is None):
669 data[STATE_SATURATION] = 0
672 if data[STATE_BRIGHTNESS] == 0:
674 data[STATE_SATURATION] = 0
680 @lru_cache(maxsize=512)
682 """Build a state dict for an entity."""
684 data: dict[str, Any] = {
686 STATE_BRIGHTNESS:
None,
688 STATE_SATURATION:
None,
689 STATE_COLOR_TEMP:
None,
691 attributes = entity.attributes
694 attributes.get(ATTR_BRIGHTNESS)
or 0
696 if (hue_sat := attributes.get(ATTR_HS_COLOR))
is not None:
700 data[STATE_HUE] =
int((hue / 360.0) * HUE_API_STATE_HUE_MAX)
701 data[STATE_SATURATION] =
int((sat / 100.0) * HUE_API_STATE_SAT_MAX)
703 data[STATE_HUE] = HUE_API_STATE_HUE_MIN
704 data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN
705 data[STATE_COLOR_TEMP] = attributes.get(ATTR_COLOR_TEMP)
or 0
708 data[STATE_BRIGHTNESS] = 0
710 data[STATE_SATURATION] = 0
711 data[STATE_COLOR_TEMP] = 0
713 if entity.domain == climate.DOMAIN:
714 temperature = attributes.get(ATTR_TEMPERATURE, 0)
716 data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100)
717 elif entity.domain == humidifier.DOMAIN:
718 humidity = attributes.get(ATTR_HUMIDITY, 0)
720 data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100)
721 elif entity.domain == media_player.DOMAIN:
722 level = attributes.get(ATTR_MEDIA_VOLUME_LEVEL, 1.0
if is_on
else 0.0)
724 data[STATE_BRIGHTNESS] = round(
min(1.0, level) * HUE_API_STATE_BRI_MAX)
725 elif entity.domain == fan.DOMAIN:
726 percentage = attributes.get(ATTR_PERCENTAGE)
or 0
728 data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100)
729 elif entity.domain == cover.DOMAIN:
730 level = attributes.get(ATTR_CURRENT_POSITION, 0)
731 data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX)
737 """Clamp brightness, hue, saturation, and color temp to valid values."""
738 for key, v_min, v_max
in (
739 (STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX),
740 (STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX),
741 (STATE_SATURATION, HUE_API_STATE_SAT_MIN, HUE_API_STATE_SAT_MAX),
742 (STATE_COLOR_TEMP, HUE_API_STATE_CT_MIN, HUE_API_STATE_CT_MAX),
744 if data[key]
is not None:
745 data[key] =
max(v_min,
min(data[key], v_max))
748 @lru_cache(maxsize=1024)
750 """Return the emulated_hue unique id for the entity_id."""
751 unique_id = hashlib.md5(entity_id.encode()).hexdigest()
753 f
"00:{unique_id[0:2]}:{unique_id[2:4]}:"
754 f
"{unique_id[4:6]}:{unique_id[6:8]}:{unique_id[8:10]}:"
755 f
"{unique_id[10:12]}:{unique_id[12:14]}-{unique_id[14:16]}"
760 """Convert an entity to its Hue bridge JSON representation."""
761 color_modes = state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES)
or []
765 json_state: dict[str, str | bool | int] = {
766 HUE_API_STATE_ON: state_dict[STATE_ON],
767 "reachable": state.state != STATE_UNAVAILABLE,
768 "mode":
"homeautomation",
770 retval: dict[str, str | dict[str, str | bool | int]] = {
772 "name": config.get_entity_name(state),
773 "uniqueid": unique_id,
774 "manufacturername":
"Home Assistant",
777 is_light = state.domain == light.DOMAIN
778 color_supported = is_light
and light.color_supported(color_modes)
779 color_temp_supported = is_light
and light.color_temp_supported(color_modes)
780 if color_supported
and color_temp_supported:
783 retval[
"type"] =
"Extended color light"
784 retval[
"modelid"] =
"HASS231"
787 HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS],
788 HUE_API_STATE_HUE: state_dict[STATE_HUE],
789 HUE_API_STATE_SAT: state_dict[STATE_SATURATION],
790 HUE_API_STATE_CT: state_dict[STATE_COLOR_TEMP],
791 HUE_API_STATE_EFFECT:
"none",
794 if state_dict[STATE_HUE] > 0
or state_dict[STATE_SATURATION] > 0:
795 json_state[HUE_API_STATE_COLORMODE] =
"hs"
797 json_state[HUE_API_STATE_COLORMODE] =
"ct"
798 elif color_supported:
801 retval[
"type"] =
"Color light"
802 retval[
"modelid"] =
"HASS213"
805 HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS],
806 HUE_API_STATE_COLORMODE:
"hs",
807 HUE_API_STATE_HUE: state_dict[STATE_HUE],
808 HUE_API_STATE_SAT: state_dict[STATE_SATURATION],
809 HUE_API_STATE_EFFECT:
"none",
812 elif color_temp_supported:
815 retval[
"type"] =
"Color temperature light"
816 retval[
"modelid"] =
"HASS312"
819 HUE_API_STATE_COLORMODE:
"ct",
820 HUE_API_STATE_CT: state_dict[STATE_COLOR_TEMP],
821 HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS],
827 retval[
"type"] =
"Dimmable light"
828 retval[
"modelid"] =
"HASS123"
829 json_state.update({HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS]})
830 elif not config.lights_all_dimmable:
833 retval[
"type"] =
"On/Off light"
834 retval[
"productname"] =
"On/Off light"
835 retval[
"modelid"] =
"HASS321"
840 retval[
"type"] =
"Dimmable light"
841 retval[
"modelid"] =
"HASS123"
842 json_state.update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX})
848 state: State, color_modes: Iterable[ColorMode]
850 """Return True if the state supports brightness."""
851 domain = state.domain
852 if domain == light.DOMAIN:
853 return light.brightness_supported(color_modes)
854 if not (required_feature := DIMMABLE_SUPPORTED_FEATURES_BY_DOMAIN.get(domain)):
856 features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
857 enum = ENTITY_FEATURES_BY_DOMAIN[domain]
858 features =
enum(features)
if type(features)
is int
else features
859 return required_feature
in features
863 entity_number: str, attr: str, value: str
865 """Create a success response for an attribute set on a light."""
866 success_key = f
"/lights/{entity_number}/state/{attr}"
867 return {
"success": {success_key: value}}
871 """Create a config resource."""
873 "name":
"HASS BRIDGE",
874 "mac":
"00:00:00:00:00:00",
875 "swversion":
"01003542",
876 "apiversion":
"1.17.0",
877 "whitelist": {HUE_API_USERNAME: {
"name":
"HASS BRIDGE"}},
878 "ipaddress": f
"{config.advertise_ip}:{config.advertise_port}",
884 """Create a list of all entities."""
885 hass = request.app[KEY_HASS]
887 config.entity_id_to_number(entity_id):
state_to_json(config, state)
888 for entity_id
in config.get_exposed_entity_ids()
889 if (state := hass.states.get(entity_id))
894 """Convert hue brightness 1..254 to hass format 0..255."""
895 return min(255, round((value / HUE_API_STATE_BRI_MAX) * 255))
899 """Convert hass brightness 0..255 to hue 1..254 scale."""
900 return max(1, round((value / 255) * HUE_API_STATE_BRI_MAX))
904 """Convert hass entity states to simple True/False on/off state for Hue."""
905 return entity.state != _OFF_STATES.get(entity.domain, STATE_OFF)
909 hass: core.HomeAssistant, entity_id: str, timeout: float
911 """Wait for an entity to change state."""
915 def _async_event_changed(event: Event[EventStateChangedData]) ->
None:
921 async
with asyncio.timeout(STATE_CHANGE_WAIT_TIMEOUT):
web.Response get(self, web.Request request, str username)
None __init__(self, Config config)
web.Response get(self, web.Request request, str username)
None __init__(self, Config config)
None __init__(self, Config config)
web.Response get(self, web.Request request, str username="")
web.Response get(self, web.Request request, str username)
None __init__(self, Config config)
None __init__(self, Config config)
web.Response put(self, web.Request request, str username)
web.Response put(self, web.Request request, str username, str entity_number)
None __init__(self, Config config)
None __init__(self, Config config)
web.Response get(self, web.Request request, str username, str entity_id)
web.Response get(self, web.Request request)
web.Response post(self, web.Request request)
dict[str, Any] state_to_json(Config config, State state)
dict[str, Any] create_list_of_entities(Config config, web.Request request)
dict[str, Any] get_entity_state_dict(Config config, State entity)
bool state_supports_hue_brightness(State state, Iterable[ColorMode] color_modes)
None wait_for_state_change_or_timeout(core.HomeAssistant hass, str entity_id, float timeout)
str _entity_unique_id(str entity_id)
int hass_to_hue_brightness(int value)
None _clamp_values(dict[str, Any] data)
dict[str, Any] _build_entity_state_dict(State entity)
dict[str, Any] create_hue_success_response(str entity_number, str attr, str value)
bool _hass_to_hue_state(State entity)
dict[str, Any] create_config_model(Config config, web.Request request)
int hue_brightness_to_hass(int value)
bool _remote_is_allowed(str address)
bool is_local(ConfigEntry entry)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)