From 222ad3ab6d077cb219295213881d0723f94fd31f Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Sat, 8 Jul 2017 01:59:41 +0200 Subject: [PATCH] Add new Dyson sensors (#8199) * Add new Dyson sensors * Add unit of measurement for dust and air quality * Code review --- homeassistant/components/dyson.py | 8 +- homeassistant/components/fan/dyson.py | 14 ++- homeassistant/components/sensor/dyson.py | 141 ++++++++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/fan/test_dyson.py | 11 +- tests/components/sensor/test_dyson.py | 118 ++++++++++++++++++- 7 files changed, 267 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson.py index eb430582ba7..c5aaba6152b 100644 --- a/homeassistant/components/dyson.py +++ b/homeassistant/components/dyson.py @@ -1,4 +1,8 @@ -"""Parent component for Dyson Pure Cool Link devices.""" +"""Parent component for Dyson Pure Cool Link devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/dyson/ +""" import logging @@ -9,7 +13,7 @@ from homeassistant.helpers import discovery from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ CONF_DEVICES -REQUIREMENTS = ['libpurecoollink==0.1.5'] +REQUIREMENTS = ['libpurecoollink==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index a2fb2b95ec4..de70c35739d 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -1,4 +1,8 @@ -"""Support for Dyson Pure Cool link fan.""" +"""Support for Dyson Pure Cool link fan. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/fan.dyson/ +""" import logging import asyncio from os import path @@ -79,9 +83,11 @@ class DysonPureCoolLinkDevice(FanEntity): def on_message(self, message): """Called when new messages received from the fan.""" - _LOGGER.debug( - "Message received for fan device %s : %s", self.name, message) - self.schedule_update_ha_state() + from libpurecoollink.dyson import DysonState + if isinstance(message, DysonState): + _LOGGER.debug("Message received for fan device %s : %s", self.name, + message) + self.schedule_update_ha_state() @property def should_poll(self): diff --git a/homeassistant/components/sensor/dyson.py b/homeassistant/components/sensor/dyson.py index e68909a1d6c..d3839f847ea 100644 --- a/homeassistant/components/sensor/dyson.py +++ b/homeassistant/components/sensor/dyson.py @@ -1,15 +1,24 @@ -"""Support for Dyson Pure Cool Link Sensors.""" +"""Support for Dyson Pure Cool Link Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.dyson/ +""" import logging import asyncio -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import TEMP_CELSIUS from homeassistant.components.dyson import DYSON_DEVICES from homeassistant.helpers.entity import Entity DEPENDENCIES = ['dyson'] -SENSOR_UNITS = {'filter_life': 'hours'} +SENSOR_UNITS = { + "filter_life": "hours", + "humidity": "%", + "dust": "level", + "air_quality": "level" +} _LOGGER = logging.getLogger(__name__) @@ -18,21 +27,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dyson Sensors.""" _LOGGER.info("Creating new Dyson fans") devices = [] + unit = hass.config.units.temperature_unit # Get Dyson Devices from parent component for device in hass.data[DYSON_DEVICES]: devices.append(DysonFilterLifeSensor(hass, device)) + devices.append(DysonDustSensor(hass, device)) + devices.append(DysonHumiditySensor(hass, device)) + devices.append(DysonTemperatureSensor(hass, device, unit)) + devices.append(DysonAirQualitySensor(hass, device)) add_devices(devices) -class DysonFilterLifeSensor(Entity): - """Representation of Dyson filter life sensor (in hours).""" +class DysonSensor(Entity): + """Representation of Dyson sensor.""" def __init__(self, hass, device): """Create a new Dyson filter life sensor.""" self.hass = hass self._device = device - self._name = "{} filter life".format(self._device.name) self._old_value = None + self._name = None @asyncio.coroutine def async_added_to_hass(self): @@ -42,10 +56,10 @@ class DysonFilterLifeSensor(Entity): def on_message(self, message): """Called when new messages received from the fan.""" - _LOGGER.debug( - "Message received for %s device: %s", self.name, message) # Prevent refreshing if not needed if self._old_value is None or self._old_value != self.state: + _LOGGER.debug("Message received for %s device: %s", self.name, + message) self._old_value = self.state self.schedule_update_ha_state() @@ -54,19 +68,116 @@ class DysonFilterLifeSensor(Entity): """No polling needed.""" return False - @property - def state(self): - """Return filter life in hours..""" - if self._device.state: - return self._device.state.filter_life - return STATE_UNKNOWN - @property def name(self): """Return the name of the dyson sensor name.""" return self._name + +class DysonFilterLifeSensor(DysonSensor): + """Representation of Dyson filter life sensor (in hours).""" + + def __init__(self, hass, device): + """Create a new Dyson filter life sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} filter life".format(self._device.name) + + @property + def state(self): + """Return filter life in hours.""" + if self._device.state: + return int(self._device.state.filter_life) + return None + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return SENSOR_UNITS['filter_life'] + + +class DysonDustSensor(DysonSensor): + """Representation of Dyson Dust sensor (lower is better).""" + + def __init__(self, hass, device): + """Create a new Dyson Dust sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} dust".format(self._device.name) + + @property + def state(self): + """Return Dust value.""" + if self._device.environmental_state: + return self._device.environmental_state.dust + return None + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_UNITS['dust'] + + +class DysonHumiditySensor(DysonSensor): + """Representation of Dyson Humidity sensor.""" + + def __init__(self, hass, device): + """Create a new Dyson Humidity sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} humidity".format(self._device.name) + + @property + def state(self): + """Return Dust value.""" + if self._device.environmental_state: + return self._device.environmental_state.humidity + return None + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_UNITS['humidity'] + + +class DysonTemperatureSensor(DysonSensor): + """Representation of Dyson Temperature sensor.""" + + def __init__(self, hass, device, unit): + """Create a new Dyson Temperature sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} temperature".format(self._device.name) + self._unit = unit + + @property + def state(self): + """Return Dust value.""" + if self._device.environmental_state: + temperature_kelvin = self._device.environmental_state.temperature + if self._unit == TEMP_CELSIUS: + return float("{0:.1f}".format(temperature_kelvin - 273.15)) + return float("{0:.1f}".format(temperature_kelvin * 9 / 5 - 459.67)) + return None + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + +class DysonAirQualitySensor(DysonSensor): + """Representation of Dyson Air Quality sensor (lower is better).""" + + def __init__(self, hass, device): + """Create a new Dyson Air Quality sensor.""" + DysonSensor.__init__(self, hass, device) + self._name = "{} air quality".format(self._device.name) + + @property + def state(self): + """Return Air QUality value.""" + if self._device.environmental_state: + return self._device.environmental_state.volatil_organic_compounds + return None + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_UNITS['air_quality'] diff --git a/requirements_all.txt b/requirements_all.txt index e9581962fa4..5458b3459f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -348,7 +348,7 @@ knxip==0.4 libnacl==1.5.1 # homeassistant.components.dyson -libpurecoollink==0.1.5 +libpurecoollink==0.2.0 # homeassistant.components.device_tracker.mikrotik librouteros==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07a3ad8a681..8ea6c98042b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,7 +62,7 @@ holidays==0.8.1 influxdb==3.0.0 # homeassistant.components.dyson -libpurecoollink==0.1.5 +libpurecoollink==0.2.0 # homeassistant.components.media_player.soundtouch libsoundtouch==0.7.2 diff --git a/tests/components/fan/test_dyson.py b/tests/components/fan/test_dyson.py index 4548b12434b..e388f31e664 100644 --- a/tests/components/fan/test_dyson.py +++ b/tests/components/fan/test_dyson.py @@ -6,6 +6,15 @@ from homeassistant.components.dyson import DYSON_DEVICES from homeassistant.components.fan import dyson from tests.common import get_test_home_assistant from libpurecoollink.const import FanSpeed, FanMode, NightMode, Oscillation +from libpurecoollink.dyson import DysonState + + +class MockDysonState(DysonState): + """Mock Dyson state.""" + + def __init__(self): + """Create new Mock Dyson State.""" + pass def _get_device_with_no_state(): @@ -257,7 +266,7 @@ class DysonTest(unittest.TestCase): component = dyson.DysonPureCoolLinkDevice(self.hass, device) component.entity_id = "entity_id" component.schedule_update_ha_state = mock.Mock() - component.on_message("Message") + component.on_message(MockDysonState()) component.schedule_update_ha_state.assert_called_with() def test_service_set_night_mode(self): diff --git a/tests/components/sensor/test_dyson.py b/tests/components/sensor/test_dyson.py index 8dc76c70147..8599346f769 100644 --- a/tests/components/sensor/test_dyson.py +++ b/tests/components/sensor/test_dyson.py @@ -2,7 +2,7 @@ import unittest from unittest import mock -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.sensor import dyson from tests.common import get_test_home_assistant @@ -12,6 +12,7 @@ def _get_device_without_state(): device = mock.Mock() device.name = "Device_name" device.state = None + device.environmental_state = None return device @@ -21,6 +22,12 @@ def _get_with_state(): device.name = "Device_name" device.state = mock.Mock() device.state.filter_life = 100 + device.environmental_state = mock.Mock() + device.environmental_state.dust = 5 + device.environmental_state.humidity = 45 + device.environmental_state.temperature = 295 + device.environmental_state.volatil_organic_compounds = 2 + return device @@ -45,27 +52,31 @@ class DysonTest(unittest.TestCase): def test_setup_component(self): """Test setup component with devices.""" def _add_device(devices): - assert len(devices) == 1 + assert len(devices) == 5 assert devices[0].name == "Device_name filter life" + assert devices[1].name == "Device_name dust" + assert devices[2].name == "Device_name humidity" + assert devices[3].name == "Device_name temperature" + assert devices[4].name == "Device_name air quality" device = _get_device_without_state() self.hass.data[dyson.DYSON_DEVICES] = [device] dyson.setup_platform(self.hass, None, _add_device) def test_dyson_filter_life_sensor(self): - """Test sensor with no value.""" + """Test filter life sensor with no value.""" sensor = dyson.DysonFilterLifeSensor(self.hass, _get_device_without_state()) sensor.entity_id = "sensor.dyson_1" self.assertFalse(sensor.should_poll) - self.assertEqual(sensor.state, STATE_UNKNOWN) + self.assertIsNone(sensor.state) self.assertEqual(sensor.unit_of_measurement, "hours") self.assertEqual(sensor.name, "Device_name filter life") self.assertEqual(sensor.entity_id, "sensor.dyson_1") sensor.on_message('message') def test_dyson_filter_life_sensor_with_values(self): - """Test sensor with values.""" + """Test filter sensor with values.""" sensor = dyson.DysonFilterLifeSensor(self.hass, _get_with_state()) sensor.entity_id = "sensor.dyson_1" self.assertFalse(sensor.should_poll) @@ -74,3 +85,100 @@ class DysonTest(unittest.TestCase): self.assertEqual(sensor.name, "Device_name filter life") self.assertEqual(sensor.entity_id, "sensor.dyson_1") sensor.on_message('message') + + def test_dyson_dust_sensor(self): + """Test dust sensor with no value.""" + sensor = dyson.DysonDustSensor(self.hass, + _get_device_without_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertIsNone(sensor.state) + self.assertEqual(sensor.unit_of_measurement, 'level') + self.assertEqual(sensor.name, "Device_name dust") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_dust_sensor_with_values(self): + """Test dust sensor with values.""" + sensor = dyson.DysonDustSensor(self.hass, _get_with_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 5) + self.assertEqual(sensor.unit_of_measurement, 'level') + self.assertEqual(sensor.name, "Device_name dust") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_humidity_sensor(self): + """Test humidity sensor with no value.""" + sensor = dyson.DysonHumiditySensor(self.hass, + _get_device_without_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertIsNone(sensor.state) + self.assertEqual(sensor.unit_of_measurement, '%') + self.assertEqual(sensor.name, "Device_name humidity") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_humidity_sensor_with_values(self): + """Test humidity sensor with values.""" + sensor = dyson.DysonHumiditySensor(self.hass, _get_with_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 45) + self.assertEqual(sensor.unit_of_measurement, '%') + self.assertEqual(sensor.name, "Device_name humidity") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_temperature_sensor(self): + """Test temperature sensor with no value.""" + sensor = dyson.DysonTemperatureSensor(self.hass, + _get_device_without_state(), + TEMP_CELSIUS) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertIsNone(sensor.state) + self.assertEqual(sensor.unit_of_measurement, '°C') + self.assertEqual(sensor.name, "Device_name temperature") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_temperature_sensor_with_values(self): + """Test temperature sensor with values.""" + sensor = dyson.DysonTemperatureSensor(self.hass, + _get_with_state(), + TEMP_CELSIUS) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 21.9) + self.assertEqual(sensor.unit_of_measurement, '°C') + self.assertEqual(sensor.name, "Device_name temperature") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + sensor = dyson.DysonTemperatureSensor(self.hass, + _get_with_state(), + TEMP_FAHRENHEIT) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 71.3) + self.assertEqual(sensor.unit_of_measurement, '°F') + self.assertEqual(sensor.name, "Device_name temperature") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_air_quality_sensor(self): + """Test air quality sensor with no value.""" + sensor = dyson.DysonAirQualitySensor(self.hass, + _get_device_without_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertIsNone(sensor.state) + self.assertEqual(sensor.unit_of_measurement, 'level') + self.assertEqual(sensor.name, "Device_name air quality") + self.assertEqual(sensor.entity_id, "sensor.dyson_1") + + def test_dyson_air_quality_sensor_with_values(self): + """Test air quality sensor with values.""" + sensor = dyson.DysonAirQualitySensor(self.hass, _get_with_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 2) + self.assertEqual(sensor.unit_of_measurement, 'level') + self.assertEqual(sensor.name, "Device_name air quality") + self.assertEqual(sensor.entity_id, "sensor.dyson_1")