From ffcdd85117ab8bab44d061f6a6653b5046234fbb Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 4 Jul 2020 14:56:16 +0200 Subject: [PATCH] Xiaomi Gateway subdevice support & AqaraHT + SensorHT devices (#36539) * Gateway subdevice support & AqaraHT + SensorHT devices * Gateway subdevice support & AqaraHT + SensorHT devices * Add starkillerOG to codeowners as proposed by @rytilahti in this issue: https://github.com/home-assistant/core/issues/36516 * add starkillerOG to xiaomi_miio * fix config flow tests * Update CODEOWNERS * Update manifest.json * prosess revieuw comments * fix missing import * use proper pressure unit hPa * subdevice --> sub_device Co-authored-by: Martin Hjelmare * subdevice --> sub_device Co-authored-by: Martin Hjelmare * use key acces instead of get Co-authored-by: Martin Hjelmare * subdevice --> sub_device Co-authored-by: Martin Hjelmare * subdevice --> sub_device Co-authored-by: Martin Hjelmare * subdevice --> sub_device Co-authored-by: Martin Hjelmare * subdevice --> sub_device * use dataclass instead of namedtuple * update to newest python-miio functions (not yet released) * Move device info to entitie * remove unused variable * improve default names * SensorHT does not support pressure * bump python-miio to 0.5.2 * bump python-miio to 0.5.2 * bump python-miio to 0.5.2 * Fix missing brackets Co-authored-by: Teemu R. Co-authored-by: Martin Hjelmare Co-authored-by: Teemu R. --- .../components/xiaomi_miio/__init__.py | 2 +- .../components/xiaomi_miio/gateway.py | 57 +++++++++++ .../components/xiaomi_miio/sensor.py | 94 ++++++++++++++++++- .../xiaomi_miio/test_config_flow.py | 7 ++ 4 files changed, 158 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 9524406a1f9..63a8bef54af 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"] +GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor"] async def async_setup(hass: core.HomeAssistant, config: dict): diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index 2195c9eecdc..8c928cb36e4 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -3,6 +3,10 @@ import logging from miio import DeviceException, gateway +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) @@ -30,9 +34,14 @@ class ConnectXiaomiGateway: _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) try: self._gateway_device = gateway.Gateway(host, token) + # get the gateway info self._gateway_info = await self._hass.async_add_executor_job( self._gateway_device.info ) + # get the connected sub devices + await self._hass.async_add_executor_job( + self._gateway_device.discover_devices + ) except DeviceException: _LOGGER.error( "DeviceException during setup of xiaomi gateway with host %s", host @@ -45,3 +54,51 @@ class ConnectXiaomiGateway: self._gateway_info.hardware_version, ) return True + + +class XiaomiGatewayDevice(Entity): + """Representation of a base Xiaomi Gateway Device.""" + + def __init__(self, sub_device, entry): + """Initialize the Xiaomi Gateway Device.""" + self._sub_device = sub_device + self._entry = entry + self._unique_id = sub_device.sid + self._name = f"{sub_device.name} ({sub_device.sid})" + self._available = None + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def device_info(self): + """Return the device info of the gateway.""" + return { + "identifiers": {(DOMAIN, self._sub_device.sid)}, + "via_device": (DOMAIN, self._entry.unique_id), + "manufacturer": "Xiaomi", + "name": self._sub_device.name, + "model": self._sub_device.model, + "sw_version": self._sub_device.firmware_version, + } + + @property + def available(self): + """Return true when state is known.""" + return self._available + + async def async_update(self): + """Fetch state from the sub device.""" + try: + await self.hass.async_add_executor_job(self._sub_device.update) + self._available = True + except gateway.GatewayException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 9f5ea1fa868..b7553e32b43 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,15 +1,31 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" +from dataclasses import dataclass import logging from miio import AirQualityMonitor, DeviceException # pylint: disable=import-error +from miio.gateway import DeviceType import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_TOKEN, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_HPA, + TEMP_CELSIUS, + UNIT_PERCENTAGE, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY +from .const import DOMAIN +from .gateway import XiaomiGatewayDevice + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Sensor" @@ -36,6 +52,51 @@ ATTR_MODEL = "model" SUCCESS = ["ok"] +@dataclass +class SensorType: + """Class that holds device specific info for a xiaomi aqara sensor.""" + + unit: str = None + icon: str = None + device_class: str = None + + +GATEWAY_SENSOR_TYPES = { + "temperature": SensorType( + unit=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE + ), + "humidity": SensorType( + unit=UNIT_PERCENTAGE, icon=None, device_class=DEVICE_CLASS_HUMIDITY + ), + "pressure": SensorType( + unit=PRESSURE_HPA, icon=None, device_class=DEVICE_CLASS_PRESSURE + ), +} + + +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] + sub_devices = gateway.devices + for sub_device in sub_devices.values(): + if sub_device.type == DeviceType.SensorHT: + sensor_variables = ["temperature", "humidity"] + if sub_device.type == DeviceType.AqaraHT: + sensor_variables = ["temperature", "humidity", "pressure"] + entities.extend( + [ + XiaomiGatewaySensor(sub_device, config_entry, variable) + for variable in sensor_variables + ] + ) + + async_add_entities(entities, update_before_add=True) + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sensor from config.""" if DATA_KEY not in hass.data: @@ -156,3 +217,34 @@ class XiaomiAirQualityMonitor(Entity): except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + + +class XiaomiGatewaySensor(XiaomiGatewayDevice): + """Representation of a XiaomiGatewaySensor.""" + + def __init__(self, sub_device, entry, data_key): + """Initialize the XiaomiSensor.""" + super().__init__(sub_device, entry) + self._data_key = data_key + self._unique_id = f"{sub_device.sid}-{data_key}" + self._name = f"{data_key} ({sub_device.sid})".capitalize() + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return GATEWAY_SENSOR_TYPES[self._data_key].icon + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return GATEWAY_SENSOR_TYPES[self._data_key].unit + + @property + def device_class(self): + """Return the device class of this entity.""" + return GATEWAY_SENSOR_TYPES[self._data_key].device_class + + @property + def state(self): + """Return the state of the sensor.""" + return self._sub_device.status[self._data_key] diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 8ae2f424f2e..01ae690bc25 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -21,6 +21,7 @@ TEST_GATEWAY_ID = TEST_MAC TEST_HARDWARE_VERSION = "AB123" TEST_FIRMWARE_VERSION = "1.2.3_456" TEST_ZEROCONF_NAME = "lumi-gateway-v3_miio12345678._miio._udp.local." +TEST_SUB_DEVICE_LIST = [] def get_mock_info( @@ -111,6 +112,9 @@ async def test_config_flow_gateway_success(hass): with patch( "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info", return_value=mock_info, + ), patch( + "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices", + return_value=TEST_SUB_DEVICE_LIST, ), patch( "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): @@ -151,6 +155,9 @@ async def test_zeroconf_gateway_success(hass): with patch( "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info", return_value=mock_info, + ), patch( + "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices", + return_value=TEST_SUB_DEVICE_LIST, ), patch( "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ):