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
17 changed files with 691 additions and 190 deletions
|
@ -420,7 +420,9 @@ omit =
|
|||
homeassistant/components/n26/*
|
||||
homeassistant/components/nad/media_player.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/nello/lock.py
|
||||
homeassistant/components/nest/*
|
||||
|
|
|
@ -187,6 +187,7 @@ homeassistant/components/mpd/* @fabaff
|
|||
homeassistant/components/mqtt/* @home-assistant/core
|
||||
homeassistant/components/mysensors/* @MartinHjelmare
|
||||
homeassistant/components/mystrom/* @fabaff
|
||||
homeassistant/components/neato/* @dshokouhi @Santobert
|
||||
homeassistant/components/nello/* @pschmitt
|
||||
homeassistant/components/ness_alarm/* @nickw444
|
||||
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."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from urllib.error import HTTPError
|
||||
|
||||
from requests.exceptions import HTTPError, ConnectionError as ConnError
|
||||
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.helpers import discovery
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
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__)
|
||||
|
||||
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(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
NEATO_DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_VENDOR, default="neato"): vol.In(
|
||||
["neato", "vorwerk"]
|
||||
),
|
||||
vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS),
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""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
|
||||
|
||||
if config[DOMAIN][CONF_VENDOR] == "neato":
|
||||
hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Neato)
|
||||
elif config[DOMAIN][CONF_VENDOR] == "vorwerk":
|
||||
hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Vorwerk)
|
||||
if entry.data[CONF_VENDOR] == "neato":
|
||||
hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Neato)
|
||||
elif entry.data[CONF_VENDOR] == "vorwerk":
|
||||
hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Vorwerk)
|
||||
|
||||
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")
|
||||
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
|
||||
|
||||
|
||||
|
@ -202,12 +133,8 @@ class NeatoHub:
|
|||
self._hass = hass
|
||||
self._vendor = vendor
|
||||
|
||||
self.my_neato = neato(
|
||||
domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD], vendor
|
||||
)
|
||||
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
|
||||
self.my_neato = None
|
||||
self.logged_in = False
|
||||
|
||||
def login(self):
|
||||
"""Login to My Neato."""
|
||||
|
@ -216,10 +143,16 @@ class NeatoHub:
|
|||
self.my_neato = self._neato(
|
||||
self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor
|
||||
)
|
||||
return True
|
||||
except HTTPError:
|
||||
self.logged_in = True
|
||||
except (HTTPError, ConnError):
|
||||
_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))
|
||||
def update_robots(self):
|
||||
|
|
|
@ -4,21 +4,30 @@ import logging
|
|||
|
||||
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__)
|
||||
|
||||
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."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up Neato camera with config entry."""
|
||||
dev = []
|
||||
for robot in hass.data[NEATO_ROBOTS]:
|
||||
if "maps" in robot.traits:
|
||||
dev.append(NeatoCleaningMap(hass, robot))
|
||||
|
||||
if not dev:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Adding robots for cleaning maps %s", dev)
|
||||
add_entities(dev, True)
|
||||
async_add_entities(dev, True)
|
||||
|
||||
|
||||
class NeatoCleaningMap(Camera):
|
||||
|
@ -61,3 +70,8 @@ class NeatoCleaningMap(Camera):
|
|||
def unique_id(self):
|
||||
"""Return unique ID."""
|
||||
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",
|
||||
"name": "Neato",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/neato",
|
||||
"requirements": [
|
||||
"pybotvac==0.0.15"
|
||||
],
|
||||
"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.helpers.entity import ToggleEntity
|
||||
|
||||
from . import NEATO_LOGIN, NEATO_ROBOTS
|
||||
from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -18,14 +18,23 @@ 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."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up Neato switch with config entry."""
|
||||
dev = []
|
||||
for robot in hass.data[NEATO_ROBOTS]:
|
||||
for type_name in SWITCH_TYPES:
|
||||
dev.append(NeatoConnectedSwitch(hass, robot, type_name))
|
||||
|
||||
if not dev:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Adding switches %s", dev)
|
||||
add_entities(dev)
|
||||
async_add_entities(dev, True)
|
||||
|
||||
|
||||
class NeatoConnectedSwitch(ToggleEntity):
|
||||
|
@ -37,14 +46,7 @@ class NeatoConnectedSwitch(ToggleEntity):
|
|||
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._state = None
|
||||
self._schedule_state = None
|
||||
self._clean_state = None
|
||||
self._robot_serial = self.robot.serial
|
||||
|
@ -94,6 +96,11 @@ class NeatoConnectedSwitch(ToggleEntity):
|
|||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Device info for neato robot."""
|
||||
return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}}
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on."""
|
||||
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
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
|
||||
from . import (
|
||||
from .const import (
|
||||
ACTION,
|
||||
ALERTS,
|
||||
ERRORS,
|
||||
MODE,
|
||||
NEATO_LOGIN,
|
||||
NEATO_DOMAIN,
|
||||
NEATO_MAP_DATA,
|
||||
NEATO_PERSISTENT_MAPS,
|
||||
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."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up Neato vacuum with config entry."""
|
||||
dev = []
|
||||
for robot in hass.data[NEATO_ROBOTS]:
|
||||
dev.append(NeatoConnectedVacuum(hass, robot))
|
||||
|
@ -93,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
return
|
||||
|
||||
_LOGGER.debug("Adding vacuums %s", dev)
|
||||
add_entities(dev, True)
|
||||
async_add_entities(dev, True)
|
||||
|
||||
def neato_custom_cleaning_service(call):
|
||||
"""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]
|
||||
return entities
|
||||
|
||||
hass.services.register(
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_NEATO_CUSTOM_CLEANING,
|
||||
neato_custom_cleaning_service,
|
||||
|
@ -144,10 +150,14 @@ class NeatoConnectedVacuum(StateVacuumDevice):
|
|||
self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS]
|
||||
self._robot_boundaries = {}
|
||||
self._robot_has_map = self.robot.has_persistent_maps
|
||||
self._robot_stats = None
|
||||
|
||||
def update(self):
|
||||
"""Update the states of Neato Vacuums."""
|
||||
_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()
|
||||
try:
|
||||
self._state = self.robot.state
|
||||
|
@ -290,6 +300,17 @@ class NeatoConnectedVacuum(StateVacuumDevice):
|
|||
|
||||
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):
|
||||
"""Start cleaning or resume cleaning."""
|
||||
if self._state["state"] == 1:
|
||||
|
|
|
@ -43,6 +43,7 @@ FLOWS = [
|
|||
"met",
|
||||
"mobile_app",
|
||||
"mqtt",
|
||||
"neato",
|
||||
"nest",
|
||||
"notion",
|
||||
"opentherm_gw",
|
||||
|
|
|
@ -296,6 +296,9 @@ pyMetno==0.4.6
|
|||
# homeassistant.components.blackbird
|
||||
pyblackbird==0.5
|
||||
|
||||
# homeassistant.components.neato
|
||||
pybotvac==0.0.15
|
||||
|
||||
# homeassistant.components.cast
|
||||
pychromecast==4.0.1
|
||||
|
||||
|
|
|
@ -122,6 +122,7 @@ TEST_REQUIREMENTS = (
|
|||
"py-canary",
|
||||
"py17track",
|
||||
"pyblackbird",
|
||||
"pybotvac",
|
||||
"pychromecast",
|
||||
"pydeconz",
|
||||
"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…
Add table
Reference in a new issue