Add ecovacs component (#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
This commit is contained in:
parent
1be61df9c0
commit
df6239e0fc
5 changed files with 293 additions and 0 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
87
homeassistant/components/ecovacs.py
Normal file
87
homeassistant/components/ecovacs.py
Normal file
|
@ -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
|
198
homeassistant/components/vacuum/ecovacs.py
Normal file
198
homeassistant/components/vacuum/ecovacs.py
Normal file
|
@ -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
|
|
@ -1339,6 +1339,9 @@ statsd==3.2.1
|
|||
# homeassistant.components.sensor.steam_online
|
||||
steamodd==4.21
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
sucks==0.9.1
|
||||
|
||||
# homeassistant.components.camera.onvif
|
||||
suds-passworddigest-homeassistant==0.1.2a0.dev0
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue