mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +00:00
Add config flow for rachio (#32757)
* Do not fail when a user has a controller with shared access on their account * Add config flow for rachio Also discoverable via homekit * Update homeassistant/components/rachio/switch.py Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io> * Split setting the default run time to an options flow Ensue the run time coming from yaml gets imported into the option flow Only get the schedule once at setup instead of each zone (was hitting rate limits) Add the config entry id to the end of the webhook so there is a unique hook per config entry Breakout the slew of exceptions rachiopy can throw into RachioAPIExceptions Remove the base url override as an option for the config flow Switch identifer for device_info to serial number Add connections to device_info (mac address) * rename to make pylint happy * Fix import of custom_url * claim rachio Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
743166d284
commit
7737387efe
@ -286,6 +286,7 @@ homeassistant/components/qnap/* @colinodell
|
|||||||
homeassistant/components/quantum_gateway/* @cisasteelersfan
|
homeassistant/components/quantum_gateway/* @cisasteelersfan
|
||||||
homeassistant/components/qvr_pro/* @oblogic7
|
homeassistant/components/qvr_pro/* @oblogic7
|
||||||
homeassistant/components/qwikswitch/* @kellerza
|
homeassistant/components/qwikswitch/* @kellerza
|
||||||
|
homeassistant/components/rachio/* @bdraco
|
||||||
homeassistant/components/rainbird/* @konikvranik
|
homeassistant/components/rainbird/* @konikvranik
|
||||||
homeassistant/components/raincloud/* @vanstinator
|
homeassistant/components/raincloud/* @vanstinator
|
||||||
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
|
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
|
||||||
|
31
homeassistant/components/rachio/.translations/en.json
Normal file
31
homeassistant/components/rachio/.translations/en.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Rachio",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Connect to your Rachio device",
|
||||||
|
"description" : "You will need the API Key from https://app.rach.io/. Select 'Account Settings, and then click on 'GET API KEY'.",
|
||||||
|
"data": {
|
||||||
|
"api_key": "The API key for the Rachio account."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect, please try again",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"manual_run_mins": "For how long, in minutes, to turn on a station when the switch is enabled."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,21 +9,36 @@ from rachiopy import Rachio
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API
|
from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API
|
||||||
from homeassistant.helpers import config_validation as cv, discovery
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_CUSTOM_URL,
|
||||||
|
CONF_MANUAL_RUN_MINS,
|
||||||
|
DEFAULT_MANUAL_RUN_MINS,
|
||||||
|
DOMAIN,
|
||||||
|
KEY_DEVICES,
|
||||||
|
KEY_ENABLED,
|
||||||
|
KEY_EXTERNAL_ID,
|
||||||
|
KEY_ID,
|
||||||
|
KEY_MAC_ADDRESS,
|
||||||
|
KEY_NAME,
|
||||||
|
KEY_SERIAL_NUMBER,
|
||||||
|
KEY_STATUS,
|
||||||
|
KEY_TYPE,
|
||||||
|
KEY_USERNAME,
|
||||||
|
KEY_ZONES,
|
||||||
|
RACHIO_API_EXCEPTIONS,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "rachio"
|
|
||||||
|
|
||||||
SUPPORTED_DOMAINS = ["switch", "binary_sensor"]
|
SUPPORTED_DOMAINS = ["switch", "binary_sensor"]
|
||||||
|
|
||||||
# Manual run length
|
|
||||||
CONF_MANUAL_RUN_MINS = "manual_run_mins"
|
|
||||||
DEFAULT_MANUAL_RUN_MINS = 10
|
|
||||||
CONF_CUSTOM_URL = "hass_url_override"
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
DOMAIN: vol.Schema(
|
DOMAIN: vol.Schema(
|
||||||
@ -39,23 +54,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Keys used in the API JSON
|
|
||||||
KEY_DEVICE_ID = "deviceId"
|
|
||||||
KEY_DEVICES = "devices"
|
|
||||||
KEY_ENABLED = "enabled"
|
|
||||||
KEY_EXTERNAL_ID = "externalId"
|
|
||||||
KEY_ID = "id"
|
|
||||||
KEY_NAME = "name"
|
|
||||||
KEY_ON = "on"
|
|
||||||
KEY_STATUS = "status"
|
|
||||||
KEY_SUBTYPE = "subType"
|
|
||||||
KEY_SUMMARY = "summary"
|
|
||||||
KEY_TYPE = "type"
|
|
||||||
KEY_URL = "url"
|
|
||||||
KEY_USERNAME = "username"
|
|
||||||
KEY_ZONE_ID = "zoneId"
|
|
||||||
KEY_ZONE_NUMBER = "zoneNumber"
|
|
||||||
KEY_ZONES = "zones"
|
|
||||||
|
|
||||||
STATUS_ONLINE = "ONLINE"
|
STATUS_ONLINE = "ONLINE"
|
||||||
STATUS_OFFLINE = "OFFLINE"
|
STATUS_OFFLINE = "OFFLINE"
|
||||||
@ -102,28 +100,69 @@ SIGNAL_RACHIO_ZONE_UPDATE = SIGNAL_RACHIO_UPDATE + "_zone"
|
|||||||
SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + "_schedule"
|
SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + "_schedule"
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config) -> bool:
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
"""Set up the Rachio component."""
|
"""Set up the rachio component from YAML."""
|
||||||
|
|
||||||
# Listen for incoming webhook connections
|
conf = config.get(DOMAIN)
|
||||||
hass.http.register_view(RachioWebhookView())
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
if not conf:
|
||||||
|
return True
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
|
for component in SUPPORTED_DOMAINS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Set up the Rachio config entry."""
|
||||||
|
|
||||||
|
config = entry.data
|
||||||
|
options = entry.options
|
||||||
|
|
||||||
|
# CONF_MANUAL_RUN_MINS can only come from a yaml import
|
||||||
|
if not options.get(CONF_MANUAL_RUN_MINS) and config.get(CONF_MANUAL_RUN_MINS):
|
||||||
|
options[CONF_MANUAL_RUN_MINS] = config[CONF_MANUAL_RUN_MINS]
|
||||||
|
|
||||||
# Configure API
|
# Configure API
|
||||||
api_key = config[DOMAIN].get(CONF_API_KEY)
|
api_key = config.get(CONF_API_KEY)
|
||||||
rachio = Rachio(api_key)
|
rachio = Rachio(api_key)
|
||||||
|
|
||||||
# Get the URL of this server
|
# Get the URL of this server
|
||||||
custom_url = config[DOMAIN].get(CONF_CUSTOM_URL)
|
custom_url = config.get(CONF_CUSTOM_URL)
|
||||||
hass_url = hass.config.api.base_url if custom_url is None else custom_url
|
hass_url = hass.config.api.base_url if custom_url is None else custom_url
|
||||||
rachio.webhook_auth = secrets.token_hex()
|
rachio.webhook_auth = secrets.token_hex()
|
||||||
rachio.webhook_url = hass_url + WEBHOOK_PATH
|
webhook_url_path = f"{WEBHOOK_PATH}-{entry.entry_id}"
|
||||||
|
rachio.webhook_url = f"{hass_url}{webhook_url_path}"
|
||||||
|
|
||||||
# Get the API user
|
# Get the API user
|
||||||
try:
|
try:
|
||||||
person = RachioPerson(hass, rachio, config[DOMAIN])
|
person = await hass.async_add_executor_job(RachioPerson, hass, rachio, entry)
|
||||||
except AssertionError as error:
|
# Yes we really do get all these exceptions (hopefully rachiopy switches to requests)
|
||||||
|
# and there is not a reasonable timeout here so it can block for a long time
|
||||||
|
except RACHIO_API_EXCEPTIONS as error:
|
||||||
_LOGGER.error("Could not reach the Rachio API: %s", error)
|
_LOGGER.error("Could not reach the Rachio API: %s", error)
|
||||||
return False
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
# Check for Rachio controller devices
|
# Check for Rachio controller devices
|
||||||
if not person.controllers:
|
if not person.controllers:
|
||||||
@ -132,11 +171,15 @@ def setup(hass, config) -> bool:
|
|||||||
_LOGGER.info("%d Rachio device(s) found", len(person.controllers))
|
_LOGGER.info("%d Rachio device(s) found", len(person.controllers))
|
||||||
|
|
||||||
# Enable component
|
# Enable component
|
||||||
hass.data[DOMAIN] = person
|
hass.data[DOMAIN][entry.entry_id] = person
|
||||||
|
|
||||||
|
# Listen for incoming webhook connections after the data is there
|
||||||
|
hass.http.register_view(RachioWebhookView(entry.entry_id, webhook_url_path))
|
||||||
|
|
||||||
# Load platforms
|
|
||||||
for component in SUPPORTED_DOMAINS:
|
for component in SUPPORTED_DOMAINS:
|
||||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -144,12 +187,12 @@ def setup(hass, config) -> bool:
|
|||||||
class RachioPerson:
|
class RachioPerson:
|
||||||
"""Represent a Rachio user."""
|
"""Represent a Rachio user."""
|
||||||
|
|
||||||
def __init__(self, hass, rachio, config):
|
def __init__(self, hass, rachio, config_entry):
|
||||||
"""Create an object from the provided API instance."""
|
"""Create an object from the provided API instance."""
|
||||||
# Use API token to get user ID
|
# Use API token to get user ID
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self.rachio = rachio
|
self.rachio = rachio
|
||||||
self.config = config
|
self.config_entry = config_entry
|
||||||
|
|
||||||
response = rachio.person.getInfo()
|
response = rachio.person.getInfo()
|
||||||
assert int(response[0][KEY_STATUS]) == 200, "API key error"
|
assert int(response[0][KEY_STATUS]) == 200, "API key error"
|
||||||
@ -200,6 +243,8 @@ class RachioIro:
|
|||||||
self.rachio = rachio
|
self.rachio = rachio
|
||||||
self._id = data[KEY_ID]
|
self._id = data[KEY_ID]
|
||||||
self._name = data[KEY_NAME]
|
self._name = data[KEY_NAME]
|
||||||
|
self._serial_number = data[KEY_SERIAL_NUMBER]
|
||||||
|
self._mac_address = data[KEY_MAC_ADDRESS]
|
||||||
self._zones = data[KEY_ZONES]
|
self._zones = data[KEY_ZONES]
|
||||||
self._init_data = data
|
self._init_data = data
|
||||||
self._webhooks = webhooks
|
self._webhooks = webhooks
|
||||||
@ -256,6 +301,16 @@ class RachioIro:
|
|||||||
"""Return the Rachio API controller ID."""
|
"""Return the Rachio API controller ID."""
|
||||||
return self._id
|
return self._id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial_number(self) -> str:
|
||||||
|
"""Return the Rachio API controller serial number."""
|
||||||
|
return self._serial_number
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mac_address(self) -> str:
|
||||||
|
"""Return the Rachio API controller mac address."""
|
||||||
|
return self._mac_address
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the user-defined name of the controller."""
|
"""Return the user-defined name of the controller."""
|
||||||
@ -304,10 +359,14 @@ class RachioWebhookView(HomeAssistantView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
requires_auth = False # Handled separately
|
requires_auth = False # Handled separately
|
||||||
url = WEBHOOK_PATH
|
|
||||||
name = url[1:].replace("/", ":")
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
def __init__(self, entry_id, webhook_url):
|
||||||
|
"""Initialize the instance of the view."""
|
||||||
|
self._entry_id = entry_id
|
||||||
|
self.url = webhook_url
|
||||||
|
self.name = webhook_url[1:].replace("/", ":")
|
||||||
|
_LOGGER.debug("Created webhook at url: %s, with name %s", self.url, self.name)
|
||||||
|
|
||||||
async def post(self, request) -> web.Response:
|
async def post(self, request) -> web.Response:
|
||||||
"""Handle webhook calls from the server."""
|
"""Handle webhook calls from the server."""
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
@ -315,7 +374,7 @@ class RachioWebhookView(HomeAssistantView):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
auth = data.get(KEY_EXTERNAL_ID, str()).split(":")[1]
|
auth = data.get(KEY_EXTERNAL_ID, str()).split(":")[1]
|
||||||
assert auth == hass.data[DOMAIN].rachio.webhook_auth
|
assert auth == hass.data[DOMAIN][self._entry_id].rachio.webhook_auth
|
||||||
except (AssertionError, IndexError):
|
except (AssertionError, IndexError):
|
||||||
return web.Response(status=web.HTTPForbidden.status_code)
|
return web.Response(status=web.HTTPForbidden.status_code)
|
||||||
|
|
||||||
|
@ -3,33 +3,41 @@ from abc import abstractmethod
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
from homeassistant.helpers import device_registry
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
DOMAIN as DOMAIN_RACHIO,
|
|
||||||
KEY_DEVICE_ID,
|
|
||||||
KEY_STATUS,
|
|
||||||
KEY_SUBTYPE,
|
|
||||||
SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||||
STATUS_OFFLINE,
|
STATUS_OFFLINE,
|
||||||
STATUS_ONLINE,
|
STATUS_ONLINE,
|
||||||
SUBTYPE_OFFLINE,
|
SUBTYPE_OFFLINE,
|
||||||
SUBTYPE_ONLINE,
|
SUBTYPE_ONLINE,
|
||||||
)
|
)
|
||||||
|
from .const import (
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DOMAIN as DOMAIN_RACHIO,
|
||||||
|
KEY_DEVICE_ID,
|
||||||
|
KEY_STATUS,
|
||||||
|
KEY_SUBTYPE,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the Rachio binary sensors."""
|
"""Set up the Rachio binary sensors."""
|
||||||
devices = []
|
devices = await hass.async_add_executor_job(_create_devices, hass, config_entry)
|
||||||
for controller in hass.data[DOMAIN_RACHIO].controllers:
|
async_add_entities(devices)
|
||||||
devices.append(RachioControllerOnlineBinarySensor(hass, controller))
|
|
||||||
|
|
||||||
add_entities(devices)
|
|
||||||
_LOGGER.info("%d Rachio binary sensor(s) added", len(devices))
|
_LOGGER.info("%d Rachio binary sensor(s) added", len(devices))
|
||||||
|
|
||||||
|
|
||||||
|
def _create_devices(hass, config_entry):
|
||||||
|
devices = []
|
||||||
|
for controller in hass.data[DOMAIN_RACHIO][config_entry.entry_id].controllers:
|
||||||
|
devices.append(RachioControllerOnlineBinarySensor(hass, controller))
|
||||||
|
return devices
|
||||||
|
|
||||||
|
|
||||||
class RachioControllerBinarySensor(BinarySensorDevice):
|
class RachioControllerBinarySensor(BinarySensorDevice):
|
||||||
"""Represent a binary sensor that reflects a Rachio state."""
|
"""Represent a binary sensor that reflects a Rachio state."""
|
||||||
|
|
||||||
@ -70,6 +78,18 @@ class RachioControllerBinarySensor(BinarySensorDevice):
|
|||||||
"""Request the state from the API."""
|
"""Request the state from the API."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device_info of the device."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN_RACHIO, self._controller.serial_number,)},
|
||||||
|
"connections": {
|
||||||
|
(device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,)
|
||||||
|
},
|
||||||
|
"name": self._controller.name,
|
||||||
|
"manufacturer": DEFAULT_NAME,
|
||||||
|
}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _handle_update(self, *args, **kwargs) -> None:
|
def _handle_update(self, *args, **kwargs) -> None:
|
||||||
"""Handle an update to the state of this sensor."""
|
"""Handle an update to the state of this sensor."""
|
||||||
|
127
homeassistant/components/rachio/config_flow.py
Normal file
127
homeassistant/components/rachio/config_flow.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""Config flow for Rachio integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from rachiopy import Rachio
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core, exceptions
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_MANUAL_RUN_MINS,
|
||||||
|
DEFAULT_MANUAL_RUN_MINS,
|
||||||
|
KEY_ID,
|
||||||
|
KEY_STATUS,
|
||||||
|
KEY_USERNAME,
|
||||||
|
RACHIO_API_EXCEPTIONS,
|
||||||
|
)
|
||||||
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(hass: core.HomeAssistant, data):
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||||
|
"""
|
||||||
|
rachio = Rachio(data[CONF_API_KEY])
|
||||||
|
username = None
|
||||||
|
try:
|
||||||
|
data = await hass.async_add_executor_job(rachio.person.getInfo)
|
||||||
|
_LOGGER.debug("rachio.person.getInfo: %s", data)
|
||||||
|
if int(data[0][KEY_STATUS]) != 200:
|
||||||
|
raise InvalidAuth
|
||||||
|
|
||||||
|
rachio_id = data[1][KEY_ID]
|
||||||
|
data = await hass.async_add_executor_job(rachio.person.get, rachio_id)
|
||||||
|
_LOGGER.debug("rachio.person.get: %s", data)
|
||||||
|
if int(data[0][KEY_STATUS]) != 200:
|
||||||
|
raise CannotConnect
|
||||||
|
|
||||||
|
username = data[1][KEY_USERNAME]
|
||||||
|
# Yes we really do get all these exceptions (hopefully rachiopy switches to requests)
|
||||||
|
except RACHIO_API_EXCEPTIONS as error:
|
||||||
|
_LOGGER.error("Could not reach the Rachio API: %s", error)
|
||||||
|
raise CannotConnect
|
||||||
|
|
||||||
|
# Return info that you want to store in the config entry.
|
||||||
|
return {"title": username}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Rachio."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
_LOGGER.debug("async_step_user: %s", user_input)
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
info = await validate_input(self.hass, user_input)
|
||||||
|
await self.async_set_unique_id(user_input[CONF_API_KEY])
|
||||||
|
return self.async_create_entry(title=info["title"], data=user_input)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_homekit(self, homekit_info):
|
||||||
|
"""Handle HomeKit discovery."""
|
||||||
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
async def async_step_import(self, user_input):
|
||||||
|
"""Handle import."""
|
||||||
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Handle a option flow for Rachio."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle options flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MANUAL_RUN_MINS,
|
||||||
|
default=self.config_entry.options.get(
|
||||||
|
CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
|
||||||
|
),
|
||||||
|
): int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAuth(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
42
homeassistant/components/rachio/const.py
Normal file
42
homeassistant/components/rachio/const.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""Constants for rachio."""
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
DEFAULT_NAME = "Rachio"
|
||||||
|
|
||||||
|
DOMAIN = "rachio"
|
||||||
|
|
||||||
|
CONF_CUSTOM_URL = "hass_url_override"
|
||||||
|
# Manual run length
|
||||||
|
CONF_MANUAL_RUN_MINS = "manual_run_mins"
|
||||||
|
DEFAULT_MANUAL_RUN_MINS = 10
|
||||||
|
|
||||||
|
# Keys used in the API JSON
|
||||||
|
KEY_DEVICE_ID = "deviceId"
|
||||||
|
KEY_IMAGE_URL = "imageUrl"
|
||||||
|
KEY_DEVICES = "devices"
|
||||||
|
KEY_ENABLED = "enabled"
|
||||||
|
KEY_EXTERNAL_ID = "externalId"
|
||||||
|
KEY_ID = "id"
|
||||||
|
KEY_NAME = "name"
|
||||||
|
KEY_ON = "on"
|
||||||
|
KEY_STATUS = "status"
|
||||||
|
KEY_SUBTYPE = "subType"
|
||||||
|
KEY_SUMMARY = "summary"
|
||||||
|
KEY_SERIAL_NUMBER = "serialNumber"
|
||||||
|
KEY_MAC_ADDRESS = "macAddress"
|
||||||
|
KEY_TYPE = "type"
|
||||||
|
KEY_URL = "url"
|
||||||
|
KEY_USERNAME = "username"
|
||||||
|
KEY_ZONE_ID = "zoneId"
|
||||||
|
KEY_ZONE_NUMBER = "zoneNumber"
|
||||||
|
KEY_ZONES = "zones"
|
||||||
|
|
||||||
|
# Yes we really do get all these exceptions (hopefully rachiopy switches to requests)
|
||||||
|
RACHIO_API_EXCEPTIONS = (
|
||||||
|
http.client.HTTPException,
|
||||||
|
ssl.SSLError,
|
||||||
|
OSError,
|
||||||
|
AssertionError,
|
||||||
|
)
|
@ -2,7 +2,17 @@
|
|||||||
"domain": "rachio",
|
"domain": "rachio",
|
||||||
"name": "Rachio",
|
"name": "Rachio",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/rachio",
|
"documentation": "https://www.home-assistant.io/integrations/rachio",
|
||||||
"requirements": ["rachiopy==0.1.3"],
|
"requirements": [
|
||||||
"dependencies": ["http"],
|
"rachiopy==0.1.3"
|
||||||
"codeowners": []
|
],
|
||||||
|
"dependencies": [
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"codeowners": ["@bdraco"],
|
||||||
|
"config_flow": true,
|
||||||
|
"homekit": {
|
||||||
|
"models": [
|
||||||
|
"Rachio"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
31
homeassistant/components/rachio/strings.json
Normal file
31
homeassistant/components/rachio/strings.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Rachio",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Connect to your Rachio device",
|
||||||
|
"description" : "You will need the API Key from https://app.rach.io/. Select 'Account Settings, and then click on 'GET API KEY'.",
|
||||||
|
"data": {
|
||||||
|
"api_key": "The API key for the Rachio account."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect, please try again",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"manual_run_mins": "For how long, in minutes, to turn on a station when the switch is enabled."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,20 +4,10 @@ from datetime import timedelta
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
|
from homeassistant.helpers import device_registry
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
CONF_MANUAL_RUN_MINS,
|
|
||||||
DOMAIN as DOMAIN_RACHIO,
|
|
||||||
KEY_DEVICE_ID,
|
|
||||||
KEY_ENABLED,
|
|
||||||
KEY_ID,
|
|
||||||
KEY_NAME,
|
|
||||||
KEY_ON,
|
|
||||||
KEY_SUBTYPE,
|
|
||||||
KEY_SUMMARY,
|
|
||||||
KEY_ZONE_ID,
|
|
||||||
KEY_ZONE_NUMBER,
|
|
||||||
SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||||
SIGNAL_RACHIO_ZONE_UPDATE,
|
SIGNAL_RACHIO_ZONE_UPDATE,
|
||||||
SUBTYPE_SLEEP_MODE_OFF,
|
SUBTYPE_SLEEP_MODE_OFF,
|
||||||
@ -26,6 +16,22 @@ from . import (
|
|||||||
SUBTYPE_ZONE_STARTED,
|
SUBTYPE_ZONE_STARTED,
|
||||||
SUBTYPE_ZONE_STOPPED,
|
SUBTYPE_ZONE_STOPPED,
|
||||||
)
|
)
|
||||||
|
from .const import (
|
||||||
|
CONF_MANUAL_RUN_MINS,
|
||||||
|
DEFAULT_MANUAL_RUN_MINS,
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DOMAIN as DOMAIN_RACHIO,
|
||||||
|
KEY_DEVICE_ID,
|
||||||
|
KEY_ENABLED,
|
||||||
|
KEY_ID,
|
||||||
|
KEY_IMAGE_URL,
|
||||||
|
KEY_NAME,
|
||||||
|
KEY_ON,
|
||||||
|
KEY_SUBTYPE,
|
||||||
|
KEY_SUMMARY,
|
||||||
|
KEY_ZONE_ID,
|
||||||
|
KEY_ZONE_NUMBER,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -33,25 +39,30 @@ ATTR_ZONE_SUMMARY = "Summary"
|
|||||||
ATTR_ZONE_NUMBER = "Zone number"
|
ATTR_ZONE_NUMBER = "Zone number"
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the Rachio switches."""
|
"""Set up the Rachio switches."""
|
||||||
manual_run_time = timedelta(
|
|
||||||
minutes=hass.data[DOMAIN_RACHIO].config.get(CONF_MANUAL_RUN_MINS)
|
|
||||||
)
|
|
||||||
_LOGGER.info("Rachio run time is %s", str(manual_run_time))
|
|
||||||
|
|
||||||
# Add all zones from all controllers as switches
|
# Add all zones from all controllers as switches
|
||||||
devices = []
|
devices = await hass.async_add_executor_job(_create_devices, hass, config_entry)
|
||||||
for controller in hass.data[DOMAIN_RACHIO].controllers:
|
async_add_entities(devices)
|
||||||
devices.append(RachioStandbySwitch(hass, controller))
|
|
||||||
|
|
||||||
for zone in controller.list_zones():
|
|
||||||
devices.append(RachioZone(hass, controller, zone, manual_run_time))
|
|
||||||
|
|
||||||
add_entities(devices)
|
|
||||||
_LOGGER.info("%d Rachio switch(es) added", len(devices))
|
_LOGGER.info("%d Rachio switch(es) added", len(devices))
|
||||||
|
|
||||||
|
|
||||||
|
def _create_devices(hass, config_entry):
|
||||||
|
devices = []
|
||||||
|
person = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
|
||||||
|
# Fetch the schedule once at startup
|
||||||
|
# in order to avoid every zone doing it
|
||||||
|
for controller in person.controllers:
|
||||||
|
devices.append(RachioStandbySwitch(hass, controller))
|
||||||
|
zones = controller.list_zones()
|
||||||
|
current_schedule = controller.current_schedule
|
||||||
|
_LOGGER.debug("Rachio setting up zones: %s", zones)
|
||||||
|
for zone in zones:
|
||||||
|
_LOGGER.debug("Rachio setting up zone: %s", zone)
|
||||||
|
devices.append(RachioZone(hass, person, controller, zone, current_schedule))
|
||||||
|
return devices
|
||||||
|
|
||||||
|
|
||||||
class RachioSwitch(SwitchDevice):
|
class RachioSwitch(SwitchDevice):
|
||||||
"""Represent a Rachio state that can be toggled."""
|
"""Represent a Rachio state that can be toggled."""
|
||||||
|
|
||||||
@ -93,6 +104,18 @@ class RachioSwitch(SwitchDevice):
|
|||||||
# For this device
|
# For this device
|
||||||
self._handle_update(args, kwargs)
|
self._handle_update(args, kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device_info of the device."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN_RACHIO, self._controller.serial_number,)},
|
||||||
|
"connections": {
|
||||||
|
(device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,)
|
||||||
|
},
|
||||||
|
"name": self._controller.name,
|
||||||
|
"manufacturer": DEFAULT_NAME,
|
||||||
|
}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _handle_update(self, *args, **kwargs) -> None:
|
def _handle_update(self, *args, **kwargs) -> None:
|
||||||
"""Handle incoming webhook data."""
|
"""Handle incoming webhook data."""
|
||||||
@ -153,15 +176,18 @@ class RachioStandbySwitch(RachioSwitch):
|
|||||||
class RachioZone(RachioSwitch):
|
class RachioZone(RachioSwitch):
|
||||||
"""Representation of one zone of sprinklers connected to the Rachio Iro."""
|
"""Representation of one zone of sprinklers connected to the Rachio Iro."""
|
||||||
|
|
||||||
def __init__(self, hass, controller, data, manual_run_time):
|
def __init__(self, hass, person, controller, data, current_schedule):
|
||||||
"""Initialize a new Rachio Zone."""
|
"""Initialize a new Rachio Zone."""
|
||||||
self._id = data[KEY_ID]
|
self._id = data[KEY_ID]
|
||||||
self._zone_name = data[KEY_NAME]
|
self._zone_name = data[KEY_NAME]
|
||||||
self._zone_number = data[KEY_ZONE_NUMBER]
|
self._zone_number = data[KEY_ZONE_NUMBER]
|
||||||
self._zone_enabled = data[KEY_ENABLED]
|
self._zone_enabled = data[KEY_ENABLED]
|
||||||
self._manual_run_time = manual_run_time
|
self._entity_picture = data.get(KEY_IMAGE_URL)
|
||||||
|
self._person = person
|
||||||
self._summary = str()
|
self._summary = str()
|
||||||
super().__init__(controller)
|
self._current_schedule = current_schedule
|
||||||
|
super().__init__(controller, poll=False)
|
||||||
|
self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID)
|
||||||
|
|
||||||
# Listen for all zone updates
|
# Listen for all zone updates
|
||||||
dispatcher_connect(hass, SIGNAL_RACHIO_ZONE_UPDATE, self._handle_update)
|
dispatcher_connect(hass, SIGNAL_RACHIO_ZONE_UPDATE, self._handle_update)
|
||||||
@ -195,6 +221,11 @@ class RachioZone(RachioSwitch):
|
|||||||
"""Return whether the zone is allowed to run."""
|
"""Return whether the zone is allowed to run."""
|
||||||
return self._zone_enabled
|
return self._zone_enabled
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_picture(self):
|
||||||
|
"""Return the entity picture to use in the frontend, if any."""
|
||||||
|
return self._entity_picture
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self) -> dict:
|
def state_attributes(self) -> dict:
|
||||||
"""Return the optional state attributes."""
|
"""Return the optional state attributes."""
|
||||||
@ -206,8 +237,18 @@ class RachioZone(RachioSwitch):
|
|||||||
self.turn_off()
|
self.turn_off()
|
||||||
|
|
||||||
# Start this zone
|
# Start this zone
|
||||||
self._controller.rachio.zone.start(self.zone_id, self._manual_run_time.seconds)
|
manual_run_time = timedelta(
|
||||||
_LOGGER.debug("Watering %s on %s", self.name, self._controller.name)
|
minutes=self._person.config_entry.options.get(
|
||||||
|
CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Watering %s on %s for %s",
|
||||||
|
self.name,
|
||||||
|
self._controller.name,
|
||||||
|
str(manual_run_time),
|
||||||
|
)
|
||||||
|
|
||||||
def turn_off(self, **kwargs) -> None:
|
def turn_off(self, **kwargs) -> None:
|
||||||
"""Stop watering all zones."""
|
"""Stop watering all zones."""
|
||||||
@ -215,8 +256,8 @@ class RachioZone(RachioSwitch):
|
|||||||
|
|
||||||
def _poll_update(self, data=None) -> bool:
|
def _poll_update(self, data=None) -> bool:
|
||||||
"""Poll the API to check whether the zone is running."""
|
"""Poll the API to check whether the zone is running."""
|
||||||
schedule = self._controller.current_schedule
|
self._current_schedule = self._controller.current_schedule
|
||||||
return self.zone_id == schedule.get(KEY_ZONE_ID)
|
return self.zone_id == self._current_schedule.get(KEY_ZONE_ID)
|
||||||
|
|
||||||
def _handle_update(self, *args, **kwargs) -> None:
|
def _handle_update(self, *args, **kwargs) -> None:
|
||||||
"""Handle incoming webhook zone data."""
|
"""Handle incoming webhook zone data."""
|
||||||
|
@ -80,6 +80,7 @@ FLOWS = [
|
|||||||
"plex",
|
"plex",
|
||||||
"point",
|
"point",
|
||||||
"ps4",
|
"ps4",
|
||||||
|
"rachio",
|
||||||
"rainmachine",
|
"rainmachine",
|
||||||
"ring",
|
"ring",
|
||||||
"samsungtv",
|
"samsungtv",
|
||||||
|
@ -43,6 +43,7 @@ HOMEKIT = {
|
|||||||
"LIFX": "lifx",
|
"LIFX": "lifx",
|
||||||
"Netatmo Relay": "netatmo",
|
"Netatmo Relay": "netatmo",
|
||||||
"Presence": "netatmo",
|
"Presence": "netatmo",
|
||||||
|
"Rachio": "rachio",
|
||||||
"TRADFRI": "tradfri",
|
"TRADFRI": "tradfri",
|
||||||
"Welcome": "netatmo",
|
"Welcome": "netatmo",
|
||||||
"Wemo": "wemo"
|
"Wemo": "wemo"
|
||||||
|
@ -613,6 +613,9 @@ pyvizio==0.1.35
|
|||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.9.2
|
||||||
|
|
||||||
|
# homeassistant.components.rachio
|
||||||
|
rachiopy==0.1.3
|
||||||
|
|
||||||
# homeassistant.components.rainmachine
|
# homeassistant.components.rainmachine
|
||||||
regenmaschine==1.5.1
|
regenmaschine==1.5.1
|
||||||
|
|
||||||
|
1
tests/components/rachio/__init__.py
Normal file
1
tests/components/rachio/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Rachio integration."""
|
104
tests/components/rachio/test_config_flow.py
Normal file
104
tests/components/rachio/test_config_flow.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
"""Test the Rachio config flow."""
|
||||||
|
from asynctest import patch
|
||||||
|
from asynctest.mock import MagicMock
|
||||||
|
|
||||||
|
from homeassistant import config_entries, setup
|
||||||
|
from homeassistant.components.rachio.const import (
|
||||||
|
CONF_CUSTOM_URL,
|
||||||
|
CONF_MANUAL_RUN_MINS,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_rachio_return_value(get=None, getInfo=None):
|
||||||
|
rachio_mock = MagicMock()
|
||||||
|
person_mock = MagicMock()
|
||||||
|
type(person_mock).get = MagicMock(return_value=get)
|
||||||
|
type(person_mock).getInfo = MagicMock(return_value=getInfo)
|
||||||
|
type(rachio_mock).person = person_mock
|
||||||
|
return rachio_mock
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass):
|
||||||
|
"""Test we get the form."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
rachio_mock = _mock_rachio_return_value(
|
||||||
|
get=({"status": 200}, {"username": "myusername"}),
|
||||||
|
getInfo=({"status": 200}, {"id": "myid"}),
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.rachio.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.rachio.async_setup_entry", return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_API_KEY: "api_key",
|
||||||
|
CONF_CUSTOM_URL: "http://custom.url",
|
||||||
|
CONF_MANUAL_RUN_MINS: 5,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == "myusername"
|
||||||
|
assert result2["data"] == {
|
||||||
|
CONF_API_KEY: "api_key",
|
||||||
|
CONF_CUSTOM_URL: "http://custom.url",
|
||||||
|
CONF_MANUAL_RUN_MINS: 5,
|
||||||
|
}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_invalid_auth(hass):
|
||||||
|
"""Test we handle invalid auth."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
rachio_mock = _mock_rachio_return_value(
|
||||||
|
get=({"status": 200}, {"username": "myusername"}),
|
||||||
|
getInfo=({"status": 412}, {"error": "auth fail"}),
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {CONF_API_KEY: "api_key"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
rachio_mock = _mock_rachio_return_value(
|
||||||
|
get=({"status": 599}, {"username": "myusername"}),
|
||||||
|
getInfo=({"status": 200}, {"id": "myid"}),
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {CONF_API_KEY: "api_key"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "cannot_connect"}
|
Loading…
x
Reference in New Issue
Block a user