From cb475070029fba78b0e2e14fbd210df1d60b629f Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Sat, 22 Oct 2016 06:14:45 +0200 Subject: [PATCH] Add support for Neato Connected robot as a switch (#3935) * Add support for Neato Connected robot as a switch Signed-off-by: Hugo D. (jabesq) * Add checklist items Signed-off-by: Hugo D. (jabesq) * Add missing docstring Signed-off-by: Hugo D. (jabesq) * [Neato] Add update function to retrieve robot state * Add docstring for update function + catch exception when retrieving state * Change type of HTTPError when updating state * Fix pylint errors --- .coveragerc | 1 + homeassistant/components/switch/neato.py | 148 +++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 152 insertions(+) create mode 100644 homeassistant/components/switch/neato.py diff --git a/.coveragerc b/.coveragerc index a57accaa188..303a7907329 100644 --- a/.coveragerc +++ b/.coveragerc @@ -292,6 +292,7 @@ omit = homeassistant/components/switch/edimax.py homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/mystrom.py + homeassistant/components/switch/neato.py homeassistant/components/switch/netio.py homeassistant/components/switch/orvibo.py homeassistant/components/switch/pulseaudio_loopback.py diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py new file mode 100644 index 00000000000..c5ff4bae861 --- /dev/null +++ b/homeassistant/components/switch/neato.py @@ -0,0 +1,148 @@ +""" +Support for Neato Connected Vaccums. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.neato/ +""" +import time +import logging +from datetime import timedelta +from urllib.error import HTTPError +from requests.exceptions import HTTPError as req_HTTPError + +import voluptuous as vol + +from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, STATE_OFF, + STATE_ON, STATE_UNAVAILABLE) +from homeassistant.helpers.entity import ToggleEntity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.1.zip' + '#pybotvac==0.0.1'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) + +MIN_TIME_TO_WAIT = timedelta(seconds=10) +MIN_TIME_TO_LOCK_UPDATE = 10 + +SWITCH_TYPES = { + 'clean': ['Clean'] +} + +DOMAIN = 'neato' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Neato platform.""" + from pybotvac import Account + + try: + auth = Account(config[CONF_USERNAME], config[CONF_PASSWORD]) + except HTTPError: + _LOGGER.error("Unable to connect to Neato API") + return False + + dev = [] + for robot in auth.robots: + for type_name in SWITCH_TYPES: + dev.append(NeatoConnectedSwitch(robot, type_name)) + add_devices(dev) + + +class NeatoConnectedSwitch(ToggleEntity): + """Neato Connected Switch (clean).""" + + def __init__(self, robot, switch_type): + """Initialize the Neato Connected switch.""" + self.type = switch_type + self.robot = robot + self.lock = False + self.last_lock_time = None + self.graceful_state = False + self._state = None + + def lock_update(self): + """Lock the update since Neato clean takes some time to start.""" + if self.is_update_locked(): + return + self.lock = True + self.last_lock_time = time.time() + + def reset_update_lock(self): + """Reset the update lock.""" + self.lock = False + self.last_lock_time = None + + def set_graceful_lock(self, state): + """Set the graceful state.""" + self.graceful_state = state + self.reset_update_lock() + self.lock_update() + + def is_update_locked(self): + """Check if the update method is locked.""" + if self.last_lock_time is None: + return False + + if time.time() - self.last_lock_time >= MIN_TIME_TO_LOCK_UPDATE: + self.last_lock_time = None + return False + + return True + + @property + def state(self): + """Return the state.""" + if not self._state: + return STATE_UNAVAILABLE + if not self._state['availableCommands']['start'] and \ + not self._state['availableCommands']['stop'] and \ + not self._state['availableCommands']['pause'] and \ + not self._state['availableCommands']['resume'] and \ + not self._state['availableCommands']['goToBase']: + return STATE_UNAVAILABLE + return STATE_ON if self.is_on else STATE_OFF + + @property + def name(self): + """Return the name of the sensor.""" + return self.robot.name + ' ' + SWITCH_TYPES[self.type][0] + + @property + def is_on(self): + """Return true if device is on.""" + if self.is_update_locked(): + return self.graceful_state + if self._state['action'] == 1 and self._state['state'] == 2: + return True + return False + + def turn_on(self, **kwargs): + """Turn the device on.""" + self.set_graceful_lock(True) + self.robot.start_cleaning() + + def turn_off(self, **kwargs): + """Turn the device off (Return Robot to base).""" + self.robot.pause_cleaning() + time.sleep(1) + self.robot.send_to_base() + + def update(self): + """Refresh Robot state from Neato API.""" + try: + self._state = self.robot.state + except req_HTTPError: + _LOGGER.error("Unable to retrieve to Robot State.") + self._state = None + return False diff --git a/requirements_all.txt b/requirements_all.txt index 0e933faae4f..00953ca1e46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -189,6 +189,9 @@ https://github.com/gadgetreactor/pyHS100/archive/ef85f939fd5b07064a0f34dfa673fa7 # homeassistant.components.netatmo https://github.com/jabesq/netatmo-api-python/archive/v0.6.0.zip#lnetatmo==0.6.0 +# homeassistant.components.switch.neato +https://github.com/jabesq/pybotvac/archive/v0.0.1.zip#pybotvac==0.0.1 + # homeassistant.components.sensor.sabnzbd https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1