"""Govee platform.""" from datetime import timedelta, datetime import logging from govee_api_laggat import Govee, GoveeDevice, GoveeError from govee_api_laggat.govee_dtos import GoveeSource from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, LightEntity, ) from homeassistant.const import CONF_DELAY from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import color from .const import ( DOMAIN, CONF_OFFLINE_IS_OFF, CONF_USE_ASSUMED_STATE, COLOR_TEMP_KELVIN_MIN, COLOR_TEMP_KELVIN_MAX, ) _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Set up the Govee Light platform.""" _LOGGER.debug("Setting up Govee lights") config = entry.data options = entry.options hub = hass.data[DOMAIN]["hub"] # refresh update_interval = timedelta( seconds=options.get(CONF_DELAY, config.get(CONF_DELAY, 10)) ) coordinator = GoveeDataUpdateCoordinator( hass, _LOGGER, update_interval=update_interval, config_entry=entry ) # Fetch initial data so we have data when entities subscribe hub.events.new_device += lambda device: add_entity( async_add_entities, hub, entry, coordinator, device ) await coordinator.async_refresh() # Add devices for device in hub.devices: add_entity(async_add_entities, hub, entry, coordinator, device) # async_add_entities( # [ # GoveeLightEntity(hub, entry.title, coordinator, device) # for device in hub.devices # ], # update_before_add=False, # ) def add_entity(async_add_entities, hub, entry, coordinator, device): async_add_entities( [GoveeLightEntity(hub, entry.title, coordinator, device)], update_before_add=False, ) class GoveeDataUpdateCoordinator(DataUpdateCoordinator): """Device state update handler.""" def __init__(self, hass, logger, update_interval=None, *, config_entry): """Initialize global data updater.""" self._config_entry = config_entry super().__init__( hass, logger, name=DOMAIN, update_interval=update_interval, update_method=self._async_update, ) @property def use_assumed_state(self): """Use assumed states.""" return self._config_entry.options.get(CONF_USE_ASSUMED_STATE, True) @property def config_offline_is_off(self): """Interpret offline led's as off (global config).""" return self._config_entry.options.get(CONF_OFFLINE_IS_OFF, False) async def _async_update(self): """Fetch data.""" self.logger.debug("_async_update") if "govee" not in self.hass.data: raise UpdateFailed("Govee instance not available") try: hub = self.hass.data[DOMAIN]["hub"] if not hub.online: # when offline, check connection, this will set hub.online await hub.check_connection() if hub.online: # set global options to library if self.config_offline_is_off: hub.config_offline_is_off = True else: hub.config_offline_is_off = None # allow override in learning info # govee will change this to a single request in 2021 device_states = await hub.get_states() for device in device_states: if device.error: self.logger.warning( "update failed for %s: %s", device.device, device.error ) return device_states except GoveeError as ex: raise UpdateFailed(f"Exception on getting states: {ex}") from ex class GoveeLightEntity(LightEntity): """Representation of a stateful light entity.""" def __init__( self, hub: Govee, title: str, coordinator: GoveeDataUpdateCoordinator, device: GoveeDevice, ): """Init a Govee light strip.""" self._hub = hub self._title = title self._coordinator = coordinator self._device = device @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" return True async def async_added_to_hass(self): """Connect to dispatcher listening for entity data notifications.""" self._coordinator.async_add_listener(self.async_write_ha_state) @property def _state(self): """Lights internal state.""" return self._device # self._hub.state(self._device) @property def supported_features(self): """Flag supported features.""" support_flags = 0 if self._device.support_brightness: support_flags |= SUPPORT_BRIGHTNESS if self._device.support_color: support_flags |= SUPPORT_COLOR if self._device.support_color_tem: support_flags |= SUPPORT_COLOR_TEMP return support_flags async def async_turn_on(self, **kwargs): """Turn device on.""" _LOGGER.debug( "async_turn_on for Govee light %s, kwargs: %s", self._device.device, kwargs ) err = None just_turn_on = True if ATTR_HS_COLOR in kwargs: hs_color = kwargs.pop(ATTR_HS_COLOR) just_turn_on = False col = color.color_hs_to_RGB(hs_color[0], hs_color[1]) _, err = await self._hub.set_color(self._device, col) if ATTR_BRIGHTNESS in kwargs: brightness = kwargs.pop(ATTR_BRIGHTNESS) just_turn_on = False bright_set = brightness - 1 _, err = await self._hub.set_brightness(self._device, bright_set) if ATTR_COLOR_TEMP in kwargs: color_temp = kwargs.pop(ATTR_COLOR_TEMP) just_turn_on = False color_temp_kelvin = color.color_temperature_mired_to_kelvin(color_temp) if color_temp_kelvin > COLOR_TEMP_KELVIN_MAX: color_temp_kelvin = COLOR_TEMP_KELVIN_MAX elif color_temp_kelvin < COLOR_TEMP_KELVIN_MIN: color_temp_kelvin = COLOR_TEMP_KELVIN_MIN _, err = await self._hub.set_color_temp(self._device, color_temp_kelvin) # if there is no known specific command - turn on if just_turn_on: _, err = await self._hub.turn_on(self._device) # debug log unknown commands if kwargs: _LOGGER.debug( "async_turn_on doesnt know how to handle kwargs: %s", repr(kwargs) ) # warn on any error if err: _LOGGER.warning( "async_turn_on failed with '%s' for %s, kwargs: %s", err, self._device.device, kwargs, ) async def async_turn_off(self, **kwargs): """Turn device off.""" _LOGGER.debug("async_turn_off for Govee light %s", self._device.device) await self._hub.turn_off(self._device) @property def unique_id(self): """Return the unique ID.""" return f"govee_{self._title}_{self._device.device}" @property def device_id(self): """Return the ID.""" return self.unique_id @property def name(self): """Return the name.""" return self._device.device_name @property def device_info(self): """Return the device info.""" return { "identifiers": {(DOMAIN, self.device_id)}, "name": self.name, "manufacturer": "Govee", "model": self._device.model, "via_device": (DOMAIN, "Govee API (cloud)"), } @property def is_on(self): """Return true if device is on.""" return self._device.power_state @property def assumed_state(self): """ Return true if the state is assumed. This can be disabled in options. """ return ( self._coordinator.use_assumed_state and self._device.source == GoveeSource.HISTORY ) @property def available(self): """Return if light is available.""" return self._device.online @property def hs_color(self): """Return the hs color value.""" return color.color_RGB_to_hs( self._device.color[0], self._device.color[1], self._device.color[2], ) @property def rgb_color(self): """Return the rgb color value.""" return [ self._device.color[0], self._device.color[1], self._device.color[2], ] @property def brightness(self): """Return the brightness value.""" # govee is reporting 0 to 254 - home assistant uses 1 to 255 return self._device.brightness + 1 @property def color_temp(self): """Return the color_temp of the light.""" if not self._device.color_temp: return None return color.color_temperature_kelvin_to_mired(self._device.color_temp) @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" return color.color_temperature_kelvin_to_mired(COLOR_TEMP_KELVIN_MAX) @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" return color.color_temperature_kelvin_to_mired(COLOR_TEMP_KELVIN_MIN) @property def extra_state_attributes(self): """Return the device state attributes.""" return { # rate limiting information on Govee API "rate_limit_total": self._hub.rate_limit_total, "rate_limit_remaining": self._hub.rate_limit_remaining, "rate_limit_reset_seconds": round(self._hub.rate_limit_reset_seconds, 2), "rate_limit_reset": datetime.fromtimestamp( self._hub.rate_limit_reset ).isoformat(), "rate_limit_on": self._hub.rate_limit_on, # general information "manufacturer": "Govee", "model": self._device.model, }