diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index a857d6657fd..fdba263d4ca 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -16,6 +16,7 @@ CONF_RETRY = 'retry' DEFAULT_TIMEOUT = 5 DEFAULT_RETRY = 10 DYSON_DEVICES = 'dyson_devices' +DYSON_PLATFORMS = ['sensor', 'fan', 'vacuum', 'climate', 'air_quality'] DOMAIN = 'dyson' @@ -91,9 +92,7 @@ def setup(hass, config): # Start fan/sensors components if hass.data[DYSON_DEVICES]: _LOGGER.debug("Starting sensor/fan components") - discovery.load_platform(hass, "sensor", DOMAIN, {}, config) - discovery.load_platform(hass, "fan", DOMAIN, {}, config) - discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) - discovery.load_platform(hass, "climate", DOMAIN, {}, config) + for platform in DYSON_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) return True diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py new file mode 100644 index 00000000000..238b8b6934d --- /dev/null +++ b/homeassistant/components/dyson/air_quality.py @@ -0,0 +1,126 @@ +"""Support for Dyson Pure Cool Air Quality Sensors.""" +import logging + +from homeassistant.components.air_quality import AirQualityEntity, DOMAIN +from . import DYSON_DEVICES + +ATTRIBUTION = 'Dyson purifier air quality sensor' + +_LOGGER = logging.getLogger(__name__) + +DYSON_AIQ_DEVICES = 'dyson_aiq_devices' + +ATTR_VOC = 'volatile_organic_compounds' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Dyson Sensors.""" + from libpurecool.dyson_pure_cool import DysonPureCool + + if discovery_info is None: + return + + hass.data.setdefault(DYSON_AIQ_DEVICES, []) + + # Get Dyson Devices from parent component + device_ids = [device.unique_id for device in hass.data[DYSON_AIQ_DEVICES]] + for device in hass.data[DYSON_DEVICES]: + if isinstance(device, DysonPureCool) and \ + device.serial not in device_ids: + hass.data[DYSON_AIQ_DEVICES].append(DysonAirSensor(device)) + add_entities(hass.data[DYSON_AIQ_DEVICES]) + + +class DysonAirSensor(AirQualityEntity): + """Representation of a generic Dyson air quality sensor.""" + + def __init__(self, device): + """Create a new generic air quality Dyson sensor.""" + self._device = device + self._old_value = None + self._name = device.name + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self.hass.async_add_executor_job( + self._device.add_message_listener, self.on_message) + + def on_message(self, message): + """Handle new messages which are received from the fan.""" + from libpurecool.dyson_pure_state_v2 import \ + DysonEnvironmentalSensorV2State + + _LOGGER.debug('%s: Message received for %s device: %s', + DOMAIN, self.name, message) + if (self._old_value is None or + self._old_value != self._device.environmental_state) and \ + isinstance(message, DysonEnvironmentalSensorV2State): + self._old_value = self._device.environmental_state + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the Dyson sensor.""" + return self._name + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def air_quality_index(self): + """Return the Air Quality Index (AQI).""" + return max(self.particulate_matter_2_5, + self.particulate_matter_10, + self.nitrogen_dioxide, + self.volatile_organic_compounds) + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + if self._device.environmental_state: + return int(self._device.environmental_state.particulate_matter_25) + return None + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + if self._device.environmental_state: + return int(self._device.environmental_state.particulate_matter_10) + return None + + @property + def nitrogen_dioxide(self): + """Return the NO2 (nitrogen dioxide) level.""" + if self._device.environmental_state: + return int(self._device.environmental_state.nitrogen_dioxide) + return None + + @property + def volatile_organic_compounds(self): + """Return the VOC (Volatile Organic Compounds) level.""" + if self._device.environmental_state: + return int(self._device. + environmental_state.volatile_organic_compounds) + return None + + @property + def unique_id(self): + """Return the sensor's unique id.""" + return self._device.serial + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + data = {} + + voc = self.volatile_organic_compounds + if voc is not None: + data[ATTR_VOC] = voc + return data diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 03a55f8abbe..65ff093d6d5 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -474,7 +474,8 @@ class DysonPureCoolDevice(FanEntity): FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM, FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM, FanSpeed.FAN_SPEED_8.value: SPEED_HIGH, - FanSpeed.FAN_SPEED_9.value: SPEED_HIGH} + FanSpeed.FAN_SPEED_9.value: SPEED_HIGH, + FanSpeed.FAN_SPEED_10.value: SPEED_HIGH} return speed_map[self._device.state.speed] diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index 56c924d1a54..9cd1c915c57 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -3,7 +3,6 @@ import logging from homeassistant.const import STATE_OFF, TEMP_CELSIUS from homeassistant.helpers.entity import Entity - from . import DYSON_DEVICES SENSOR_UNITS = { @@ -21,26 +20,38 @@ SENSOR_ICONS = { 'temperature': 'mdi:thermometer', } +DYSON_SENSOR_DEVICES = 'dyson_sensor_devices' + _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson Sensors.""" - _LOGGER.debug("Creating new Dyson fans") - devices = [] - unit = hass.config.units.temperature_unit - # Get Dyson Devices from parent component - from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink + from libpurecool.dyson_pure_cool import DysonPureCool - for device in [d for d in hass.data[DYSON_DEVICES] - if isinstance(d, DysonPureCoolLink) and - not isinstance(d, DysonPureCool)]: - devices.append(DysonFilterLifeSensor(device)) - devices.append(DysonDustSensor(device)) - devices.append(DysonHumiditySensor(device)) - devices.append(DysonTemperatureSensor(device, unit)) - devices.append(DysonAirQualitySensor(device)) + if discovery_info is None: + return + + hass.data.setdefault(DYSON_SENSOR_DEVICES, []) + unit = hass.config.units.temperature_unit + devices = hass.data[DYSON_SENSOR_DEVICES] + + # Get Dyson Devices from parent component + device_ids = [device.unique_id for device in + hass.data[DYSON_SENSOR_DEVICES]] + for device in hass.data[DYSON_DEVICES]: + if isinstance(device, DysonPureCool): + if '{}-{}'.format(device.serial, 'temperature') not in device_ids: + devices.append(DysonTemperatureSensor(device, unit)) + if '{}-{}'.format(device.serial, 'humidity') not in device_ids: + devices.append(DysonHumiditySensor(device)) + elif isinstance(device, DysonPureCoolLink): + devices.append(DysonFilterLifeSensor(device)) + devices.append(DysonDustSensor(device)) + devices.append(DysonHumiditySensor(device)) + devices.append(DysonTemperatureSensor(device, unit)) + devices.append(DysonAirQualitySensor(device)) add_entities(devices) @@ -56,7 +67,7 @@ class DysonSensor(Entity): async def async_added_to_hass(self): """Call when entity is added to hass.""" - self.hass.async_add_job( + self.hass.async_add_executor_job( self._device.add_message_listener, self.on_message) def on_message(self, message): @@ -88,6 +99,11 @@ class DysonSensor(Entity): """Return the icon for this sensor.""" return SENSOR_ICONS[self._sensor_type] + @property + def unique_id(self): + """Return the sensor's unique id.""" + return '{}-{}'.format(self._device.serial, self._sensor_type) + class DysonFilterLifeSensor(DysonSensor): """Representation of Dyson Filter Life sensor (in hours).""" diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py new file mode 100644 index 00000000000..ab068823d64 --- /dev/null +++ b/tests/components/dyson/test_air_quality.py @@ -0,0 +1,145 @@ +"""Test the Dyson air quality component.""" +import json +from unittest import mock + +import asynctest +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State + +import homeassistant.components.dyson.air_quality as dyson +from homeassistant.components import dyson as dyson_parent +from homeassistant.components.air_quality import DOMAIN as AIQ_DOMAIN, \ + ATTR_PM_2_5, ATTR_PM_10, ATTR_NO2 +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component + + +def _get_dyson_purecool_device(): + """Return a valid device as provided by the Dyson web services.""" + device = mock.Mock(spec=DysonPureCool) + device.serial = 'XX-XXXXX-XX' + device.name = 'Living room' + device.connect = mock.Mock(return_value=True) + device.auto_connect = mock.Mock(return_value=True) + device.environmental_state.particulate_matter_25 = '0014' + device.environmental_state.particulate_matter_10 = '0025' + device.environmental_state.nitrogen_dioxide = '0042' + device.environmental_state.volatile_organic_compounds = '0035' + return device + + +def _get_config(): + """Return a config dictionary.""" + return {dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: 'email', + dyson_parent.CONF_PASSWORD: 'password', + dyson_parent.CONF_LANGUAGE: 'GB', + dyson_parent.CONF_DEVICES: [ + { + 'device_id': 'XX-XXXXX-XX', + 'device_ip': '192.168.0.1' + } + ] + }} + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_aiq_attributes(devices, login, hass): + """Test state attributes.""" + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + fan_state = hass.states.get("air_quality.living_room") + attributes = fan_state.attributes + + assert fan_state.state == '14' + assert attributes[ATTR_PM_2_5] == 14 + assert attributes[ATTR_PM_10] == 25 + assert attributes[ATTR_NO2] == 42 + assert attributes[dyson.ATTR_VOC] == 35 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_aiq_update_state(devices, login, hass): + """Test state update.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + event = { + "msg": "ENVIRONMENTAL-CURRENT-SENSOR-DATA", + "time": "2019-03-29T10:00:01.000Z", + "data": { + "pm10": "0080", + "p10r": "0151", + "hact": "0040", + "va10": "0055", + "p25r": "0161", + "noxl": "0069", + "pm25": "0035", + "sltm": "OFF", + "tact": "2960" + } + } + device.environmental_state = \ + DysonEnvironmentalSensorV2State(json.dumps(event)) + + callback = device.add_message_listener.call_args_list[2][0][0] + callback(device.environmental_state) + await hass.async_block_till_done() + fan_state = hass.states.get("air_quality.living_room") + attributes = fan_state.attributes + + assert fan_state.state == '35' + assert attributes[ATTR_PM_2_5] == 35 + assert attributes[ATTR_PM_10] == 80 + assert attributes[ATTR_NO2] == 69 + assert attributes[dyson.ATTR_VOC] == 55 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_component_setup_only_once(devices, login, hass): + """Test if entities are created only once.""" + config = _get_config() + await async_setup_component(hass, dyson_parent.DOMAIN, config) + await hass.async_block_till_done() + discovery.load_platform(hass, AIQ_DOMAIN, + dyson_parent.DOMAIN, {}, config) + await hass.async_block_till_done() + + assert len(hass.data[dyson.DYSON_AIQ_DEVICES]) == 1 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_aiq_without_discovery(devices, login, hass): + """Test if component correctly returns if discovery not set.""" + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + add_entities_mock = mock.MagicMock() + + dyson.setup_platform(hass, None, add_entities_mock, None) + + assert add_entities_mock.call_count == 0 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_aiq_empty_environment_state(devices, login, hass): + """Test device with empty environmental state.""" + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + device = hass.data[dyson.DYSON_AIQ_DEVICES][0] + device._device.environmental_state = None + + assert device.state is None + assert device.particulate_matter_2_5 is None + assert device.particulate_matter_10 is None + assert device.nitrogen_dioxide is None + assert device.volatile_organic_compounds is None diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 778b3bdad49..83ddbfed242 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -2,6 +2,7 @@ import unittest from unittest import mock +import asynctest from libpurecool.const import (FocusMode, HeatMode, HeatState, HeatTarget, TiltState) from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink @@ -10,7 +11,7 @@ from libpurecool.dyson_pure_state import DysonPureHotCoolState from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import climate as dyson from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from tests.common import get_test_home_assistant @@ -22,6 +23,25 @@ class MockDysonState(DysonPureHotCoolState): pass +def _get_config(): + """Return a config dictionary.""" + return {dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: "email", + dyson_parent.CONF_PASSWORD: "password", + dyson_parent.CONF_LANGUAGE: "GB", + dyson_parent.CONF_DEVICES: [ + { + "device_id": "XX-XXXXX-XX", + "device_ip": "192.168.0.1" + }, + { + "device_id": "YY-YYYYY-YY", + "device_ip": "192.168.0.2" + } + ] + }} + + def _get_device_with_no_state(): """Return a device with no state.""" device = mock.Mock(spec=DysonPureHotCoolLink) @@ -60,6 +80,7 @@ def _get_device_cool(): """Return a device with state of cooling.""" device = mock.Mock(spec=DysonPureHotCoolLink) device.name = "Device_name" + device.serial = "XX-XXXXX-XX" device.state.tilt = TiltState.TILT_FALSE.value device.state.focus_mode = FocusMode.FOCUS_OFF.value device.state.heat_target = HeatTarget.celsius(12) @@ -89,6 +110,7 @@ def _get_device_heat_on(): """Return a device with state of heating.""" device = mock.Mock(spec=DysonPureHotCoolLink) device.name = "Device_name" + device.serial = "YY-YYYYY-YY" device.state = mock.Mock() device.state.tilt = TiltState.TILT_FALSE.value device.state.focus_mode = FocusMode.FOCUS_ON.value @@ -111,24 +133,6 @@ class DysonTest(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @mock.patch('libpurecool.dyson.DysonAccount.devices', - return_value=[_get_device_heat_on(), _get_device_cool()]) - @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) - def test_setup_component_with_parent_discovery(self, mocked_login, - mocked_devices): - """Test setup_component using discovery.""" - setup_component(self.hass, dyson_parent.DOMAIN, { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "US", - } - }) - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 2 - self.hass.block_till_done() - for m in mocked_devices.return_value: - assert m.add_message_listener.called - def test_setup_component_without_devices(self): """Test setup component with no devices.""" self.hass.data[dyson.DYSON_DEVICES] = [] @@ -357,3 +361,15 @@ class DysonTest(unittest.TestCase): device = _get_device_heat_on() entity = dyson.DysonPureHotCoolLinkDevice(device) assert entity.target_temperature == 23 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_device_heat_on(), _get_device_cool()]) +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +async def test_setup_component_with_parent_discovery(mocked_login, + mocked_devices, hass): + """Test setup_component using discovery.""" + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + + assert len(hass.data[dyson.DYSON_DEVICES]) == 2 diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 0a9469ae807..09622e4d36d 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -13,7 +13,7 @@ from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State import homeassistant.components.dyson.fan as dyson from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import DYSON_DEVICES -from homeassistant.components.fan import (DOMAIN, ATTR_SPEED, ATTR_SPEED_LIST, +from homeassistant.components.fan import (DOMAIN, ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SERVICE_OSCILLATE) @@ -21,7 +21,7 @@ from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.helpers import discovery -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.setup import async_setup_component from tests.common import get_test_home_assistant @@ -55,6 +55,21 @@ def _get_dyson_purecool_device(): return device +def _get_dyson_purecoollink_device(): + """Return a valid device as provided by the Dyson web services.""" + device = mock.Mock(spec=DysonPureCoolLink) + device.serial = "XX-XXXXX-XX" + device.name = "Living room" + device.connect = mock.Mock(return_value=True) + device.auto_connect = mock.Mock(return_value=True) + device.state = mock.Mock() + device.state.oscillation = "ON" + device.state.fan_mode = "FAN" + device.state.speed = FanSpeed.FAN_SPEED_AUTO.value + device.state.night_mode = "OFF" + return device + + def _get_supported_speeds(): return [ int(FanSpeed.FAN_SPEED_1.value), @@ -173,45 +188,6 @@ class DysonTest(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @mock.patch('libpurecool.dyson.DysonAccount.devices', - return_value=[_get_device_on()]) - @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) - def test_get_state_attributes(self, mocked_login, mocked_devices): - """Test async added to hass.""" - setup_component(self.hass, dyson_parent.DOMAIN, { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "US", - } - }) - self.hass.block_till_done() - state = self.hass.states.get("{}.{}".format( - DOMAIN, - mocked_devices.return_value[0].name)) - - assert dyson.ATTR_NIGHT_MODE in state.attributes - assert dyson.ATTR_AUTO_MODE in state.attributes - assert ATTR_SPEED in state.attributes - assert ATTR_SPEED_LIST in state.attributes - assert ATTR_OSCILLATING in state.attributes - - @mock.patch('libpurecool.dyson.DysonAccount.devices', - return_value=[_get_device_on()]) - @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) - def test_async_added_to_hass(self, mocked_login, mocked_devices): - """Test async added to hass.""" - setup_component(self.hass, dyson_parent.DOMAIN, { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "US", - } - }) - self.hass.block_till_done() - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 - assert mocked_devices.return_value[0].add_message_listener.called - def test_dyson_set_speed(self): """Test set fan speed.""" device = _get_device_on() @@ -415,6 +391,22 @@ class DysonTest(unittest.TestCase): dyson_device.set_night_mode.assert_called_with(True) +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecoollink_device()]) +async def test_purecoollink_attributes(devices, login, hass): + """Test state attributes.""" + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + fan_state = hass.states.get("fan.living_room") + attributes = fan_state.attributes + + assert fan_state.state == "on" + assert attributes[dyson.ATTR_NIGHT_MODE] is False + assert attributes[ATTR_SPEED] == FanSpeed.FAN_SPEED_AUTO.value + assert attributes[ATTR_OSCILLATING] is True + + @asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) @asynctest.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_purecool_device()]) @@ -670,31 +662,6 @@ async def test_purecool_set_timer(devices, login, hass): assert device.disable_sleep_timer.call_count == 1 -@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) -@asynctest.patch('libpurecool.dyson.DysonAccount.devices', - return_value=[_get_dyson_purecool_device()]) -async def test_purecool_attributes(devices, login, hass): - """Test state attributes.""" - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - fan_state = hass.states.get("fan.living_room") - attributes = fan_state.attributes - - assert fan_state.state == "on" - assert attributes[dyson.ATTR_NIGHT_MODE] is False - assert attributes[dyson.ATTR_AUTO_MODE] is True - assert attributes[dyson.ATTR_ANGLE_LOW] == 90 - assert attributes[dyson.ATTR_ANGLE_HIGH] == 180 - assert attributes[dyson.ATTR_FLOW_DIRECTION_FRONT] is True - assert attributes[dyson.ATTR_TIMER] == 60 - assert attributes[dyson.ATTR_HEPA_FILTER] == 90 - assert attributes[dyson.ATTR_CARBON_FILTER] == 80 - assert attributes[dyson.ATTR_DYSON_SPEED] == FanSpeed.FAN_SPEED_AUTO.value - assert attributes[ATTR_SPEED] == SPEED_MEDIUM - assert attributes[ATTR_OSCILLATING] is True - assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() - - @asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) @asynctest.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_purecool_device()]) @@ -713,7 +680,7 @@ async def test_purecool_update_state(devices, login, hass): "osau": "0095", "ancp": "CUST"}} device.state = DysonPureCoolV2State(json.dumps(event)) - callback = device.add_message_listener.call_args_list[0][0][0] + callback = device.add_message_listener.call_args_list[3][0][0] callback(device.state) await hass.async_block_till_done() fan_state = hass.states.get("fan.living_room") diff --git a/tests/components/dyson/test_init.py b/tests/components/dyson/test_init.py index cc8c04a1559..9c1ea7ebabf 100644 --- a/tests/components/dyson/test_init.py +++ b/tests/components/dyson/test_init.py @@ -87,7 +87,7 @@ class DysonTest(unittest.TestCase): assert mocked_login.call_count == 1 assert mocked_devices.call_count == 1 assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 - assert mocked_discovery.call_count == 4 + assert mocked_discovery.call_count == 5 @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) @@ -172,7 +172,7 @@ class DysonTest(unittest.TestCase): assert mocked_login.call_count == 1 assert mocked_devices.call_count == 1 assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 - assert mocked_discovery.call_count == 4 + assert mocked_discovery.call_count == 5 @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 67c34d4d180..d7b478776dc 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -2,17 +2,51 @@ import unittest from unittest import mock +import asynctest +from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink +from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import sensor as dyson from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, \ STATE_OFF +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component from tests.common import get_test_home_assistant +def _get_dyson_purecool_device(): + """Return a valid device provide by Dyson web services.""" + device = mock.Mock(spec=DysonPureCool) + device.serial = "XX-XXXXX-XX" + device.name = "Living room" + device.connect = mock.Mock(return_value=True) + device.auto_connect = mock.Mock(return_value=True) + device.environmental_state.humidity = 42 + device.environmental_state.temperature = 280 + device.state.hepa_filter_state = 90 + device.state.carbon_filter_state = 80 + return device + + +def _get_config(): + """Return a config dictionary.""" + return {dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: "email", + dyson_parent.CONF_PASSWORD: "password", + dyson_parent.CONF_LANGUAGE: "GB", + dyson_parent.CONF_DEVICES: [ + { + "device_id": "XX-XXXXX-XX", + "device_ip": "192.168.0.1" + } + ] + }} + + def _get_device_without_state(): """Return a valid device provide by Dyson web services.""" - device = mock.Mock() + device = mock.Mock(spec=DysonPureCoolLink) device.name = "Device_name" device.state = None device.environmental_state = None @@ -21,7 +55,7 @@ def _get_device_without_state(): def _get_with_state(): """Return a valid device with state values.""" - device = mock.Mock(spec=DysonPureCoolLink) + device = mock.Mock() device.name = "Device_name" device.state = mock.Mock() device.state.filter_life = 100 @@ -65,7 +99,7 @@ class DysonTest(unittest.TestCase): self.hass.data[dyson.DYSON_DEVICES] = [] add_entities = mock.MagicMock() dyson.setup_platform(self.hass, None, add_entities) - add_entities.assert_called_with([]) + add_entities.assert_not_called() def test_setup_component(self): """Test setup component with devices.""" @@ -80,7 +114,7 @@ class DysonTest(unittest.TestCase): device_fan = _get_device_without_state() device_non_fan = _get_with_state() self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan] - dyson.setup_platform(self.hass, None, _add_device) + dyson.setup_platform(self.hass, None, _add_device, mock.MagicMock()) def test_dyson_filter_life_sensor(self): """Test filter life sensor with no value.""" @@ -228,3 +262,17 @@ class DysonTest(unittest.TestCase): assert sensor.unit_of_measurement is None assert sensor.name == "Device_name AQI" assert sensor.entity_id == "sensor.dyson_1" + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_component_setup_only_once(devices, login, hass): + """Test if entities are created only once.""" + config = _get_config() + await async_setup_component(hass, dyson_parent.DOMAIN, config) + await hass.async_block_till_done() + discovery.load_platform(hass, "sensor", dyson_parent.DOMAIN, {}, config) + await hass.async_block_till_done() + + assert len(hass.data[dyson.DYSON_SENSOR_DEVICES]) == 2