Add support fo map data from Neato (#6939)
* Responsiveness * Delay was not needed as commands does not return until done. * Add support for cleaning maps and cleaning data * Hound * Docstring * Update requirements * Review changes * External lib now returns the raw data. * debug * Sensor did not refresh * Error handling * Issue warning on connection error * Update requirements * Review changes
This commit is contained in:
parent
01c7616147
commit
38ad5714cd
5 changed files with 164 additions and 22 deletions
65
homeassistant/components/camera/neato.py
Normal file
65
homeassistant/components/camera/neato.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
"""
|
||||||
|
Camera that loads a picture from a local file.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/camera.neato/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from homeassistant.components.camera import Camera
|
||||||
|
from homeassistant.components.neato import (
|
||||||
|
NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN)
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['neato']
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Camera."""
|
||||||
|
dev = []
|
||||||
|
for robot in hass.data[NEATO_ROBOTS]:
|
||||||
|
if 'maps' in robot.traits:
|
||||||
|
dev.append(NeatoCleaningMap(hass, robot))
|
||||||
|
_LOGGER.debug('Adding robots for cleaning maps %s', dev)
|
||||||
|
add_devices(dev, True)
|
||||||
|
|
||||||
|
|
||||||
|
class NeatoCleaningMap(Camera):
|
||||||
|
"""Neato cleaning map for last clean."""
|
||||||
|
|
||||||
|
def __init__(self, hass, robot):
|
||||||
|
"""Initialize Neato cleaning map."""
|
||||||
|
super().__init__()
|
||||||
|
self.robot = robot
|
||||||
|
self._robot_name = self.robot.name + ' Cleaning Map'
|
||||||
|
self._robot_serial = self.robot.serial
|
||||||
|
self.neato = hass.data[NEATO_LOGIN]
|
||||||
|
self._image_url = None
|
||||||
|
self._image = None
|
||||||
|
|
||||||
|
def camera_image(self):
|
||||||
|
"""Return image response."""
|
||||||
|
self.update()
|
||||||
|
return self._image
|
||||||
|
|
||||||
|
@Throttle(timedelta(seconds=10))
|
||||||
|
def update(self):
|
||||||
|
"""Check the contents of the map list."""
|
||||||
|
self.neato.update_robots()
|
||||||
|
image_url = None
|
||||||
|
map_data = self.hass.data[NEATO_MAP_DATA]
|
||||||
|
image_url = map_data[self._robot_serial]['maps'][0]['url']
|
||||||
|
if image_url == self._image_url:
|
||||||
|
_LOGGER.debug('The map image_url is the same as old')
|
||||||
|
return
|
||||||
|
image = self.neato.download_map(image_url)
|
||||||
|
self._image = image.read()
|
||||||
|
self._image_url = image_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of this camera."""
|
||||||
|
return self._robot_name
|
|
@ -17,12 +17,13 @@ import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.1.zip'
|
REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.3.zip'
|
||||||
'#pybotvac==0.0.1']
|
'#pybotvac==0.0.3']
|
||||||
|
|
||||||
DOMAIN = 'neato'
|
DOMAIN = 'neato'
|
||||||
NEATO_ROBOTS = 'neato_robots'
|
NEATO_ROBOTS = 'neato_robots'
|
||||||
NEATO_LOGIN = 'neato_login'
|
NEATO_LOGIN = 'neato_login'
|
||||||
|
NEATO_MAP_DATA = 'neato_map_data'
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
|
@ -89,7 +90,7 @@ def setup(hass, config):
|
||||||
_LOGGER.debug('Failed to login to Neato API')
|
_LOGGER.debug('Failed to login to Neato API')
|
||||||
return False
|
return False
|
||||||
hub.update_robots()
|
hub.update_robots()
|
||||||
for component in ('sensor', 'switch'):
|
for component in ('camera', 'sensor', 'switch'):
|
||||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -108,6 +109,7 @@ class NeatoHub(object):
|
||||||
domain_config[CONF_USERNAME],
|
domain_config[CONF_USERNAME],
|
||||||
domain_config[CONF_PASSWORD])
|
domain_config[CONF_PASSWORD])
|
||||||
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
||||||
|
self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
"""Login to My Neato."""
|
"""Login to My Neato."""
|
||||||
|
@ -126,3 +128,9 @@ class NeatoHub(object):
|
||||||
_LOGGER.debug('Running HUB.update_robots %s',
|
_LOGGER.debug('Running HUB.update_robots %s',
|
||||||
self._hass.data[NEATO_ROBOTS])
|
self._hass.data[NEATO_ROBOTS])
|
||||||
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
||||||
|
self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps
|
||||||
|
|
||||||
|
def download_map(self, url):
|
||||||
|
"""Download a new map image."""
|
||||||
|
map_image_data = self.my_neato.get_map_image(url)
|
||||||
|
return map_image_data
|
||||||
|
|
|
@ -8,9 +8,12 @@ import logging
|
||||||
import requests
|
import requests
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.components.neato import (
|
from homeassistant.components.neato import (
|
||||||
NEATO_ROBOTS, NEATO_LOGIN, ACTION, ERRORS, MODE, ALERTS)
|
NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['neato']
|
||||||
|
|
||||||
SENSOR_TYPE_STATUS = 'status'
|
SENSOR_TYPE_STATUS = 'status'
|
||||||
SENSOR_TYPE_BATTERY = 'battery'
|
SENSOR_TYPE_BATTERY = 'battery'
|
||||||
|
|
||||||
|
@ -19,12 +22,17 @@ SENSOR_TYPES = {
|
||||||
SENSOR_TYPE_BATTERY: ['Battery']
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Neato sensor platform."""
|
"""Setup the Neato sensor platform."""
|
||||||
if not hass.data['neato_robots']:
|
|
||||||
return False
|
|
||||||
|
|
||||||
dev = []
|
dev = []
|
||||||
for robot in hass.data[NEATO_ROBOTS]:
|
for robot in hass.data[NEATO_ROBOTS]:
|
||||||
for type_name in SENSOR_TYPES:
|
for type_name in SENSOR_TYPES:
|
||||||
|
@ -42,22 +50,37 @@ class NeatoConnectedSensor(Entity):
|
||||||
self.robot = robot
|
self.robot = robot
|
||||||
self.neato = hass.data[NEATO_LOGIN]
|
self.neato = hass.data[NEATO_LOGIN]
|
||||||
self._robot_name = self.robot.name + ' ' + SENSOR_TYPES[self.type][0]
|
self._robot_name = self.robot.name + ' ' + SENSOR_TYPES[self.type][0]
|
||||||
self._state = self.robot.state
|
|
||||||
self._battery_state = None
|
|
||||||
self._status_state = None
|
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):
|
def update(self):
|
||||||
"""Update the properties of sensor."""
|
"""Update the properties of sensor."""
|
||||||
_LOGGER.debug('Update of sensor')
|
_LOGGER.debug('Update of sensor')
|
||||||
self.neato.update_robots()
|
self.neato.update_robots()
|
||||||
if not self._state:
|
self._mapdata = self.hass.data[NEATO_MAP_DATA]
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
self._state = self.robot.state
|
self._state = self.robot.state
|
||||||
except requests.exceptions.HTTPError as ex:
|
except (requests.exceptions.ConnectionError,
|
||||||
|
requests.exceptions.HTTPError) as ex:
|
||||||
self._state = None
|
self._state = None
|
||||||
self._status_state = 'Offline'
|
self._status_state = 'Offline'
|
||||||
_LOGGER.debug('Neato connection issue: %s', ex)
|
_LOGGER.warning('Neato connection error: %s', ex)
|
||||||
|
return
|
||||||
|
if not self._state:
|
||||||
return
|
return
|
||||||
_LOGGER.debug('self._state=%s', self._state)
|
_LOGGER.debug('self._state=%s', self._state)
|
||||||
if self.type == SENSOR_TYPE_STATUS:
|
if self.type == SENSOR_TYPE_STATUS:
|
||||||
|
@ -82,6 +105,27 @@ class NeatoConnectedSensor(Entity):
|
||||||
self._status_state = ERRORS.get(self._state['error'])
|
self._status_state = ERRORS.get(self._state['error'])
|
||||||
if self.type == SENSOR_TYPE_BATTERY:
|
if self.type == SENSOR_TYPE_BATTERY:
|
||||||
self._battery_state = self._state['details']['charge']
|
self._battery_state = self._state['details']['charge']
|
||||||
|
if self._mapdata is None:
|
||||||
|
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
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
|
@ -109,3 +153,25 @@ class NeatoConnectedSensor(Entity):
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return self._robot_name
|
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
|
||||||
|
|
|
@ -12,6 +12,8 @@ from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['neato']
|
||||||
|
|
||||||
SWITCH_TYPE_CLEAN = 'clean'
|
SWITCH_TYPE_CLEAN = 'clean'
|
||||||
SWITCH_TYPE_SCHEDULE = 'scedule'
|
SWITCH_TYPE_SCHEDULE = 'scedule'
|
||||||
|
|
||||||
|
@ -23,9 +25,6 @@ SWITCH_TYPES = {
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Neato switches."""
|
"""Setup the Neato switches."""
|
||||||
if not hass.data[NEATO_ROBOTS]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
dev = []
|
dev = []
|
||||||
for robot in hass.data[NEATO_ROBOTS]:
|
for robot in hass.data[NEATO_ROBOTS]:
|
||||||
for type_name in SWITCH_TYPES:
|
for type_name in SWITCH_TYPES:
|
||||||
|
@ -43,7 +42,12 @@ class NeatoConnectedSwitch(ToggleEntity):
|
||||||
self.robot = robot
|
self.robot = robot
|
||||||
self.neato = hass.data[NEATO_LOGIN]
|
self.neato = hass.data[NEATO_LOGIN]
|
||||||
self._robot_name = self.robot.name + ' ' + SWITCH_TYPES[self.type][0]
|
self._robot_name = self.robot.name + ' ' + SWITCH_TYPES[self.type][0]
|
||||||
self._state = self.robot.state
|
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
|
||||||
self._schedule_state = None
|
self._schedule_state = None
|
||||||
self._clean_state = None
|
self._clean_state = None
|
||||||
|
|
||||||
|
@ -51,14 +55,13 @@ class NeatoConnectedSwitch(ToggleEntity):
|
||||||
"""Update the states of Neato switches."""
|
"""Update the states of Neato switches."""
|
||||||
_LOGGER.debug('Running switch update')
|
_LOGGER.debug('Running switch update')
|
||||||
self.neato.update_robots()
|
self.neato.update_robots()
|
||||||
if not self._state:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
self._state = self.robot.state
|
self._state = self.robot.state
|
||||||
except requests.exceptions.HTTPError:
|
except (requests.exceptions.ConnectionError,
|
||||||
|
requests.exceptions.HTTPError) as ex:
|
||||||
|
_LOGGER.warning('Neato connection error: %s', ex)
|
||||||
self._state = None
|
self._state = None
|
||||||
return
|
return
|
||||||
self._state = self.robot.state
|
|
||||||
_LOGGER.debug('self._state=%s', self._state)
|
_LOGGER.debug('self._state=%s', self._state)
|
||||||
if self.type == SWITCH_TYPE_CLEAN:
|
if self.type == SWITCH_TYPE_CLEAN:
|
||||||
if (self.robot.state['action'] == 1 or
|
if (self.robot.state['action'] == 1 or
|
||||||
|
|
|
@ -269,7 +269,7 @@ https://github.com/gurumitts/pylutron-caseta/archive/v0.2.5.zip#pylutron-caseta=
|
||||||
https://github.com/jabesq/netatmo-api-python/archive/v0.9.1.zip#lnetatmo==0.9.1
|
https://github.com/jabesq/netatmo-api-python/archive/v0.9.1.zip#lnetatmo==0.9.1
|
||||||
|
|
||||||
# homeassistant.components.neato
|
# homeassistant.components.neato
|
||||||
https://github.com/jabesq/pybotvac/archive/v0.0.1.zip#pybotvac==0.0.1
|
https://github.com/jabesq/pybotvac/archive/v0.0.3.zip#pybotvac==0.0.3
|
||||||
|
|
||||||
# homeassistant.components.sensor.sabnzbd
|
# homeassistant.components.sensor.sabnzbd
|
||||||
https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
|
https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue