diff --git a/config/custom_components/camera/neato.py b/config/custom_components/camera/neato.py new file mode 100755 index 00000000..689129e1 --- /dev/null +++ b/config/custom_components/camera/neato.py @@ -0,0 +1,65 @@ +""" +Camera that loads a picture from Neato. + +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): + """Set up the Neato 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 = '{} {}'.format(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=60)) + 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 diff --git a/config/custom_components/neato.py b/config/custom_components/neato.py new file mode 100755 index 00000000..9fe68f44 --- /dev/null +++ b/config/custom_components/neato.py @@ -0,0 +1,158 @@ +""" +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 + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import discovery +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['https://github.com/dshokouhi/pybotvac/archive/v0.0.8.zip' + '#pybotvac==0.0.8'] + +DOMAIN = 'neato' +NEATO_ROBOTS = 'neato_robots' +NEATO_LOGIN = 'neato_login' +NEATO_MAP_DATA = 'neato_map_data' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +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', + 11: 'Map cleaning', + 12: 'Exploring map (creating a persistent map)', + 13: 'Acquiring Persistent Map IDs', + 14: 'Creating & Uploading Map', + 15: 'Suspended Exploration' +} + +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_backdrop_leftbump': 'Clear my path', + 'ui_error_navigation_noprogress': 'Clear my path', + 'ui_error_navigation_origin_unclean': 'Clear my path', + 'ui_error_navigation_pathproblems_returninghome': 'Cannot return to base', + 'ui_error_navigation_falling': 'Clear my path', + 'ui_error_picked_up': 'Picked up', + 'ui_error_stuck': 'Stuck!', + 'dustbin_full': 'Dust bin full', + 'dustbin_missing': 'Dust bin missing', + 'maint_brush_stuck': 'Brush stuck', + 'maint_brush_overload': 'Brush overloaded', + 'maint_bumper_stuck': 'Bumper stuck', + 'maint_vacuum_stuck': 'Vacuum is stuck', + 'maint_left_drop_stuck': 'Vacuum is stuck', + 'maint_left_wheel_stuck': 'Vacuum is stuck', + 'maint_right_drop_stuck': 'Vacuum is stuck', + 'maint_right_wheel_stuck': 'Vacuum is stuck', + 'not_on_charge_base': 'Not on the charge base', + 'nav_robot_falling': 'Clear my path', + 'nav_no_path': 'Clear my path', + 'nav_path_problem': 'Clear my path' +} + +ALERTS = { + 'ui_alert_dust_bin_full': 'Please empty dust bin', + 'ui_alert_recovering_location': 'Returning to start', + 'dustbin_full': 'Please empty dust bin', + 'maint_brush_change': 'Change the brush', + 'maint_filter_change': 'Change the filter' +} + + +def setup(hass, config): + """Set up the Neato 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 ('camera', 'vacuum', '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 + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + + 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=60)) + 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 + 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 diff --git a/config/custom_components/switch/neato.py b/config/custom_components/switch/neato.py new file mode 100755 index 00000000..117c2306 --- /dev/null +++ b/config/custom_components/switch/neato.py @@ -0,0 +1,102 @@ +""" +Support for Neato Connected Vacuums switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.neato/ +""" +import logging +import requests +from datetime import timedelta +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['neato'] + +SWITCH_TYPE_SCHEDULE = 'schedule' + +SWITCH_TYPES = { + SWITCH_TYPE_SCHEDULE: ['Schedule'] +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Neato switches.""" + dev = [] + for robot in hass.data[NEATO_ROBOTS]: + for type_name in SWITCH_TYPES: + dev.append(NeatoConnectedSwitch(hass, robot, type_name)) + _LOGGER.debug("Adding switches %s", dev) + add_devices(dev) + + +class NeatoConnectedSwitch(ToggleEntity): + """Neato Connected Switches.""" + + def __init__(self, hass, robot, switch_type): + """Initialize the Neato Connected switches.""" + self.type = switch_type + self.robot = robot + self.neato = hass.data[NEATO_LOGIN] + self._robot_name = '{} {}'.format( + self.robot.name, SWITCH_TYPES[self.type][0]) + 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._clean_state = None + + @Throttle(timedelta(seconds=60)) + def update(self): + """Update the states of Neato switches.""" + _LOGGER.debug("Running switch 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.type == SWITCH_TYPE_SCHEDULE: + _LOGGER.debug("State: %s", self._state) + if self.robot.schedule_enabled: + self._schedule_state = STATE_ON + else: + self._schedule_state = STATE_OFF + _LOGGER.debug("Schedule state: %s", self._schedule_state) + + @property + def name(self): + """Return the name of the switch.""" + return self._robot_name + + @property + def available(self): + """Return True if entity is available.""" + return self._state + + @property + def is_on(self): + """Return true if switch is on.""" + 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_SCHEDULE: + self.robot.enable_schedule() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + if self.type == SWITCH_TYPE_SCHEDULE: + self.robot.disable_schedule() diff --git a/config/custom_components/vacuum/neato.py b/config/custom_components/vacuum/neato.py new file mode 100755 index 00000000..e3e007b7 --- /dev/null +++ b/config/custom_components/vacuum/neato.py @@ -0,0 +1,212 @@ +""" +Support for Neato Connected Vacuums. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/vacuum.neato/ +""" +import logging + +import requests + +from datetime import timedelta +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) +from homeassistant.util import Throttle + +_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 + +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): + """Representation of a Neato Connected Vacuum.""" + + def __init__(self, hass, robot): + """Initialize the Neato Connected Vacuum.""" + 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 + + @Throttle(timedelta(seconds=60)) + def update(self): + """Update the states of Neato Vacuums.""" + _LOGGER.debug("Running Neato 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'] == 11 or + self.robot.state['action'] == 12 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 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() diff --git a/config/packages/grafana.yaml b/config/packages/grafana.yaml index d1f0185d..34f489be 100755 --- a/config/packages/grafana.yaml +++ b/config/packages/grafana.yaml @@ -12,7 +12,7 @@ homeassistant: sensor: - platform: command_line name: "Download Grafana Solar" - command: 'curl -s -H "Authorization: Bearer eyJrIjoiZ0UwS1hpRVdCbFVVSTNSdER0Z29maHc4QmVuSXhxalYiLCJuIjoiSEFDYW1lcmEiLCJpZCI6MX0=" "http://192.168.10.10:3000/render/d/h_wosLIik/solar?orgId=1&panelId=1&width=1000&height=700&from=now-42h&to=now-25h" > /config/www/custom_ui/floorplan/images/branding/solar_readings.png' + command: 'curl -s -H "Authorization: Bearer eyJrIjoiZ0UwS1hpRVdCbFVVSTNSdER0Z29maHc4QmVuSXhxalYiLCJuIjoiSEFDYW1lcmEiLCJpZCI6MX0=" "http://192.168.10.10:3000/render/d/h_wosLIik/solar?orgId=1&panelId=1&width=1200&height=700&from=now-1d/d&to=now-1d/d" > /config/www/custom_ui/floorplan/images/branding/solar_readings.png' scan_interval: 3000 camera: diff --git a/config/www/custom_ui/floorplan/images/branding/solar_readings.png b/config/www/custom_ui/floorplan/images/branding/solar_readings.png index 3bbc80e9..77ea900e 100644 Binary files a/config/www/custom_ui/floorplan/images/branding/solar_readings.png and b/config/www/custom_ui/floorplan/images/branding/solar_readings.png differ