Add support to Dyson 360 Eye robot vacuum using new vacuum platform (#8852)
* Add support to Dyson 360 Eye robot vacuum using new vacuum platform * Fix tests with Python 3.5 * Code review * Code review - v2 * Code review - v3pull/8866/head
parent
82a7dffc03
commit
83afd12807
|
@ -13,7 +13,7 @@ from homeassistant.helpers import discovery
|
||||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \
|
||||||
CONF_DEVICES
|
CONF_DEVICES
|
||||||
|
|
||||||
REQUIREMENTS = ['libpurecoollink==0.4.1']
|
REQUIREMENTS = ['libpurecoollink==0.4.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -69,14 +69,17 @@ def setup(hass, config):
|
||||||
dyson_device = next((d for d in dyson_devices if
|
dyson_device = next((d for d in dyson_devices if
|
||||||
d.serial == device["device_id"]), None)
|
d.serial == device["device_id"]), None)
|
||||||
if dyson_device:
|
if dyson_device:
|
||||||
connected = dyson_device.connect(None, device["device_ip"],
|
try:
|
||||||
timeout, retry)
|
connected = dyson_device.connect(device["device_ip"])
|
||||||
if connected:
|
if connected:
|
||||||
_LOGGER.info("Connected to device %s", dyson_device)
|
_LOGGER.info("Connected to device %s", dyson_device)
|
||||||
hass.data[DYSON_DEVICES].append(dyson_device)
|
hass.data[DYSON_DEVICES].append(dyson_device)
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning("Unable to connect to device %s",
|
_LOGGER.warning("Unable to connect to device %s",
|
||||||
dyson_device)
|
dyson_device)
|
||||||
|
except OSError as ose:
|
||||||
|
_LOGGER.error("Unable to connect to device %s: %s",
|
||||||
|
str(dyson_device.network_device), str(ose))
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Unable to find device %s in Dyson account",
|
"Unable to find device %s in Dyson account",
|
||||||
|
@ -86,7 +89,7 @@ def setup(hass, config):
|
||||||
for device in dyson_devices:
|
for device in dyson_devices:
|
||||||
_LOGGER.info("Trying to connect to device %s with timeout=%i "
|
_LOGGER.info("Trying to connect to device %s with timeout=%i "
|
||||||
"and retry=%i", device, timeout, retry)
|
"and retry=%i", device, timeout, retry)
|
||||||
connected = device.connect(None, None, timeout, retry)
|
connected = device.auto_connect(timeout, retry)
|
||||||
if connected:
|
if connected:
|
||||||
_LOGGER.info("Connected to device %s", device)
|
_LOGGER.info("Connected to device %s", device)
|
||||||
hass.data[DYSON_DEVICES].append(device)
|
hass.data[DYSON_DEVICES].append(device)
|
||||||
|
@ -98,5 +101,6 @@ def setup(hass, config):
|
||||||
_LOGGER.debug("Starting sensor/fan components")
|
_LOGGER.debug("Starting sensor/fan components")
|
||||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||||
discovery.load_platform(hass, "fan", DOMAIN, {}, config)
|
discovery.load_platform(hass, "fan", DOMAIN, {}, config)
|
||||||
|
discovery.load_platform(hass, "vacuum", DOMAIN, {}, config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -36,7 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
hass.data[DYSON_FAN_DEVICES] = []
|
hass.data[DYSON_FAN_DEVICES] = []
|
||||||
|
|
||||||
# Get Dyson Devices from parent component
|
# Get Dyson Devices from parent component
|
||||||
for device in hass.data[DYSON_DEVICES]:
|
from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink
|
||||||
|
for device in [d for d in hass.data[DYSON_DEVICES] if
|
||||||
|
isinstance(d, DysonPureCoolLink)]:
|
||||||
dyson_entity = DysonPureCoolLinkDevice(hass, device)
|
dyson_entity = DysonPureCoolLinkDevice(hass, device)
|
||||||
hass.data[DYSON_FAN_DEVICES].append(dyson_entity)
|
hass.data[DYSON_FAN_DEVICES].append(dyson_entity)
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
devices = []
|
devices = []
|
||||||
unit = hass.config.units.temperature_unit
|
unit = hass.config.units.temperature_unit
|
||||||
# Get Dyson Devices from parent component
|
# Get Dyson Devices from parent component
|
||||||
for device in hass.data[DYSON_DEVICES]:
|
from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink
|
||||||
|
for device in [d for d in hass.data[DYSON_DEVICES] if
|
||||||
|
isinstance(d, DysonPureCoolLink)]:
|
||||||
devices.append(DysonFilterLifeSensor(hass, device))
|
devices.append(DysonFilterLifeSensor(hass, device))
|
||||||
devices.append(DysonDustSensor(hass, device))
|
devices.append(DysonDustSensor(hass, device))
|
||||||
devices.append(DysonHumiditySensor(hass, device))
|
devices.append(DysonHumiditySensor(hass, device))
|
||||||
|
|
|
@ -168,9 +168,6 @@ def send_command(hass, command, params=None, entity_id=None):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Set up the vacuum component."""
|
"""Set up the vacuum component."""
|
||||||
if not config[DOMAIN]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
component = EntityComponent(
|
component = EntityComponent(
|
||||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_VACUUMS)
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_VACUUMS)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
"""
|
||||||
|
Support for the Dyson 360 eye vacuum cleaner robot.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/vacuum.dyson/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.dyson import DYSON_DEVICES
|
||||||
|
from homeassistant.components.vacuum import (SUPPORT_BATTERY,
|
||||||
|
SUPPORT_FAN_SPEED, SUPPORT_PAUSE,
|
||||||
|
SUPPORT_RETURN_HOME,
|
||||||
|
SUPPORT_STATUS, SUPPORT_STOP,
|
||||||
|
SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
|
||||||
|
VacuumDevice)
|
||||||
|
from homeassistant.util.icon import icon_for_battery_level
|
||||||
|
|
||||||
|
ATTR_FULL_CLEAN_TYPE = "full_clean_type"
|
||||||
|
ATTR_CLEAN_ID = "clean_id"
|
||||||
|
ATTR_POSITION = "position"
|
||||||
|
|
||||||
|
DEPENDENCIES = ['dyson']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DYSON_360_EYE_DEVICES = "dyson_360_eye_devices"
|
||||||
|
|
||||||
|
ICON = "mdi:roomba"
|
||||||
|
|
||||||
|
SUPPORT_DYSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \
|
||||||
|
SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | SUPPORT_STATUS | \
|
||||||
|
SUPPORT_BATTERY | SUPPORT_STOP
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Set up the Dyson 360 Eye robot vacuum platform."""
|
||||||
|
_LOGGER.info("Creating new Dyson 360 Eye robot vacuum")
|
||||||
|
if DYSON_360_EYE_DEVICES not in hass.data:
|
||||||
|
hass.data[DYSON_360_EYE_DEVICES] = []
|
||||||
|
|
||||||
|
# Get Dyson Devices from parent component
|
||||||
|
from libpurecoollink.dyson_360_eye import Dyson360Eye
|
||||||
|
for device in [d for d in hass.data[DYSON_DEVICES] if
|
||||||
|
isinstance(d, Dyson360Eye)]:
|
||||||
|
dyson_entity = Dyson360EyeDevice(device)
|
||||||
|
hass.data[DYSON_360_EYE_DEVICES].append(dyson_entity)
|
||||||
|
|
||||||
|
add_devices(hass.data[DYSON_360_EYE_DEVICES])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Dyson360EyeDevice(VacuumDevice):
|
||||||
|
"""Dyson 360 Eye robot vacuum device."""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
"""Dyson 360 Eye robot vacuum device."""
|
||||||
|
_LOGGER.info("Creating device %s", device.name)
|
||||||
|
self._device = device
|
||||||
|
self._icon = ICON
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_added_to_hass(self):
|
||||||
|
"""Callback when entity is added to hass."""
|
||||||
|
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 vacuum."""
|
||||||
|
_LOGGER.debug("Message received for %s device: %s", self.name, message)
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Return True if entity has to be polled for state.
|
||||||
|
|
||||||
|
False if entity pushes its state to HA.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._device.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use for device."""
|
||||||
|
return self._icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
"""Return the status of the vacuum cleaner."""
|
||||||
|
from libpurecoollink.const import Dyson360EyeMode
|
||||||
|
dyson_labels = {
|
||||||
|
Dyson360EyeMode.INACTIVE_CHARGING: "Stopped - Charging",
|
||||||
|
Dyson360EyeMode.INACTIVE_CHARGED: "Stopped - Charged",
|
||||||
|
Dyson360EyeMode.FULL_CLEAN_PAUSED: "Paused",
|
||||||
|
Dyson360EyeMode.FULL_CLEAN_RUNNING: "Cleaning",
|
||||||
|
Dyson360EyeMode.FULL_CLEAN_ABORTED: "Returning home",
|
||||||
|
Dyson360EyeMode.FULL_CLEAN_INITIATED: "Start cleaning",
|
||||||
|
Dyson360EyeMode.FAULT_USER_RECOVERABLE: "Error - device blocked",
|
||||||
|
Dyson360EyeMode.FAULT_REPLACE_ON_DOCK:
|
||||||
|
"Error - Replace device on dock",
|
||||||
|
Dyson360EyeMode.FULL_CLEAN_FINISHED: "Finished",
|
||||||
|
Dyson360EyeMode.FULL_CLEAN_NEEDS_CHARGE: "Need charging"
|
||||||
|
}
|
||||||
|
return dyson_labels.get(self._device.state.state,
|
||||||
|
self._device.state.state)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_level(self):
|
||||||
|
"""Return the battery level of the vacuum cleaner."""
|
||||||
|
return self._device.state.battery_level
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_speed(self):
|
||||||
|
"""Return the fan speed of the vacuum cleaner."""
|
||||||
|
from libpurecoollink.const import PowerMode
|
||||||
|
speed_labels = {
|
||||||
|
PowerMode.MAX: "Max",
|
||||||
|
PowerMode.QUIET: "Quiet"
|
||||||
|
}
|
||||||
|
return speed_labels[self._device.state.power_mode]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_speed_list(self):
|
||||||
|
"""Get the list of available fan speed steps of the vacuum cleaner."""
|
||||||
|
return ["Quiet", "Max"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the specific state attributes of this vacuum cleaner."""
|
||||||
|
return {
|
||||||
|
ATTR_POSITION: str(self._device.state.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return True if entity is on."""
|
||||||
|
from libpurecoollink.const import Dyson360EyeMode
|
||||||
|
return self._device.state.state in [
|
||||||
|
Dyson360EyeMode.FULL_CLEAN_INITIATED,
|
||||||
|
Dyson360EyeMode.FULL_CLEAN_ABORTED,
|
||||||
|
Dyson360EyeMode.FULL_CLEAN_RUNNING
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag vacuum cleaner robot features that are supported."""
|
||||||
|
return SUPPORT_DYSON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_icon(self):
|
||||||
|
"""Return the battery icon for the vacuum cleaner."""
|
||||||
|
from libpurecoollink.const import Dyson360EyeMode
|
||||||
|
charging = self._device.state.state in [
|
||||||
|
Dyson360EyeMode.INACTIVE_CHARGING]
|
||||||
|
return icon_for_battery_level(
|
||||||
|
battery_level=self.battery_level, charging=charging)
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Turn the vacuum on."""
|
||||||
|
_LOGGER.debug("Turn on device %s", self.name)
|
||||||
|
from libpurecoollink.const import Dyson360EyeMode
|
||||||
|
if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]:
|
||||||
|
self._device.resume()
|
||||||
|
else:
|
||||||
|
self._device.start()
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn the vacuum off and return to home."""
|
||||||
|
_LOGGER.debug("Turn off device %s", self.name)
|
||||||
|
self._device.pause()
|
||||||
|
|
||||||
|
def stop(self, **kwargs):
|
||||||
|
"""Stop the vacuum cleaner."""
|
||||||
|
_LOGGER.debug("Stop device %s", self.name)
|
||||||
|
self._device.pause()
|
||||||
|
|
||||||
|
def set_fan_speed(self, fan_speed, **kwargs):
|
||||||
|
"""Set fan speed."""
|
||||||
|
_LOGGER.debug("Set fan speed %s on device %s", fan_speed, self.name)
|
||||||
|
from libpurecoollink.const import PowerMode
|
||||||
|
power_modes = {
|
||||||
|
"Quiet": PowerMode.QUIET,
|
||||||
|
"Max": PowerMode.MAX
|
||||||
|
}
|
||||||
|
self._device.set_power_mode(power_modes[fan_speed])
|
||||||
|
|
||||||
|
def start_pause(self, **kwargs):
|
||||||
|
"""Start, pause or resume the cleaning task."""
|
||||||
|
from libpurecoollink.const import Dyson360EyeMode
|
||||||
|
if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]:
|
||||||
|
_LOGGER.debug("Resume device %s", self.name)
|
||||||
|
self._device.resume()
|
||||||
|
elif self._device.state.state in [Dyson360EyeMode.INACTIVE_CHARGED,
|
||||||
|
Dyson360EyeMode.INACTIVE_CHARGING]:
|
||||||
|
_LOGGER.debug("Start device %s", self.name)
|
||||||
|
self._device.start()
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("Pause device %s", self.name)
|
||||||
|
self._device.pause()
|
||||||
|
|
||||||
|
def return_to_base(self, **kwargs):
|
||||||
|
"""Set the vacuum cleaner to return to the dock."""
|
||||||
|
_LOGGER.debug("Return to base device %s", self.name)
|
||||||
|
self._device.abort()
|
|
@ -354,7 +354,7 @@ knxip==0.5
|
||||||
libnacl==1.5.2
|
libnacl==1.5.2
|
||||||
|
|
||||||
# homeassistant.components.dyson
|
# homeassistant.components.dyson
|
||||||
libpurecoollink==0.4.1
|
libpurecoollink==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.device_tracker.mikrotik
|
# homeassistant.components.device_tracker.mikrotik
|
||||||
librouteros==1.0.2
|
librouteros==1.0.2
|
||||||
|
|
|
@ -62,7 +62,7 @@ holidays==0.8.1
|
||||||
influxdb==3.0.0
|
influxdb==3.0.0
|
||||||
|
|
||||||
# homeassistant.components.dyson
|
# homeassistant.components.dyson
|
||||||
libpurecoollink==0.4.1
|
libpurecoollink==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.media_player.soundtouch
|
# homeassistant.components.media_player.soundtouch
|
||||||
libsoundtouch==0.7.2
|
libsoundtouch==0.7.2
|
||||||
|
|
|
@ -7,6 +7,7 @@ from homeassistant.components.fan import dyson
|
||||||
from tests.common import get_test_home_assistant
|
from tests.common import get_test_home_assistant
|
||||||
from libpurecoollink.const import FanSpeed, FanMode, NightMode, Oscillation
|
from libpurecoollink.const import FanSpeed, FanMode, NightMode, Oscillation
|
||||||
from libpurecoollink.dyson_pure_state import DysonPureCoolState
|
from libpurecoollink.dyson_pure_state import DysonPureCoolState
|
||||||
|
from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink
|
||||||
|
|
||||||
|
|
||||||
class MockDysonState(DysonPureCoolState):
|
class MockDysonState(DysonPureCoolState):
|
||||||
|
@ -49,7 +50,7 @@ def _get_device_auto():
|
||||||
|
|
||||||
def _get_device_on():
|
def _get_device_on():
|
||||||
"""Return a valid state on."""
|
"""Return a valid state on."""
|
||||||
device = mock.Mock()
|
device = mock.Mock(spec=DysonPureCoolLink)
|
||||||
device.name = "Device_name"
|
device.name = "Device_name"
|
||||||
device.state = mock.Mock()
|
device.state = mock.Mock()
|
||||||
device.state.fan_mode = "FAN"
|
device.state.fan_mode = "FAN"
|
||||||
|
@ -84,8 +85,10 @@ class DysonTest(unittest.TestCase):
|
||||||
assert len(devices) == 1
|
assert len(devices) == 1
|
||||||
assert devices[0].name == "Device_name"
|
assert devices[0].name == "Device_name"
|
||||||
|
|
||||||
device = _get_device_on()
|
device_fan = _get_device_on()
|
||||||
self.hass.data[dyson.DYSON_DEVICES] = [device]
|
device_non_fan = _get_device_off()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
def test_dyson_set_speed(self):
|
def test_dyson_set_speed(self):
|
||||||
|
|
|
@ -6,11 +6,12 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, \
|
||||||
STATE_OFF
|
STATE_OFF
|
||||||
from homeassistant.components.sensor import dyson
|
from homeassistant.components.sensor import dyson
|
||||||
from tests.common import get_test_home_assistant
|
from tests.common import get_test_home_assistant
|
||||||
|
from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink
|
||||||
|
|
||||||
|
|
||||||
def _get_device_without_state():
|
def _get_device_without_state():
|
||||||
"""Return a valid device provide by Dyson web services."""
|
"""Return a valid device provide by Dyson web services."""
|
||||||
device = mock.Mock()
|
device = mock.Mock(spec=DysonPureCoolLink)
|
||||||
device.name = "Device_name"
|
device.name = "Device_name"
|
||||||
device.state = None
|
device.state = None
|
||||||
device.environmental_state = None
|
device.environmental_state = None
|
||||||
|
@ -75,8 +76,9 @@ class DysonTest(unittest.TestCase):
|
||||||
assert devices[3].name == "Device_name temperature"
|
assert devices[3].name == "Device_name temperature"
|
||||||
assert devices[4].name == "Device_name air quality"
|
assert devices[4].name == "Device_name air quality"
|
||||||
|
|
||||||
device = _get_device_without_state()
|
device_fan = _get_device_without_state()
|
||||||
self.hass.data[dyson.DYSON_DEVICES] = [device]
|
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)
|
||||||
|
|
||||||
def test_dyson_filter_life_sensor(self):
|
def test_dyson_filter_life_sensor(self):
|
||||||
|
|
|
@ -11,6 +11,7 @@ def _get_dyson_account_device_available():
|
||||||
device = mock.Mock()
|
device = mock.Mock()
|
||||||
device.serial = "XX-XXXXX-XX"
|
device.serial = "XX-XXXXX-XX"
|
||||||
device.connect = mock.Mock(return_value=True)
|
device.connect = mock.Mock(return_value=True)
|
||||||
|
device.auto_connect = mock.Mock(return_value=True)
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,6 +20,15 @@ def _get_dyson_account_device_not_available():
|
||||||
device = mock.Mock()
|
device = mock.Mock()
|
||||||
device.serial = "XX-XXXXX-XX"
|
device.serial = "XX-XXXXX-XX"
|
||||||
device.connect = mock.Mock(return_value=False)
|
device.connect = mock.Mock(return_value=False)
|
||||||
|
device.auto_connect = mock.Mock(return_value=False)
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dyson_account_device_error():
|
||||||
|
"""Return an invalid device raising OSError while connecting."""
|
||||||
|
device = mock.Mock()
|
||||||
|
device.serial = "XX-XXXXX-XX"
|
||||||
|
device.connect = mock.Mock(side_effect=OSError("Network error"))
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,7 +87,7 @@ class DysonTest(unittest.TestCase):
|
||||||
self.assertEqual(mocked_login.call_count, 1)
|
self.assertEqual(mocked_login.call_count, 1)
|
||||||
self.assertEqual(mocked_devices.call_count, 1)
|
self.assertEqual(mocked_devices.call_count, 1)
|
||||||
self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1)
|
self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1)
|
||||||
self.assertEqual(mocked_discovery.call_count, 2)
|
self.assertEqual(mocked_discovery.call_count, 3)
|
||||||
|
|
||||||
@mock.patch('libpurecoollink.dyson.DysonAccount.devices',
|
@mock.patch('libpurecoollink.dyson.DysonAccount.devices',
|
||||||
return_value=[_get_dyson_account_device_not_available()])
|
return_value=[_get_dyson_account_device_not_available()])
|
||||||
|
@ -100,6 +110,27 @@ class DysonTest(unittest.TestCase):
|
||||||
self.assertEqual(mocked_devices.call_count, 1)
|
self.assertEqual(mocked_devices.call_count, 1)
|
||||||
self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0)
|
self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0)
|
||||||
|
|
||||||
|
@mock.patch('libpurecoollink.dyson.DysonAccount.devices',
|
||||||
|
return_value=[_get_dyson_account_device_error()])
|
||||||
|
@mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True)
|
||||||
|
def test_dyson_custom_conf_device_error(self, mocked_login,
|
||||||
|
mocked_devices):
|
||||||
|
"""Test device connection with device raising an exception."""
|
||||||
|
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('homeassistant.helpers.discovery.load_platform')
|
@mock.patch('homeassistant.helpers.discovery.load_platform')
|
||||||
@mock.patch('libpurecoollink.dyson.DysonAccount.devices',
|
@mock.patch('libpurecoollink.dyson.DysonAccount.devices',
|
||||||
return_value=[_get_dyson_account_device_available()])
|
return_value=[_get_dyson_account_device_available()])
|
||||||
|
@ -141,7 +172,7 @@ class DysonTest(unittest.TestCase):
|
||||||
self.assertEqual(mocked_login.call_count, 1)
|
self.assertEqual(mocked_login.call_count, 1)
|
||||||
self.assertEqual(mocked_devices.call_count, 1)
|
self.assertEqual(mocked_devices.call_count, 1)
|
||||||
self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1)
|
self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1)
|
||||||
self.assertEqual(mocked_discovery.call_count, 2)
|
self.assertEqual(mocked_discovery.call_count, 3)
|
||||||
|
|
||||||
@mock.patch('libpurecoollink.dyson.DysonAccount.devices',
|
@mock.patch('libpurecoollink.dyson.DysonAccount.devices',
|
||||||
return_value=[_get_dyson_account_device_not_available()])
|
return_value=[_get_dyson_account_device_not_available()])
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
"""Test the Dyson 360 eye robot vacuum component."""
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from libpurecoollink.dyson_360_eye import Dyson360Eye
|
||||||
|
from libpurecoollink.const import Dyson360EyeMode, PowerMode
|
||||||
|
|
||||||
|
from homeassistant.components.vacuum import dyson
|
||||||
|
from homeassistant.components.vacuum.dyson import Dyson360EyeDevice
|
||||||
|
from tests.common import get_test_home_assistant
|
||||||
|
|
||||||
|
|
||||||
|
def _get_non_vacuum_device():
|
||||||
|
"""Return a non vacuum device."""
|
||||||
|
device = mock.Mock()
|
||||||
|
device.name = "Device_Fan"
|
||||||
|
device.state = None
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def _get_vacuum_device_cleaning():
|
||||||
|
"""Return a vacuum device running."""
|
||||||
|
device = mock.Mock(spec=Dyson360Eye)
|
||||||
|
device.name = "Device_Vacuum"
|
||||||
|
device.state = mock.MagicMock()
|
||||||
|
device.state.state = Dyson360EyeMode.FULL_CLEAN_RUNNING
|
||||||
|
device.state.battery_level = 85
|
||||||
|
device.state.power_mode = PowerMode.QUIET
|
||||||
|
device.state.position = (0, 0)
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def _get_vacuum_device_charging():
|
||||||
|
"""Return a vacuum device charging."""
|
||||||
|
device = mock.Mock(spec=Dyson360Eye)
|
||||||
|
device.name = "Device_Vacuum"
|
||||||
|
device.state = mock.MagicMock()
|
||||||
|
device.state.state = Dyson360EyeMode.INACTIVE_CHARGING
|
||||||
|
device.state.battery_level = 40
|
||||||
|
device.state.power_mode = PowerMode.QUIET
|
||||||
|
device.state.position = (0, 0)
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def _get_vacuum_device_pause():
|
||||||
|
"""Return a vacuum device in pause."""
|
||||||
|
device = mock.MagicMock(spec=Dyson360Eye)
|
||||||
|
device.name = "Device_Vacuum"
|
||||||
|
device.state = mock.MagicMock()
|
||||||
|
device.state.state = Dyson360EyeMode.FULL_CLEAN_PAUSED
|
||||||
|
device.state.battery_level = 40
|
||||||
|
device.state.power_mode = PowerMode.QUIET
|
||||||
|
device.state.position = (0, 0)
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def _get_vacuum_device_unknown_state():
|
||||||
|
"""Return a vacuum device with unknown state."""
|
||||||
|
device = mock.Mock(spec=Dyson360Eye)
|
||||||
|
device.name = "Device_Vacuum"
|
||||||
|
device.state = mock.MagicMock()
|
||||||
|
device.state.state = "Unknown"
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
class DysonTest(unittest.TestCase):
|
||||||
|
"""Dyson 360 eye robot vacuum 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, {}, 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_Vacuum"
|
||||||
|
|
||||||
|
device_vacuum = _get_vacuum_device_cleaning()
|
||||||
|
device_non_vacuum = _get_non_vacuum_device()
|
||||||
|
self.hass.data[dyson.DYSON_DEVICES] = [device_vacuum,
|
||||||
|
device_non_vacuum]
|
||||||
|
dyson.setup_platform(self.hass, {}, _add_device)
|
||||||
|
|
||||||
|
def test_on_message(self):
|
||||||
|
"""Test when message is received."""
|
||||||
|
device = _get_vacuum_device_cleaning()
|
||||||
|
component = Dyson360EyeDevice(device)
|
||||||
|
component.entity_id = "entity_id"
|
||||||
|
component.schedule_update_ha_state = mock.Mock()
|
||||||
|
component.on_message(mock.Mock())
|
||||||
|
self.assertTrue(component.schedule_update_ha_state.called)
|
||||||
|
|
||||||
|
def test_should_poll(self):
|
||||||
|
"""Test polling is disable."""
|
||||||
|
device = _get_vacuum_device_cleaning()
|
||||||
|
component = Dyson360EyeDevice(device)
|
||||||
|
self.assertFalse(component.should_poll)
|
||||||
|
|
||||||
|
def test_properties(self):
|
||||||
|
"""Test component properties."""
|
||||||
|
device1 = _get_vacuum_device_cleaning()
|
||||||
|
device2 = _get_vacuum_device_unknown_state()
|
||||||
|
device3 = _get_vacuum_device_charging()
|
||||||
|
component = Dyson360EyeDevice(device1)
|
||||||
|
component2 = Dyson360EyeDevice(device2)
|
||||||
|
component3 = Dyson360EyeDevice(device3)
|
||||||
|
self.assertEqual(component.name, "Device_Vacuum")
|
||||||
|
self.assertTrue(component.is_on)
|
||||||
|
self.assertEqual(component.icon, "mdi:roomba")
|
||||||
|
self.assertEqual(component.status, "Cleaning")
|
||||||
|
self.assertEqual(component2.status, "Unknown")
|
||||||
|
self.assertEqual(component.battery_level, 85)
|
||||||
|
self.assertEqual(component.fan_speed, "Quiet")
|
||||||
|
self.assertEqual(component.fan_speed_list, ["Quiet", "Max"])
|
||||||
|
self.assertEqual(component.device_state_attributes['position'],
|
||||||
|
'(0, 0)')
|
||||||
|
self.assertTrue(component.available)
|
||||||
|
self.assertEqual(component.supported_features, 255)
|
||||||
|
self.assertEqual(component.battery_icon, "mdi:battery-80")
|
||||||
|
self.assertEqual(component3.battery_icon, "mdi:battery-charging-40")
|
||||||
|
|
||||||
|
def test_turn_on(self):
|
||||||
|
"""Test turn on vacuum."""
|
||||||
|
device1 = _get_vacuum_device_charging()
|
||||||
|
component1 = Dyson360EyeDevice(device1)
|
||||||
|
component1.turn_on()
|
||||||
|
self.assertTrue(device1.start.called)
|
||||||
|
|
||||||
|
device2 = _get_vacuum_device_pause()
|
||||||
|
component2 = Dyson360EyeDevice(device2)
|
||||||
|
component2.turn_on()
|
||||||
|
self.assertTrue(device2.resume.called)
|
||||||
|
|
||||||
|
def test_turn_off(self):
|
||||||
|
"""Test turn off vacuum."""
|
||||||
|
device1 = _get_vacuum_device_cleaning()
|
||||||
|
component1 = Dyson360EyeDevice(device1)
|
||||||
|
component1.turn_off()
|
||||||
|
self.assertTrue(device1.pause.called)
|
||||||
|
|
||||||
|
def test_stop(self):
|
||||||
|
"""Test stop vacuum."""
|
||||||
|
device1 = _get_vacuum_device_cleaning()
|
||||||
|
component1 = Dyson360EyeDevice(device1)
|
||||||
|
component1.stop()
|
||||||
|
self.assertTrue(device1.pause.called)
|
||||||
|
|
||||||
|
def test_set_fan_speed(self):
|
||||||
|
"""Test set fan speed vacuum."""
|
||||||
|
device1 = _get_vacuum_device_cleaning()
|
||||||
|
component1 = Dyson360EyeDevice(device1)
|
||||||
|
component1.set_fan_speed("Max")
|
||||||
|
device1.set_power_mode.assert_called_with(PowerMode.MAX)
|
||||||
|
|
||||||
|
def test_start_pause(self):
|
||||||
|
"""Test start/pause."""
|
||||||
|
device1 = _get_vacuum_device_charging()
|
||||||
|
component1 = Dyson360EyeDevice(device1)
|
||||||
|
component1.start_pause()
|
||||||
|
self.assertTrue(device1.start.called)
|
||||||
|
|
||||||
|
device2 = _get_vacuum_device_pause()
|
||||||
|
component2 = Dyson360EyeDevice(device2)
|
||||||
|
component2.start_pause()
|
||||||
|
self.assertTrue(device2.resume.called)
|
||||||
|
|
||||||
|
device3 = _get_vacuum_device_cleaning()
|
||||||
|
component3 = Dyson360EyeDevice(device3)
|
||||||
|
component3.start_pause()
|
||||||
|
self.assertTrue(device3.pause.called)
|
||||||
|
|
||||||
|
def test_return_to_base(self):
|
||||||
|
"""Test return to base."""
|
||||||
|
device = _get_vacuum_device_pause()
|
||||||
|
component = Dyson360EyeDevice(device)
|
||||||
|
component.return_to_base()
|
||||||
|
self.assertTrue(device.abort.called)
|
Loading…
Reference in New Issue