diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 63a8bef54af..6957862ab7a 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -11,7 +11,7 @@ from .gateway import ConnectXiaomiGateway _LOGGER = logging.getLogger(__name__) -GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor"] +GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"] async def async_setup(hass: core.HomeAssistant, config: dict): diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index 7df9dc54e5a..89fcb45b864 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -3,7 +3,7 @@ from functools import partial import logging -from miio import DeviceException +from miio.gateway import GatewayException from homeassistant.components.alarm_control_panel import ( SUPPORT_ALARM_ARM_AWAY, @@ -103,7 +103,7 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): partial(func, *args, **kwargs) ) _LOGGER.debug("Response received from miio device: %s", result) - except DeviceException as exc: + except GatewayException as exc: _LOGGER.error(mask_error, exc) async def async_alarm_arm_away(self, code=None): @@ -122,7 +122,7 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._gateway.alarm.status) - except DeviceException as ex: + except GatewayException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 7de55835916..6a62fbead13 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -17,6 +17,7 @@ CONF_FLOW_TYPE = "config_flow_device" CONF_GATEWAY = "gateway" DEFAULT_GATEWAY_NAME = "Xiaomi Gateway" ZEROCONF_GATEWAY = "lumi-gateway" +ZEROCONF_ACPARTNER = "lumi-acpartner" GATEWAY_SETTINGS = { vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), @@ -61,7 +62,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_xiaomi_miio") # Check which device is discovered. - if name.startswith(ZEROCONF_GATEWAY): + if name.startswith(ZEROCONF_GATEWAY) or name.startswith(ZEROCONF_ACPARTNER): unique_id = format_mac(mac_address) await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index a148b98ee22..29c80faa892 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -14,6 +14,12 @@ from miio import ( # pylint: disable=import-error PhilipsEyecare, PhilipsMoonlight, ) +from miio.gateway import ( + GATEWAY_MODEL_AC_V1, + GATEWAY_MODEL_AC_V2, + GATEWAY_MODEL_AC_V3, + GatewayException, +) import voluptuous as vol from homeassistant.components.light import ( @@ -31,6 +37,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import color, dt +from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY from .const import ( DOMAIN, SERVICE_EYECARE_MODE_OFF, @@ -123,6 +130,25 @@ SERVICE_TO_METHOD = { } +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Xiaomi light from a config entry.""" + entities = [] + + if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: + gateway = hass.data[DOMAIN][config_entry.entry_id] + # Gateway light + if gateway.model not in [ + GATEWAY_MODEL_AC_V1, + GATEWAY_MODEL_AC_V2, + GATEWAY_MODEL_AC_V3, + ]: + entities.append( + XiaomiGatewayLight(gateway, config_entry.title, config_entry.unique_id) + ) + + async_add_entities(entities, update_before_add=True) + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the light from config.""" if DATA_KEY not in hass.data: @@ -938,3 +964,104 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): async def async_set_delayed_turn_off(self, time_period: timedelta): """Set delayed turn off. Unsupported.""" return + + +class XiaomiGatewayLight(LightEntity): + """Representation of a gateway device's light.""" + + def __init__(self, gateway_device, gateway_name, gateway_device_id): + """Initialize the XiaomiGatewayLight.""" + self._gateway = gateway_device + self._name = f"{gateway_name} Light" + self._gateway_device_id = gateway_device_id + self._unique_id = gateway_device_id + self._available = False + self._is_on = None + self._brightness_pct = 100 + self._rgb = (255, 255, 255) + self._hs = (0, 0) + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info of the gateway.""" + return { + "identifiers": {(DOMAIN, self._gateway_device_id)}, + } + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def available(self): + """Return true when state is known.""" + return self._available + + @property + def is_on(self): + """Return true if it is on.""" + return self._is_on + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int(255 * self._brightness_pct / 100) + + @property + def hs_color(self): + """Return the hs color value.""" + return self._hs + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + + def turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_HS_COLOR in kwargs: + rgb = color.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + else: + rgb = self._rgb + + if ATTR_BRIGHTNESS in kwargs: + brightness_pct = int(100 * kwargs[ATTR_BRIGHTNESS] / 255) + else: + brightness_pct = self._brightness_pct + + self._gateway.light.set_rgb(brightness_pct, rgb) + + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the light off.""" + self._gateway.light.set_rgb(0, self._rgb) + self.schedule_update_ha_state() + + async def async_update(self): + """Fetch state from the device.""" + try: + state_dict = await self.hass.async_add_executor_job( + self._gateway.light.rgb_status + ) + except GatewayException as ex: + if self._available: + self._available = False + _LOGGER.error( + "Got exception while fetching the gateway light state: %s", ex + ) + return + + self._available = True + self._is_on = state_dict["is_on"] + + if self._is_on: + self._brightness_pct = state_dict["brightness"] + self._rgb = state_dict["rgb"] + self._hs = color.color_RGB_to_hs(*self._rgb) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 924d6d8a23d..260a1cc8ae7 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -3,7 +3,13 @@ from dataclasses import dataclass import logging from miio import AirQualityMonitor, DeviceException # pylint: disable=import-error -from miio.gateway import DeviceType +from miio.gateway import ( + GATEWAY_MODEL_AC_V1, + GATEWAY_MODEL_AC_V2, + GATEWAY_MODEL_AC_V3, + DeviceType, + GatewayException, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -12,6 +18,7 @@ from homeassistant.const import ( CONF_NAME, CONF_TOKEN, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PRESSURE_HPA, @@ -78,9 +85,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Xiaomi sensor from a config entry.""" entities = [] - # Gateway sub devices if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: gateway = hass.data[DOMAIN][config_entry.entry_id] + # Gateway illuminance sensor + if gateway.model not in [ + GATEWAY_MODEL_AC_V1, + GATEWAY_MODEL_AC_V2, + GATEWAY_MODEL_AC_V3, + ]: + entities.append( + XiaomiGatewayIlluminanceSensor( + gateway, config_entry.title, config_entry.unique_id + ) + ) + # Gateway sub devices sub_devices = gateway.devices for sub_device in sub_devices.values(): sensor_variables = None @@ -250,3 +268,67 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice): def state(self): """Return the state of the sensor.""" return self._sub_device.status[self._data_key] + + +class XiaomiGatewayIlluminanceSensor(Entity): + """Representation of the gateway device's illuminance sensor.""" + + def __init__(self, gateway_device, gateway_name, gateway_device_id): + """Initialize the entity.""" + self._gateway = gateway_device + self._name = f"{gateway_name} Illuminance" + self._gateway_device_id = gateway_device_id + self._unique_id = f"{gateway_device_id}-illuminance" + self._available = False + self._state = None + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info of the gateway.""" + return { + "identifiers": {(DOMAIN, self._gateway_device_id)}, + } + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def available(self): + """Return true when state is known.""" + return self._available + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return "lux" + + @property + def device_class(self): + """Return the device class of this entity.""" + return DEVICE_CLASS_ILLUMINANCE + + @property + def state(self): + """Return the state of the device.""" + return self._state + + async def async_update(self): + """Fetch state from the device.""" + try: + self._state = await self.hass.async_add_executor_job( + self._gateway.get_illumination + ) + self._available = True + except GatewayException as ex: + if self._available: + self._available = False + _LOGGER.error( + "Got exception while fetching the gateway illuminance state: %s", ex + )