mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Neato config flow (#26579)
* initial commit * Minor changes * add async setup entry * Add translations and some other stuff * add and remove entry * use async_setup_entry * Update config_flows.py * dshokouhi's changes * Improve workflow * Add valid_vendors * Add entity registry * Add device registry * Update entry from configuration.yaml * Revert unneccesary changes * Update .coveragerc * Prepared tests * Add dshokouhi and Santobert as codeowners * Fix unload entry and abort when already_configured * First tests * Add test for abort cases * Add test for invalid credentials on import * Add one last test * Add test_init.py with some tests * Address reviews, part 1 * Update outdated entry * await instead of add_job * run IO inside an executor * remove faulty test * Fix pylint issues * Move IO out of constructur * Edit error translations * Edit imports * Minor changes * Remove test for invalid vendor * Async setup platform * Edit login function * Moved IO out if init * Update switches after added to hass * Revert update outdated entry * try and update new entrys from config.yaml * Add test invalid vendor * Default to neato
This commit is contained in:
parent
476f24e451
commit
bd6bbcd5af
@ -420,7 +420,9 @@ omit =
|
|||||||
homeassistant/components/n26/*
|
homeassistant/components/n26/*
|
||||||
homeassistant/components/nad/media_player.py
|
homeassistant/components/nad/media_player.py
|
||||||
homeassistant/components/nanoleaf/light.py
|
homeassistant/components/nanoleaf/light.py
|
||||||
homeassistant/components/neato/*
|
homeassistant/components/neato/camera.py
|
||||||
|
homeassistant/components/neato/vacuum.py
|
||||||
|
homeassistant/components/neato/switch.py
|
||||||
homeassistant/components/nederlandse_spoorwegen/sensor.py
|
homeassistant/components/nederlandse_spoorwegen/sensor.py
|
||||||
homeassistant/components/nello/lock.py
|
homeassistant/components/nello/lock.py
|
||||||
homeassistant/components/nest/*
|
homeassistant/components/nest/*
|
||||||
|
@ -187,6 +187,7 @@ homeassistant/components/mpd/* @fabaff
|
|||||||
homeassistant/components/mqtt/* @home-assistant/core
|
homeassistant/components/mqtt/* @home-assistant/core
|
||||||
homeassistant/components/mysensors/* @MartinHjelmare
|
homeassistant/components/mysensors/* @MartinHjelmare
|
||||||
homeassistant/components/mystrom/* @fabaff
|
homeassistant/components/mystrom/* @fabaff
|
||||||
|
homeassistant/components/neato/* @dshokouhi @Santobert
|
||||||
homeassistant/components/nello/* @pschmitt
|
homeassistant/components/nello/* @pschmitt
|
||||||
homeassistant/components/ness_alarm/* @nickw444
|
homeassistant/components/ness_alarm/* @nickw444
|
||||||
homeassistant/components/nest/* @awarecan
|
homeassistant/components/nest/* @awarecan
|
||||||
|
26
homeassistant/components/neato/.translations/en.json
Normal file
26
homeassistant/components/neato/.translations/en.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Neato",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Neato Account Info",
|
||||||
|
"data": {
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"vendor": "Vendor"
|
||||||
|
},
|
||||||
|
"description": "See [Neato documentation]({docs_url})."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_credentials": "Invalid credentials"
|
||||||
|
},
|
||||||
|
"create_entry": {
|
||||||
|
"default": "See [Neato documentation]({docs_url})."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Already configured",
|
||||||
|
"invalid_credentials": "Invalid credentials"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,194 +1,125 @@
|
|||||||
"""Support for Neato botvac connected vacuum cleaners."""
|
"""Support for Neato botvac connected vacuum cleaners."""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib.error import HTTPError
|
|
||||||
|
|
||||||
|
from requests.exceptions import HTTPError, ConnectionError as ConnError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
from .config_flow import NeatoConfigFlow
|
||||||
|
from .const import (
|
||||||
|
CONF_VENDOR,
|
||||||
|
NEATO_CONFIG,
|
||||||
|
NEATO_DOMAIN,
|
||||||
|
NEATO_LOGIN,
|
||||||
|
NEATO_ROBOTS,
|
||||||
|
NEATO_PERSISTENT_MAPS,
|
||||||
|
NEATO_MAP_DATA,
|
||||||
|
VALID_VENDORS,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_VENDOR = "vendor"
|
|
||||||
DOMAIN = "neato"
|
|
||||||
NEATO_ROBOTS = "neato_robots"
|
|
||||||
NEATO_LOGIN = "neato_login"
|
|
||||||
NEATO_MAP_DATA = "neato_map_data"
|
|
||||||
NEATO_PERSISTENT_MAPS = "neato_persistent_maps"
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
DOMAIN: vol.Schema(
|
NEATO_DOMAIN: vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_VENDOR, default="neato"): vol.In(
|
vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS),
|
||||||
["neato", "vorwerk"]
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
MODE = {1: "Eco", 2: "Turbo"}
|
|
||||||
|
|
||||||
ACTION = {
|
async def async_setup(hass, config):
|
||||||
0: "Invalid",
|
|
||||||
1: "House Cleaning",
|
|
||||||
2: "Spot Cleaning",
|
|
||||||
3: "Manual Cleaning",
|
|
||||||
4: "Docking",
|
|
||||||
5: "User Menu Active",
|
|
||||||
6: "Suspended Cleaning",
|
|
||||||
7: "Updating",
|
|
||||||
8: "Copying logs",
|
|
||||||
9: "Recovering Location",
|
|
||||||
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_battery_battundervoltlithiumsafety": "Replace battery",
|
|
||||||
"ui_error_battery_critical": "Replace battery",
|
|
||||||
"ui_error_battery_invalidsensor": "Replace battery",
|
|
||||||
"ui_error_battery_lithiumadapterfailure": "Replace battery",
|
|
||||||
"ui_error_battery_mismatch": "Replace battery",
|
|
||||||
"ui_error_battery_nothermistor": "Replace battery",
|
|
||||||
"ui_error_battery_overtemp": "Replace battery",
|
|
||||||
"ui_error_battery_overvolt": "Replace battery",
|
|
||||||
"ui_error_battery_undercurrent": "Replace battery",
|
|
||||||
"ui_error_battery_undertemp": "Replace battery",
|
|
||||||
"ui_error_battery_undervolt": "Replace battery",
|
|
||||||
"ui_error_battery_unplugged": "Replace battery",
|
|
||||||
"ui_error_brush_stuck": "Brush stuck",
|
|
||||||
"ui_error_brush_overloaded": "Brush overloaded",
|
|
||||||
"ui_error_bumper_stuck": "Bumper stuck",
|
|
||||||
"ui_error_check_battery_switch": "Check battery",
|
|
||||||
"ui_error_corrupt_scb": "Call customer service corrupt board",
|
|
||||||
"ui_error_deck_debris": "Deck debris",
|
|
||||||
"ui_error_dflt_app": "Check Neato app",
|
|
||||||
"ui_error_disconnect_chrg_cable": "Disconnected charge cable",
|
|
||||||
"ui_error_disconnect_usb_cable": "Disconnected USB cable",
|
|
||||||
"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_hardware_failure": "Hardware failure",
|
|
||||||
"ui_error_ldrop_stuck": "Clear my path",
|
|
||||||
"ui_error_lds_jammed": "Clear my path",
|
|
||||||
"ui_error_lds_bad_packets": "Check Neato app",
|
|
||||||
"ui_error_lds_disconnected": "Check Neato app",
|
|
||||||
"ui_error_lds_missed_packets": "Check Neato app",
|
|
||||||
"ui_error_lwheel_stuck": "Clear my path",
|
|
||||||
"ui_error_navigation_backdrop_frontbump": "Clear my path",
|
|
||||||
"ui_error_navigation_backdrop_leftbump": "Clear my path",
|
|
||||||
"ui_error_navigation_backdrop_wheelextended": "Clear my path",
|
|
||||||
"ui_error_navigation_noprogress": "Clear my path",
|
|
||||||
"ui_error_navigation_origin_unclean": "Clear my path",
|
|
||||||
"ui_error_navigation_pathproblems": "Cannot return to base",
|
|
||||||
"ui_error_navigation_pinkycommsfail": "Clear my path",
|
|
||||||
"ui_error_navigation_falling": "Clear my path",
|
|
||||||
"ui_error_navigation_noexitstogo": "Clear my path",
|
|
||||||
"ui_error_navigation_nomotioncommands": "Clear my path",
|
|
||||||
"ui_error_navigation_rightdrop_leftbump": "Clear my path",
|
|
||||||
"ui_error_navigation_undockingfailed": "Clear my path",
|
|
||||||
"ui_error_picked_up": "Picked up",
|
|
||||||
"ui_error_qa_fail": "Check Neato app",
|
|
||||||
"ui_error_rdrop_stuck": "Clear my path",
|
|
||||||
"ui_error_reconnect_failed": "Reconnect failed",
|
|
||||||
"ui_error_rwheel_stuck": "Clear my path",
|
|
||||||
"ui_error_stuck": "Stuck!",
|
|
||||||
"ui_error_unable_to_return_to_base": "Unable to return to base",
|
|
||||||
"ui_error_unable_to_see": "Clean vacuum sensors",
|
|
||||||
"ui_error_vacuum_slip": "Clear my path",
|
|
||||||
"ui_error_vacuum_stuck": "Clear my path",
|
|
||||||
"ui_error_warning": "Error check app",
|
|
||||||
"batt_base_connect_fail": "Battery failed to connect to base",
|
|
||||||
"batt_base_no_power": "Battery base has no power",
|
|
||||||
"batt_low": "Battery low",
|
|
||||||
"batt_on_base": "Battery on base",
|
|
||||||
"clean_tilt_on_start": "Clean the tilt on start",
|
|
||||||
"dustbin_full": "Dust bin full",
|
|
||||||
"dustbin_missing": "Dust bin missing",
|
|
||||||
"gen_picked_up": "Picked up",
|
|
||||||
"hw_fail": "Hardware failure",
|
|
||||||
"hw_tof_sensor_sensor": "Hardware sensor disconnected",
|
|
||||||
"lds_bad_packets": "Bad packets",
|
|
||||||
"lds_deck_debris": "Debris on deck",
|
|
||||||
"lds_disconnected": "Disconnected",
|
|
||||||
"lds_jammed": "Jammed",
|
|
||||||
"lds_missed_packets": "Missed packets",
|
|
||||||
"maint_brush_stuck": "Brush stuck",
|
|
||||||
"maint_brush_overload": "Brush overloaded",
|
|
||||||
"maint_bumper_stuck": "Bumper stuck",
|
|
||||||
"maint_customer_support_qa": "Contact customer support",
|
|
||||||
"maint_vacuum_stuck": "Vacuum is stuck",
|
|
||||||
"maint_vacuum_slip": "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",
|
|
||||||
"nav_backdrop_frontbump": "Clear my path",
|
|
||||||
"nav_backdrop_leftbump": "Clear my path",
|
|
||||||
"nav_backdrop_wheelextended": "Clear my path",
|
|
||||||
"nav_mag_sensor": "Clear my path",
|
|
||||||
"nav_no_exit": "Clear my path",
|
|
||||||
"nav_no_movement": "Clear my path",
|
|
||||||
"nav_rightdrop_leftbump": "Clear my path",
|
|
||||||
"nav_undocking_failed": "Clear my path",
|
|
||||||
}
|
|
||||||
|
|
||||||
ALERTS = {
|
|
||||||
"ui_alert_dust_bin_full": "Please empty dust bin",
|
|
||||||
"ui_alert_recovering_location": "Returning to start",
|
|
||||||
"ui_alert_battery_chargebasecommerr": "Battery error",
|
|
||||||
"ui_alert_busy_charging": "Busy charging",
|
|
||||||
"ui_alert_charging_base": "Base charging",
|
|
||||||
"ui_alert_charging_power": "Charging power",
|
|
||||||
"ui_alert_connect_chrg_cable": "Connect charge cable",
|
|
||||||
"ui_alert_info_thank_you": "Thank you",
|
|
||||||
"ui_alert_invalid": "Invalid check app",
|
|
||||||
"ui_alert_old_error": "Old error",
|
|
||||||
"ui_alert_swupdate_fail": "Update failed",
|
|
||||||
"dustbin_full": "Please empty dust bin",
|
|
||||||
"maint_brush_change": "Change the brush",
|
|
||||||
"maint_filter_change": "Change the filter",
|
|
||||||
"clean_completed_to_start": "Cleaning completed",
|
|
||||||
"nav_floorplan_not_created": "No floorplan found",
|
|
||||||
"nav_floorplan_load_fail": "Failed to load floorplan",
|
|
||||||
"nav_floorplan_localization_fail": "Failed to load floorplan",
|
|
||||||
"clean_incomplete_to_start": "Cleaning incomplete",
|
|
||||||
"log_upload_failed": "Logs failed to upload",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Set up the Neato component."""
|
"""Set up the Neato component."""
|
||||||
|
|
||||||
|
if NEATO_DOMAIN not in config:
|
||||||
|
# There is an entry and nothing in configuration.yaml
|
||||||
|
return True
|
||||||
|
|
||||||
|
entries = hass.config_entries.async_entries(NEATO_DOMAIN)
|
||||||
|
hass.data[NEATO_CONFIG] = config[NEATO_DOMAIN]
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
# There is an entry and something in the configuration.yaml
|
||||||
|
entry = entries[0]
|
||||||
|
conf = config[NEATO_DOMAIN]
|
||||||
|
if (
|
||||||
|
entry.data[CONF_USERNAME] == conf[CONF_USERNAME]
|
||||||
|
and entry.data[CONF_PASSWORD] == conf[CONF_PASSWORD]
|
||||||
|
and entry.data[CONF_VENDOR] == conf[CONF_VENDOR]
|
||||||
|
):
|
||||||
|
# The entry is not outdated
|
||||||
|
return True
|
||||||
|
|
||||||
|
# The entry is outdated
|
||||||
|
error = await hass.async_add_executor_job(
|
||||||
|
NeatoConfigFlow.try_login,
|
||||||
|
conf[CONF_USERNAME],
|
||||||
|
conf[CONF_PASSWORD],
|
||||||
|
conf[CONF_VENDOR],
|
||||||
|
)
|
||||||
|
if error is not None:
|
||||||
|
_LOGGER.error(error)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Update the entry
|
||||||
|
hass.config_entries.async_update_entry(entry, data=config[NEATO_DOMAIN])
|
||||||
|
else:
|
||||||
|
# Create the new entry
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
NEATO_DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=config[NEATO_DOMAIN],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Set up config entry."""
|
||||||
from pybotvac import Account, Neato, Vorwerk
|
from pybotvac import Account, Neato, Vorwerk
|
||||||
|
|
||||||
if config[DOMAIN][CONF_VENDOR] == "neato":
|
if entry.data[CONF_VENDOR] == "neato":
|
||||||
hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Neato)
|
hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Neato)
|
||||||
elif config[DOMAIN][CONF_VENDOR] == "vorwerk":
|
elif entry.data[CONF_VENDOR] == "vorwerk":
|
||||||
hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Vorwerk)
|
hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Vorwerk)
|
||||||
|
|
||||||
hub = hass.data[NEATO_LOGIN]
|
hub = hass.data[NEATO_LOGIN]
|
||||||
if not hub.login():
|
await hass.async_add_executor_job(hub.login)
|
||||||
|
if not hub.logged_in:
|
||||||
_LOGGER.debug("Failed to login to Neato API")
|
_LOGGER.debug("Failed to login to Neato API")
|
||||||
return False
|
return False
|
||||||
hub.update_robots()
|
|
||||||
for component in ("camera", "vacuum", "switch"):
|
|
||||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(hub.update_robots)
|
||||||
|
for component in ("camera", "vacuum", "switch"):
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload config entry."""
|
||||||
|
hass.data.pop(NEATO_LOGIN)
|
||||||
|
await asyncio.gather(
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, "camera"),
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, "vacuum"),
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, "switch"),
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -202,12 +133,8 @@ class NeatoHub:
|
|||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._vendor = vendor
|
self._vendor = vendor
|
||||||
|
|
||||||
self.my_neato = neato(
|
self.my_neato = None
|
||||||
domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD], vendor
|
self.logged_in = False
|
||||||
)
|
|
||||||
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
|
||||||
self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps
|
|
||||||
self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps
|
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
"""Login to My Neato."""
|
"""Login to My Neato."""
|
||||||
@ -216,10 +143,16 @@ class NeatoHub:
|
|||||||
self.my_neato = self._neato(
|
self.my_neato = self._neato(
|
||||||
self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor
|
self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor
|
||||||
)
|
)
|
||||||
return True
|
self.logged_in = True
|
||||||
except HTTPError:
|
except (HTTPError, ConnError):
|
||||||
_LOGGER.error("Unable to connect to Neato API")
|
_LOGGER.error("Unable to connect to Neato API")
|
||||||
return False
|
self.logged_in = False
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug("Successfully connected to Neato API")
|
||||||
|
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
||||||
|
self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps
|
||||||
|
self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps
|
||||||
|
|
||||||
@Throttle(timedelta(seconds=300))
|
@Throttle(timedelta(seconds=300))
|
||||||
def update_robots(self):
|
def update_robots(self):
|
||||||
|
@ -4,21 +4,30 @@ import logging
|
|||||||
|
|
||||||
from homeassistant.components.camera import Camera
|
from homeassistant.components.camera import Camera
|
||||||
|
|
||||||
from . import NEATO_LOGIN, NEATO_MAP_DATA, NEATO_ROBOTS
|
from .const import NEATO_DOMAIN, NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=10)
|
SCAN_INTERVAL = timedelta(minutes=10)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Neato Camera."""
|
"""Set up the Neato Camera."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
|
"""Set up Neato camera with config entry."""
|
||||||
dev = []
|
dev = []
|
||||||
for robot in hass.data[NEATO_ROBOTS]:
|
for robot in hass.data[NEATO_ROBOTS]:
|
||||||
if "maps" in robot.traits:
|
if "maps" in robot.traits:
|
||||||
dev.append(NeatoCleaningMap(hass, robot))
|
dev.append(NeatoCleaningMap(hass, robot))
|
||||||
|
|
||||||
|
if not dev:
|
||||||
|
return
|
||||||
|
|
||||||
_LOGGER.debug("Adding robots for cleaning maps %s", dev)
|
_LOGGER.debug("Adding robots for cleaning maps %s", dev)
|
||||||
add_entities(dev, True)
|
async_add_entities(dev, True)
|
||||||
|
|
||||||
|
|
||||||
class NeatoCleaningMap(Camera):
|
class NeatoCleaningMap(Camera):
|
||||||
@ -61,3 +70,8 @@ class NeatoCleaningMap(Camera):
|
|||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return unique ID."""
|
"""Return unique ID."""
|
||||||
return self._robot_serial
|
return self._robot_serial
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Device info for neato robot."""
|
||||||
|
return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}}
|
||||||
|
112
homeassistant/components/neato/config_flow.py
Normal file
112
homeassistant/components/neato/config_flow.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
"""Config flow to configure Neato integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from requests.exceptions import HTTPError, ConnectionError as ConnError
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
# pylint: disable=unused-import
|
||||||
|
from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS
|
||||||
|
|
||||||
|
|
||||||
|
DOCS_URL = "https://www.home-assistant.io/components/neato"
|
||||||
|
DEFAULT_VENDOR = "neato"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN):
|
||||||
|
"""Neato integration config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize flow."""
|
||||||
|
self._username = vol.UNDEFINED
|
||||||
|
self._password = vol.UNDEFINED
|
||||||
|
self._vendor = vol.UNDEFINED
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
self._username = user_input["username"]
|
||||||
|
self._password = user_input["password"]
|
||||||
|
self._vendor = user_input["vendor"]
|
||||||
|
|
||||||
|
error = await self.hass.async_add_executor_job(
|
||||||
|
self.try_login, self._username, self._password, self._vendor
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
errors["base"] = error
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_USERNAME],
|
||||||
|
data=user_input,
|
||||||
|
description_placeholders={"docs_url": DOCS_URL},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
description_placeholders={"docs_url": DOCS_URL},
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, user_input):
|
||||||
|
"""Import a config flow from configuration."""
|
||||||
|
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
|
||||||
|
username = user_input[CONF_USERNAME]
|
||||||
|
password = user_input[CONF_PASSWORD]
|
||||||
|
vendor = user_input[CONF_VENDOR]
|
||||||
|
|
||||||
|
error = await self.hass.async_add_executor_job(
|
||||||
|
self.try_login, username, password, vendor
|
||||||
|
)
|
||||||
|
if error is not None:
|
||||||
|
_LOGGER.error(error)
|
||||||
|
return self.async_abort(reason=error)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"{username} (from configuration)",
|
||||||
|
data={
|
||||||
|
CONF_USERNAME: username,
|
||||||
|
CONF_PASSWORD: password,
|
||||||
|
CONF_VENDOR: vendor,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def try_login(username, password, vendor):
|
||||||
|
"""Try logging in to device and return any errors."""
|
||||||
|
from pybotvac import Account, Neato, Vorwerk
|
||||||
|
|
||||||
|
this_vendor = None
|
||||||
|
if vendor == "vorwerk":
|
||||||
|
this_vendor = Vorwerk()
|
||||||
|
else: # Neato
|
||||||
|
this_vendor = Neato()
|
||||||
|
|
||||||
|
try:
|
||||||
|
Account(username, password, this_vendor)
|
||||||
|
except (HTTPError, ConnError):
|
||||||
|
return "invalid_credentials"
|
||||||
|
|
||||||
|
return None
|
150
homeassistant/components/neato/const.py
Normal file
150
homeassistant/components/neato/const.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
"""Constants for Neato integration."""
|
||||||
|
|
||||||
|
NEATO_DOMAIN = "neato"
|
||||||
|
|
||||||
|
CONF_VENDOR = "vendor"
|
||||||
|
NEATO_ROBOTS = "neato_robots"
|
||||||
|
NEATO_LOGIN = "neato_login"
|
||||||
|
NEATO_CONFIG = "neato_config"
|
||||||
|
NEATO_MAP_DATA = "neato_map_data"
|
||||||
|
NEATO_PERSISTENT_MAPS = "neato_persistent_maps"
|
||||||
|
|
||||||
|
VALID_VENDORS = ["neato", "vorwerk"]
|
||||||
|
|
||||||
|
MODE = {1: "Eco", 2: "Turbo"}
|
||||||
|
|
||||||
|
ACTION = {
|
||||||
|
0: "Invalid",
|
||||||
|
1: "House Cleaning",
|
||||||
|
2: "Spot Cleaning",
|
||||||
|
3: "Manual Cleaning",
|
||||||
|
4: "Docking",
|
||||||
|
5: "User Menu Active",
|
||||||
|
6: "Suspended Cleaning",
|
||||||
|
7: "Updating",
|
||||||
|
8: "Copying logs",
|
||||||
|
9: "Recovering Location",
|
||||||
|
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_battery_battundervoltlithiumsafety": "Replace battery",
|
||||||
|
"ui_error_battery_critical": "Replace battery",
|
||||||
|
"ui_error_battery_invalidsensor": "Replace battery",
|
||||||
|
"ui_error_battery_lithiumadapterfailure": "Replace battery",
|
||||||
|
"ui_error_battery_mismatch": "Replace battery",
|
||||||
|
"ui_error_battery_nothermistor": "Replace battery",
|
||||||
|
"ui_error_battery_overtemp": "Replace battery",
|
||||||
|
"ui_error_battery_overvolt": "Replace battery",
|
||||||
|
"ui_error_battery_undercurrent": "Replace battery",
|
||||||
|
"ui_error_battery_undertemp": "Replace battery",
|
||||||
|
"ui_error_battery_undervolt": "Replace battery",
|
||||||
|
"ui_error_battery_unplugged": "Replace battery",
|
||||||
|
"ui_error_brush_stuck": "Brush stuck",
|
||||||
|
"ui_error_brush_overloaded": "Brush overloaded",
|
||||||
|
"ui_error_bumper_stuck": "Bumper stuck",
|
||||||
|
"ui_error_check_battery_switch": "Check battery",
|
||||||
|
"ui_error_corrupt_scb": "Call customer service corrupt board",
|
||||||
|
"ui_error_deck_debris": "Deck debris",
|
||||||
|
"ui_error_dflt_app": "Check Neato app",
|
||||||
|
"ui_error_disconnect_chrg_cable": "Disconnected charge cable",
|
||||||
|
"ui_error_disconnect_usb_cable": "Disconnected USB cable",
|
||||||
|
"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_hardware_failure": "Hardware failure",
|
||||||
|
"ui_error_ldrop_stuck": "Clear my path",
|
||||||
|
"ui_error_lds_jammed": "Clear my path",
|
||||||
|
"ui_error_lds_bad_packets": "Check Neato app",
|
||||||
|
"ui_error_lds_disconnected": "Check Neato app",
|
||||||
|
"ui_error_lds_missed_packets": "Check Neato app",
|
||||||
|
"ui_error_lwheel_stuck": "Clear my path",
|
||||||
|
"ui_error_navigation_backdrop_frontbump": "Clear my path",
|
||||||
|
"ui_error_navigation_backdrop_leftbump": "Clear my path",
|
||||||
|
"ui_error_navigation_backdrop_wheelextended": "Clear my path",
|
||||||
|
"ui_error_navigation_noprogress": "Clear my path",
|
||||||
|
"ui_error_navigation_origin_unclean": "Clear my path",
|
||||||
|
"ui_error_navigation_pathproblems": "Cannot return to base",
|
||||||
|
"ui_error_navigation_pinkycommsfail": "Clear my path",
|
||||||
|
"ui_error_navigation_falling": "Clear my path",
|
||||||
|
"ui_error_navigation_noexitstogo": "Clear my path",
|
||||||
|
"ui_error_navigation_nomotioncommands": "Clear my path",
|
||||||
|
"ui_error_navigation_rightdrop_leftbump": "Clear my path",
|
||||||
|
"ui_error_navigation_undockingfailed": "Clear my path",
|
||||||
|
"ui_error_picked_up": "Picked up",
|
||||||
|
"ui_error_qa_fail": "Check Neato app",
|
||||||
|
"ui_error_rdrop_stuck": "Clear my path",
|
||||||
|
"ui_error_reconnect_failed": "Reconnect failed",
|
||||||
|
"ui_error_rwheel_stuck": "Clear my path",
|
||||||
|
"ui_error_stuck": "Stuck!",
|
||||||
|
"ui_error_unable_to_return_to_base": "Unable to return to base",
|
||||||
|
"ui_error_unable_to_see": "Clean vacuum sensors",
|
||||||
|
"ui_error_vacuum_slip": "Clear my path",
|
||||||
|
"ui_error_vacuum_stuck": "Clear my path",
|
||||||
|
"ui_error_warning": "Error check app",
|
||||||
|
"batt_base_connect_fail": "Battery failed to connect to base",
|
||||||
|
"batt_base_no_power": "Battery base has no power",
|
||||||
|
"batt_low": "Battery low",
|
||||||
|
"batt_on_base": "Battery on base",
|
||||||
|
"clean_tilt_on_start": "Clean the tilt on start",
|
||||||
|
"dustbin_full": "Dust bin full",
|
||||||
|
"dustbin_missing": "Dust bin missing",
|
||||||
|
"gen_picked_up": "Picked up",
|
||||||
|
"hw_fail": "Hardware failure",
|
||||||
|
"hw_tof_sensor_sensor": "Hardware sensor disconnected",
|
||||||
|
"lds_bad_packets": "Bad packets",
|
||||||
|
"lds_deck_debris": "Debris on deck",
|
||||||
|
"lds_disconnected": "Disconnected",
|
||||||
|
"lds_jammed": "Jammed",
|
||||||
|
"lds_missed_packets": "Missed packets",
|
||||||
|
"maint_brush_stuck": "Brush stuck",
|
||||||
|
"maint_brush_overload": "Brush overloaded",
|
||||||
|
"maint_bumper_stuck": "Bumper stuck",
|
||||||
|
"maint_customer_support_qa": "Contact customer support",
|
||||||
|
"maint_vacuum_stuck": "Vacuum is stuck",
|
||||||
|
"maint_vacuum_slip": "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",
|
||||||
|
"nav_backdrop_frontbump": "Clear my path",
|
||||||
|
"nav_backdrop_leftbump": "Clear my path",
|
||||||
|
"nav_backdrop_wheelextended": "Clear my path",
|
||||||
|
"nav_mag_sensor": "Clear my path",
|
||||||
|
"nav_no_exit": "Clear my path",
|
||||||
|
"nav_no_movement": "Clear my path",
|
||||||
|
"nav_rightdrop_leftbump": "Clear my path",
|
||||||
|
"nav_undocking_failed": "Clear my path",
|
||||||
|
}
|
||||||
|
|
||||||
|
ALERTS = {
|
||||||
|
"ui_alert_dust_bin_full": "Please empty dust bin",
|
||||||
|
"ui_alert_recovering_location": "Returning to start",
|
||||||
|
"ui_alert_battery_chargebasecommerr": "Battery error",
|
||||||
|
"ui_alert_busy_charging": "Busy charging",
|
||||||
|
"ui_alert_charging_base": "Base charging",
|
||||||
|
"ui_alert_charging_power": "Charging power",
|
||||||
|
"ui_alert_connect_chrg_cable": "Connect charge cable",
|
||||||
|
"ui_alert_info_thank_you": "Thank you",
|
||||||
|
"ui_alert_invalid": "Invalid check app",
|
||||||
|
"ui_alert_old_error": "Old error",
|
||||||
|
"ui_alert_swupdate_fail": "Update failed",
|
||||||
|
"dustbin_full": "Please empty dust bin",
|
||||||
|
"maint_brush_change": "Change the brush",
|
||||||
|
"maint_filter_change": "Change the filter",
|
||||||
|
"clean_completed_to_start": "Cleaning completed",
|
||||||
|
"nav_floorplan_not_created": "No floorplan found",
|
||||||
|
"nav_floorplan_load_fail": "Failed to load floorplan",
|
||||||
|
"nav_floorplan_localization_fail": "Failed to load floorplan",
|
||||||
|
"clean_incomplete_to_start": "Cleaning incomplete",
|
||||||
|
"log_upload_failed": "Logs failed to upload",
|
||||||
|
}
|
@ -1,10 +1,14 @@
|
|||||||
{
|
{
|
||||||
"domain": "neato",
|
"domain": "neato",
|
||||||
"name": "Neato",
|
"name": "Neato",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/neato",
|
"documentation": "https://www.home-assistant.io/integrations/neato",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"pybotvac==0.0.15"
|
"pybotvac==0.0.15"
|
||||||
],
|
],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": []
|
"codeowners": [
|
||||||
}
|
"@dshokouhi",
|
||||||
|
"@Santobert"
|
||||||
|
]
|
||||||
|
}
|
26
homeassistant/components/neato/strings.json
Normal file
26
homeassistant/components/neato/strings.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Neato",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Neato Account Info",
|
||||||
|
"data": {
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"vendor": "Vendor"
|
||||||
|
},
|
||||||
|
"description": "See [Neato documentation]({docs_url})."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_credentials": "Invalid credentials"
|
||||||
|
},
|
||||||
|
"create_entry": {
|
||||||
|
"default": "See [Neato documentation]({docs_url})."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Already configured",
|
||||||
|
"invalid_credentials": "Invalid credentials"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ import requests
|
|||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
|
|
||||||
from . import NEATO_LOGIN, NEATO_ROBOTS
|
from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -18,14 +18,23 @@ SWITCH_TYPE_SCHEDULE = "schedule"
|
|||||||
SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]}
|
SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Neato switches."""
|
"""Set up the Neato switches."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
|
"""Set up Neato switch with config entry."""
|
||||||
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:
|
||||||
dev.append(NeatoConnectedSwitch(hass, robot, type_name))
|
dev.append(NeatoConnectedSwitch(hass, robot, type_name))
|
||||||
|
|
||||||
|
if not dev:
|
||||||
|
return
|
||||||
|
|
||||||
_LOGGER.debug("Adding switches %s", dev)
|
_LOGGER.debug("Adding switches %s", dev)
|
||||||
add_entities(dev)
|
async_add_entities(dev, True)
|
||||||
|
|
||||||
|
|
||||||
class NeatoConnectedSwitch(ToggleEntity):
|
class NeatoConnectedSwitch(ToggleEntity):
|
||||||
@ -37,14 +46,7 @@ class NeatoConnectedSwitch(ToggleEntity):
|
|||||||
self.robot = robot
|
self.robot = robot
|
||||||
self.neato = hass.data[NEATO_LOGIN]
|
self.neato = hass.data[NEATO_LOGIN]
|
||||||
self._robot_name = "{} {}".format(self.robot.name, SWITCH_TYPES[self.type][0])
|
self._robot_name = "{} {}".format(self.robot.name, SWITCH_TYPES[self.type][0])
|
||||||
try:
|
self._state = None
|
||||||
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
|
||||||
self._robot_serial = self.robot.serial
|
self._robot_serial = self.robot.serial
|
||||||
@ -94,6 +96,11 @@ class NeatoConnectedSwitch(ToggleEntity):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Device info for neato robot."""
|
||||||
|
return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}}
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the switch on."""
|
"""Turn the switch on."""
|
||||||
if self.type == SWITCH_TYPE_SCHEDULE:
|
if self.type == SWITCH_TYPE_SCHEDULE:
|
||||||
|
@ -31,12 +31,13 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.service import extract_entity_ids
|
from homeassistant.helpers.service import extract_entity_ids
|
||||||
|
|
||||||
from . import (
|
from .const import (
|
||||||
ACTION,
|
ACTION,
|
||||||
ALERTS,
|
ALERTS,
|
||||||
ERRORS,
|
ERRORS,
|
||||||
MODE,
|
MODE,
|
||||||
NEATO_LOGIN,
|
NEATO_LOGIN,
|
||||||
|
NEATO_DOMAIN,
|
||||||
NEATO_MAP_DATA,
|
NEATO_MAP_DATA,
|
||||||
NEATO_PERSISTENT_MAPS,
|
NEATO_PERSISTENT_MAPS,
|
||||||
NEATO_ROBOTS,
|
NEATO_ROBOTS,
|
||||||
@ -83,8 +84,13 @@ SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Neato vacuum."""
|
"""Set up the Neato vacuum."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
|
"""Set up Neato vacuum with config entry."""
|
||||||
dev = []
|
dev = []
|
||||||
for robot in hass.data[NEATO_ROBOTS]:
|
for robot in hass.data[NEATO_ROBOTS]:
|
||||||
dev.append(NeatoConnectedVacuum(hass, robot))
|
dev.append(NeatoConnectedVacuum(hass, robot))
|
||||||
@ -93,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.debug("Adding vacuums %s", dev)
|
_LOGGER.debug("Adding vacuums %s", dev)
|
||||||
add_entities(dev, True)
|
async_add_entities(dev, True)
|
||||||
|
|
||||||
def neato_custom_cleaning_service(call):
|
def neato_custom_cleaning_service(call):
|
||||||
"""Zone cleaning service that allows user to change options."""
|
"""Zone cleaning service that allows user to change options."""
|
||||||
@ -111,7 +117,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
entities = [entity for entity in dev if entity.entity_id in entity_ids]
|
entities = [entity for entity in dev if entity.entity_id in entity_ids]
|
||||||
return entities
|
return entities
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_NEATO_CUSTOM_CLEANING,
|
SERVICE_NEATO_CUSTOM_CLEANING,
|
||||||
neato_custom_cleaning_service,
|
neato_custom_cleaning_service,
|
||||||
@ -144,10 +150,14 @@ class NeatoConnectedVacuum(StateVacuumDevice):
|
|||||||
self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS]
|
self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS]
|
||||||
self._robot_boundaries = {}
|
self._robot_boundaries = {}
|
||||||
self._robot_has_map = self.robot.has_persistent_maps
|
self._robot_has_map = self.robot.has_persistent_maps
|
||||||
|
self._robot_stats = None
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update the states of Neato Vacuums."""
|
"""Update the states of Neato Vacuums."""
|
||||||
_LOGGER.debug("Running Neato Vacuums update")
|
_LOGGER.debug("Running Neato Vacuums update")
|
||||||
|
if self._robot_stats is None:
|
||||||
|
self._robot_stats = self.robot.get_robot_info().json()
|
||||||
|
|
||||||
self.neato.update_robots()
|
self.neato.update_robots()
|
||||||
try:
|
try:
|
||||||
self._state = self.robot.state
|
self._state = self.robot.state
|
||||||
@ -290,6 +300,17 @@ class NeatoConnectedVacuum(StateVacuumDevice):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Device info for neato robot."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(NEATO_DOMAIN, self._robot_serial)},
|
||||||
|
"name": self._name,
|
||||||
|
"manufacturer": self._robot_stats["data"]["mfg_name"],
|
||||||
|
"model": self._robot_stats["data"]["modelName"],
|
||||||
|
"sw_version": self._state["meta"]["firmware"],
|
||||||
|
}
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Start cleaning or resume cleaning."""
|
"""Start cleaning or resume cleaning."""
|
||||||
if self._state["state"] == 1:
|
if self._state["state"] == 1:
|
||||||
|
@ -43,6 +43,7 @@ FLOWS = [
|
|||||||
"met",
|
"met",
|
||||||
"mobile_app",
|
"mobile_app",
|
||||||
"mqtt",
|
"mqtt",
|
||||||
|
"neato",
|
||||||
"nest",
|
"nest",
|
||||||
"notion",
|
"notion",
|
||||||
"opentherm_gw",
|
"opentherm_gw",
|
||||||
|
@ -296,6 +296,9 @@ pyMetno==0.4.6
|
|||||||
# homeassistant.components.blackbird
|
# homeassistant.components.blackbird
|
||||||
pyblackbird==0.5
|
pyblackbird==0.5
|
||||||
|
|
||||||
|
# homeassistant.components.neato
|
||||||
|
pybotvac==0.0.15
|
||||||
|
|
||||||
# homeassistant.components.cast
|
# homeassistant.components.cast
|
||||||
pychromecast==4.0.1
|
pychromecast==4.0.1
|
||||||
|
|
||||||
|
@ -122,6 +122,7 @@ TEST_REQUIREMENTS = (
|
|||||||
"py-canary",
|
"py-canary",
|
||||||
"py17track",
|
"py17track",
|
||||||
"pyblackbird",
|
"pyblackbird",
|
||||||
|
"pybotvac",
|
||||||
"pychromecast",
|
"pychromecast",
|
||||||
"pydeconz",
|
"pydeconz",
|
||||||
"pydispatcher",
|
"pydispatcher",
|
||||||
|
1
tests/components/neato/__init__.py
Normal file
1
tests/components/neato/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Neato component."""
|
129
tests/components/neato/test_config_flow.py
Normal file
129
tests/components/neato/test_config_flow.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
"""Tests for the Neato config flow."""
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.neato import config_flow
|
||||||
|
from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
USERNAME = "myUsername"
|
||||||
|
PASSWORD = "myPassword"
|
||||||
|
VENDOR_NEATO = "neato"
|
||||||
|
VENDOR_VORWERK = "vorwerk"
|
||||||
|
VENDOR_INVALID = "invalid"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="account")
|
||||||
|
def mock_controller_login():
|
||||||
|
"""Mock a successful login."""
|
||||||
|
with patch("pybotvac.Account", return_value=True):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def init_config_flow(hass):
|
||||||
|
"""Init a configuration flow."""
|
||||||
|
flow = config_flow.NeatoConfigFlow()
|
||||||
|
flow.hass = hass
|
||||||
|
return flow
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user(hass, account):
|
||||||
|
"""Test user config."""
|
||||||
|
flow = init_config_flow(hass)
|
||||||
|
|
||||||
|
result = await flow.async_step_user()
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await flow.async_step_user(
|
||||||
|
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == USERNAME
|
||||||
|
assert result["data"][CONF_USERNAME] == USERNAME
|
||||||
|
assert result["data"][CONF_PASSWORD] == PASSWORD
|
||||||
|
assert result["data"][CONF_VENDOR] == VENDOR_NEATO
|
||||||
|
|
||||||
|
result = await flow.async_step_user(
|
||||||
|
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_VORWERK}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == USERNAME
|
||||||
|
assert result["data"][CONF_USERNAME] == USERNAME
|
||||||
|
assert result["data"][CONF_PASSWORD] == PASSWORD
|
||||||
|
assert result["data"][CONF_VENDOR] == VENDOR_VORWERK
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import(hass, account):
|
||||||
|
"""Test import step."""
|
||||||
|
flow = init_config_flow(hass)
|
||||||
|
|
||||||
|
result = await flow.async_step_import(
|
||||||
|
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == f"{USERNAME} (from configuration)"
|
||||||
|
assert result["data"][CONF_USERNAME] == USERNAME
|
||||||
|
assert result["data"][CONF_PASSWORD] == PASSWORD
|
||||||
|
assert result["data"][CONF_VENDOR] == VENDOR_NEATO
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abort_if_already_setup(hass, account):
|
||||||
|
"""Test we abort if Neato is already setup."""
|
||||||
|
flow = init_config_flow(hass)
|
||||||
|
MockConfigEntry(
|
||||||
|
domain=NEATO_DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_VENDOR: VENDOR_NEATO,
|
||||||
|
},
|
||||||
|
).add_to_hass(hass)
|
||||||
|
|
||||||
|
# Should fail, same USERNAME (import)
|
||||||
|
result = await flow.async_step_import(
|
||||||
|
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
# Should fail, same USERNAME (flow)
|
||||||
|
result = await flow.async_step_user(
|
||||||
|
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abort_on_invalid_credentials(hass):
|
||||||
|
"""Test when we have invalid credentials."""
|
||||||
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
|
flow = init_config_flow(hass)
|
||||||
|
|
||||||
|
with patch("pybotvac.Account", side_effect=HTTPError()):
|
||||||
|
result = await flow.async_step_user(
|
||||||
|
{
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_VENDOR: VENDOR_NEATO,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "invalid_credentials"}
|
||||||
|
|
||||||
|
result = await flow.async_step_import(
|
||||||
|
{
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_VENDOR: VENDOR_NEATO,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "invalid_credentials"
|
70
tests/components/neato/test_init.py
Normal file
70
tests/components/neato/test_init.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
"""Tests for the Neato init file."""
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
USERNAME = "myUsername"
|
||||||
|
PASSWORD = "myPassword"
|
||||||
|
VENDOR_NEATO = "neato"
|
||||||
|
VENDOR_VORWERK = "vorwerk"
|
||||||
|
VENDOR_INVALID = "invalid"
|
||||||
|
|
||||||
|
VALID_CONFIG = {
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_VENDOR: VENDOR_NEATO,
|
||||||
|
}
|
||||||
|
|
||||||
|
INVALID_CONFIG = {
|
||||||
|
CONF_USERNAME: USERNAME,
|
||||||
|
CONF_PASSWORD: PASSWORD,
|
||||||
|
CONF_VENDOR: VENDOR_INVALID,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="account")
|
||||||
|
def mock_controller_login():
|
||||||
|
"""Mock a successful login."""
|
||||||
|
with patch("pybotvac.Account", return_value=True):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_config_entry(hass):
|
||||||
|
"""There is nothing in configuration.yaml."""
|
||||||
|
res = await async_setup_component(hass, NEATO_DOMAIN, {})
|
||||||
|
assert res is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_entries_in_sync(hass, account):
|
||||||
|
"""The config entry and configuration.yaml are in sync."""
|
||||||
|
MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass)
|
||||||
|
|
||||||
|
assert hass.config_entries.async_entries(NEATO_DOMAIN)
|
||||||
|
assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entries = hass.config_entries.async_entries(NEATO_DOMAIN)
|
||||||
|
assert entries
|
||||||
|
assert entries[0].data[CONF_USERNAME] == USERNAME
|
||||||
|
assert entries[0].data[CONF_PASSWORD] == PASSWORD
|
||||||
|
assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_entries_not_in_sync(hass, account):
|
||||||
|
"""The config entry and configuration.yaml are not in sync."""
|
||||||
|
MockConfigEntry(domain=NEATO_DOMAIN, data=INVALID_CONFIG).add_to_hass(hass)
|
||||||
|
|
||||||
|
assert hass.config_entries.async_entries(NEATO_DOMAIN)
|
||||||
|
assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entries = hass.config_entries.async_entries(NEATO_DOMAIN)
|
||||||
|
assert entries
|
||||||
|
assert entries[0].data[CONF_USERNAME] == USERNAME
|
||||||
|
assert entries[0].data[CONF_PASSWORD] == PASSWORD
|
||||||
|
assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO
|
Loading…
x
Reference in New Issue
Block a user