New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ecovacs component #15520
Add ecovacs component #15520
Changes from all commits
71597c6
da08c60
51e15d2
3f9de5f
5158f67
17e7f43
0085f12
8762825
bf1d433
ef4bae1
16de5c6
6c821f7
c6684ad
1279b46
1e33d77
854ca6e
b95e137
fc38f57
c9a0e20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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), | ||
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
Sorry, something went wrong. |
||
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 |
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I/O should be avoided in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case it doesn't block the event loop since we instantiate the entity in But I like to avoid I/O in init on a general basis anyway. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would the preferred implementation to be to connect to the device in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems a flaw in the vacuum ABC if you need to implement this yourself. Your implementation seems like how it should be? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It definitely is an issue with the ABC having no formal support for determining if the bot is charging. The reason I chose not to fix the ABC here is because the proper fix is the rearchitecture discussed here: home-assistant/architecture#29 I plan on updating this component after/alongside @cnrd makes the foundational changes |
||
"""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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason you went for configuration via configuration.yaml and not using our shiny new config entry?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You know what, I created the bulk of this component 9 months ago and when I came back to it I didn't think to convert it. I'll read up on the config entry docs and see about converting it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hit me up on Discord, would love to help you with this. Hue is a good (although maybe a bit complicated) example.