From caf034dff01e4eb5093febd6cb920b1be4cf4e34 Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Mon, 17 Apr 2017 06:29:50 +0200 Subject: [PATCH 1/7] Add Dyson Pure Cool Link support --- homeassistant/components/dyson.py | 90 ++++++++++ homeassistant/components/fan/dyson.py | 153 +++++++++++++++++ homeassistant/components/sensor/dyson.py | 67 ++++++++ requirements_all.txt | 4 + requirements_test_all.txt | 4 + tests/components/fan/test_dyson.py | 201 +++++++++++++++++++++++ tests/components/sensor/test_dyson.py | 75 +++++++++ tests/components/test_dyson.py | 152 +++++++++++++++++ 8 files changed, 746 insertions(+) create mode 100644 homeassistant/components/dyson.py create mode 100644 homeassistant/components/fan/dyson.py create mode 100644 homeassistant/components/sensor/dyson.py create mode 100644 tests/components/fan/test_dyson.py create mode 100644 tests/components/sensor/test_dyson.py create mode 100644 tests/components/test_dyson.py diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson.py new file mode 100644 index 00000000000000..dd3d9101cffced --- /dev/null +++ b/homeassistant/components/dyson.py @@ -0,0 +1,90 @@ +"""Parent component for Dyson Pure Cool Link devices.""" + +import logging +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['libpurecoollink==0.1.5'] + +_LOGGER = logging.getLogger(__name__) + +CONF_USERNAME = "username" +CONF_PASSWORD = "password" +CONF_LANGUAGE = "language" +CONF_TIMEOUT = "timeout" +CONF_RETRY = "retry" +CONF_DEVICES = "devices" + +DEFAULT_TIMEOUT = 5 +DEFAULT_RETRY = 10 + +DOMAIN = "dyson" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_LANGUAGE): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, + vol.Optional(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [dict]), + }) +}, extra=vol.ALLOW_EXTRA) + +DYSON_DEVICES = "dyson_devices" + + +def setup(hass, config): + """Set up the Dyson parent component.""" + _LOGGER.info("Creating new Dyson component") + + if DYSON_DEVICES not in hass.data: + hass.data[DYSON_DEVICES] = [] + + from libpurecoollink.dyson import DysonAccount + dyson_account = DysonAccount(config[DOMAIN].get(CONF_USERNAME), + config[DOMAIN].get(CONF_PASSWORD), + config[DOMAIN].get(CONF_LANGUAGE)) + + logged = dyson_account.login() + + timeout = config[DOMAIN].get(CONF_TIMEOUT) + retry = config[DOMAIN].get(CONF_RETRY) + + if logged: + _LOGGER.info("Connected to Dyson account") + dyson_devices = dyson_account.devices() + if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES): + configured_devices = config[DOMAIN].get(CONF_DEVICES) + for device in configured_devices: + dyson_device = next((d for d in dyson_devices if + d.serial == device["device_id"]), None) + if dyson_device: + connected = dyson_device.connect(None, device["device_ip"], + timeout, retry) + if connected: + _LOGGER.info("Connected to device %s", dyson_device) + hass.data[DYSON_DEVICES].append(dyson_device) + else: + _LOGGER.warning("Unable to connect to device %s", + dyson_device) + else: + _LOGGER.warning( + "Unable to find device %s in Dyson account", + device["device_id"]) + else: + # Not yet reliable + for device in dyson_devices: + _LOGGER.info("Trying to connect to device %s with timeout=%i " + "and retry=%i", device, timeout, retry) + connected = device.connect(None, None, timeout, retry) + if connected: + _LOGGER.info("Connected to device %s", device) + hass.data[DYSON_DEVICES].append(device) + else: + _LOGGER.warning("Unable to connect to device %s", device) + return True + else: + _LOGGER.error("Not connected to Dyson account. Unable to add devices") + return False diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py new file mode 100644 index 00000000000000..a7598419a4de7e --- /dev/null +++ b/homeassistant/components/fan/dyson.py @@ -0,0 +1,153 @@ +"""Support for Dyson Pure Cool link fan.""" +import logging +from homeassistant.components.fan import (FanEntity, + SUPPORT_OSCILLATE, SUPPORT_SET_SPEED) +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.dyson import DYSON_DEVICES + +DEPENDENCIES = ['dyson'] +REQUIREMENTS = ['libpurecoollink==0.1.5'] + +_LOGGER = logging.getLogger(__name__) + +NIGHT_MODE = 'NIGHT_MODE' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Dyson fan components.""" + _LOGGER.info("Creating new Dyson fans") + devices = [] + # Get Dyson Devices from parent component + for device in hass.data[DYSON_DEVICES]: + devices.append(DysonPureCoolLinkDevice(hass, device)) + add_devices(devices) + + +class DysonPureCoolLinkDevice(FanEntity): + """Representation of a Dyson fan.""" + + 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) + if self.hass and self.entity_id: + self.schedule_update_ha_state() + + def __init__(self, hass, device): + """Initialize the fan.""" + _LOGGER.info("Creating device %s", device.name) + self.hass = hass + self._device = device + self._device.add_message_listener(self.on_message) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the display name of this fan.""" + return self._device.name + + def set_speed(self: ToggleEntity, speed: str) -> None: + """Set the speed of the fan. Never called ??.""" + _LOGGER.debug("Set fan speed to: " + speed) + from libpurecoollink.const import FanSpeed, FanMode, NightMode + fan_speed = FanSpeed(speed) + self._device.set_configuration(fan_mode=FanMode.FAN, + night_mode=NightMode.NIGHT_MODE_OFF, + fan_speed=fan_speed) + + def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None: + """Turn on the fan.""" + _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) + from libpurecoollink.const import FanSpeed, FanMode, NightMode + if speed: + # Turn on fan with specified speed + if speed == NIGHT_MODE: + night_mode = NightMode.NIGHT_MODE_ON + self._device.set_configuration(fan_mode=FanMode.AUTO, + night_mode=night_mode) + else: + fan_speed = FanSpeed(speed) + night_mode = NightMode.NIGHT_MODE_OFF + if fan_speed == FanSpeed.FAN_SPEED_AUTO: + self._device.set_configuration(fan_mode=FanMode.AUTO, + night_mode=night_mode) + else: + self._device.set_configuration(fan_mode=FanMode.FAN, + night_mode=night_mode, + fan_speed=fan_speed) + else: + # Speed not set, just turn on + self._device.set_configuration(fan_mode=FanMode.FAN) + + def turn_off(self: ToggleEntity, **kwargs) -> None: + """Turn off the fan.""" + _LOGGER.debug("Turn off fan %s", self.name) + from libpurecoollink.const import FanMode + self._device.set_configuration(fan_mode=FanMode.OFF) + + def oscillate(self: ToggleEntity, oscillating: bool) -> None: + """Turn on/off oscillating.""" + _LOGGER.debug("Turn oscillation %s for device %s", oscillating, + self.name) + from libpurecoollink.const import Oscillation + + if oscillating: + self._device.set_configuration( + oscillation=Oscillation.OSCILLATION_ON) + else: + self._device.set_configuration( + oscillation=Oscillation.OSCILLATION_OFF) + + @property + def oscillating(self): + """Return the oscillation state.""" + return self._device.state and self._device.state.oscillation == "ON" + + @property + def is_on(self): + """Return true if the entity is on.""" + if self._device.state: + return self._device.state.fan_mode in ['FAN', 'AUTO'] + return False + + @property + def speed(self) -> str: + """Return the current speed.""" + if self._device.state: + if self._device.state.night_mode == 'ON': + return NIGHT_MODE + else: + return self._device.state.speed + return None + + @property + def current_direction(self): + """Return direction of the fan [forward, reverse].""" + return None + + @property + def speed_list(self: ToggleEntity) -> list: + """Get the list of available speeds.""" + from libpurecoollink.const import FanSpeed + supported_speeds = [FanSpeed.FAN_SPEED_AUTO.value, NIGHT_MODE, + FanSpeed.FAN_SPEED_1.value, + FanSpeed.FAN_SPEED_2.value, + FanSpeed.FAN_SPEED_3.value, + FanSpeed.FAN_SPEED_4.value, + FanSpeed.FAN_SPEED_5.value, + FanSpeed.FAN_SPEED_6.value, + FanSpeed.FAN_SPEED_7.value, + FanSpeed.FAN_SPEED_8.value, + FanSpeed.FAN_SPEED_9.value, + FanSpeed.FAN_SPEED_10.value] + + return supported_speeds + + @property + def supported_features(self: ToggleEntity) -> int: + """Flag supported features.""" + return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED diff --git a/homeassistant/components/sensor/dyson.py b/homeassistant/components/sensor/dyson.py new file mode 100644 index 00000000000000..f96e0e25556057 --- /dev/null +++ b/homeassistant/components/sensor/dyson.py @@ -0,0 +1,67 @@ +"""Support for Dyson Pure Cool Link Sensors.""" +import logging + +from homeassistant.components.dyson import DYSON_DEVICES + +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['dyson'] + +SENSOR_UNITS = {'filter_life': 'hours'} + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Dyson Sensors.""" + _LOGGER.info("Creating new Dyson fans") + devices = [] + # Get Dyson Devices from parent component + for device in hass.data[DYSON_DEVICES]: + devices.append(DysonFilterLifeSensor(hass, device)) + add_devices(devices) + + +class DysonFilterLifeSensor(Entity): + """Representation of Dyson filter life sensor (in hours).""" + + def on_message(self, message): + """Called when new messages received from the fan.""" + _LOGGER.debug("Message received for %s device: %s", self.name, + message) + if self.hass and self.entity_id: + # Prevent refreshing if not needed + if self._old_value is None or self._old_value != self.state: + self._old_value = self.state + self.schedule_update_ha_state() + + 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._device.add_message_listener(self.on_message) + self._old_value = None + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def state(self): + """Return filter life in hours..""" + if self._device.state: + return self._device.state.filter_life + else: + return None + + @property + def name(self): + """Return the name of the dyson sensor name.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_UNITS['filter_life'] diff --git a/requirements_all.txt b/requirements_all.txt index 5db9e62639dba6..701c7ac3444bec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -341,6 +341,10 @@ knxip==0.3.3 # homeassistant.components.device_tracker.owntracks libnacl==1.5.0 +# homeassistant.components.dyson +# homeassistant.components.fan.dyson +libpurecoollink==0.1.5 + # homeassistant.components.device_tracker.mikrotik librouteros==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21cfb74380fdfc..02ca6c75eda076 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -61,6 +61,10 @@ holidays==0.8.1 # homeassistant.components.sensor.influxdb influxdb==3.0.0 +# homeassistant.components.dyson +# homeassistant.components.fan.dyson +libpurecoollink==0.1.5 + # homeassistant.components.media_player.soundtouch libsoundtouch==0.3.0 diff --git a/tests/components/fan/test_dyson.py b/tests/components/fan/test_dyson.py new file mode 100644 index 00000000000000..e34bdd0cb209f7 --- /dev/null +++ b/tests/components/fan/test_dyson.py @@ -0,0 +1,201 @@ +"""Test the Dyson fan component.""" +import unittest +from unittest import mock + +from homeassistant.components.fan import dyson +from tests.common import get_test_home_assistant +from libpurecoollink.const import FanSpeed, FanMode, NightMode, Oscillation + + +def _get_device_with_no_state(): + """Return a device with no state.""" + device = mock.Mock() + device.name = "Device_name" + device.state = None + return device + + +def _get_device_off(): + """Return a device with state off.""" + device = mock.Mock() + device.name = "Device_name" + device.state = mock.Mock() + device.state.fan_mode = "OFF" + device.state.night_mode = "ON" + return device + + +def _get_device_on(): + """Return a valid state on.""" + device = mock.Mock() + device.name = "Device_name" + device.state = mock.Mock() + device.state.fan_mode = "FAN" + device.state.oscillation = "ON" + device.state.speed = "0001" + return device + + +class DysonTest(unittest.TestCase): + """Dyson Sensor component test class.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_component_with_no_devices(self): + """Test setup component with no devices.""" + self.hass.data[dyson.DYSON_DEVICES] = [] + add_devices = mock.MagicMock() + dyson.setup_platform(self.hass, None, add_devices) + add_devices.assert_called_with([]) + + def test_setup_component(self): + """Test setup component with devices.""" + def _add_device(devices): + assert len(devices) == 1 + assert devices[0].name == "Device_name" + + device = _get_device_on() + self.hass.data[dyson.DYSON_DEVICES] = [device] + dyson.setup_platform(self.hass, None, _add_device) + + def test_dyson_set_speed(self): + """Test set fan speed.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.should_poll) + component.set_speed("0001") + set_config = device.set_configuration + set_config.assert_called_with(fan_mode=FanMode.FAN, + fan_speed=FanSpeed.FAN_SPEED_1, + night_mode=NightMode.NIGHT_MODE_OFF) + + def test_dyson_turn_on(self): + """Test turn on fan.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.should_poll) + component.turn_on() + set_config = device.set_configuration + set_config.assert_called_with(fan_mode=FanMode.FAN) + + def test_dyson_turn_on_night_mode(self): + """Test turn on fan with night mode.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.should_poll) + component.turn_on("NIGHT_MODE") + set_config = device.set_configuration + set_config.assert_called_with(fan_mode=FanMode.AUTO, + night_mode=NightMode.NIGHT_MODE_ON) + + def test_dyson_turn_on_auto_mode(self): + """Test turn on fan with auto mode.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.should_poll) + component.turn_on("AUTO") + set_config = device.set_configuration + set_config.assert_called_with(fan_mode=FanMode.AUTO, + night_mode=NightMode.NIGHT_MODE_OFF) + + def test_dyson_turn_on_speed(self): + """Test turn on fan with specified speed.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.should_poll) + component.turn_on("0001") + set_config = device.set_configuration + set_config.assert_called_with(fan_mode=FanMode.FAN, + fan_speed=FanSpeed.FAN_SPEED_1, + night_mode=NightMode.NIGHT_MODE_OFF) + + def test_dyson_turn_off(self): + """Test turn off fan.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.should_poll) + component.turn_off() + set_config = device.set_configuration + set_config.assert_called_with(fan_mode=FanMode.OFF) + + def test_dyson_oscillate_off(self): + """Test turn off oscillation.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + component.oscillate(False) + set_config = device.set_configuration + set_config.assert_called_with(oscillation=Oscillation.OSCILLATION_OFF) + + def test_dyson_oscillate_on(self): + """Test turn on oscillation.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + component.oscillate(True) + set_config = device.set_configuration + set_config.assert_called_with(oscillation=Oscillation.OSCILLATION_ON) + + def test_dyson_oscillate_value_on(self): + """Test get oscillation value on.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertTrue(component.oscillating) + + def test_dyson_oscillate_value_off(self): + """Test get oscillation value off.""" + device = _get_device_off() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.oscillating) + + def test_dyson_on(self): + """Test device is on.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertTrue(component.is_on) + + def test_dyson_off(self): + """Test device is off.""" + device = _get_device_off() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.is_on) + + device = _get_device_with_no_state() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.is_on) + + def test_dyson_get_speed(self): + """Test get device speed.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertEqual(component.speed, "0001") + + device = _get_device_off() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertEqual(component.speed, "NIGHT_MODE") + + device = _get_device_with_no_state() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertIsNone(component.speed) + + def test_dyson_get_direction(self): + """Test get device direction.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertIsNone(component.current_direction) + + def test_dyson_get_speed_list(self): + """Test get speeds list.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertEqual(len(component.speed_list), 12) + + def test_dyson_supported_features(self): + """Test supported features.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertEqual(component.supported_features, 3) diff --git a/tests/components/sensor/test_dyson.py b/tests/components/sensor/test_dyson.py new file mode 100644 index 00000000000000..3057c7d5accedb --- /dev/null +++ b/tests/components/sensor/test_dyson.py @@ -0,0 +1,75 @@ +"""Test the Dyson sensor(s) component.""" +import unittest +from unittest import mock + +from homeassistant.components.sensor import dyson +from tests.common import get_test_home_assistant + + +def _get_device_without_state(): + """Return a valid device provide by Dyson web services.""" + device = mock.Mock() + device.name = "Device_name" + device.state = None + return device + + +def _get_with_state(): + """Return a valid device with state values.""" + device = mock.Mock() + device.name = "Device_name" + device.state = mock.Mock() + device.state.filter_life = 100 + return device + + +class DysonTest(unittest.TestCase): + """Dyson Sensor component test class.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_component_with_no_devices(self): + """Test setup component with no devices.""" + self.hass.data[dyson.DYSON_DEVICES] = [] + add_devices = mock.MagicMock() + dyson.setup_platform(self.hass, None, add_devices) + add_devices.assert_called_with([]) + + def test_setup_component(self): + """Test setup component with devices.""" + def _add_device(devices): + assert len(devices) == 1 + assert devices[0].name == "Device_name filter life" + + 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.""" + sensor = dyson.DysonFilterLifeSensor(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, "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.""" + sensor = dyson.DysonFilterLifeSensor(self.hass, _get_with_state()) + sensor.entity_id = "sensor.dyson_1" + self.assertFalse(sensor.should_poll) + self.assertEqual(sensor.state, 100) + 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') diff --git a/tests/components/test_dyson.py b/tests/components/test_dyson.py new file mode 100644 index 00000000000000..5ca2fc48d525c9 --- /dev/null +++ b/tests/components/test_dyson.py @@ -0,0 +1,152 @@ +"""Test the parent Dyson component.""" +import unittest +from unittest import mock + +from homeassistant.components import dyson +from tests.common import get_test_home_assistant + + +def _get_dyson_account_device_available(): + """Return a valid device provide by Dyson web services.""" + device = mock.Mock() + device.serial = "XX-XXXXX-XX" + device.connect = mock.Mock(return_value=True) + return device + + +def _get_dyson_account_device_not_available(): + """Return an invalid device provide by Dyson web services.""" + device = mock.Mock() + device.serial = "XX-XXXXX-XX" + device.connect = mock.Mock(return_value=False) + return device + + +class DysonTest(unittest.TestCase): + """Dyson parent component test class.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=False) + def test_dyson_login_failed(self, mocked_login): + """Test if Dyson connection failed.""" + dyson.setup(self.hass, {dyson.DOMAIN: { + dyson.CONF_USERNAME: "email", + dyson.CONF_PASSWORD: "password", + dyson.CONF_LANGUAGE: "FR" + }}) + self.assertEqual(mocked_login.call_count, 1) + + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_dyson_login(self, mocked_login, mocked_devices): + """Test valid connection to dyson web service.""" + dyson.setup(self.hass, {dyson.DOMAIN: { + dyson.CONF_USERNAME: "email", + dyson.CONF_PASSWORD: "password", + dyson.CONF_LANGUAGE: "FR" + }}) + self.assertEqual(mocked_login.call_count, 1) + self.assertEqual(mocked_devices.call_count, 1) + self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0) + + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + return_value=[_get_dyson_account_device_available()]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_dyson_custom_conf(self, mocked_login, mocked_devices): + """Test device connection using custom configuration.""" + dyson.setup(self.hass, {dyson.DOMAIN: { + dyson.CONF_USERNAME: "email", + dyson.CONF_PASSWORD: "password", + dyson.CONF_LANGUAGE: "FR", + dyson.CONF_DEVICES: [ + { + "device_id": "XX-XXXXX-XX", + "device_ip": "192.168.0.1" + } + ] + }}) + self.assertEqual(mocked_login.call_count, 1) + self.assertEqual(mocked_devices.call_count, 1) + self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1) + + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + return_value=[_get_dyson_account_device_not_available()]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_dyson_custom_conf_device_not_available(self, mocked_login, + mocked_devices): + """Test device connection with an invalid device.""" + dyson.setup(self.hass, {dyson.DOMAIN: { + dyson.CONF_USERNAME: "email", + dyson.CONF_PASSWORD: "password", + dyson.CONF_LANGUAGE: "FR", + dyson.CONF_DEVICES: [ + { + "device_id": "XX-XXXXX-XX", + "device_ip": "192.168.0.1" + } + ] + }}) + self.assertEqual(mocked_login.call_count, 1) + self.assertEqual(mocked_devices.call_count, 1) + self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0) + + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + return_value=[_get_dyson_account_device_available()]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_dyson_custom_conf_with_unknown_device(self, mocked_login, + mocked_devices): + """Test device connection with custom conf and unknown device.""" + dyson.setup(self.hass, {dyson.DOMAIN: { + dyson.CONF_USERNAME: "email", + dyson.CONF_PASSWORD: "password", + dyson.CONF_LANGUAGE: "FR", + dyson.CONF_DEVICES: [ + { + "device_id": "XX-XXXXX-XY", + "device_ip": "192.168.0.1" + } + ] + }}) + self.assertEqual(mocked_login.call_count, 1) + self.assertEqual(mocked_devices.call_count, 1) + self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0) + + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + return_value=[_get_dyson_account_device_available()]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_dyson_discovery(self, mocked_login, mocked_devices): + """Test device connection using discovery.""" + dyson.setup(self.hass, {dyson.DOMAIN: { + dyson.CONF_USERNAME: "email", + dyson.CONF_PASSWORD: "password", + dyson.CONF_LANGUAGE: "FR", + dyson.CONF_TIMEOUT: 5, + dyson.CONF_RETRY: 2 + }}) + self.assertEqual(mocked_login.call_count, 1) + self.assertEqual(mocked_devices.call_count, 1) + self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1) + + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + return_value=[_get_dyson_account_device_not_available()]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_dyson_discovery_device_not_available(self, mocked_login, + mocked_devices): + """Test device connection with discovery and invalid device.""" + dyson.setup(self.hass, {dyson.DOMAIN: { + dyson.CONF_USERNAME: "email", + dyson.CONF_PASSWORD: "password", + dyson.CONF_LANGUAGE: "FR", + dyson.CONF_TIMEOUT: 5, + dyson.CONF_RETRY: 2 + }}) + self.assertEqual(mocked_login.call_count, 1) + self.assertEqual(mocked_devices.call_count, 1) + self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0) From da3ac66ef6a295c5178b95f5884f4afb4cefee56 Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Sat, 3 Jun 2017 11:26:05 +0200 Subject: [PATCH 2/7] Code review --- homeassistant/components/dyson.py | 82 +++++++++++++++++-------------- tests/components/test_dyson.py | 10 +++- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson.py index dd3d9101cffced..eb430582ba717e 100644 --- a/homeassistant/components/dyson.py +++ b/homeassistant/components/dyson.py @@ -1,19 +1,20 @@ """Parent component for Dyson Pure Cool Link devices.""" import logging + import voluptuous as vol + import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ + CONF_DEVICES REQUIREMENTS = ['libpurecoollink==0.1.5'] _LOGGER = logging.getLogger(__name__) -CONF_USERNAME = "username" -CONF_PASSWORD = "password" CONF_LANGUAGE = "language" -CONF_TIMEOUT = "timeout" CONF_RETRY = "retry" -CONF_DEVICES = "devices" DEFAULT_TIMEOUT = 5 DEFAULT_RETRY = 10 @@ -52,39 +53,46 @@ def setup(hass, config): timeout = config[DOMAIN].get(CONF_TIMEOUT) retry = config[DOMAIN].get(CONF_RETRY) - if logged: - _LOGGER.info("Connected to Dyson account") - dyson_devices = dyson_account.devices() - if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES): - configured_devices = config[DOMAIN].get(CONF_DEVICES) - for device in configured_devices: - dyson_device = next((d for d in dyson_devices if - d.serial == device["device_id"]), None) - if dyson_device: - connected = dyson_device.connect(None, device["device_ip"], - timeout, retry) - if connected: - _LOGGER.info("Connected to device %s", dyson_device) - hass.data[DYSON_DEVICES].append(dyson_device) - else: - _LOGGER.warning("Unable to connect to device %s", - dyson_device) - else: - _LOGGER.warning( - "Unable to find device %s in Dyson account", - device["device_id"]) - else: - # Not yet reliable - for device in dyson_devices: - _LOGGER.info("Trying to connect to device %s with timeout=%i " - "and retry=%i", device, timeout, retry) - connected = device.connect(None, None, timeout, retry) + if not logged: + _LOGGER.error("Not connected to Dyson account. Unable to add devices") + return False + + _LOGGER.info("Connected to Dyson account") + dyson_devices = dyson_account.devices() + if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES): + configured_devices = config[DOMAIN].get(CONF_DEVICES) + for device in configured_devices: + dyson_device = next((d for d in dyson_devices if + d.serial == device["device_id"]), None) + if dyson_device: + connected = dyson_device.connect(None, device["device_ip"], + timeout, retry) if connected: - _LOGGER.info("Connected to device %s", device) - hass.data[DYSON_DEVICES].append(device) + _LOGGER.info("Connected to device %s", dyson_device) + hass.data[DYSON_DEVICES].append(dyson_device) else: - _LOGGER.warning("Unable to connect to device %s", device) - return True + _LOGGER.warning("Unable to connect to device %s", + dyson_device) + else: + _LOGGER.warning( + "Unable to find device %s in Dyson account", + device["device_id"]) else: - _LOGGER.error("Not connected to Dyson account. Unable to add devices") - return False + # Not yet reliable + for device in dyson_devices: + _LOGGER.info("Trying to connect to device %s with timeout=%i " + "and retry=%i", device, timeout, retry) + connected = device.connect(None, None, timeout, retry) + if connected: + _LOGGER.info("Connected to device %s", device) + hass.data[DYSON_DEVICES].append(device) + else: + _LOGGER.warning("Unable to connect to device %s", device) + + # 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) + + return True diff --git a/tests/components/test_dyson.py b/tests/components/test_dyson.py index 5ca2fc48d525c9..003f9b8b50c0c6 100644 --- a/tests/components/test_dyson.py +++ b/tests/components/test_dyson.py @@ -97,11 +97,13 @@ def test_dyson_custom_conf_device_not_available(self, mocked_login, self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0) + @mock.patch('homeassistant.helpers.discovery.load_platform') @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_available()]) @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) def test_dyson_custom_conf_with_unknown_device(self, mocked_login, - mocked_devices): + mocked_devices, + mocked_discovery): """Test device connection with custom conf and unknown device.""" dyson.setup(self.hass, {dyson.DOMAIN: { dyson.CONF_USERNAME: "email", @@ -117,11 +119,14 @@ def test_dyson_custom_conf_with_unknown_device(self, mocked_login, self.assertEqual(mocked_login.call_count, 1) self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0) + self.assertEqual(mocked_discovery.call_count, 0) + @mock.patch('homeassistant.helpers.discovery.load_platform') @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_available()]) @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) - def test_dyson_discovery(self, mocked_login, mocked_devices): + def test_dyson_discovery(self, mocked_login, mocked_devices, + mocked_discovery): """Test device connection using discovery.""" dyson.setup(self.hass, {dyson.DOMAIN: { dyson.CONF_USERNAME: "email", @@ -133,6 +138,7 @@ def test_dyson_discovery(self, mocked_login, mocked_devices): self.assertEqual(mocked_login.call_count, 1) self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1) + self.assertEqual(mocked_discovery.call_count, 2) @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) From f95a710666dca6ccb9bdf3ae0d8caab80aa04fb5 Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Tue, 6 Jun 2017 17:45:53 +0200 Subject: [PATCH 3/7] Improve auto/night mode --- homeassistant/components/fan/__init__.py | 72 ++++++++++++++ homeassistant/components/fan/dyson.py | 103 +++++++++++++-------- homeassistant/components/fan/services.yaml | 24 ++++- tests/components/fan/test_dyson.py | 97 +++++++++++++++---- 4 files changed, 236 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index f2ade5354264ea..4c61376872eefb 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -37,10 +37,14 @@ SUPPORT_SET_SPEED = 1 SUPPORT_OSCILLATE = 2 SUPPORT_DIRECTION = 4 +SUPPORT_NIGHT_MODE = 8 +SUPPORT_AUTO_MODE = 16 SERVICE_SET_SPEED = 'set_speed' SERVICE_OSCILLATE = 'oscillate' SERVICE_SET_DIRECTION = 'set_direction' +SERVICE_NIGHT_MODE = 'set_night_mode' +SERVICE_AUTO_MODE = 'set_auto_mode' SPEED_OFF = 'off' SPEED_LOW = 'low' @@ -54,12 +58,16 @@ ATTR_SPEED_LIST = 'speed_list' ATTR_OSCILLATING = 'oscillating' ATTR_DIRECTION = 'direction' +ATTR_NIGHT_MODE = 'night_mode' +ATTR_AUTO_MODE = 'auto_mode' PROP_TO_ATTR = { 'speed': ATTR_SPEED, 'speed_list': ATTR_SPEED_LIST, 'oscillating': ATTR_OSCILLATING, 'direction': ATTR_DIRECTION, + 'is_night_mode': ATTR_NIGHT_MODE, + 'is_auto_mode': ATTR_AUTO_MODE } # type: dict FAN_SET_SPEED_SCHEMA = vol.Schema({ @@ -81,6 +89,16 @@ vol.Required(ATTR_OSCILLATING): cv.boolean }) # type: dict +FAN_NIGHT_MODE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_NIGHT_MODE): cv.boolean +}) # type: dict + +FAN_AUTO_MODE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_AUTO_MODE): cv.boolean +}) # type: dict + FAN_TOGGLE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids }) @@ -115,6 +133,14 @@ 'method': 'async_set_direction', 'schema': FAN_SET_DIRECTION_SCHEMA, }, + SERVICE_NIGHT_MODE: { + 'method': 'async_night_mode', + 'schema': FAN_NIGHT_MODE_SCHEMA, + }, + SERVICE_AUTO_MODE: { + 'method': 'async_auto_mode', + 'schema': FAN_AUTO_MODE_SCHEMA, + }, } @@ -189,6 +215,30 @@ def set_direction(hass, entity_id: str=None, direction: str=None) -> None: hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data) +def set_auto_mode(hass, entity_id: str=None, auto_mode: bool=False) -> None: + """Set auto mode for all or specified fan.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_AUTO_MODE, auto_mode), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_AUTO_MODE, data) + + +def set_night_mode(hass, entity_id: str=None, night_mode: bool=False) -> None: + """Set night mode for all or specified fan.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_NIGHT_MODE, night_mode), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_NIGHT_MODE, data) + + @asyncio.coroutine def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" @@ -291,6 +341,28 @@ def async_oscillate(self: ToggleEntity, oscillating: bool): """ return self.hass.async_add_job(self.oscillate, oscillating) + def night_mode(self: ToggleEntity, night_mode: bool) -> None: + """Turn fan in night mode.""" + pass + + def async_night_mode(self: ToggleEntity, night_mode: bool): + """Turn fan in night mode. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.night_mode, night_mode) + + def auto_mode(self: ToggleEntity, auto_mode: bool) -> None: + """Turn fan in auto mode.""" + pass + + def async_auto_mode(self: ToggleEntity, auto_mode: bool): + """Turn fan in auto mode. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.auto_mode, auto_mode) + @property def is_on(self): """Return true if the entity is on.""" diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index a7598419a4de7e..82512941c83263 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -1,7 +1,9 @@ """Support for Dyson Pure Cool link fan.""" import logging -from homeassistant.components.fan import (FanEntity, - SUPPORT_OSCILLATE, SUPPORT_SET_SPEED) +from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + SUPPORT_NIGHT_MODE, + SUPPORT_AUTO_MODE) from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.dyson import DYSON_DEVICES @@ -10,8 +12,6 @@ _LOGGER = logging.getLogger(__name__) -NIGHT_MODE = 'NIGHT_MODE' - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Dyson fan components.""" @@ -53,32 +53,25 @@ def name(self): def set_speed(self: ToggleEntity, speed: str) -> None: """Set the speed of the fan. Never called ??.""" _LOGGER.debug("Set fan speed to: " + speed) - from libpurecoollink.const import FanSpeed, FanMode, NightMode - fan_speed = FanSpeed(speed) - self._device.set_configuration(fan_mode=FanMode.FAN, - night_mode=NightMode.NIGHT_MODE_OFF, - fan_speed=fan_speed) + from libpurecoollink.const import FanSpeed, FanMode + if speed == FanSpeed.FAN_SPEED_AUTO.value: + self._device.set_configuration(fan_mode=FanMode.AUTO) + else: + fan_speed = FanSpeed('{0:04d}'.format(int(speed))) + self._device.set_configuration(fan_mode=FanMode.FAN, + fan_speed=fan_speed) def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None: """Turn on the fan.""" _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) - from libpurecoollink.const import FanSpeed, FanMode, NightMode + from libpurecoollink.const import FanSpeed, FanMode if speed: - # Turn on fan with specified speed - if speed == NIGHT_MODE: - night_mode = NightMode.NIGHT_MODE_ON - self._device.set_configuration(fan_mode=FanMode.AUTO, - night_mode=night_mode) + if speed == FanSpeed.FAN_SPEED_AUTO.value: + self._device.set_configuration(fan_mode=FanMode.AUTO) else: - fan_speed = FanSpeed(speed) - night_mode = NightMode.NIGHT_MODE_OFF - if fan_speed == FanSpeed.FAN_SPEED_AUTO: - self._device.set_configuration(fan_mode=FanMode.AUTO, - night_mode=night_mode) - else: - self._device.set_configuration(fan_mode=FanMode.FAN, - night_mode=night_mode, - fan_speed=fan_speed) + fan_speed = FanSpeed('{0:04d}'.format(int(speed))) + self._device.set_configuration(fan_mode=FanMode.FAN, + fan_speed=fan_speed) else: # Speed not set, just turn on self._device.set_configuration(fan_mode=FanMode.FAN) @@ -111,17 +104,18 @@ def oscillating(self): def is_on(self): """Return true if the entity is on.""" if self._device.state: - return self._device.state.fan_mode in ['FAN', 'AUTO'] + return self._device.state.fan_state == "FAN" return False @property def speed(self) -> str: """Return the current speed.""" if self._device.state: - if self._device.state.night_mode == 'ON': - return NIGHT_MODE - else: + from libpurecoollink.const import FanSpeed + if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: return self._device.state.speed + else: + return int(self._device.state.speed) return None @property @@ -129,25 +123,54 @@ def current_direction(self): """Return direction of the fan [forward, reverse].""" return None + @property + def is_night_mode(self): + """Return Night mode.""" + return self._device.state.night_mode == "ON" + + def night_mode(self: ToggleEntity, night_mode: bool) -> None: + """Turn fan in night mode.""" + _LOGGER.debug("Set %s night mode %s", self.name, night_mode) + from libpurecoollink.const import NightMode + if night_mode: + self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON) + else: + self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_OFF) + + @property + def is_auto_mode(self): + """Return auto mode.""" + return self._device.state.fan_mode == "AUTO" + + def auto_mode(self: ToggleEntity, auto_mode: bool) -> None: + """Turn fan in auto mode.""" + _LOGGER.debug("Set %s auto mode %s", self.name, auto_mode) + from libpurecoollink.const import FanMode + if auto_mode: + self._device.set_configuration(fan_mode=FanMode.AUTO) + else: + self._device.set_configuration(fan_mode=FanMode.FAN) + @property def speed_list(self: ToggleEntity) -> list: """Get the list of available speeds.""" from libpurecoollink.const import FanSpeed - supported_speeds = [FanSpeed.FAN_SPEED_AUTO.value, NIGHT_MODE, - FanSpeed.FAN_SPEED_1.value, - FanSpeed.FAN_SPEED_2.value, - FanSpeed.FAN_SPEED_3.value, - FanSpeed.FAN_SPEED_4.value, - FanSpeed.FAN_SPEED_5.value, - FanSpeed.FAN_SPEED_6.value, - FanSpeed.FAN_SPEED_7.value, - FanSpeed.FAN_SPEED_8.value, - FanSpeed.FAN_SPEED_9.value, - FanSpeed.FAN_SPEED_10.value] + supported_speeds = [FanSpeed.FAN_SPEED_AUTO.value, + int(FanSpeed.FAN_SPEED_1.value), + int(FanSpeed.FAN_SPEED_2.value), + int(FanSpeed.FAN_SPEED_3.value), + int(FanSpeed.FAN_SPEED_4.value), + int(FanSpeed.FAN_SPEED_5.value), + int(FanSpeed.FAN_SPEED_6.value), + int(FanSpeed.FAN_SPEED_7.value), + int(FanSpeed.FAN_SPEED_8.value), + int(FanSpeed.FAN_SPEED_9.value), + int(FanSpeed.FAN_SPEED_10.value)] return supported_speeds @property def supported_features(self: ToggleEntity) -> int: """Flag supported features.""" - return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED + return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED | SUPPORT_NIGHT_MODE \ + | SUPPORT_AUTO_MODE diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 7862aa9a7c3c03..74c6c1d5ae0ce8 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -58,7 +58,29 @@ set_direction: fields: entity_id: description: Name(s) of the entities to toggle - exampl: 'fan.living_room' + example: 'fan.living_room' direction: description: The direction to rotate example: 'left' + +set_auto_mode: + description: Set the fan in auto mode + + fields: + entity_id: + description: Name(s) of the entities to enable/disable auto mode + example: 'fan.living_room' + auto_mode: + description: Auto mode status + example: true + +set_night_mode: + description: Set the fan in night mode + + fields: + entity_id: + description: Name(s) of the entities to enable/disable night mode + example: 'fan.living_room' + night_mode: + description: Auto mode status + example: true diff --git a/tests/components/fan/test_dyson.py b/tests/components/fan/test_dyson.py index e34bdd0cb209f7..71bd53f7ddd4d3 100644 --- a/tests/components/fan/test_dyson.py +++ b/tests/components/fan/test_dyson.py @@ -22,6 +22,18 @@ def _get_device_off(): device.state = mock.Mock() device.state.fan_mode = "OFF" device.state.night_mode = "ON" + device.state.speed = "0004" + return device + + +def _get_device_auto(): + """Return a device with state auto.""" + device = mock.Mock() + device.name = "Device_name" + device.state = mock.Mock() + device.state.fan_mode = "AUTO" + device.state.night_mode = "ON" + device.state.speed = "AUTO" return device @@ -31,7 +43,9 @@ def _get_device_on(): device.name = "Device_name" device.state = mock.Mock() device.state.fan_mode = "FAN" + device.state.fan_state = "FAN" device.state.oscillation = "ON" + device.state.night_mode = "OFF" device.state.speed = "0001" return device @@ -69,11 +83,14 @@ def test_dyson_set_speed(self): device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) self.assertFalse(component.should_poll) - component.set_speed("0001") + component.set_speed("1") set_config = device.set_configuration set_config.assert_called_with(fan_mode=FanMode.FAN, - fan_speed=FanSpeed.FAN_SPEED_1, - night_mode=NightMode.NIGHT_MODE_OFF) + fan_speed=FanSpeed.FAN_SPEED_1) + + component.set_speed("AUTO") + set_config = device.set_configuration + set_config.assert_called_with(fan_mode=FanMode.AUTO) def test_dyson_turn_on(self): """Test turn on fan.""" @@ -84,36 +101,65 @@ def test_dyson_turn_on(self): set_config = device.set_configuration set_config.assert_called_with(fan_mode=FanMode.FAN) - def test_dyson_turn_on_night_mode(self): + def test_dyson_turn_night_mode(self): """Test turn on fan with night mode.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) self.assertFalse(component.should_poll) - component.turn_on("NIGHT_MODE") + component.night_mode(True) set_config = device.set_configuration - set_config.assert_called_with(fan_mode=FanMode.AUTO, - night_mode=NightMode.NIGHT_MODE_ON) + set_config.assert_called_with(night_mode=NightMode.NIGHT_MODE_ON) + + component.night_mode(False) + set_config = device.set_configuration + set_config.assert_called_with(night_mode=NightMode.NIGHT_MODE_OFF) + + def test_is_night_mode(self): + """Test night mode.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.is_night_mode) + + device = _get_device_off() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertTrue(component.is_night_mode) - def test_dyson_turn_on_auto_mode(self): - """Test turn on fan with auto mode.""" + def test_dyson_turn_auto_mode(self): + """Test turn on/off fan with auto mode.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) self.assertFalse(component.should_poll) - component.turn_on("AUTO") + component.auto_mode(True) + set_config = device.set_configuration + set_config.assert_called_with(fan_mode=FanMode.AUTO) + + component.auto_mode(False) set_config = device.set_configuration - set_config.assert_called_with(fan_mode=FanMode.AUTO, - night_mode=NightMode.NIGHT_MODE_OFF) + set_config.assert_called_with(fan_mode=FanMode.FAN) + + def test_is_auto_mode(self): + """Test auto mode.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertFalse(component.is_auto_mode) + + device = _get_device_auto() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertTrue(component.is_auto_mode) def test_dyson_turn_on_speed(self): """Test turn on fan with specified speed.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) self.assertFalse(component.should_poll) - component.turn_on("0001") + component.turn_on("1") set_config = device.set_configuration set_config.assert_called_with(fan_mode=FanMode.FAN, - fan_speed=FanSpeed.FAN_SPEED_1, - night_mode=NightMode.NIGHT_MODE_OFF) + fan_speed=FanSpeed.FAN_SPEED_1) + + component.turn_on("AUTO") + set_config = device.set_configuration + set_config.assert_called_with(fan_mode=FanMode.AUTO) def test_dyson_turn_off(self): """Test turn off fan.""" @@ -172,16 +218,20 @@ def test_dyson_get_speed(self): """Test get device speed.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) - self.assertEqual(component.speed, "0001") + self.assertEqual(component.speed, 1) device = _get_device_off() component = dyson.DysonPureCoolLinkDevice(self.hass, device) - self.assertEqual(component.speed, "NIGHT_MODE") + self.assertEqual(component.speed, 4) device = _get_device_with_no_state() component = dyson.DysonPureCoolLinkDevice(self.hass, device) self.assertIsNone(component.speed) + device = _get_device_auto() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + self.assertEqual(component.speed, "AUTO") + def test_dyson_get_direction(self): """Test get device direction.""" device = _get_device_on() @@ -192,10 +242,19 @@ def test_dyson_get_speed_list(self): """Test get speeds list.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) - self.assertEqual(len(component.speed_list), 12) + self.assertEqual(len(component.speed_list), 11) def test_dyson_supported_features(self): """Test supported features.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) - self.assertEqual(component.supported_features, 3) + self.assertEqual(component.supported_features, 27) + + def test_on_message(self): + """Test when message is received.""" + device = _get_device_on() + component = dyson.DysonPureCoolLinkDevice(self.hass, device) + component.entity_id = "entity_id" + component.schedule_update_ha_state = mock.Mock() + component.on_message("Message") + component.schedule_update_ha_state.assert_called_with() From 73171515768dc4d4894655834f168f5726fbec46 Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Tue, 13 Jun 2017 23:40:16 +0200 Subject: [PATCH 4/7] Move night_mode to Dyson fan component --- homeassistant/components/fan/__init__.py | 72 ---------------------- homeassistant/components/fan/dyson.py | 67 +++++++++++++++----- homeassistant/components/fan/services.yaml | 15 +---- requirements_all.txt | 1 - requirements_test_all.txt | 1 - script/gen_requirements_all.py | 1 + tests/components/fan/test_dyson.py | 21 ++++++- 7 files changed, 75 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 4c61376872eefb..f2ade5354264ea 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -37,14 +37,10 @@ SUPPORT_SET_SPEED = 1 SUPPORT_OSCILLATE = 2 SUPPORT_DIRECTION = 4 -SUPPORT_NIGHT_MODE = 8 -SUPPORT_AUTO_MODE = 16 SERVICE_SET_SPEED = 'set_speed' SERVICE_OSCILLATE = 'oscillate' SERVICE_SET_DIRECTION = 'set_direction' -SERVICE_NIGHT_MODE = 'set_night_mode' -SERVICE_AUTO_MODE = 'set_auto_mode' SPEED_OFF = 'off' SPEED_LOW = 'low' @@ -58,16 +54,12 @@ ATTR_SPEED_LIST = 'speed_list' ATTR_OSCILLATING = 'oscillating' ATTR_DIRECTION = 'direction' -ATTR_NIGHT_MODE = 'night_mode' -ATTR_AUTO_MODE = 'auto_mode' PROP_TO_ATTR = { 'speed': ATTR_SPEED, 'speed_list': ATTR_SPEED_LIST, 'oscillating': ATTR_OSCILLATING, 'direction': ATTR_DIRECTION, - 'is_night_mode': ATTR_NIGHT_MODE, - 'is_auto_mode': ATTR_AUTO_MODE } # type: dict FAN_SET_SPEED_SCHEMA = vol.Schema({ @@ -89,16 +81,6 @@ vol.Required(ATTR_OSCILLATING): cv.boolean }) # type: dict -FAN_NIGHT_MODE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_NIGHT_MODE): cv.boolean -}) # type: dict - -FAN_AUTO_MODE_SCHEMA = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_AUTO_MODE): cv.boolean -}) # type: dict - FAN_TOGGLE_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids }) @@ -133,14 +115,6 @@ 'method': 'async_set_direction', 'schema': FAN_SET_DIRECTION_SCHEMA, }, - SERVICE_NIGHT_MODE: { - 'method': 'async_night_mode', - 'schema': FAN_NIGHT_MODE_SCHEMA, - }, - SERVICE_AUTO_MODE: { - 'method': 'async_auto_mode', - 'schema': FAN_AUTO_MODE_SCHEMA, - }, } @@ -215,30 +189,6 @@ def set_direction(hass, entity_id: str=None, direction: str=None) -> None: hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data) -def set_auto_mode(hass, entity_id: str=None, auto_mode: bool=False) -> None: - """Set auto mode for all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_AUTO_MODE, auto_mode), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_AUTO_MODE, data) - - -def set_night_mode(hass, entity_id: str=None, night_mode: bool=False) -> None: - """Set night mode for all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_NIGHT_MODE, night_mode), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_NIGHT_MODE, data) - - @asyncio.coroutine def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" @@ -341,28 +291,6 @@ def async_oscillate(self: ToggleEntity, oscillating: bool): """ return self.hass.async_add_job(self.oscillate, oscillating) - def night_mode(self: ToggleEntity, night_mode: bool) -> None: - """Turn fan in night mode.""" - pass - - def async_night_mode(self: ToggleEntity, night_mode: bool): - """Turn fan in night mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.night_mode, night_mode) - - def auto_mode(self: ToggleEntity, auto_mode: bool) -> None: - """Turn fan in auto mode.""" - pass - - def async_auto_mode(self: ToggleEntity, auto_mode: bool): - """Turn fan in auto mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.auto_mode, auto_mode) - @property def is_on(self): """Return true if the entity is on.""" diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index 82512941c83263..39bdf68a226e93 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -1,38 +1,69 @@ """Support for Dyson Pure Cool link fan.""" import logging +from os import path +import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, - SUPPORT_NIGHT_MODE, - SUPPORT_AUTO_MODE) + DOMAIN) from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.dyson import DYSON_DEVICES +from homeassistant.config import load_yaml_config_file DEPENDENCIES = ['dyson'] -REQUIREMENTS = ['libpurecoollink==0.1.5'] _LOGGER = logging.getLogger(__name__) +DYSON_FAN_DEVICES = "dyson_fan_devices" +SERVICE_SET_NIGHT_MODE = 'dyson_set_night_mode' + +DYSON_SET_NIGHT_MODE_SCHEMA = vol.Schema({ + vol.Required('entity_id'): cv.entity_id, + vol.Required('night_mode'): cv.boolean +}) + + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Dyson fan components.""" _LOGGER.info("Creating new Dyson fans") - devices = [] + if DYSON_FAN_DEVICES not in hass.data: + hass.data[DYSON_FAN_DEVICES] = [] + # Get Dyson Devices from parent component for device in hass.data[DYSON_DEVICES]: - devices.append(DysonPureCoolLinkDevice(hass, device)) - add_devices(devices) + dyson_entity = DysonPureCoolLinkDevice(hass, device) + hass.data[DYSON_FAN_DEVICES].append(dyson_entity) + + add_devices(hass.data[DYSON_FAN_DEVICES]) + + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + def service_handle(service): + """Handle dyson services.""" + entity_id = service.data.get('entity_id') + night_mode = service.data.get('night_mode') + fan_device = next([fan for fan in hass.data[DYSON_FAN_DEVICES] if + fan.entity_id == entity_id].__iter__(), None) + if fan_device is None: + _LOGGER.warning("Unable to find Dyson fan device %s", + str(entity_id)) + return + + if service.service == SERVICE_SET_NIGHT_MODE: + fan_device.night_mode(night_mode) + + # Register dyson service(s) + hass.services.register(DOMAIN, SERVICE_SET_NIGHT_MODE, + service_handle, + descriptions.get(SERVICE_SET_NIGHT_MODE), + schema=DYSON_SET_NIGHT_MODE_SCHEMA) class DysonPureCoolLinkDevice(FanEntity): """Representation of a Dyson fan.""" - 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) - if self.hass and self.entity_id: - self.schedule_update_ha_state() - def __init__(self, hass, device): """Initialize the fan.""" _LOGGER.info("Creating device %s", device.name) @@ -40,6 +71,13 @@ def __init__(self, hass, device): self._device = device self._device.add_message_listener(self.on_message) + 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) + if self.entity_id: + self.schedule_update_ha_state() + @property def should_poll(self): """No polling needed.""" @@ -172,5 +210,4 @@ def speed_list(self: ToggleEntity) -> list: @property def supported_features(self: ToggleEntity) -> int: """Flag supported features.""" - return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED | SUPPORT_NIGHT_MODE \ - | SUPPORT_AUTO_MODE + return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 74c6c1d5ae0ce8..4a91f49e3829b6 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -63,18 +63,7 @@ set_direction: description: The direction to rotate example: 'left' -set_auto_mode: - description: Set the fan in auto mode - - fields: - entity_id: - description: Name(s) of the entities to enable/disable auto mode - example: 'fan.living_room' - auto_mode: - description: Auto mode status - example: true - -set_night_mode: +dyson_set_night_mode: description: Set the fan in night mode fields: @@ -82,5 +71,5 @@ set_night_mode: description: Name(s) of the entities to enable/disable night mode example: 'fan.living_room' night_mode: - description: Auto mode status + description: Night mode status example: true diff --git a/requirements_all.txt b/requirements_all.txt index 701c7ac3444bec..e1a97b14a93b19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -342,7 +342,6 @@ knxip==0.3.3 libnacl==1.5.0 # homeassistant.components.dyson -# homeassistant.components.fan.dyson libpurecoollink==0.1.5 # homeassistant.components.device_tracker.mikrotik diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02ca6c75eda076..49b6f2ae2f5d37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,7 +62,6 @@ holidays==0.8.1 influxdb==3.0.0 # homeassistant.components.dyson -# homeassistant.components.fan.dyson libpurecoollink==0.1.5 # homeassistant.components.media_player.soundtouch diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d25c1f887804a3..833c351b750b44 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -40,6 +40,7 @@ 'aioautomatic', 'SoCo', 'libsoundtouch', + 'libpurecoollink', 'rxv', 'apns2', 'sqlalchemy', diff --git a/tests/components/fan/test_dyson.py b/tests/components/fan/test_dyson.py index 71bd53f7ddd4d3..4548b12434b54b 100644 --- a/tests/components/fan/test_dyson.py +++ b/tests/components/fan/test_dyson.py @@ -2,6 +2,7 @@ import unittest from unittest import mock +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 @@ -248,7 +249,7 @@ def test_dyson_supported_features(self): """Test supported features.""" device = _get_device_on() component = dyson.DysonPureCoolLinkDevice(self.hass, device) - self.assertEqual(component.supported_features, 27) + self.assertEqual(component.supported_features, 3) def test_on_message(self): """Test when message is received.""" @@ -258,3 +259,21 @@ def test_on_message(self): component.schedule_update_ha_state = mock.Mock() component.on_message("Message") component.schedule_update_ha_state.assert_called_with() + + def test_service_set_night_mode(self): + """Test set night mode service.""" + dyson_device = mock.MagicMock() + self.hass.data[DYSON_DEVICES] = [] + dyson_device.entity_id = 'fan.living_room' + self.hass.data[dyson.DYSON_FAN_DEVICES] = [dyson_device] + dyson.setup_platform(self.hass, None, mock.MagicMock()) + + self.hass.services.call(dyson.DOMAIN, dyson.SERVICE_SET_NIGHT_MODE, + {"entity_id": "fan.bed_room", + "night_mode": True}, True) + assert not dyson_device.night_mode.called + + self.hass.services.call(dyson.DOMAIN, dyson.SERVICE_SET_NIGHT_MODE, + {"entity_id": "fan.living_room", + "night_mode": True}, True) + dyson_device.night_mode.assert_called_with(True) From 7a01b25b00cd5f4dba7f5705d2e2d225e0fafe61 Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Wed, 14 Jun 2017 01:37:27 +0200 Subject: [PATCH 5/7] Code review --- homeassistant/components/fan/dyson.py | 9 ++++++-- homeassistant/components/sensor/dyson.py | 29 ++++++++++++++---------- tests/components/sensor/test_dyson.py | 3 ++- tests/components/test_dyson.py | 5 +++- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index 39bdf68a226e93..ad1e5c2d9e9c89 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -1,5 +1,6 @@ """Support for Dyson Pure Cool link fan.""" import logging +import asyncio from os import path import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -69,14 +70,18 @@ def __init__(self, hass, device): _LOGGER.info("Creating device %s", device.name) self.hass = hass self._device = device + + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" self._device.add_message_listener(self.on_message) 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) - if self.entity_id: - self.schedule_update_ha_state() + + 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 f96e0e25556057..65ce5d2825cc0e 100644 --- a/homeassistant/components/sensor/dyson.py +++ b/homeassistant/components/sensor/dyson.py @@ -1,6 +1,8 @@ """Support for Dyson Pure Cool Link Sensors.""" import logging +import asyncio +from homeassistant.const import STATE_UNKNOWN from homeassistant.components.dyson import DYSON_DEVICES from homeassistant.helpers.entity import Entity @@ -25,24 +27,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DysonFilterLifeSensor(Entity): """Representation of Dyson filter life sensor (in hours).""" - def on_message(self, message): - """Called when new messages received from the fan.""" - _LOGGER.debug("Message received for %s device: %s", self.name, - message) - if self.hass and self.entity_id: - # Prevent refreshing if not needed - if self._old_value is None or self._old_value != self.state: - self._old_value = self.state - self.schedule_update_ha_state() - 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._device.add_message_listener(self.on_message) self._old_value = None + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self._device.add_message_listener(self.on_message) + + 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: + self._old_value = self.state + self.schedule_update_ha_state() + @property def should_poll(self): """No polling needed.""" @@ -54,7 +59,7 @@ def state(self): if self._device.state: return self._device.state.filter_life else: - return None + return STATE_UNKNOWN @property def name(self): diff --git a/tests/components/sensor/test_dyson.py b/tests/components/sensor/test_dyson.py index 3057c7d5accedb..8dc76c701471b3 100644 --- a/tests/components/sensor/test_dyson.py +++ b/tests/components/sensor/test_dyson.py @@ -2,6 +2,7 @@ import unittest from unittest import mock +from homeassistant.const import STATE_UNKNOWN from homeassistant.components.sensor import dyson from tests.common import get_test_home_assistant @@ -57,7 +58,7 @@ def test_dyson_filter_life_sensor(self): _get_device_without_state()) sensor.entity_id = "sensor.dyson_1" self.assertFalse(sensor.should_poll) - self.assertIsNone(sensor.state) + self.assertEqual(sensor.state, STATE_UNKNOWN) self.assertEqual(sensor.unit_of_measurement, "hours") self.assertEqual(sensor.name, "Device_name filter life") self.assertEqual(sensor.entity_id, "sensor.dyson_1") diff --git a/tests/components/test_dyson.py b/tests/components/test_dyson.py index 003f9b8b50c0c6..fce88fefc2c69c 100644 --- a/tests/components/test_dyson.py +++ b/tests/components/test_dyson.py @@ -56,10 +56,12 @@ def test_dyson_login(self, mocked_login, mocked_devices): self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0) + @mock.patch('homeassistant.helpers.discovery.load_platform') @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_available()]) @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) - def test_dyson_custom_conf(self, mocked_login, mocked_devices): + def test_dyson_custom_conf(self, mocked_login, mocked_devices, + mocked_discovery): """Test device connection using custom configuration.""" dyson.setup(self.hass, {dyson.DOMAIN: { dyson.CONF_USERNAME: "email", @@ -75,6 +77,7 @@ def test_dyson_custom_conf(self, mocked_login, mocked_devices): self.assertEqual(mocked_login.call_count, 1) self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1) + self.assertEqual(mocked_discovery.call_count, 2) @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) From 8c99c86a9dbc83974d331ece3f04eed5068db0ce Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 14 Jun 2017 10:53:23 +0200 Subject: [PATCH 6/7] fix asynchrone/sync --- homeassistant/components/fan/dyson.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index ad1e5c2d9e9c89..f879c250a16a86 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -74,13 +74,13 @@ def __init__(self, hass, device): @asyncio.coroutine def async_added_to_hass(self): """Callback when entity is added to hass.""" - self._device.add_message_listener(self.on_message) + self.hass.async_add_job( + self._device.add_message_listener(self.on_message)) 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) - + _LOGGER.debug( + "Message received for fan device %s : %s", self.name, message) self.schedule_update_ha_state() @property From a2e12344220767fed560505f771314bfdab60a31 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 14 Jun 2017 10:57:13 +0200 Subject: [PATCH 7/7] Create dyson.py --- homeassistant/components/sensor/dyson.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/dyson.py b/homeassistant/components/sensor/dyson.py index 65ce5d2825cc0e..d2c872c668cc13 100644 --- a/homeassistant/components/sensor/dyson.py +++ b/homeassistant/components/sensor/dyson.py @@ -37,12 +37,13 @@ def __init__(self, hass, device): @asyncio.coroutine def async_added_to_hass(self): """Callback when entity is added to hass.""" - self._device.add_message_listener(self.on_message) + self.hass.async_add_job( + self._device.add_message_listener(self.on_message)) def on_message(self, message): """Called when new messages received from the fan.""" - _LOGGER.debug("Message received for %s device: %s", self.name, - message) + _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: self._old_value = self.state