From 1347c3191fed0636e92bc9bbc357d73761be89cb Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Fri, 3 Nov 2017 14:25:26 +0100 Subject: [PATCH] Refactor Neato botvac components as a vacuum (#9946) * Refactor Neato botvac components as a vacuum A switch is still use to enable/disable the schedule Signed-off-by: Hugo D. (jabesq) * CI Hound fixes * Fix lint errors Signed-off-by: Hugo D. (jabesq) * [Neato vacumm] Add sensor attributes to vacuum Signed-off-by: Hugo D. (jabesq) * Remove line breaks and fix docstring * PR fixes --- homeassistant/components/neato.py | 2 +- homeassistant/components/sensor/neato.py | 174 ------------------ homeassistant/components/switch/neato.py | 28 +-- homeassistant/components/vacuum/neato.py | 214 +++++++++++++++++++++++ 4 files changed, 219 insertions(+), 199 deletions(-) delete mode 100644 homeassistant/components/sensor/neato.py create mode 100644 homeassistant/components/vacuum/neato.py diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index 2401bc6604f..e10878833e4 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -90,7 +90,7 @@ def setup(hass, config): _LOGGER.debug("Failed to login to Neato API") return False hub.update_robots() - for component in ('camera', 'sensor', 'switch'): + for component in ('camera', 'vacuum', 'switch'): discovery.load_platform(hass, component, DOMAIN, {}, config) return True diff --git a/homeassistant/components/sensor/neato.py b/homeassistant/components/sensor/neato.py deleted file mode 100644 index 5179816eb35..00000000000 --- a/homeassistant/components/sensor/neato.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -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 -import requests -from homeassistant.helpers.entity import Entity -from homeassistant.components.neato import ( - NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['neato'] - -SENSOR_TYPE_STATUS = 'status' -SENSOR_TYPE_BATTERY = 'battery' - -SENSOR_TYPES = { - SENSOR_TYPE_STATUS: ['Status'], - SENSOR_TYPE_BATTERY: ['Battery'] -} - -ATTR_CLEAN_START = 'clean_start' -ATTR_CLEAN_STOP = 'clean_stop' -ATTR_CLEAN_AREA = 'clean_area' -ATTR_CLEAN_BATTERY_START = 'battery_level_at_clean_start' -ATTR_CLEAN_BATTERY_END = 'battery_level_at_clean_end' -ATTR_CLEAN_SUSP_COUNT = 'clean_suspension_count' -ATTR_CLEAN_SUSP_TIME = 'clean_suspension_time' - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Neato sensor platform.""" - 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._status_state = None - try: - self._state = self.robot.state - except (requests.exceptions.ConnectionError, - requests.exceptions.HTTPError) as ex: - self._state = None - _LOGGER.warning("Neato connection error: %s", ex) - self._mapdata = hass.data[NEATO_MAP_DATA] - self.clean_time_start = None - self.clean_time_stop = None - self.clean_area = None - self.clean_battery_start = None - self.clean_battery_end = None - self.clean_suspension_charge_count = None - self.clean_suspension_time = None - self._battery_state = None - - def update(self): - """Update the properties of sensor.""" - _LOGGER.debug('Update of sensor') - self.neato.update_robots() - self._mapdata = self.hass.data[NEATO_MAP_DATA] - try: - self._state = self.robot.state - except (requests.exceptions.ConnectionError, - requests.exceptions.HTTPError) as ex: - self._state = None - self._status_state = 'Offline' - _LOGGER.warning("Neato connection error: %s", ex) - return - if not self._state: - return - _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'] - if not self._mapdata.get(self.robot.serial, {}).get('maps', []): - return - self.clean_time_start = ( - (self._mapdata[self.robot.serial]['maps'][0]['start_at'] - .strip('Z')) - .replace('T', ' ')) - self.clean_time_stop = ( - (self._mapdata[self.robot.serial]['maps'][0]['end_at'].strip('Z')) - .replace('T', ' ')) - self.clean_area = ( - self._mapdata[self.robot.serial]['maps'][0]['cleaned_area']) - self.clean_suspension_charge_count = ( - self._mapdata[self.robot.serial]['maps'][0] - ['suspended_cleaning_charging_count']) - self.clean_suspension_time = ( - self._mapdata[self.robot.serial]['maps'][0] - ['time_in_suspended_cleaning']) - self.clean_battery_start = ( - self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_start']) - self.clean_battery_end = ( - self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_end']) - - @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.""" - return self._state - - @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 - - @property - def device_state_attributes(self): - """Return the device specific attributes.""" - data = {} - if self.type is SENSOR_TYPE_STATUS: - if self.clean_time_start: - data[ATTR_CLEAN_START] = self.clean_time_start - if self.clean_time_stop: - data[ATTR_CLEAN_STOP] = self.clean_time_stop - if self.clean_area: - data[ATTR_CLEAN_AREA] = self.clean_area - if self.clean_suspension_charge_count: - data[ATTR_CLEAN_SUSP_COUNT] = ( - self.clean_suspension_charge_count) - if self.clean_suspension_time: - data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time - if self.clean_battery_start: - data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start - if self.clean_battery_end: - data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end - return data diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index f29dc31eaf0..62bc5f99d01 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -14,11 +14,9 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] -SWITCH_TYPE_CLEAN = 'clean' -SWITCH_TYPE_SCHEDULE = 'scedule' +SWITCH_TYPE_SCHEDULE = 'schedule' SWITCH_TYPES = { - SWITCH_TYPE_CLEAN: ['Clean'], SWITCH_TYPE_SCHEDULE: ['Schedule'] } @@ -64,15 +62,6 @@ class NeatoConnectedSwitch(ToggleEntity): self._state = None return _LOGGER.debug('self._state=%s', self._state) - if self.type == SWITCH_TYPE_CLEAN: - if (self.robot.state['action'] == 1 or - self.robot.state['action'] == 2 or - self.robot.state['action'] == 3 and - self.robot.state['state'] == 2): - self._clean_state = STATE_ON - else: - self._clean_state = STATE_OFF - _LOGGER.debug("Clean state: %s", self._clean_state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) if self.robot.schedule_enabled: @@ -94,26 +83,17 @@ class NeatoConnectedSwitch(ToggleEntity): @property def is_on(self): """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.type == SWITCH_TYPE_SCHEDULE: if self._schedule_state == STATE_ON: return True return False def turn_on(self, **kwargs): """Turn the switch on.""" - if self.type == SWITCH_TYPE_CLEAN: - self.robot.start_cleaning() - elif self.type == SWITCH_TYPE_SCHEDULE: + if self.type == SWITCH_TYPE_SCHEDULE: self.robot.enable_schedule() def turn_off(self, **kwargs): """Turn the switch off.""" - if self.type == SWITCH_TYPE_CLEAN: - self.robot.pause_cleaning() - self.robot.send_to_base() - elif self.type == SWITCH_TYPE_SCHEDULE: + if self.type == SWITCH_TYPE_SCHEDULE: self.robot.disable_schedule() diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py new file mode 100644 index 00000000000..e1c4a5952af --- /dev/null +++ b/homeassistant/components/vacuum/neato.py @@ -0,0 +1,214 @@ +""" +Support for Neato Connected Vaccums. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/vacuum.neato/ +""" +import logging + +import requests + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.components.vacuum import ( + VacuumDevice, SUPPORT_BATTERY, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, + SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON) +from homeassistant.components.neato import ( + NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['neato'] + +SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ + SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ + SUPPORT_STATUS | SUPPORT_MAP + +ICON = "mdi:roomba" + +ATTR_CLEAN_START = 'clean_start' +ATTR_CLEAN_STOP = 'clean_stop' +ATTR_CLEAN_AREA = 'clean_area' +ATTR_CLEAN_BATTERY_START = 'battery_level_at_clean_start' +ATTR_CLEAN_BATTERY_END = 'battery_level_at_clean_end' +ATTR_CLEAN_SUSP_COUNT = 'clean_suspension_count' +ATTR_CLEAN_SUSP_TIME = 'clean_suspension_time' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Neato vacuum.""" + dev = [] + for robot in hass.data[NEATO_ROBOTS]: + dev.append(NeatoConnectedVacuum(hass, robot)) + _LOGGER.debug("Adding vacuums %s", dev) + add_devices(dev, True) + + +class NeatoConnectedVacuum(VacuumDevice): + """Neato Connected Vacuums.""" + + def __init__(self, hass, robot): + """Initialize the Neato Connected Vacuums.""" + self.robot = robot + self.neato = hass.data[NEATO_LOGIN] + self._name = '{}'.format(self.robot.name) + self._status_state = None + self._clean_state = None + self._state = None + self._mapdata = hass.data[NEATO_MAP_DATA] + self.clean_time_start = None + self.clean_time_stop = None + self.clean_area = None + self.clean_battery_start = None + self.clean_battery_end = None + self.clean_suspension_charge_count = None + self.clean_suspension_time = None + + def update(self): + """Update the states of Neato Vacuums.""" + _LOGGER.debug("Running Vacuums update") + self.neato.update_robots() + try: + self._state = self.robot.state + except (requests.exceptions.ConnectionError, + requests.exceptions.HTTPError) as ex: + _LOGGER.warning("Neato connection error: %s", ex) + self._state = None + return + _LOGGER.debug('self._state=%s', self._state) + 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.robot.state['action'] == 1 or + self.robot.state['action'] == 2 or + self.robot.state['action'] == 3 and + self.robot.state['state'] == 2): + self._clean_state = STATE_ON + else: + self._clean_state = STATE_OFF + + if not self._mapdata.get(self.robot.serial, {}).get('maps', []): + return + self.clean_time_start = ( + (self._mapdata[self.robot.serial]['maps'][0]['start_at'] + .strip('Z')) + .replace('T', ' ')) + self.clean_time_stop = ( + (self._mapdata[self.robot.serial]['maps'][0]['end_at'].strip('Z')) + .replace('T', ' ')) + self.clean_area = ( + self._mapdata[self.robot.serial]['maps'][0]['cleaned_area']) + self.clean_suspension_charge_count = ( + self._mapdata[self.robot.serial]['maps'][0] + ['suspended_cleaning_charging_count']) + self.clean_suspension_time = ( + self._mapdata[self.robot.serial]['maps'][0] + ['time_in_suspended_cleaning']) + self.clean_battery_start = ( + self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_start']) + self.clean_battery_end = ( + self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_end']) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def icon(self): + """Return the icon to use for device.""" + return ICON + + @property + def supported_features(self): + """Flag vacuum cleaner robot features that are supported.""" + return SUPPORT_NEATO + + @property + def battery_level(self): + """Return the battery level of the vacuum cleaner.""" + return self._state['details']['charge'] + + @property + def status(self): + """Return the status of the vacuum cleaner.""" + return self._status_state + + @property + def state_attributes(self): + """Return the state attributes of the vacuum cleaner.""" + data = {} + + if self.status is not None: + data[ATTR_STATUS] = self.status + + if self.battery_level is not None: + data[ATTR_BATTERY_LEVEL] = self.battery_level + data[ATTR_BATTERY_ICON] = self.battery_icon + + if self.clean_time_start is not None: + data[ATTR_CLEAN_START] = self.clean_time_start + if self.clean_time_stop is not None: + data[ATTR_CLEAN_STOP] = self.clean_time_stop + if self.clean_area is not None: + data[ATTR_CLEAN_AREA] = self.clean_area + if self.clean_suspension_charge_count is not None: + data[ATTR_CLEAN_SUSP_COUNT] = ( + self.clean_suspension_charge_count) + if self.clean_suspension_time is not None: + data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time + if self.clean_battery_start is not None: + data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start + if self.clean_battery_end is not None: + data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end + + return data + + def turn_on(self, **kwargs): + """Turn the vacuum on and start cleaning.""" + self.robot.start_cleaning() + + @property + def is_on(self): + """Return true if switch is on.""" + return self._clean_state == STATE_ON + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self.robot.pause_cleaning() + self.robot.send_to_base() + + def return_to_base(self, **kwargs): + """Set the vacuum cleaner to return to the dock.""" + self.robot.send_to_base() + + def stop(self, **kwargs): + """Stop the vacuum cleaner.""" + self.robot.stop_cleaning() + + def start_pause(self, **kwargs): + """Start, pause or resume the cleaning task.""" + if self._state['state'] == 1: + self.robot.start_cleaning() + elif self._state['state'] == 2 and\ + ALERTS.get(self._state['error']) is None: + self.robot.pause_cleaning() + if self._state['state'] == 3: + self.robot.resume_cleaning()