From 679d500e61bedad19452e476581fcefa6b84825e Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sat, 19 Nov 2016 09:14:40 +0100 Subject: [PATCH] Neato refactor and support for sensors (#4319) * Imporvements to neato * Review changes --- .coveragerc | 4 +- homeassistant/components/neato.py | 81 +++++++++++ homeassistant/components/sensor/neato.py | 150 +++++++++++++++++++ homeassistant/components/switch/neato.py | 176 +++++++++-------------- requirements_all.txt | 2 +- 5 files changed, 304 insertions(+), 109 deletions(-) create mode 100644 homeassistant/components/neato.py create mode 100644 homeassistant/components/sensor/neato.py diff --git a/.coveragerc b/.coveragerc index d9e74d99d78..ecc19c6382e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -101,6 +101,9 @@ omit = homeassistant/components/netatmo.py homeassistant/components/*/netatmo.py + homeassistant/components/neato.py + homeassistant/components/*/neato.py + homeassistant/components/homematic.py homeassistant/components/*/homematic.py @@ -311,7 +314,6 @@ 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/pilight.py diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py new file mode 100644 index 00000000000..0c77c3a6b5c --- /dev/null +++ b/homeassistant/components/neato.py @@ -0,0 +1,81 @@ +""" +Support for Neato botvac connected vacuum cleaners. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/neato/ +""" +import logging +from datetime import timedelta +from urllib.error import HTTPError + +import voluptuous as vol + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import discovery +from homeassistant.util import Throttle +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'] + +DOMAIN = 'neato' +NEATO_ROBOTS = 'neato_robots' +NEATO_LOGIN = 'neato_login' + +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(hass, config): + """Setup the Verisure component.""" + from pybotvac import Account + + hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account) + hub = hass.data[NEATO_LOGIN] + if not hub.login(): + _LOGGER.debug('Failed to login to Neato API') + return False + hub.update_robots() + for component in ('sensor', 'switch'): + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +class NeatoHub(object): + """A My Neato hub wrapper class.""" + + def __init__(self, hass, domain_config, neato): + """Initialize the Neato hub.""" + self.config = domain_config + self._neato = neato + self._hass = hass + + self.my_neato = neato( + domain_config[CONF_USERNAME], + domain_config[CONF_PASSWORD]) + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + + def login(self): + """Login to My Neato.""" + try: + _LOGGER.debug('Trying to connect to Neato API') + self.my_neato = self._neato(self.config[CONF_USERNAME], + self.config[CONF_PASSWORD]) + return True + except HTTPError: + _LOGGER.error("Unable to connect to Neato API") + return False + + @Throttle(timedelta(seconds=1)) + def update_robots(self): + """Update the robot states.""" + _LOGGER.debug('Running HUB.update_robots %s', + self._hass.data[NEATO_ROBOTS]) + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots diff --git a/homeassistant/components/sensor/neato.py b/homeassistant/components/sensor/neato.py new file mode 100644 index 00000000000..42bffe9c4d1 --- /dev/null +++ b/homeassistant/components/sensor/neato.py @@ -0,0 +1,150 @@ +""" +Support for Neato Connected Vaccums sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.neato/ +""" +import logging + +from homeassistant.helpers.entity import Entity +from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN + +_LOGGER = logging.getLogger(__name__) +SENSOR_TYPE_STATUS = 'status' +SENSOR_TYPE_BATTERY = 'battery' + +SENSOR_TYPES = { + SENSOR_TYPE_STATUS: ['Status'], + SENSOR_TYPE_BATTERY: ['Battery'] +} + +STATES = { + 1: 'Idle', + 2: 'Busy', + 3: 'Pause', + 4: 'Error' +} + +MODE = { + 1: 'Eco', + 2: 'Turbo' +} + +ACTION = { + 0: 'No action', + 1: 'House cleaning', + 2: 'Spot cleaning', + 3: 'Manual cleaning', + 4: 'Docking', + 5: 'User menu active', + 6: 'Cleaning cancelled', + 7: 'Updating...', + 8: 'Copying logs...', + 9: 'Calculating position...', + 10: 'IEC test' +} + +ERRORS = { + 'ui_error_brush_stuck': 'Brush stuck', + 'ui_error_brush_overloaded': 'Brush overloaded', + 'ui_error_bumper_stuck': 'Bumper stuck', + 'ui_error_dust_bin_missing': 'Dust bin missing', + 'ui_error_dust_bin_full': 'Dust bin full', + 'ui_error_dust_bin_emptied': 'Dust bin emptied', + 'ui_error_navigation_noprogress': 'Clear my path', + 'ui_error_navigation_origin_unclean': 'Clear my path', + 'ui_error_navigation_falling': 'Clear my path', + 'ui_error_picked_up': 'Picked up', + 'ui_error_stuck': 'Stuck!' + +} + +ALERTS = { + 'ui_alert_dust_bin_full': 'Please empty dust bin', + 'ui_alert_recovering_location': 'Returning to start' +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Neato sensor platform.""" + if not hass.data['neato_robots']: + return False + + dev = [] + for robot in hass.data[NEATO_ROBOTS]: + for type_name in SENSOR_TYPES: + dev.append(NeatoConnectedSensor(hass, robot, type_name)) + _LOGGER.debug('Adding sensors %s', dev) + add_devices(dev) + + +class NeatoConnectedSensor(Entity): + """Neato Connected Sensor.""" + + def __init__(self, hass, robot, sensor_type): + """Initialize the Neato Connected sensor.""" + self.type = sensor_type + self.robot = robot + self.neato = hass.data[NEATO_LOGIN] + self._robot_name = self.robot.name + ' ' + SENSOR_TYPES[self.type][0] + self._state = self.robot.state + self._battery_state = None + self._status_state = None + + def update(self): + """Update the properties of sensor.""" + _LOGGER.debug('Update of sensor') + self.neato.update_robots() + if not self._state: + return + self._state = self.robot.state + _LOGGER.debug('self._state=%s', self._state) + if self.type == SENSOR_TYPE_STATUS: + if self._state['state'] == 1: + if self._state['details']['isCharging']: + self._status_state = 'Charging' + elif (self._state['details']['isDocked'] and + not self._state['details']['isCharging']): + self._status_state = 'Docked' + else: + self._status_state = 'Stopped' + elif self._state['state'] == 2: + if ALERTS.get(self._state['error']) is None: + self._status_state = (MODE.get( + self._state['cleaning']['mode']) + ' ' + + ACTION.get(self._state['action'])) + else: + self._status_state = ALERTS.get(self._state['error']) + elif self._state['state'] == 3: + self._status_state = 'Paused' + elif self._state['state'] == 4: + self._status_state = ERRORS.get(self._state['error']) + if self.type == SENSOR_TYPE_BATTERY: + self._battery_state = self._state['details']['charge'] + + @property + def unit_of_measurement(self): + """Return unit for the sensor.""" + if self.type == SENSOR_TYPE_BATTERY: + return '%' + + @property + def available(self): + """Return True if sensor data is available.""" + if not self._state: + return False + else: + return True + + @property + def state(self): + """Return the sensor state.""" + if self.type == SENSOR_TYPE_STATUS: + return self._status_state + if self.type == SENSOR_TYPE_BATTERY: + return self._battery_state + + @property + def name(self): + """Return the name of the sensor.""" + return self._robot_name diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index c5ff4bae861..e7eedee5a0b 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -1,148 +1,110 @@ """ -Support for Neato Connected Vaccums. +Support for Neato Connected Vaccums switches. 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.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity -import homeassistant.helpers.config_validation as cv +from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN _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_TYPE_CLEAN = 'clean' +SWITCH_TYPE_SCHEDULE = 'scedule' SWITCH_TYPES = { - 'clean': ['Clean'] + SWITCH_TYPE_CLEAN: ['Clean'], + SWITCH_TYPE_SCHEDULE: ['Schedule'] } -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") + """Setup the Neato switches.""" + if not hass.data[NEATO_ROBOTS]: return False dev = [] - for robot in auth.robots: + for robot in hass.data[NEATO_ROBOTS]: for type_name in SWITCH_TYPES: - dev.append(NeatoConnectedSwitch(robot, type_name)) + dev.append(NeatoConnectedSwitch(hass, robot, type_name)) + _LOGGER.debug('Adding switches %s', dev) add_devices(dev) class NeatoConnectedSwitch(ToggleEntity): - """Neato Connected Switch (clean).""" + """Neato Connected Switches.""" - def __init__(self, robot, switch_type): - """Initialize the Neato Connected switch.""" + def __init__(self, hass, robot, switch_type): + """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.lock = False - self.last_lock_time = None - self.graceful_state = False - self._state = None + self.neato = hass.data[NEATO_LOGIN] + self._robot_name = self.robot.name + ' ' + SWITCH_TYPES[self.type][0] + self._state = self.robot.state + self._schedule_state = None + self._clean_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.""" + def update(self): + """Update the states of Neato switches.""" + _LOGGER.debug('Running switch update') + self.neato.update_robots() 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 + return + _LOGGER.debug('self._state=%s', self._state) + if self.type == SWITCH_TYPE_CLEAN: + if (self.robot.state['action'] == 1 and + self.robot.state['state'] == 2): + self._clean_state = STATE_ON + else: + self._clean_state = STATE_OFF + if self.type == SWITCH_TYPE_SCHEDULE: + _LOGGER.debug('self._state=%s', self._state) + if self.robot.schedule_enabled: + self._schedule_state = STATE_ON + else: + self._schedule_state = STATE_OFF @property def name(self): - """Return the name of the sensor.""" - return self.robot.name + ' ' + SWITCH_TYPES[self.type][0] + """Return the name of the switch.""" + return self._robot_name + + @property + def available(self): + """Return True if entity is available.""" + if not self._state: + return False + else: + return True @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 + """Return true if switch is on.""" + if self.type == SWITCH_TYPE_CLEAN: + if self._clean_state == STATE_ON: + return True + return False + elif self.type == SWITCH_TYPE_SCHEDULE: + if self._schedule_state == STATE_ON: + return True + return False def turn_on(self, **kwargs): - """Turn the device on.""" - self.set_graceful_lock(True) - self.robot.start_cleaning() + """Turn the switch on.""" + if self.type == SWITCH_TYPE_CLEAN: + self.robot.start_cleaning() + elif self.type == SWITCH_TYPE_SCHEDULE: + self.robot.enable_schedule() 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 + """Turn the switch off.""" + if self.type == SWITCH_TYPE_CLEAN: + self.robot.pause_cleaning() + time.sleep(1) + self.robot.send_to_base() + elif self.type == SWITCH_TYPE_SCHEDULE: + self.robot.disable_schedule() diff --git a/requirements_all.txt b/requirements_all.txt index 8fe00b301c5..f8a5d4dac24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9. # homeassistant.components.netatmo https://github.com/jabesq/netatmo-api-python/archive/v0.7.0.zip#lnetatmo==0.7.0 -# homeassistant.components.switch.neato +# homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.1.zip#pybotvac==0.0.1 # homeassistant.components.sensor.sabnzbd