forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ecovacs component (home-assistant#15520)
* Ecovacs Deebot vacuums * All core features implemented Getting fan speed and locating the vac are still unsupported until sucks adds support * Move init queries to the added_to_hass method * 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. * Add OverloadUT as ecovacs code owner * Full support for Ecovacs vacuums (Deebot) * Add requirements * Linting fixes * Make API Device ID random on each boot * Fix unique ID Never worked before, as it should have been looking for a key, not an attribute * Fix random string generation to work in Python 3.5 (thanks, Travis!) * Add new files to .coveragerc * Code review changes (Will require a sucks version bump in a coming commit; waiting for it to release) * Bump sucks to 0.9.1 now that it has released * Update requirements_all.txt as well * Bump sucks version to fix lifespan value errors * 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. * Switch state_attributes to device_state_attributes
- Loading branch information
1 parent
1be61df
commit df6239e
Showing
5 changed files
with
293 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
"""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 random | ||
import string | ||
|
||
import voluptuous as vol | ||
|
||
import homeassistant.helpers.config_validation as cv | ||
from homeassistant.helpers import discovery | ||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \ | ||
EVENT_HOMEASSISTANT_STOP | ||
|
||
REQUIREMENTS = ['sucks==0.9.1'] | ||
|
||
_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.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string), | ||
vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string), | ||
}) | ||
}, extra=vol.ALLOW_EXTRA) | ||
|
||
ECOVACS_DEVICES = "ecovacs_devices" | ||
|
||
# Generate a random device ID on each bootup | ||
ECOVACS_API_DEVICEID = ''.join( | ||
random.choice(string.ascii_uppercase + string.digits) for _ in range(8) | ||
) | ||
|
||
|
||
def setup(hass, config): | ||
"""Set up the Ecovacs component.""" | ||
_LOGGER.debug("Creating new Ecovacs component") | ||
|
||
hass.data[ECOVACS_DEVICES] = [] | ||
|
||
from sucks import EcoVacsAPI, VacBot | ||
|
||
ecovacs_api = EcoVacsAPI(ECOVACS_API_DEVICEID, | ||
config[DOMAIN].get(CONF_USERNAME), | ||
EcoVacsAPI.md5(config[DOMAIN].get(CONF_PASSWORD)), | ||
config[DOMAIN].get(CONF_COUNTRY), | ||
config[DOMAIN].get(CONF_CONTINENT)) | ||
|
||
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, | ||
config[DOMAIN].get(CONF_CONTINENT).lower(), | ||
monitor=True) | ||
hass.data[ECOVACS_DEVICES].append(vacbot) | ||
|
||
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']) | ||
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, "vacuum", DOMAIN, {}, config) | ||
|
||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
""" | ||
Support for Ecovacs Ecovacs Vaccums. | ||
For more details about this platform, please refer to the documentation at | ||
https://home-assistant.io/components/vacuum.neato/ | ||
""" | ||
import logging | ||
|
||
from homeassistant.components.vacuum import ( | ||
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 | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
DEPENDENCIES = ['ecovacs'] | ||
|
||
SUPPORT_ECOVACS = ( | ||
SUPPORT_BATTERY | SUPPORT_RETURN_HOME | SUPPORT_CLEAN_SPOT | | ||
SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | SUPPORT_LOCATE | | ||
SUPPORT_STATUS | SUPPORT_SEND_COMMAND | SUPPORT_FAN_SPEED) | ||
|
||
ATTR_ERROR = 'error' | ||
ATTR_COMPONENT_PREFIX = 'component_' | ||
|
||
|
||
def setup_platform(hass, config, add_devices, discovery_info=None): | ||
"""Set up the Ecovacs vacuums.""" | ||
vacuums = [] | ||
for device in hass.data[ECOVACS_DEVICES]: | ||
vacuums.append(EcovacsVacuum(device)) | ||
_LOGGER.debug("Adding Ecovacs Vacuums to Hass: %s", vacuums) | ||
add_devices(vacuums, True) | ||
|
||
|
||
class EcovacsVacuum(VacuumDevice): | ||
"""Ecovacs Vacuums such as Deebot.""" | ||
|
||
def __init__(self, device): | ||
"""Initialize the Ecovacs Vacuum.""" | ||
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._fan_speed = None | ||
self._error = None | ||
_LOGGER.debug("Vacuum initialized: %s", self.name) | ||
|
||
async def async_added_to_hass(self) -> None: | ||
"""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) | ||
|
||
def on_error(self, error): | ||
"""Handle an error event from the robot. | ||
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 False | ||
|
||
@property | ||
def unique_id(self) -> str: | ||
"""Return an unique ID.""" | ||
return self.device.vacuum.get('did', None) | ||
|
||
@property | ||
def is_on(self): | ||
"""Return true if vacuum is currently cleaning.""" | ||
return self.device.is_cleaning | ||
|
||
@property | ||
def is_charging(self): | ||
"""Return true if vacuum is currently charging.""" | ||
return self.device.is_charging | ||
|
||
@property | ||
def name(self): | ||
"""Return the name of the device.""" | ||
return self._name | ||
|
||
@property | ||
def supported_features(self): | ||
"""Flag vacuum cleaner robot features that are supported.""" | ||
return SUPPORT_ECOVACS | ||
|
||
@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.""" | ||
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.""" | ||
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.device.fan_speed | ||
|
||
@property | ||
def fan_speed_list(self): | ||
"""Get the list of available fan speed steps of the vacuum cleaner.""" | ||
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 sucks import Clean | ||
self.device.run(Clean()) | ||
|
||
def turn_off(self, **kwargs): | ||
"""Turn the vacuum off stopping the cleaning and returning home.""" | ||
self.return_to_base() | ||
|
||
def stop(self, **kwargs): | ||
"""Stop the vacuum cleaner.""" | ||
from sucks import Stop | ||
self.device.run(Stop()) | ||
|
||
def clean_spot(self, **kwargs): | ||
"""Perform a spot clean-up.""" | ||
from sucks import Spot | ||
self.device.run(Spot()) | ||
|
||
def locate(self, **kwargs): | ||
"""Locate the vacuum cleaner.""" | ||
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.device.clean_status, speed=fan_speed)) | ||
|
||
def send_command(self, command, params=None, **kwargs): | ||
"""Send a command to a vacuum cleaner.""" | ||
from sucks import VacBotCommand | ||
self.device.run(VacBotCommand(command, params)) | ||
|
||
@property | ||
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(): | ||
attr_name = ATTR_COMPONENT_PREFIX + key | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters