Skip to content

Commit

Permalink
Add support to Dyson 360 Eye robot vacuum using new vacuum platform (#…
Browse files Browse the repository at this point in the history
…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 - v3
  • Loading branch information
CharlesBlonde authored and MartinHjelmare committed Aug 6, 2017
1 parent 82a7dff commit 83afd12
Show file tree
Hide file tree
Showing 11 changed files with 468 additions and 25 deletions.
24 changes: 14 additions & 10 deletions homeassistant/components/dyson.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \
CONF_DEVICES

REQUIREMENTS = ['libpurecoollink==0.4.1']
REQUIREMENTS = ['libpurecoollink==0.4.2']

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -69,14 +69,17 @@ def setup(hass, config):
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)
try:
connected = dyson_device.connect(device["device_ip"])
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)
except OSError as ose:
_LOGGER.error("Unable to connect to device %s: %s",
str(dyson_device.network_device), str(ose))
else:
_LOGGER.warning(
"Unable to find device %s in Dyson account",
Expand All @@ -86,7 +89,7 @@ def setup(hass, config):
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)
connected = device.auto_connect(timeout, retry)
if connected:
_LOGGER.info("Connected to device %s", device)
hass.data[DYSON_DEVICES].append(device)
Expand All @@ -98,5 +101,6 @@ def setup(hass, config):
_LOGGER.debug("Starting sensor/fan components")
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
discovery.load_platform(hass, "fan", DOMAIN, {}, config)
discovery.load_platform(hass, "vacuum", DOMAIN, {}, config)

return True
4 changes: 3 additions & 1 deletion homeassistant/components/fan/dyson.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hass.data[DYSON_FAN_DEVICES] = []

# 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)
hass.data[DYSON_FAN_DEVICES].append(dyson_entity)

Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/sensor/dyson.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
unit = hass.config.units.temperature_unit
# 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(DysonDustSensor(hass, device))
devices.append(DysonHumiditySensor(hass, device))
Expand Down
3 changes: 0 additions & 3 deletions homeassistant/components/vacuum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,6 @@ def send_command(hass, command, params=None, entity_id=None):
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the vacuum component."""
if not config[DOMAIN]:
return False

component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_VACUUMS)

Expand Down
213 changes: 213 additions & 0 deletions homeassistant/components/vacuum/dyson.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ knxip==0.5
libnacl==1.5.2

# homeassistant.components.dyson
libpurecoollink==0.4.1
libpurecoollink==0.4.2

# homeassistant.components.device_tracker.mikrotik
librouteros==1.0.2
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ holidays==0.8.1
influxdb==3.0.0

# homeassistant.components.dyson
libpurecoollink==0.4.1
libpurecoollink==0.4.2

# homeassistant.components.media_player.soundtouch
libsoundtouch==0.7.2
Expand Down
9 changes: 6 additions & 3 deletions tests/components/fan/test_dyson.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from tests.common import get_test_home_assistant
from libpurecoollink.const import FanSpeed, FanMode, NightMode, Oscillation
from libpurecoollink.dyson_pure_state import DysonPureCoolState
from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink


class MockDysonState(DysonPureCoolState):
Expand Down Expand Up @@ -49,7 +50,7 @@ def _get_device_auto():

def _get_device_on():
"""Return a valid state on."""
device = mock.Mock()
device = mock.Mock(spec=DysonPureCoolLink)
device.name = "Device_name"
device.state = mock.Mock()
device.state.fan_mode = "FAN"
Expand Down Expand Up @@ -84,8 +85,10 @@ 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]
device_fan = _get_device_on()
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)

def test_dyson_set_speed(self):
Expand Down
8 changes: 5 additions & 3 deletions tests/components/sensor/test_dyson.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
STATE_OFF
from homeassistant.components.sensor import dyson
from tests.common import get_test_home_assistant
from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink


def _get_device_without_state():
"""Return a valid device provide by Dyson web services."""
device = mock.Mock()
device = mock.Mock(spec=DysonPureCoolLink)
device.name = "Device_name"
device.state = None
device.environmental_state = None
Expand Down Expand Up @@ -75,8 +76,9 @@ def _add_device(devices):
assert devices[3].name == "Device_name temperature"
assert devices[4].name == "Device_name air quality"

device = _get_device_without_state()
self.hass.data[dyson.DYSON_DEVICES] = [device]
device_fan = _get_device_without_state()
device_non_fan = _get_with_state()
self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan]
dyson.setup_platform(self.hass, None, _add_device)

def test_dyson_filter_life_sensor(self):
Expand Down
Loading

0 comments on commit 83afd12

Please sign in to comment.