From 71597c698245658c5adc711a38a972cff35ccd12 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sat, 9 Dec 2017 00:24:54 -0800 Subject: [PATCH 01/19] Ecovacs Deebot vacuums WIP --- homeassistant/components/ecovacs.py | 83 +++++++++ homeassistant/components/vacuum/ecovacs.py | 185 +++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 homeassistant/components/ecovacs.py create mode 100644 homeassistant/components/vacuum/ecovacs.py diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py new file mode 100644 index 00000000000000..a4d6eeb58364c5 --- /dev/null +++ b/homeassistant/components/ecovacs.py @@ -0,0 +1,83 @@ +"""Parent component for Ecovacs Deebot vacuums. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/ecovacs/ +""" + +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, EVENT_HOMEASSISTANT_STOP + +REQUIREMENTS = ['sucks==0.8.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "ecovacs" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [dict]), + }) +}, extra=vol.ALLOW_EXTRA) + +ECOVACS_DEVICES = "ecovacs_devices" +# TODO: const or generated? +ECOVACS_API_DEVICEID = "6d6ce034ef01ae6c66b21729ffa3b23e" + +def setup(hass, config): + """Set up the Ecovacs component.""" + _LOGGER.info("Creating new Ecovacs component") + + if ECOVACS_DEVICES not in hass.data: + hass.data[ECOVACS_DEVICES] = [] + + from sucks import EcoVacsAPI, VacBot + + # Convenient hack for debugging to pipe sucks logging to the Hass logger + import sucks + sucks.logging = _LOGGER + + ecovacs_api = EcoVacsAPI(ECOVACS_API_DEVICEID, + config[DOMAIN].get(CONF_USERNAME), + EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), + 'us', #TODO: Make configurable + 'na') #TODO: Make configurable + + devices = ecovacs_api.devices() + _LOGGER.debug("Ecobot devices: %s", devices) + + for device in devices: + _LOGGER.info("Discovered Ecovacs device on account: %s", + device['nick']) + vacbot = VacBot(ecovacs_api.uid, + ecovacs_api.REALM, + ecovacs_api.resource, + ecovacs_api.user_access_token, + device, + 'na') #TODO: Make configurable + hass.data[ECOVACS_DEVICES].append(vacbot) + + # pylint: disable=unused-argument + def stop(event: object) -> None: + for device in hass.data[ECOVACS_DEVICES]: + _LOGGER.info("Shutting down connection to Ecovacs device %s", + device.vacuum['nick']) + device.disconnect() + + # Listen for HA stop to disconnect. + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) + + if hass.data[ECOVACS_DEVICES]: + _LOGGER.debug("Starting vacuum components") + # discovery.load_platform(hass, "sensor", DOMAIN, {}, config) + discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py new file mode 100644 index 00000000000000..363997cdde2a0e --- /dev/null +++ b/homeassistant/components/vacuum/ecovacs.py @@ -0,0 +1,185 @@ +""" +Support for Ecovacs Deebot Vaccums. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/vacuum.neato/ +""" +import logging + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.components.vacuum import ( + VacuumDevice, SUPPORT_BATTERY, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, + SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) +from homeassistant.components.ecovacs import ( + ECOVACS_DEVICES) +from homeassistant.helpers.icon import icon_for_battery_level + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['ecovacs'] + +SUPPORT_DEEBOT = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ + SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ + SUPPORT_STATUS + +ICON = "mdi:roomba" + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Deebot vacuums.""" + vacuums = [] + for device in hass.data[ECOVACS_DEVICES]: + vacuums.append(DeebotVacuum(device)) + _LOGGER.debug("Adding Deebot Vacuums to Hass: %s", vacuums) + add_devices(vacuums, True) + + +class DeebotVacuum(VacuumDevice): + """Neato Connected Vacuums.""" + + def __init__(self, device): + """Initialize the Neato Connected Vacuums.""" + self.device = device + self.device.connect_and_wait_until_ready() + try: + self._name = '{}'.format(self.device.vacuum['nick']) + except KeyError: + # In case there is no nickname defined, use the device id + self._name = '{}'.format(self.device.vacuum['did']) + + self._clean_status = None + self._charge_status = None + self._battery_level = None + self._state = None + self._first_update_done = False + _LOGGER.debug("Vacuum initialized: %s", self.name) + + def update(self): + if not self._first_update_done: + from sucks import VacBotCommand + # Fire off some queries to get initial state + self.device.run(VacBotCommand('GetCleanState', {})) + self.device.run(VacBotCommand('GetChargeState', {})) + self.device.run(VacBotCommand('GetBatteryInfo', {})) + self._first_update_done = True + + self._clean_status = self.device.clean_status + self._charge_status = self.device.charge_status + + try: + if self.device.battery_status is not None: + self._battery_level = self.device.battery_status * 100 + except AttributeError: + # No battery_status property + pass + + @property + def is_on(self): + """Return true if vacuum is currently cleaning.""" + if self._clean_status is None: + return False + else: + return self._clean_status != 'stop' + + @property + def is_charging(self): + """Return true if vacuum is currently cleaning.""" + if self._charge_status is None: + return False + else: + return self._charge_status != 'charging' + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def icon(self): + """Return the icon to use for device.""" + return ICON + + @property + def supported_features(self): + """Flag vacuum cleaner robot features that are supported.""" + return SUPPORT_DEEBOT + + def return_to_base(self, **kwargs): + """Set the vacuum cleaner to return to the dock.""" + from sucks import Charge + self.device.run(Charge()) + + @property + def battery_icon(self): + """Return the battery icon for the vacuum cleaner.""" + return icon_for_battery_level( + battery_level=self.battery_level, charging=self.is_charging) + + @property + def battery_level(self): + """Return the battery level of the vacuum cleaner.""" + return self._battery_level + + @property + def fan_speed(self): + """Return the fan speed of the vacuum cleaner.""" + # TODO: Implement + return None + + @property + def fan_speed_list(self): + """Get the list of available fan speed steps of the vacuum cleaner.""" + # TODO: Implement + raise NotImplementedError() + + def turn_on(self, **kwargs): + """Turn the vacuum on and start cleaning.""" + # TODO: Implement + raise NotImplementedError() + + def turn_off(self, **kwargs): + """Turn the vacuum off stopping the cleaning and returning home.""" + # TODO: Implement + raise NotImplementedError() + + def stop(self, **kwargs): + """Stop the vacuum cleaner.""" + # TODO: Implement + raise NotImplementedError() + + + def clean_spot(self, **kwargs): + """Perform a spot clean-up.""" + # TODO: Implement + raise NotImplementedError() + + def locate(self, **kwargs): + """Locate the vacuum cleaner.""" + # TODO: Implement + raise NotImplementedError() + + def set_fan_speed(self, fan_speed, **kwargs): + """Set fan speed.""" + # TODO: Implement + raise NotImplementedError() + + def start_pause(self, **kwargs): + """Start, pause or resume the cleaning task.""" + # TODO: Implement + raise NotImplementedError() + + def send_command(self, command, params=None, **kwargs): + """Send a command to a vacuum cleaner.""" + # TODO: Implement + raise NotImplementedError() + + @property + def state_attributes(self): + """Return the state attributes of the vacuum cleaner.""" + # TODO: Implement + data = super().state_attributes + + # TODO: attribute names should be consts + data['clean_status'] = self._clean_status + data['charge_status'] = self._charge_status + + return data From da08c60bb66130a43f3f76da62c2efe259067178 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Mon, 18 Dec 2017 20:11:50 -0800 Subject: [PATCH 02/19] More WIP --- homeassistant/components/ecovacs.py | 3 +- homeassistant/components/vacuum/ecovacs.py | 39 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index a4d6eeb58364c5..918d4aa14358b2 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ CONF_DEVICES, EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['sucks==0.8.1'] +REQUIREMENTS = ['sucks==0.8.3'] _LOGGER = logging.getLogger(__name__) @@ -42,6 +42,7 @@ def setup(hass, config): from sucks import EcoVacsAPI, VacBot # Convenient hack for debugging to pipe sucks logging to the Hass logger + # TODO: Comment out before commit import sucks sucks.logging = _LOGGER diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index 363997cdde2a0e..6d87f696d99020 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -1,5 +1,5 @@ """ -Support for Ecovacs Deebot Vaccums. +Support for Ecovacs Ecovacs Vaccums. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/vacuum.neato/ @@ -18,26 +18,28 @@ DEPENDENCIES = ['ecovacs'] -SUPPORT_DEEBOT = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ +SUPPORT_ECOVACS = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ SUPPORT_STATUS +ECOVACS_FAN_SPEED_LIST = ['standard', 'strong'] + ICON = "mdi:roomba" def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Deebot vacuums.""" + """Set up the Ecovacs vacuums.""" vacuums = [] for device in hass.data[ECOVACS_DEVICES]: - vacuums.append(DeebotVacuum(device)) - _LOGGER.debug("Adding Deebot Vacuums to Hass: %s", vacuums) + vacuums.append(EcovacsVacuum(device)) + _LOGGER.debug("Adding Ecovacs Vacuums to Hass: %s", vacuums) add_devices(vacuums, True) -class DeebotVacuum(VacuumDevice): - """Neato Connected Vacuums.""" +class EcovacsVacuum(VacuumDevice): + """Ecovacs Vacuums such as Deebot.""" def __init__(self, device): - """Initialize the Neato Connected Vacuums.""" + """Initialize the Ecovacs Vacuum.""" self.device = device self.device.connect_and_wait_until_ready() try: @@ -82,11 +84,11 @@ def is_on(self): @property def is_charging(self): - """Return true if vacuum is currently cleaning.""" + """Return true if vacuum is currently charging.""" if self._charge_status is None: return False else: - return self._charge_status != 'charging' + return self._charge_status == 'charging' @property def name(self): @@ -101,7 +103,7 @@ def icon(self): @property def supported_features(self): """Flag vacuum cleaner robot features that are supported.""" - return SUPPORT_DEEBOT + return SUPPORT_ECOVACS def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" @@ -128,24 +130,21 @@ def fan_speed(self): @property def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" - # TODO: Implement - raise NotImplementedError() + return ECOVACS_FAN_SPEED_LIST def turn_on(self, **kwargs): """Turn the vacuum on and start cleaning.""" - # TODO: Implement - raise NotImplementedError() + from sucks import Clean + self.device.run(Clean(False)) def turn_off(self, **kwargs): """Turn the vacuum off stopping the cleaning and returning home.""" - # TODO: Implement - raise NotImplementedError() + self.return_to_base() def stop(self, **kwargs): """Stop the vacuum cleaner.""" - # TODO: Implement - raise NotImplementedError() - + from sucks import VacBotCommand + self.device.run(VacBotCommand('Move', {'action': 'stop'})) def clean_spot(self, **kwargs): """Perform a spot clean-up.""" From 51e15d2c758d8aa7cdf501a17e731b9cb23aa0e1 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 23 Jan 2018 13:12:40 -0800 Subject: [PATCH 03/19] All core features implemented Getting fan speed and locating the vac are still unsupported until sucks adds support --- homeassistant/components/ecovacs.py | 2 +- homeassistant/components/vacuum/ecovacs.py | 74 ++++++++++++---------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index 918d4aa14358b2..91076c5585c30f 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ CONF_DEVICES, EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['sucks==0.8.3'] +REQUIREMENTS = ['sucks==0.8.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index 6d87f696d99020..2de63802f685ec 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -6,10 +6,10 @@ """ import logging -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.components.vacuum import ( - VacuumDevice, SUPPORT_BATTERY, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, - SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) + VacuumDevice, SUPPORT_BATTERY, SUPPORT_RETURN_HOME, SUPPORT_CLEAN_SPOT, + SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_LOCATE, SUPPORT_FAN_SPEED, SUPPORT_SEND_COMMAND, ) from homeassistant.components.ecovacs import ( ECOVACS_DEVICES) from homeassistant.helpers.icon import icon_for_battery_level @@ -18,11 +18,14 @@ DEPENDENCIES = ['ecovacs'] -SUPPORT_ECOVACS = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ - SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ - SUPPORT_STATUS +# Note: SUPPORT_FAN_SPEED gets dynamically added to this list based on vacuum +# state in the supported_features Property getter +SUPPORT_ECOVACS = ( + SUPPORT_BATTERY | SUPPORT_RETURN_HOME | SUPPORT_CLEAN_SPOT | + SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | + SUPPORT_STATUS | SUPPORT_LOCATE | SUPPORT_SEND_COMMAND) -ECOVACS_FAN_SPEED_LIST = ['standard', 'strong'] +ECOVACS_FAN_SPEED_LIST = ['normal', 'high'] ICON = "mdi:roomba" @@ -50,6 +53,7 @@ def __init__(self, device): self._clean_status = None self._charge_status = None + self._fan_speed = None self._battery_level = None self._state = None self._first_update_done = False @@ -66,13 +70,11 @@ def update(self): self._clean_status = self.device.clean_status self._charge_status = self.device.charge_status + self._fan_speed = 'unknown' # TODO: implement in sucks - try: - if self.device.battery_status is not None: - self._battery_level = self.device.battery_status * 100 - except AttributeError: - # No battery_status property - pass + if (hasattr(self.device, 'battery_status') + and self.device.battery_status is not None): + self._battery_level = self.device.battery_status * 100 @property def is_on(self): @@ -80,7 +82,9 @@ def is_on(self): if self._clean_status is None: return False else: - return self._clean_status != 'stop' + return ( + self._clean_status != 'stop' + and self._charge_status != 'charging') @property def is_charging(self): @@ -103,7 +107,12 @@ def icon(self): @property def supported_features(self): """Flag vacuum cleaner robot features that are supported.""" - return SUPPORT_ECOVACS + support = SUPPORT_ECOVACS + if self.is_on: + # Fan speed can only be adjusted while cleaning + support = support | SUPPORT_FAN_SPEED + + return support def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" @@ -124,8 +133,7 @@ def battery_level(self): @property def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" - # TODO: Implement - return None + return self._fan_speed @property def fan_speed_list(self): @@ -135,7 +143,7 @@ def fan_speed_list(self): def turn_on(self, **kwargs): """Turn the vacuum on and start cleaning.""" from sucks import Clean - self.device.run(Clean(False)) + self.device.run(Clean()) def turn_off(self, **kwargs): """Turn the vacuum off stopping the cleaning and returning home.""" @@ -143,41 +151,41 @@ def turn_off(self, **kwargs): def stop(self, **kwargs): """Stop the vacuum cleaner.""" - from sucks import VacBotCommand - self.device.run(VacBotCommand('Move', {'action': 'stop'})) + from sucks import Stop + self.device.run(Stop()) def clean_spot(self, **kwargs): """Perform a spot clean-up.""" - # TODO: Implement - raise NotImplementedError() + from sucks import Spot + self.device.run(Spot()) def locate(self, **kwargs): """Locate the vacuum cleaner.""" - # TODO: Implement raise NotImplementedError() + # TODO: Needs support in sucks library + from sucks import Locate + self.device.run(Locate()) + def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" - # TODO: Implement - raise NotImplementedError() - - def start_pause(self, **kwargs): - """Start, pause or resume the cleaning task.""" - # TODO: Implement - raise NotImplementedError() + if self.is_on: + from sucks import Clean + self.device.run(Clean( + mode=self._clean_status, speed=fan_speed)) def send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" - # TODO: Implement - raise NotImplementedError() + from sucks import VacBotCommand + self.device.run(VacBotCommand(command, params)) @property def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" - # TODO: Implement data = super().state_attributes # TODO: attribute names should be consts + # TODO: Maybe don't need these attributes at all? data['clean_status'] = self._clean_status data['charge_status'] = self._charge_status From 3f9de5f306620beac3620b606f8c973cdbd6c12f Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Wed, 7 Feb 2018 16:22:12 -0800 Subject: [PATCH 04/19] Move init queries to the added_to_hass method --- homeassistant/components/vacuum/ecovacs.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index 2de63802f685ec..7951c8f23a259d 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/vacuum.neato/ """ +import asyncio import logging from homeassistant.components.vacuum import ( @@ -56,18 +57,17 @@ def __init__(self, device): self._fan_speed = None self._battery_level = None self._state = None - self._first_update_done = False _LOGGER.debug("Vacuum initialized: %s", self.name) - def update(self): - if not self._first_update_done: - from sucks import VacBotCommand - # Fire off some queries to get initial state - self.device.run(VacBotCommand('GetCleanState', {})) - self.device.run(VacBotCommand('GetChargeState', {})) - self.device.run(VacBotCommand('GetBatteryInfo', {})) - self._first_update_done = True + @asyncio.coroutine + def async_added_to_hass(self) -> None: + # Fire off some queries to get initial state + from sucks import VacBotCommand + self.device.run(VacBotCommand('GetCleanState', {})) + self.device.run(VacBotCommand('GetChargeState', {})) + self.device.run(VacBotCommand('GetBatteryInfo', {})) + def update(self): self._clean_status = self.device.clean_status self._charge_status = self.device.charge_status self._fan_speed = 'unknown' # TODO: implement in sucks From 5158f67490e7efa002647d78ffe742539ee913a3 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Mon, 26 Feb 2018 17:12:20 -0800 Subject: [PATCH 05/19] Adding support for subscribing to events from the sucks library This support does not exist in sucks yet; this commit serves as a sort of TDD approach of what such support COULD look like. --- homeassistant/components/vacuum/ecovacs.py | 85 ++++++++++++++++------ 1 file changed, 61 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index 7951c8f23a259d..2c0fae77c08477 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -28,6 +28,23 @@ ECOVACS_FAN_SPEED_LIST = ['normal', 'high'] +# These consts represent bot statuses that can come from the `sucks` library +# TODO: These should probably be exposed in the sucks library and just imported +STATUS_AUTO = 'auto' +STATUS_EDGE = 'edge' +STATUS_SPOT = 'spot' +STATUS_SINGLE_ROOM = 'single_room' +STATUS_STOP = 'stop' +STATUS_RETURNING = 'returning' +STATUS_CHARGING = 'charging' +STATUS_IDLE = 'idle' +STATUS_ERROR = 'error' + +# Any status that represents active cleaning +STATUSES_CLEANING = [STATUS_AUTO, STATUS_EDGE, STATUS_SPOT, STATUS_SINGLE_ROOM] +# Any status that represents sitting on the charger +STATUSES_CHARGING = [STATUS_CHARGING, STATUS_IDLE] + ICON = "mdi:roomba" def setup_platform(hass, config, add_devices, discovery_info=None): @@ -52,47 +69,69 @@ def __init__(self, device): # In case there is no nickname defined, use the device id self._name = '{}'.format(self.device.vacuum['did']) - self._clean_status = None - self._charge_status = None + self._status = None self._fan_speed = None self._battery_level = None - self._state = None _LOGGER.debug("Vacuum initialized: %s", self.name) @asyncio.coroutine def async_added_to_hass(self) -> None: # Fire off some queries to get initial state from sucks import VacBotCommand + self.device.statusEvents.subscribe(self.on_status) + self.device.batteryEvents.subscribe(self.on_battery) + self.device.errorEvents.subscribe(self.on_error) + + # TODO: Once sucks does internal state handling, these shouldn't be + # TODO: necessary. Perhaps this will be replaced by a single call to + # TODO: turn on state handling, or turn on each feature to track? self.device.run(VacBotCommand('GetCleanState', {})) self.device.run(VacBotCommand('GetChargeState', {})) self.device.run(VacBotCommand('GetBatteryInfo', {})) - def update(self): - self._clean_status = self.device.clean_status - self._charge_status = self.device.charge_status - self._fan_speed = 'unknown' # TODO: implement in sucks + def on_status(self, status): + """Handle the status of the robot changing.""" + self._status = status + self.schedule_update_ha_state() + + def on_battery(self, battery_level): + """Handle the battery level changing on the robot.""" + self._battery_level = battery_level * 100 + self.schedule_update_ha_state() + + def on_error(self, error): + """Handle an error event from the robot. - if (hasattr(self.device, 'battery_status') - and self.device.battery_status is not None): - self._battery_level = self.device.battery_status * 100 + This will not change the entity's state. If the error caused the state + to change, that will come through as a separate on_status event + """ + self.hass.bus.fire('ecovacs_error', { + 'entity_id': self.entity_id, + 'error': error + }) + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + """ + return False + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + if hasattr(self.device.vacuum, 'did'): + return self.device.vacuum['did'] + return None @property def is_on(self): """Return true if vacuum is currently cleaning.""" - if self._clean_status is None: - return False - else: - return ( - self._clean_status != 'stop' - and self._charge_status != 'charging') + return self._status in STATUSES_CLEANING @property def is_charging(self): """Return true if vacuum is currently charging.""" - if self._charge_status is None: - return False - else: - return self._charge_status == 'charging' + return self._status in STATUSES_CHARGING @property def name(self): @@ -172,7 +211,7 @@ def set_fan_speed(self, fan_speed, **kwargs): if self.is_on: from sucks import Clean self.device.run(Clean( - mode=self._clean_status, speed=fan_speed)) + mode=self._status, speed=fan_speed)) def send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" @@ -185,8 +224,6 @@ def state_attributes(self): data = super().state_attributes # TODO: attribute names should be consts - # TODO: Maybe don't need these attributes at all? - data['clean_status'] = self._clean_status - data['charge_status'] = self._charge_status + data['status'] = self._status return data From 17e7f43b46dc680575584f662509bbc6cdc20f9b Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Thu, 12 Jul 2018 13:46:02 -0700 Subject: [PATCH 06/19] Add OverloadUT as ecovacs code owner --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 556791b879c64e..ff402cba8befad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -87,6 +87,8 @@ homeassistant/components/*/axis.py @kane610 homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/deconz.py @kane610 +homeassistant/components/ecovacs.py @OverloadUT +homeassistant/components/*/ecovacs.py @OverloadUT homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/hive.py @Rendili @KJonline From 0085f1245d1aef54e1d98a5d38d61f34f932e383 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Fri, 13 Jul 2018 22:12:00 -0700 Subject: [PATCH 07/19] Full support for Ecovacs vacuums (Deebot) --- homeassistant/components/ecovacs.py | 31 ++++---- homeassistant/components/vacuum/ecovacs.py | 87 +++++++++++----------- 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index 91076c5585c30f..386f42de3eb598 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -10,27 +10,30 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ - CONF_DEVICES, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \ + EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['sucks==0.8.4'] +REQUIREMENTS = ['sucks==0.9.0'] _LOGGER = logging.getLogger(__name__) DOMAIN = "ecovacs" +CONF_COUNTRY = "country" +CONF_CONTINENT = "continent" + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_DEVICES, default=[]): - vol.All(cv.ensure_list, [dict]), + vol.Required(CONF_COUNTRY): cv.string, + vol.Required(CONF_CONTINENT): cv.string, }) }, extra=vol.ALLOW_EXTRA) ECOVACS_DEVICES = "ecovacs_devices" -# TODO: const or generated? -ECOVACS_API_DEVICEID = "6d6ce034ef01ae6c66b21729ffa3b23e" +ECOVACS_API_DEVICEID = "homeasst" + def setup(hass, config): """Set up the Ecovacs component.""" @@ -42,15 +45,15 @@ def setup(hass, config): from sucks import EcoVacsAPI, VacBot # Convenient hack for debugging to pipe sucks logging to the Hass logger - # TODO: Comment out before commit - import sucks - sucks.logging = _LOGGER + if _LOGGER.getEffectiveLevel() <= logging.DEBUG: + import sucks + sucks.logging = _LOGGER ecovacs_api = EcoVacsAPI(ECOVACS_API_DEVICEID, config[DOMAIN].get(CONF_USERNAME), EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), - 'us', #TODO: Make configurable - 'na') #TODO: Make configurable + config[DOMAIN].get(CONF_COUNTRY).lower(), + config[DOMAIN].get(CONF_CONTINENT).lower()) devices = ecovacs_api.devices() _LOGGER.debug("Ecobot devices: %s", devices) @@ -63,11 +66,13 @@ def setup(hass, config): ecovacs_api.resource, ecovacs_api.user_access_token, device, - 'na') #TODO: Make configurable + config[DOMAIN].get(CONF_CONTINENT).lower(), + monitor=True) hass.data[ECOVACS_DEVICES].append(vacbot) # pylint: disable=unused-argument def stop(event: object) -> None: + """Shut down open connections to Ecovacs XMPP server.""" for device in hass.data[ECOVACS_DEVICES]: _LOGGER.info("Shutting down connection to Ecovacs device %s", device.vacuum['nick']) diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index 2c0fae77c08477..f546a59bdbd0a0 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/vacuum.neato/ """ -import asyncio import logging from homeassistant.components.vacuum import ( @@ -19,17 +18,14 @@ DEPENDENCIES = ['ecovacs'] -# Note: SUPPORT_FAN_SPEED gets dynamically added to this list based on vacuum -# state in the supported_features Property getter SUPPORT_ECOVACS = ( SUPPORT_BATTERY | SUPPORT_RETURN_HOME | SUPPORT_CLEAN_SPOT | - SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | - SUPPORT_STATUS | SUPPORT_LOCATE | SUPPORT_SEND_COMMAND) + SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | SUPPORT_LOCATE | + SUPPORT_STATUS | SUPPORT_SEND_COMMAND | SUPPORT_FAN_SPEED) ECOVACS_FAN_SPEED_LIST = ['normal', 'high'] # These consts represent bot statuses that can come from the `sucks` library -# TODO: These should probably be exposed in the sucks library and just imported STATUS_AUTO = 'auto' STATUS_EDGE = 'edge' STATUS_SPOT = 'spot' @@ -45,7 +41,9 @@ # Any status that represents sitting on the charger STATUSES_CHARGING = [STATUS_CHARGING, STATUS_IDLE] -ICON = "mdi:roomba" +ATTR_ERROR = 'error' +ATTR_COMPONENT_PREFIX = 'component_' + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Ecovacs vacuums.""" @@ -69,34 +67,28 @@ def __init__(self, device): # In case there is no nickname defined, use the device id self._name = '{}'.format(self.device.vacuum['did']) - self._status = None self._fan_speed = None - self._battery_level = None + self._error = None _LOGGER.debug("Vacuum initialized: %s", self.name) - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: + """d.""" # Fire off some queries to get initial state - from sucks import VacBotCommand self.device.statusEvents.subscribe(self.on_status) self.device.batteryEvents.subscribe(self.on_battery) self.device.errorEvents.subscribe(self.on_error) - - # TODO: Once sucks does internal state handling, these shouldn't be - # TODO: necessary. Perhaps this will be replaced by a single call to - # TODO: turn on state handling, or turn on each feature to track? - self.device.run(VacBotCommand('GetCleanState', {})) - self.device.run(VacBotCommand('GetChargeState', {})) - self.device.run(VacBotCommand('GetBatteryInfo', {})) + self.device.lifespanEvents.subscribe(self.on_lifespan) def on_status(self, status): """Handle the status of the robot changing.""" - self._status = status self.schedule_update_ha_state() def on_battery(self, battery_level): """Handle the battery level changing on the robot.""" - self._battery_level = battery_level * 100 + self.schedule_update_ha_state() + + def on_lifespan(self, lifespan): + """Handle component lifespan reports from the robot.""" self.schedule_update_ha_state() def on_error(self, error): @@ -105,53 +97,55 @@ def on_error(self, error): This will not change the entity's state. If the error caused the state to change, that will come through as a separate on_status event """ + + if error == 'no_error': + self._error = None + else: + self._error = error + self.hass.bus.fire('ecovacs_error', { 'entity_id': self.entity_id, 'error': error }) + self.schedule_update_ha_state() @property def should_poll(self) -> bool: - """Return True if entity has to be polled for state. - """ + """Return True if entity has to be polled for state.""" return False @property def unique_id(self) -> str: """Return an unique ID.""" if hasattr(self.device.vacuum, 'did'): + # `did` is the Ecovacs-reported Device ID return self.device.vacuum['did'] return None @property def is_on(self): """Return true if vacuum is currently cleaning.""" - return self._status in STATUSES_CLEANING + return self.device.vacuum_status in STATUSES_CLEANING @property def is_charging(self): """Return true if vacuum is currently charging.""" - return self._status in STATUSES_CHARGING + return self.device.vacuum_status in STATUSES_CHARGING @property def name(self): """Return the name of the device.""" return self._name - @property - def icon(self): - """Return the icon to use for device.""" - return ICON - @property def supported_features(self): """Flag vacuum cleaner robot features that are supported.""" - support = SUPPORT_ECOVACS - if self.is_on: - # Fan speed can only be adjusted while cleaning - support = support | SUPPORT_FAN_SPEED + return SUPPORT_ECOVACS - return support + @property + def status(self): + """Return the status of the vacuum cleaner.""" + return self.device.vacuum_status def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" @@ -167,12 +161,15 @@ def battery_icon(self): @property def battery_level(self): """Return the battery level of the vacuum cleaner.""" - return self._battery_level + if self.device.battery_status is not None: + return self.device.battery_status * 100 + + return super().battery_level @property def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" - return self._fan_speed + return self.device.fan_speed @property def fan_speed_list(self): @@ -200,18 +197,15 @@ def clean_spot(self, **kwargs): def locate(self, **kwargs): """Locate the vacuum cleaner.""" - raise NotImplementedError() - - # TODO: Needs support in sucks library - from sucks import Locate - self.device.run(Locate()) + from sucks import PlaySound + self.device.run(PlaySound()) def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if self.is_on: from sucks import Clean self.device.run(Clean( - mode=self._status, speed=fan_speed)) + mode=self.device.clean_status, speed=fan_speed)) def send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" @@ -223,7 +217,10 @@ def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" data = super().state_attributes - # TODO: attribute names should be consts - data['status'] = self._status + data[ATTR_ERROR] = self._error + + for key, val in self.device.components.items(): + attr_name = ATTR_COMPONENT_PREFIX + key + data[attr_name] = int(val * 100) return data From 876282565ca4ba65b78e32e692955826d246c823 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Fri, 13 Jul 2018 22:49:52 -0700 Subject: [PATCH 08/19] Add requirements --- requirements_all.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index 0690539bdee039..2d7c62c84bd124 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1295,6 +1295,9 @@ statsd==3.2.1 # homeassistant.components.sensor.steam_online steamodd==4.21 +# homeassistant.components.ecovacs +sucks==0.9.0 + # homeassistant.components.camera.onvif suds-passworddigest-homeassistant==0.1.2a0.dev0 From bf1d4338c3e6dc2058fcb41b4376658ca76e055e Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Fri, 13 Jul 2018 22:58:06 -0700 Subject: [PATCH 09/19] Linting fixes --- homeassistant/components/vacuum/ecovacs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index f546a59bdbd0a0..b4c6ae0492e351 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -97,7 +97,6 @@ def on_error(self, error): This will not change the entity's state. If the error caused the state to change, that will come through as a separate on_status event """ - if error == 'no_error': self._error = None else: From ef4bae157a943e5f3b02f85ea99922d69eb3c0a5 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sat, 14 Jul 2018 21:46:37 -0700 Subject: [PATCH 10/19] Make API Device ID random on each boot --- homeassistant/components/ecovacs.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index 386f42de3eb598..ab1b3a4adfc6ae 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -5,6 +5,8 @@ """ import logging +import random +import string import voluptuous as vol @@ -32,7 +34,11 @@ }, extra=vol.ALLOW_EXTRA) ECOVACS_DEVICES = "ecovacs_devices" -ECOVACS_API_DEVICEID = "homeasst" + +# Generate a random device ID on each bootup +ECOVACS_API_DEVICEID = ''.join( + random.choices(string.ascii_uppercase + string.digits, k=8) +) def setup(hass, config): From 16de5c6014515ffe32e76cbba92bbced05db5426 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sat, 14 Jul 2018 21:47:13 -0700 Subject: [PATCH 11/19] Fix unique ID Never worked before, as it should have been looking for a key, not an attribute --- homeassistant/components/vacuum/ecovacs.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index b4c6ae0492e351..fc7f2b114b09d2 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -116,10 +116,7 @@ def should_poll(self) -> bool: @property def unique_id(self) -> str: """Return an unique ID.""" - if hasattr(self.device.vacuum, 'did'): - # `did` is the Ecovacs-reported Device ID - return self.device.vacuum['did'] - return None + return self.device.vacuum.get('did', None) @property def is_on(self): From 6c821f74437dde932d7aa99cef20cb14edaa3547 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Mon, 16 Jul 2018 14:35:06 -0700 Subject: [PATCH 12/19] Fix random string generation to work in Python 3.5 (thanks, Travis!) --- homeassistant/components/ecovacs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index ab1b3a4adfc6ae..807120609969f3 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -37,7 +37,7 @@ # Generate a random device ID on each bootup ECOVACS_API_DEVICEID = ''.join( - random.choices(string.ascii_uppercase + string.digits, k=8) + random.choice(string.ascii_uppercase + string.digits) for _ in range(8) ) From c6684ad46c289ddfe2cd3740623985a73683c08f Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 17 Jul 2018 14:53:54 -0700 Subject: [PATCH 13/19] Add new files to .coveragerc --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 73a79c2d87bf7a..292f9c03ff71f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -104,6 +104,9 @@ omit = homeassistant/components/fritzbox.py homeassistant/components/switch/fritzbox.py + homeassistant/components/ecovacs.py + homeassistant/components/*/ecovacs.py + homeassistant/components/eufy.py homeassistant/components/*/eufy.py From 1279b46eb0d71d24cb2dceb0b4fbc3e5648e6799 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Thu, 19 Jul 2018 14:48:20 -0700 Subject: [PATCH 14/19] Code review changes (Will require a sucks version bump in a coming commit; waiting for it to release) --- homeassistant/components/ecovacs.py | 20 +++------ homeassistant/components/vacuum/ecovacs.py | 49 +++++----------------- 2 files changed, 17 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index 807120609969f3..8d0f25a29ee342 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -28,8 +28,8 @@ DOMAIN: vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_COUNTRY): cv.string, - vol.Required(CONF_CONTINENT): cv.string, + vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string), + vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string), }) }, extra=vol.ALLOW_EXTRA) @@ -43,23 +43,17 @@ def setup(hass, config): """Set up the Ecovacs component.""" - _LOGGER.info("Creating new Ecovacs component") + _LOGGER.debug("Creating new Ecovacs component") - if ECOVACS_DEVICES not in hass.data: - hass.data[ECOVACS_DEVICES] = [] + hass.data[ECOVACS_DEVICES] = [] from sucks import EcoVacsAPI, VacBot - # Convenient hack for debugging to pipe sucks logging to the Hass logger - if _LOGGER.getEffectiveLevel() <= logging.DEBUG: - import sucks - sucks.logging = _LOGGER - ecovacs_api = EcoVacsAPI(ECOVACS_API_DEVICEID, config[DOMAIN].get(CONF_USERNAME), EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), - config[DOMAIN].get(CONF_COUNTRY).lower(), - config[DOMAIN].get(CONF_CONTINENT).lower()) + config[DOMAIN].get(CONF_COUNTRY), + config[DOMAIN].get(CONF_CONTINENT)) devices = ecovacs_api.devices() _LOGGER.debug("Ecobot devices: %s", devices) @@ -76,7 +70,6 @@ def setup(hass, config): monitor=True) hass.data[ECOVACS_DEVICES].append(vacbot) - # pylint: disable=unused-argument def stop(event: object) -> None: """Shut down open connections to Ecovacs XMPP server.""" for device in hass.data[ECOVACS_DEVICES]: @@ -89,7 +82,6 @@ def stop(event: object) -> None: if hass.data[ECOVACS_DEVICES]: _LOGGER.debug("Starting vacuum components") - # discovery.load_platform(hass, "sensor", DOMAIN, {}, config) discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) return True diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index fc7f2b114b09d2..3d73e9256eddcc 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -23,24 +23,6 @@ SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | SUPPORT_LOCATE | SUPPORT_STATUS | SUPPORT_SEND_COMMAND | SUPPORT_FAN_SPEED) -ECOVACS_FAN_SPEED_LIST = ['normal', 'high'] - -# These consts represent bot statuses that can come from the `sucks` library -STATUS_AUTO = 'auto' -STATUS_EDGE = 'edge' -STATUS_SPOT = 'spot' -STATUS_SINGLE_ROOM = 'single_room' -STATUS_STOP = 'stop' -STATUS_RETURNING = 'returning' -STATUS_CHARGING = 'charging' -STATUS_IDLE = 'idle' -STATUS_ERROR = 'error' - -# Any status that represents active cleaning -STATUSES_CLEANING = [STATUS_AUTO, STATUS_EDGE, STATUS_SPOT, STATUS_SINGLE_ROOM] -# Any status that represents sitting on the charger -STATUSES_CHARGING = [STATUS_CHARGING, STATUS_IDLE] - ATTR_ERROR = 'error' ATTR_COMPONENT_PREFIX = 'component_' @@ -72,24 +54,14 @@ def __init__(self, device): _LOGGER.debug("Vacuum initialized: %s", self.name) async def async_added_to_hass(self) -> None: - """d.""" - # Fire off some queries to get initial state - self.device.statusEvents.subscribe(self.on_status) - self.device.batteryEvents.subscribe(self.on_battery) + """Set up the event listeners now that hass is ready.""" + self.device.statusEvents.subscribe(lambda _: + self.schedule_update_ha_state()) + self.device.batteryEvents.subscribe(lambda _: + self.schedule_update_ha_state()) + self.device.lifespanEvents.subscribe(lambda _: + self.schedule_update_ha_state()) self.device.errorEvents.subscribe(self.on_error) - self.device.lifespanEvents.subscribe(self.on_lifespan) - - def on_status(self, status): - """Handle the status of the robot changing.""" - self.schedule_update_ha_state() - - def on_battery(self, battery_level): - """Handle the battery level changing on the robot.""" - self.schedule_update_ha_state() - - def on_lifespan(self, lifespan): - """Handle component lifespan reports from the robot.""" - self.schedule_update_ha_state() def on_error(self, error): """Handle an error event from the robot. @@ -121,12 +93,12 @@ def unique_id(self) -> str: @property def is_on(self): """Return true if vacuum is currently cleaning.""" - return self.device.vacuum_status in STATUSES_CLEANING + return self.device.is_cleaning @property def is_charging(self): """Return true if vacuum is currently charging.""" - return self.device.vacuum_status in STATUSES_CHARGING + return self.device.is_charging @property def name(self): @@ -170,7 +142,8 @@ def fan_speed(self): @property def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" - return ECOVACS_FAN_SPEED_LIST + from sucks import FAN_SPEED_NORMAL, FAN_SPEED_HIGH + return [FAN_SPEED_NORMAL, FAN_SPEED_HIGH] def turn_on(self, **kwargs): """Turn the vacuum on and start cleaning.""" From 1e33d7734b08a11bc4c4e8550cc46eb1f6b9daa9 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sat, 21 Jul 2018 08:32:39 -0700 Subject: [PATCH 15/19] Bump sucks to 0.9.1 now that it has released --- homeassistant/components/ecovacs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index 8d0f25a29ee342..2e51b048d15e43 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \ EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['sucks==0.9.0'] +REQUIREMENTS = ['sucks==0.9.1'] _LOGGER = logging.getLogger(__name__) From 854ca6ed1d4651988015103c5797d5b1e79f3f0b Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sat, 21 Jul 2018 08:59:40 -0700 Subject: [PATCH 16/19] Update requirements_all.txt as well --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 2d7c62c84bd124..bea0dc89c87641 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1296,7 +1296,7 @@ statsd==3.2.1 steamodd==4.21 # homeassistant.components.ecovacs -sucks==0.9.0 +sucks==0.9.1 # homeassistant.components.camera.onvif suds-passworddigest-homeassistant==0.1.2a0.dev0 From b95e1371883002482db423b36f3f503bf6709661 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sun, 29 Jul 2018 15:32:32 -0700 Subject: [PATCH 17/19] Bump sucks version to fix lifespan value errors --- homeassistant/components/ecovacs.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index 2e51b048d15e43..1bbc0adaa44462 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \ EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['sucks==0.9.1'] +REQUIREMENTS = ['sucks==0.9.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bea0dc89c87641..df17221813fc90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1296,7 +1296,7 @@ statsd==3.2.1 steamodd==4.21 # homeassistant.components.ecovacs -sucks==0.9.1 +sucks==0.9.2 # homeassistant.components.camera.onvif suds-passworddigest-homeassistant==0.1.2a0.dev0 From fc38f578900f69e5f08184c8327e1572014f5d0d Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Fri, 17 Aug 2018 22:49:44 -0700 Subject: [PATCH 18/19] Revert to sucks 0.9.1 and include a fix for a bug in that release Sucks is being slow to release currently, so doing this so we can get a version out the door. --- homeassistant/components/ecovacs.py | 2 +- homeassistant/components/vacuum/ecovacs.py | 6 +++++- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs.py b/homeassistant/components/ecovacs.py index 1bbc0adaa44462..2e51b048d15e43 100644 --- a/homeassistant/components/ecovacs.py +++ b/homeassistant/components/ecovacs.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \ EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['sucks==0.9.2'] +REQUIREMENTS = ['sucks==0.9.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index 3d73e9256eddcc..ec2dc308e4f4a5 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -190,6 +190,10 @@ def state_attributes(self): for key, val in self.device.components.items(): attr_name = ATTR_COMPONENT_PREFIX + key - data[attr_name] = int(val * 100) + data[attr_name] = int(val * 100 / 0.2777778) + # The above calculation includes a fix for a bug in sucks 0.9.1 + # When sucks 0.9.2+ is released, it should be changed to the + # following: + # data[attr_name] = int(val * 100) return data diff --git a/requirements_all.txt b/requirements_all.txt index df17221813fc90..bea0dc89c87641 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1296,7 +1296,7 @@ statsd==3.2.1 steamodd==4.21 # homeassistant.components.ecovacs -sucks==0.9.2 +sucks==0.9.1 # homeassistant.components.camera.onvif suds-passworddigest-homeassistant==0.1.2a0.dev0 From c9a0e2082f2a7de7d3e10c7b41af738bd1ca82d9 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sat, 18 Aug 2018 13:38:02 -0700 Subject: [PATCH 19/19] Switch state_attributes to device_state_attributes --- homeassistant/components/vacuum/ecovacs.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vacuum/ecovacs.py b/homeassistant/components/vacuum/ecovacs.py index ec2dc308e4f4a5..e0870a4886124b 100644 --- a/homeassistant/components/vacuum/ecovacs.py +++ b/homeassistant/components/vacuum/ecovacs.py @@ -182,10 +182,9 @@ def send_command(self, command, params=None, **kwargs): self.device.run(VacBotCommand(command, params)) @property - def state_attributes(self): - """Return the state attributes of the vacuum cleaner.""" - data = super().state_attributes - + def device_state_attributes(self): + """Return the device-specific state attributes of this vacuum.""" + data = {} data[ATTR_ERROR] = self._error for key, val in self.device.components.items():