async def async_turn_on(self, **kwargs): """Turn the entity on.""" supported_color_modes = self._supported_color_modes attributes = {} if ATTR_HS_COLOR in kwargs and COLOR_MODE_HS in supported_color_modes: hs_color = kwargs[ATTR_HS_COLOR] attributes["color_hs"] = [hs_color[0], hs_color[1]] if ATTR_WHITE in kwargs and COLOR_MODE_WHITE in supported_color_modes: attributes["white_value"] = scale_brightness(kwargs[ATTR_WHITE]) if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs and brightness_supported( supported_color_modes): attributes["brightness"] = scale_brightness( kwargs[ATTR_BRIGHTNESS]) if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes: attributes["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) if ATTR_EFFECT in kwargs: attributes["effect"] = kwargs[ATTR_EFFECT] self._tasmota_entity.set_state(True, attributes)
def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) self.chars = [] state = self.hass.states.get(self.entity_id) self._features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) self._color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) if brightness_supported(self._color_modes): self.chars.append(CHAR_BRIGHTNESS) if color_supported(self._color_modes): self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) elif color_temp_supported(self._color_modes): # ColorTemperature and Hue characteristic should not be # exposed both. Both states are tracked separately in HomeKit, # causing "source of truth" problems. self.chars.append(CHAR_COLOR_TEMPERATURE) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) self.char_on = serv_light.configure_char(CHAR_ON, value=0) if CHAR_BRIGHTNESS in self.chars: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id).attributes.get( ATTR_MIN_MIREDS, 153) max_mireds = self.hass.states.get(self.entity_id).attributes.get( ATTR_MAX_MIREDS, 500) self.char_color_temperature = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={ PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds }, ) if CHAR_HUE in self.chars: self.char_hue = serv_light.configure_char(CHAR_HUE, value=0) if CHAR_SATURATION in self.chars: self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) self.async_update_state(state) serv_light.setter_callback = self._set_chars
def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) self.chars = [] self._event_timer = None self._pending_events = {} state = self.hass.states.get(self.entity_id) attributes = state.attributes self.color_modes = color_modes = ( attributes.get(ATTR_SUPPORTED_COLOR_MODES) or []) self.color_supported = color_supported(color_modes) self.color_temp_supported = color_temp_supported(color_modes) self.brightness_supported = brightness_supported(color_modes) if self.brightness_supported: self.chars.append(CHAR_BRIGHTNESS) if self.color_supported: self.chars.extend([CHAR_HUE, CHAR_SATURATION]) if self.color_temp_supported: self.chars.append(CHAR_COLOR_TEMPERATURE) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) self.char_on = serv_light.configure_char(CHAR_ON, value=0) if self.brightness_supported: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) if self.color_temp_supported: min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153)) max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500)) self.char_color_temp = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={ PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds }, ) if self.color_supported: self.char_hue = serv_light.configure_char(CHAR_HUE, value=0) self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) self.async_update_state(state) serv_light.setter_callback = self._set_chars
def supported(self) -> bool: """Test if capability is supported.""" features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if self.state.domain == light.DOMAIN: if features & light.SUPPORT_BRIGHTNESS: return True if light.brightness_supported(self.state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES)): return True return False
def interfaces(self): """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) color_modes = self.entity.attributes.get( light.ATTR_SUPPORTED_COLOR_MODES) if light.brightness_supported(color_modes): yield AlexaBrightnessController(self.entity) if light.color_supported(color_modes): yield AlexaColorController(self.entity) if light.color_temp_supported(color_modes): yield AlexaColorTemperatureController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass)
async def async_update(self) -> None: """Update entity attributes when the device status has changed.""" # Brightness and transition if brightness_supported(self._attr_supported_color_modes): self._brightness = int( convert_scale(self._device.status.level, 100, 255, 0)) # Color Temperature if ColorMode.COLOR_TEMP in self._attr_supported_color_modes: self._color_temp = color_util.color_temperature_kelvin_to_mired( self._device.status.color_temperature) # Color if ColorMode.HS in self._attr_supported_color_modes: self._hs_color = ( convert_scale(self._device.status.hue, 100, 360), self._device.status.saturation, )
async def async_turn_on(self, **kwargs): """Turn the entity on.""" supported_color_modes = self._supported_color_modes attributes = {} if ATTR_RGB_COLOR in kwargs and COLOR_MODE_RGB in supported_color_modes: rgb = kwargs[ATTR_RGB_COLOR] attributes["color"] = [rgb[0], rgb[1], rgb[2]] if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: rgbw = kwargs[ATTR_RGBW_COLOR] # Tasmota does not support direct RGBW control, the light must be set to # either white mode or color mode. Set the mode to white if white channel # is on, and to color otheruse if rgbw[3] == 0: attributes["color"] = [rgbw[0], rgbw[1], rgbw[2]] else: white_value_normalized = rgbw[3] / DEFAULT_BRIGHTNESS_MAX device_white_value = min( round(white_value_normalized * TASMOTA_BRIGHTNESS_MAX), TASMOTA_BRIGHTNESS_MAX, ) attributes["white_value"] = device_white_value if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs and brightness_supported( supported_color_modes): brightness_normalized = kwargs[ ATTR_BRIGHTNESS] / DEFAULT_BRIGHTNESS_MAX device_brightness = min( round(brightness_normalized * TASMOTA_BRIGHTNESS_MAX), TASMOTA_BRIGHTNESS_MAX, ) # Make sure the brightness is not rounded down to 0 device_brightness = max(device_brightness, 1) attributes["brightness"] = device_brightness if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes: attributes["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) if ATTR_EFFECT in kwargs: attributes["effect"] = kwargs[ATTR_EFFECT] self._tasmota_entity.set_state(True, attributes)
async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" if self.block.type == "relay": self.control_result = await self.set_state(turn="on") self.async_write_ha_state() return set_mode = None supported_color_modes = self._supported_color_modes params = {"turn": "on"} if ATTR_BRIGHTNESS in kwargs and brightness_supported( supported_color_modes): brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255) if hasattr(self.block, "gain"): params["gain"] = brightness_pct if hasattr(self.block, "brightness"): params["brightness"] = brightness_pct if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes: color_temp = color_temperature_mired_to_kelvin( kwargs[ATTR_COLOR_TEMP]) color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) # Color temperature change - used only in white mode, switch device mode to white set_mode = "white" params["temp"] = int(color_temp) if ATTR_RGB_COLOR in kwargs and COLOR_MODE_RGB in supported_color_modes: # Color channels change - used only in color mode, switch device mode to color set_mode = "color" (params["red"], params["green"], params["blue"]) = kwargs[ATTR_RGB_COLOR] if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: # Color channels change - used only in color mode, switch device mode to color set_mode = "color" (params["red"], params["green"], params["blue"], params["white"]) = kwargs[ATTR_RGBW_COLOR] if await self.set_light_mode(set_mode): self.control_result = await self.set_state(**params) self.async_write_ha_state()
async def async_turn_off(self, **kwargs): """Turn the entity off.""" duration = kwargs.get(light.ATTR_TRANSITION) supports_level = brightness_supported(self._attr_supported_color_modes) if duration and supports_level: result = await self._level_channel.move_to_level_with_on_off( 0, duration * 10) else: result = await self._on_off_channel.off() self.debug("turned off: %s", result) if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = False if duration and supports_level: # store current brightness so that the next turn_on uses it. self._off_brightness = self._brightness self.async_write_ha_state()
def _update(self): """Really update the state.""" # Brightness handling if brightness_supported(self.supported_color_modes): self._attr_brightness = scaleto255( int(self.fibaro_device.properties.value)) # Color handling if (color_supported(self.supported_color_modes) and "color" in self.fibaro_device.properties and "," in self.fibaro_device.properties.color): # Fibaro communicates the color as an 'R, G, B, W' string rgbw_s = self.fibaro_device.properties.color if rgbw_s == "0,0,0,0" and "lastColorSet" in self.fibaro_device.properties: rgbw_s = self.fibaro_device.properties.lastColorSet rgbw_list = [int(i) for i in rgbw_s.split(",")][:4] if self._attr_color_mode == ColorMode.RGB: self._attr_rgb_color = tuple(rgbw_list[:3]) else: self._attr_rgbw_color = tuple(rgbw_list)
def update_dynamic_attributes(self): """Update dynamic attributes of the luminary.""" self._is_on = self._luminary.on() self._available = self._luminary.reachable( ) and not self._luminary.deleted() if brightness_supported(self._attr_supported_color_modes): self._brightness = int(self._luminary.lum() * 2.55) if ColorMode.COLOR_TEMP in self._attr_supported_color_modes: self._color_temp = color_util.color_temperature_kelvin_to_mired( self._luminary.temp() or DEFAULT_KELVIN) if ColorMode.HS in self._attr_supported_color_modes: self._rgb_color = self._luminary.rgb() if len(self._attr_supported_color_modes > 1): # The light supports hs + color temp, determine which one it is if self._rgb_color == (0, 0, 0): self._attr_color_mode = ColorMode.COLOR_TEMP else: self._attr_color_mode = ColorMode.HS
async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" if self.block.type == "relay": self.control_result = await self.set_state(turn="on") self.async_write_ha_state() return set_mode = None supported_color_modes = self._attr_supported_color_modes params: dict[str, Any] = {"turn": "on"} if ATTR_TRANSITION in kwargs: params["transition"] = min(int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME) if ATTR_BRIGHTNESS in kwargs and brightness_supported( supported_color_modes): brightness_pct = int(100 * (kwargs[ATTR_BRIGHTNESS] + 1) / 255) if hasattr(self.block, "gain"): params["gain"] = brightness_pct if hasattr(self.block, "brightness"): params["brightness"] = brightness_pct if ATTR_COLOR_TEMP in kwargs and ColorMode.COLOR_TEMP in supported_color_modes: color_temp = color_temperature_mired_to_kelvin( kwargs[ATTR_COLOR_TEMP]) color_temp = min(self._max_kelvin, max(self._min_kelvin, color_temp)) # Color temperature change - used only in white mode, switch device mode to white set_mode = "white" params["temp"] = int(color_temp) if ATTR_RGB_COLOR in kwargs and ColorMode.RGB in supported_color_modes: # Color channels change - used only in color mode, switch device mode to color set_mode = "color" (params["red"], params["green"], params["blue"]) = kwargs[ATTR_RGB_COLOR] if ATTR_RGBW_COLOR in kwargs and ColorMode.RGBW in supported_color_modes: # Color channels change - used only in color mode, switch device mode to color set_mode = "color" (params["red"], params["green"], params["blue"], params["white"]) = kwargs[ATTR_RGBW_COLOR] if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP not in kwargs: # Color effect change - used only in color mode, switch device mode to color set_mode = "color" if self.wrapper.model == "SHBLB-1": effect_dict = SHBLB_1_RGB_EFFECTS else: effect_dict = STANDARD_RGB_EFFECTS if kwargs[ATTR_EFFECT] in effect_dict.values(): params["effect"] = [ k for k, v in effect_dict.items() if v == kwargs[ATTR_EFFECT] ][0] else: LOGGER.error( "Effect '%s' not supported by device %s", kwargs[ATTR_EFFECT], self.wrapper.model, ) if (set_mode and set_mode != self.mode and self.wrapper.model in DUAL_MODE_LIGHT_MODELS): params["mode"] = set_mode self.control_result = await self.set_state(**params) self.async_write_ha_state()
async def put(self, request, username, entity_number): # noqa: C901 """Process a request to set the state of an individual light.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) config = self.config hass = request.app["hass"] entity_id = config.number_to_entity_id(entity_number) if entity_id is None: _LOGGER.error("Unknown entity number: %s", entity_number) return self.json_message("Entity not found", HTTP_NOT_FOUND) entity = hass.states.get(entity_id) if entity is None: _LOGGER.error("Entity not found: %s", entity_id) return self.json_message("Entity not found", HTTP_NOT_FOUND) if not config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) try: request_json = await request.json() except ValueError: _LOGGER.error("Received invalid json") return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) # Get the entity's supported features entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if entity.domain == light.DOMAIN: color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) # Parse the request parsed = { STATE_ON: False, STATE_BRIGHTNESS: None, STATE_HUE: None, STATE_SATURATION: None, STATE_COLOR_TEMP: None, STATE_XY: None, STATE_TRANSITON: None, } if HUE_API_STATE_ON in request_json: if not isinstance(request_json[HUE_API_STATE_ON], bool): _LOGGER.error("Unable to parse data: %s", request_json) return self.json_message("Bad request", HTTP_BAD_REQUEST) parsed[STATE_ON] = request_json[HUE_API_STATE_ON] else: parsed[STATE_ON] = entity.state != STATE_OFF for (key, attr) in ( (HUE_API_STATE_BRI, STATE_BRIGHTNESS), (HUE_API_STATE_HUE, STATE_HUE), (HUE_API_STATE_SAT, STATE_SATURATION), (HUE_API_STATE_CT, STATE_COLOR_TEMP), (HUE_API_STATE_TRANSITION, STATE_TRANSITON), ): if key in request_json: try: parsed[attr] = int(request_json[key]) except ValueError: _LOGGER.error("Unable to parse data (2): %s", request_json) return self.json_message("Bad request", HTTP_BAD_REQUEST) if HUE_API_STATE_XY in request_json: try: parsed[STATE_XY] = ( float(request_json[HUE_API_STATE_XY][0]), float(request_json[HUE_API_STATE_XY][1]), ) except ValueError: _LOGGER.error("Unable to parse data (2): %s", request_json) return self.json_message("Bad request", HTTP_BAD_REQUEST) if HUE_API_STATE_BRI in request_json: if entity.domain == light.DOMAIN: if light.brightness_supported(color_modes): parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 else: parsed[STATE_BRIGHTNESS] = None elif entity.domain == scene.DOMAIN: parsed[STATE_BRIGHTNESS] = None parsed[STATE_ON] = True elif entity.domain in [ script.DOMAIN, media_player.DOMAIN, fan.DOMAIN, cover.DOMAIN, climate.DOMAIN, humidifier.DOMAIN, ]: # Convert 0-254 to 0-100 level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 parsed[STATE_BRIGHTNESS] = round(level) parsed[STATE_ON] = True # Choose general HA domain domain = core.DOMAIN # Entity needs separate call to turn on turn_on_needed = False # Convert the resulting "on" status into the service we need to call service = SERVICE_TURN_ON if parsed[STATE_ON] else SERVICE_TURN_OFF # Construct what we need to send to the service data = {ATTR_ENTITY_ID: entity_id} # If the requested entity is a light, set the brightness, hue, # saturation and color temp if entity.domain == light.DOMAIN: if parsed[STATE_ON]: if ( light.brightness_supported(color_modes) and parsed[STATE_BRIGHTNESS] is not None ): data[ATTR_BRIGHTNESS] = hue_brightness_to_hass( parsed[STATE_BRIGHTNESS] ) if light.color_supported(color_modes): if any((parsed[STATE_HUE], parsed[STATE_SATURATION])): if parsed[STATE_HUE] is not None: hue = parsed[STATE_HUE] else: hue = 0 if parsed[STATE_SATURATION] is not None: sat = parsed[STATE_SATURATION] else: sat = 0 # Convert hs values to hass hs values hue = int((hue / HUE_API_STATE_HUE_MAX) * 360) sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) data[ATTR_HS_COLOR] = (hue, sat) if parsed[STATE_XY] is not None: data[ATTR_XY_COLOR] = parsed[STATE_XY] if ( light.color_temp_supported(color_modes) and parsed[STATE_COLOR_TEMP] is not None ): data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] if ( entity_features & SUPPORT_TRANSITION and parsed[STATE_TRANSITON] is not None ): data[ATTR_TRANSITION] = parsed[STATE_TRANSITON] / 10 # If the requested entity is a script, add some variables elif entity.domain == script.DOMAIN: data["variables"] = { "requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF } if parsed[STATE_BRIGHTNESS] is not None: data["variables"]["requested_level"] = parsed[STATE_BRIGHTNESS] # If the requested entity is a climate, set the temperature elif entity.domain == climate.DOMAIN: # We don't support turning climate devices on or off, # only setting the temperature service = None if ( entity_features & SUPPORT_TARGET_TEMPERATURE and parsed[STATE_BRIGHTNESS] is not None ): domain = entity.domain service = SERVICE_SET_TEMPERATURE data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS] # If the requested entity is a humidifier, set the humidity elif entity.domain == humidifier.DOMAIN: if parsed[STATE_BRIGHTNESS] is not None: turn_on_needed = True domain = entity.domain service = SERVICE_SET_HUMIDITY data[ATTR_HUMIDITY] = parsed[STATE_BRIGHTNESS] # If the requested entity is a media player, convert to volume elif entity.domain == media_player.DOMAIN: if ( entity_features & SUPPORT_VOLUME_SET and parsed[STATE_BRIGHTNESS] is not None ): turn_on_needed = True domain = entity.domain service = SERVICE_VOLUME_SET # Convert 0-100 to 0.0-1.0 data[ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0 # If the requested entity is a cover, convert to open_cover/close_cover elif entity.domain == cover.DOMAIN: domain = entity.domain if service == SERVICE_TURN_ON: service = SERVICE_OPEN_COVER else: service = SERVICE_CLOSE_COVER if ( entity_features & SUPPORT_SET_POSITION and parsed[STATE_BRIGHTNESS] is not None ): domain = entity.domain service = SERVICE_SET_COVER_POSITION data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS] # If the requested entity is a fan, convert to speed elif ( entity.domain == fan.DOMAIN and entity_features & SUPPORT_SET_SPEED and parsed[STATE_BRIGHTNESS] is not None ): domain = entity.domain # Convert 0-100 to a fan speed brightness = parsed[STATE_BRIGHTNESS] if brightness == 0: data[ATTR_SPEED] = SPEED_OFF elif 0 < brightness <= 33.3: data[ATTR_SPEED] = SPEED_LOW elif 33.3 < brightness <= 66.6: data[ATTR_SPEED] = SPEED_MEDIUM elif 66.6 < brightness <= 100: data[ATTR_SPEED] = SPEED_HIGH # Map the off command to on if entity.domain in config.off_maps_to_on_domains: service = SERVICE_TURN_ON # Separate call to turn on needed if turn_on_needed: hass.async_create_task( hass.services.async_call( core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) ) if service is not None: state_will_change = parsed[STATE_ON] != (entity.state != STATE_OFF) hass.async_create_task( hass.services.async_call(domain, service, data, blocking=True) ) if state_will_change: # Wait for the state to change. await wait_for_state_change_or_timeout( hass, entity_id, STATE_CACHED_TIMEOUT ) # Create success responses for all received keys json_response = [ create_hue_success_response( entity_number, HUE_API_STATE_ON, parsed[STATE_ON] ) ] for (key, val) in ( (STATE_BRIGHTNESS, HUE_API_STATE_BRI), (STATE_HUE, HUE_API_STATE_HUE), (STATE_SATURATION, HUE_API_STATE_SAT), (STATE_COLOR_TEMP, HUE_API_STATE_CT), (STATE_XY, HUE_API_STATE_XY), (STATE_TRANSITON, HUE_API_STATE_TRANSITION), ): if parsed[key] is not None: json_response.append( create_hue_success_response(entity_number, val, parsed[key]) ) if entity.domain in config.off_maps_to_on_domains: # Caching is required because things like scripts and scenes won't # report as "off" to Alexa if an "off" command is received, because # they'll map to "on". Thus, instead of reporting its actual # status, we report what Alexa will want to see, which is the same # as the actual requested command. config.cached_states[entity_id] = [parsed, None] else: config.cached_states[entity_id] = [parsed, time.time()] return self.json(json_response)
def state_to_json(config: Config, state: State) -> dict[str, Any]: """Convert an entity to its Hue bridge JSON representation.""" 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) 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", } if light.color_supported(color_modes) and light.color_temp_supported( color_modes): # Extended Color light (Zigbee Device ID: 0x0210) # Same as Color light, but which supports additional setting of color temperature retval["type"] = "Extended color light" retval["modelid"] = "HASS231" json_state.update({ 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_dict[STATE_HUE] > 0 or state_dict[STATE_SATURATION] > 0: json_state[HUE_API_STATE_COLORMODE] = "hs" else: 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" json_state.update({ HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], HUE_API_STATE_COLORMODE: "hs", HUE_API_STATE_HUE: state_dict[STATE_HUE], HUE_API_STATE_SAT: state_dict[STATE_SATURATION], HUE_API_STATE_EFFECT: "none", }) elif light.color_temp_supported(color_modes): # Color temperature light (Zigbee Device ID: 0x0220) # Supports groups, scenes, on/off, dimming, and setting of a color temperature retval["type"] = "Color temperature light" retval["modelid"] = "HASS312" json_state.update({ HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state_dict[STATE_COLOR_TEMP], HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], }) elif entity_features & (CoverEntityFeature.SET_POSITION | FanEntityFeature.SET_SPEED | MediaPlayerEntityFeature.VOLUME_SET | ClimateEntityFeature.TARGET_TEMPERATURE ) or light.brightness_supported(color_modes): # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" 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 retval["type"] = "On/Off light" retval["productname"] = "On/Off light" retval["modelid"] = "HASS321" else: # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming # Reports fixed brightness for compatibility with Alexa. retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" json_state.update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX}) return retval
def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) self.chars_primary = [] self.chars_secondary = [] state = self.hass.states.get(self.entity_id) attributes = state.attributes color_modes = attributes.get(ATTR_SUPPORTED_COLOR_MODES) self.is_color_supported = color_supported(color_modes) self.is_color_temp_supported = color_temp_supported(color_modes) self.color_and_temp_supported = (self.is_color_supported and self.is_color_temp_supported) self.is_brightness_supported = brightness_supported(color_modes) if self.is_brightness_supported: self.chars_primary.append(CHAR_BRIGHTNESS) if self.is_color_supported: self.chars_primary.append(CHAR_HUE) self.chars_primary.append(CHAR_SATURATION) if self.is_color_temp_supported: if self.color_and_temp_supported: self.chars_primary.append(CHAR_NAME) self.chars_secondary.append(CHAR_NAME) self.chars_secondary.append(CHAR_COLOR_TEMPERATURE) if self.is_brightness_supported: self.chars_secondary.append(CHAR_BRIGHTNESS) else: self.chars_primary.append(CHAR_COLOR_TEMPERATURE) serv_light_primary = self.add_preload_service(SERV_LIGHTBULB, self.chars_primary) serv_light_secondary = None self.char_on_primary = serv_light_primary.configure_char(CHAR_ON, value=0) if self.color_and_temp_supported: serv_light_secondary = self.add_preload_service( SERV_LIGHTBULB, self.chars_secondary) serv_light_primary.add_linked_service(serv_light_secondary) serv_light_primary.configure_char(CHAR_NAME, value="RGB") self.char_on_secondary = serv_light_secondary.configure_char( CHAR_ON, value=0) serv_light_secondary.configure_char(CHAR_NAME, value="Temperature") if self.is_brightness_supported: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. self.char_brightness_primary = serv_light_primary.configure_char( CHAR_BRIGHTNESS, value=100) if self.chars_secondary: self.char_brightness_secondary = serv_light_secondary.configure_char( CHAR_BRIGHTNESS, value=100) if self.is_color_temp_supported: min_mireds = attributes.get(ATTR_MIN_MIREDS, 153) max_mireds = attributes.get(ATTR_MAX_MIREDS, 500) serv_light = serv_light_secondary or serv_light_primary self.char_color_temperature = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={ PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds }, ) if self.is_color_supported: self.char_hue = serv_light_primary.configure_char(CHAR_HUE, value=0) self.char_saturation = serv_light_primary.configure_char( CHAR_SATURATION, value=75) self.async_update_state(state) if self.color_and_temp_supported: serv_light_primary.setter_callback = self._set_chars_primary serv_light_secondary.setter_callback = self._set_chars_secondary else: serv_light_primary.setter_callback = self._set_chars
class HueOneLightChangeView(HomeAssistantView): """Handle requests for setting info about entities.""" url = "/api/{username}/lights/{entity_number}/state" name = "emulated_hue:light:state" requires_auth = False def __init__(self, config): """Initialize the instance of the view.""" self.config = config async def put(self, request, username, entity_number): # noqa: C901 """Process a request to set the state of an individual light.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) config = self.config hass = request.app["hass"] entity_id = config.number_to_entity_id(entity_number) if entity_id is None: _LOGGER.error("Unknown entity number: %s", entity_number) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) if (entity := hass.states.get(entity_id)) is None: _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): _LOGGER.error("Entity not exposed: %s", entity_id) return self.json_message("Entity not exposed", HTTPStatus.UNAUTHORIZED) try: request_json = await request.json() except ValueError: _LOGGER.error("Received invalid json") return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST) # Get the entity's supported features entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if entity.domain == light.DOMAIN: color_modes = entity.attributes.get( light.ATTR_SUPPORTED_COLOR_MODES, []) # Parse the request parsed = { STATE_ON: False, STATE_BRIGHTNESS: None, STATE_HUE: None, STATE_SATURATION: None, STATE_COLOR_TEMP: None, STATE_XY: None, STATE_TRANSITION: None, } if HUE_API_STATE_ON in request_json: if not isinstance(request_json[HUE_API_STATE_ON], bool): _LOGGER.error("Unable to parse data: %s", request_json) return self.json_message("Bad request", HTTPStatus.BAD_REQUEST) parsed[STATE_ON] = request_json[HUE_API_STATE_ON] else: parsed[STATE_ON] = entity.state != STATE_OFF for (key, attr) in ( (HUE_API_STATE_BRI, STATE_BRIGHTNESS), (HUE_API_STATE_HUE, STATE_HUE), (HUE_API_STATE_SAT, STATE_SATURATION), (HUE_API_STATE_CT, STATE_COLOR_TEMP), (HUE_API_STATE_TRANSITION, STATE_TRANSITION), ): if key in request_json: try: parsed[attr] = int(request_json[key]) except ValueError: _LOGGER.error("Unable to parse data (2): %s", request_json) return self.json_message("Bad request", HTTPStatus.BAD_REQUEST) if HUE_API_STATE_XY in request_json: try: parsed[STATE_XY] = ( float(request_json[HUE_API_STATE_XY][0]), float(request_json[HUE_API_STATE_XY][1]), ) except ValueError: _LOGGER.error("Unable to parse data (2): %s", request_json) return self.json_message("Bad request", HTTPStatus.BAD_REQUEST) if HUE_API_STATE_BRI in request_json: if entity.domain == light.DOMAIN: if light.brightness_supported(color_modes): parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 else: parsed[STATE_BRIGHTNESS] = None elif entity.domain == scene.DOMAIN: parsed[STATE_BRIGHTNESS] = None parsed[STATE_ON] = True elif entity.domain in [ script.DOMAIN, media_player.DOMAIN, fan.DOMAIN, cover.DOMAIN, climate.DOMAIN, humidifier.DOMAIN, ]: # Convert 0-254 to 0-100 level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 parsed[STATE_BRIGHTNESS] = round(level) parsed[STATE_ON] = True # Choose general HA domain domain = core.DOMAIN # Entity needs separate call to turn on turn_on_needed = False # Convert the resulting "on" status into the service we need to call service = SERVICE_TURN_ON if parsed[STATE_ON] else SERVICE_TURN_OFF # Construct what we need to send to the service data = {ATTR_ENTITY_ID: entity_id} # If the requested entity is a light, set the brightness, hue, # saturation and color temp if entity.domain == light.DOMAIN: if parsed[STATE_ON]: if (light.brightness_supported(color_modes) and parsed[STATE_BRIGHTNESS] is not None): data[ATTR_BRIGHTNESS] = hue_brightness_to_hass( parsed[STATE_BRIGHTNESS]) if light.color_supported(color_modes): if any((parsed[STATE_HUE], parsed[STATE_SATURATION])): if parsed[STATE_HUE] is not None: hue = parsed[STATE_HUE] else: hue = 0 if parsed[STATE_SATURATION] is not None: sat = parsed[STATE_SATURATION] else: sat = 0 # Convert hs values to hass hs values hue = int((hue / HUE_API_STATE_HUE_MAX) * 360) sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) data[ATTR_HS_COLOR] = (hue, sat) if parsed[STATE_XY] is not None: data[ATTR_XY_COLOR] = parsed[STATE_XY] if (light.color_temp_supported(color_modes) and parsed[STATE_COLOR_TEMP] is not None): data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] if (entity_features & SUPPORT_TRANSITION and parsed[STATE_TRANSITION] is not None): data[ATTR_TRANSITION] = parsed[STATE_TRANSITION] / 10 # If the requested entity is a script, add some variables elif entity.domain == script.DOMAIN: data["variables"] = { "requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF } if parsed[STATE_BRIGHTNESS] is not None: data["variables"]["requested_level"] = parsed[STATE_BRIGHTNESS] # If the requested entity is a climate, set the temperature elif entity.domain == climate.DOMAIN: # We don't support turning climate devices on or off, # only setting the temperature service = None if (entity_features & SUPPORT_TARGET_TEMPERATURE and parsed[STATE_BRIGHTNESS] is not None): domain = entity.domain service = SERVICE_SET_TEMPERATURE data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS] # If the requested entity is a humidifier, set the humidity elif entity.domain == humidifier.DOMAIN: if parsed[STATE_BRIGHTNESS] is not None: turn_on_needed = True domain = entity.domain service = SERVICE_SET_HUMIDITY data[ATTR_HUMIDITY] = parsed[STATE_BRIGHTNESS] # If the requested entity is a media player, convert to volume elif entity.domain == media_player.DOMAIN: if (entity_features & SUPPORT_VOLUME_SET and parsed[STATE_BRIGHTNESS] is not None): turn_on_needed = True domain = entity.domain service = SERVICE_VOLUME_SET # Convert 0-100 to 0.0-1.0 data[ ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0 # If the requested entity is a cover, convert to open_cover/close_cover elif entity.domain == cover.DOMAIN: domain = entity.domain if service == SERVICE_TURN_ON: service = SERVICE_OPEN_COVER else: service = SERVICE_CLOSE_COVER if (entity_features & SUPPORT_SET_POSITION and parsed[STATE_BRIGHTNESS] is not None): domain = entity.domain service = SERVICE_SET_COVER_POSITION data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS] # If the requested entity is a fan, convert to speed elif (entity.domain == fan.DOMAIN and entity_features & SUPPORT_SET_SPEED and parsed[STATE_BRIGHTNESS] is not None): domain = entity.domain # Convert 0-100 to a fan speed if (brightness := parsed[STATE_BRIGHTNESS]) == 0: data[ATTR_SPEED] = SPEED_OFF elif 0 < brightness <= 33.3: data[ATTR_SPEED] = SPEED_LOW elif 33.3 < brightness <= 66.6: data[ATTR_SPEED] = SPEED_MEDIUM elif 66.6 < brightness <= 100: data[ATTR_SPEED] = SPEED_HIGH
def entity_to_json(config, entity): """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]}" state = get_entity_state(config, entity) retval = { "state": { HUE_API_STATE_ON: state[STATE_ON], "reachable": entity.state != STATE_UNAVAILABLE, "mode": "homeautomation", }, "name": config.get_entity_name(entity), "uniqueid": unique_id, "manufacturername": "Home Assistant", "swversion": "123", } if light.color_supported(color_modes) and light.color_temp_supported( color_modes): # Extended Color light (Zigbee Device ID: 0x0210) # Same as Color light, but which supports additional setting of color temperature retval["type"] = "Extended color light" retval["modelid"] = "HASS231" retval["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_EFFECT: "none", }) if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0: retval["state"][HUE_API_STATE_COLORMODE] = "hs" else: retval["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({ HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], HUE_API_STATE_COLORMODE: "hs", HUE_API_STATE_HUE: state[STATE_HUE], HUE_API_STATE_SAT: state[STATE_SATURATION], HUE_API_STATE_EFFECT: "none", }) elif light.color_temp_supported(color_modes): # Color temperature light (Zigbee Device ID: 0x0220) # Supports groups, scenes, on/off, dimming, and setting of a color temperature retval["type"] = "Color temperature light" retval["modelid"] = "HASS312" retval["state"].update({ HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP], HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], }) elif entity_features & (SUPPORT_SET_POSITION | SUPPORT_SET_SPEED | SUPPORT_VOLUME_SET | SUPPORT_TARGET_TEMPERATURE ) or light.brightness_supported(color_modes): # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) elif not config.lights_all_dimmable: # On/Off light (ZigBee Device ID: 0x0000) # Supports groups, scenes and on/off control retval["type"] = "On/Off light" retval["productname"] = "On/Off light" retval["modelid"] = "HASS321" else: # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming # 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}) return retval
async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) duration = (transition * 10 if transition else self._default_transition * 10 if self._default_transition else DEFAULT_TRANSITION) brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) if brightness is None and self._off_brightness is not None: brightness = self._off_brightness t_log = {} if (brightness is not None or transition) and brightness_supported( self._attr_supported_color_modes): if brightness is not None: level = min(254, brightness) else: level = self._brightness or 254 result = await self._level_channel.move_to_level_with_on_off( level, duration) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = bool(level) if level: self._brightness = level if brightness is None or (self._FORCE_ON and brightness): # since some lights don't always turn on with move_to_level_with_on_off, # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() t_log["on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = True if light.ATTR_COLOR_TEMP in kwargs: temperature = kwargs[light.ATTR_COLOR_TEMP] result = await self._color_channel.move_to_color_temp( temperature, duration) t_log["move_to_color_temp"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._color_temp = temperature self._hs_color = None if light.ATTR_HS_COLOR in kwargs: hs_color = kwargs[light.ATTR_HS_COLOR] xy_color = color_util.color_hs_to_xy(*hs_color) result = await self._color_channel.move_to_color( int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration) t_log["move_to_color"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._hs_color = hs_color self._color_temp = None if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION | UPDATE_COLORLOOP_DIRECTION | UPDATE_COLORLOOP_TIME, 0x2, # start from current hue 0x1, # only support up transition if transition else 7, # transition 0, # no hue ) t_log["color_loop_set"] = result self._effect = light.EFFECT_COLORLOOP elif (self._effect == light.EFFECT_COLORLOOP and effect != light.EFFECT_COLORLOOP): result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION, 0x0, 0x0, 0x0, 0x0, # update action only, action off, no dir, time, hue ) t_log["color_loop_set"] = result self._effect = None if flash is not None: result = await self._identify_channel.trigger_effect( FLASH_EFFECTS[flash], EFFECT_DEFAULT_VARIANT) t_log["trigger_effect"] = result self._off_brightness = None self.debug("turned on: %s", t_log) self.async_write_ha_state()