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:
J. Nick Koston 2020-03-14 00:46:17 -05:00 committed by GitHub
parent 743166d284
commit 7737387efe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 561 additions and 89 deletions

View File

@ -286,6 +286,7 @@ homeassistant/components/qnap/* @colinodell
homeassistant/components/quantum_gateway/* @cisasteelersfan
homeassistant/components/qvr_pro/* @oblogic7
homeassistant/components/qwikswitch/* @kellerza
homeassistant/components/rachio/* @bdraco
homeassistant/components/rainbird/* @konikvranik
homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert

View 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."
}
}
}
}
}

View File

@ -9,21 +9,36 @@ from rachiopy import Rachio
import voluptuous as vol
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.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 .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__)
DOMAIN = "rachio"
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(
{
DOMAIN: vol.Schema(
@ -39,23 +54,6 @@ CONFIG_SCHEMA = vol.Schema(
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_OFFLINE = "OFFLINE"
@ -102,28 +100,69 @@ SIGNAL_RACHIO_ZONE_UPDATE = SIGNAL_RACHIO_UPDATE + "_zone"
SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + "_schedule"
def setup(hass, config) -> bool:
"""Set up the Rachio component."""
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the rachio component from YAML."""
# Listen for incoming webhook connections
hass.http.register_view(RachioWebhookView())
conf = config.get(DOMAIN)
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
api_key = config[DOMAIN].get(CONF_API_KEY)
api_key = config.get(CONF_API_KEY)
rachio = Rachio(api_key)
# 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
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
try:
person = RachioPerson(hass, rachio, config[DOMAIN])
except AssertionError as error:
person = await hass.async_add_executor_job(RachioPerson, hass, rachio, entry)
# 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)
return False
raise ConfigEntryNotReady
# Check for Rachio controller devices
if not person.controllers:
@ -132,11 +171,15 @@ def setup(hass, config) -> bool:
_LOGGER.info("%d Rachio device(s) found", len(person.controllers))
# 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:
discovery.load_platform(hass, component, DOMAIN, {}, config)
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
@ -144,12 +187,12 @@ def setup(hass, config) -> bool:
class RachioPerson:
"""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."""
# Use API token to get user ID
self._hass = hass
self.rachio = rachio
self.config = config
self.config_entry = config_entry
response = rachio.person.getInfo()
assert int(response[0][KEY_STATUS]) == 200, "API key error"
@ -200,6 +243,8 @@ class RachioIro:
self.rachio = rachio
self._id = data[KEY_ID]
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._init_data = data
self._webhooks = webhooks
@ -256,6 +301,16 @@ class RachioIro:
"""Return the Rachio API controller 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
def name(self) -> str:
"""Return the user-defined name of the controller."""
@ -304,10 +359,14 @@ class RachioWebhookView(HomeAssistantView):
}
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:
"""Handle webhook calls from the server."""
hass = request.app["hass"]
@ -315,7 +374,7 @@ class RachioWebhookView(HomeAssistantView):
try:
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):
return web.Response(status=web.HTTPForbidden.status_code)

View File

@ -3,33 +3,41 @@ from abc import abstractmethod
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.helpers import device_registry
from homeassistant.helpers.dispatcher import dispatcher_connect
from . import (
DOMAIN as DOMAIN_RACHIO,
KEY_DEVICE_ID,
KEY_STATUS,
KEY_SUBTYPE,
SIGNAL_RACHIO_CONTROLLER_UPDATE,
STATUS_OFFLINE,
STATUS_ONLINE,
SUBTYPE_OFFLINE,
SUBTYPE_ONLINE,
)
from .const import (
DEFAULT_NAME,
DOMAIN as DOMAIN_RACHIO,
KEY_DEVICE_ID,
KEY_STATUS,
KEY_SUBTYPE,
)
_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."""
devices = []
for controller in hass.data[DOMAIN_RACHIO].controllers:
devices.append(RachioControllerOnlineBinarySensor(hass, controller))
add_entities(devices)
devices = await hass.async_add_executor_job(_create_devices, hass, config_entry)
async_add_entities(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):
"""Represent a binary sensor that reflects a Rachio state."""
@ -70,6 +78,18 @@ class RachioControllerBinarySensor(BinarySensorDevice):
"""Request the state from the API."""
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
def _handle_update(self, *args, **kwargs) -> None:
"""Handle an update to the state of this sensor."""

View 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."""

View 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,
)

View File

@ -2,7 +2,17 @@
"domain": "rachio",
"name": "Rachio",
"documentation": "https://www.home-assistant.io/integrations/rachio",
"requirements": ["rachiopy==0.1.3"],
"dependencies": ["http"],
"codeowners": []
"requirements": [
"rachiopy==0.1.3"
],
"dependencies": [
"http"
],
"codeowners": ["@bdraco"],
"config_flow": true,
"homekit": {
"models": [
"Rachio"
]
}
}

View 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."
}
}
}
}
}

View File

@ -4,20 +4,10 @@ from datetime import timedelta
import logging
from homeassistant.components.switch import SwitchDevice
from homeassistant.helpers import device_registry
from homeassistant.helpers.dispatcher import dispatcher_connect
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_ZONE_UPDATE,
SUBTYPE_SLEEP_MODE_OFF,
@ -26,6 +16,22 @@ from . import (
SUBTYPE_ZONE_STARTED,
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__)
@ -33,25 +39,30 @@ ATTR_ZONE_SUMMARY = "Summary"
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."""
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
devices = []
for controller in hass.data[DOMAIN_RACHIO].controllers:
devices.append(RachioStandbySwitch(hass, controller))
for zone in controller.list_zones():
devices.append(RachioZone(hass, controller, zone, manual_run_time))
add_entities(devices)
devices = await hass.async_add_executor_job(_create_devices, hass, config_entry)
async_add_entities(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):
"""Represent a Rachio state that can be toggled."""
@ -93,6 +104,18 @@ class RachioSwitch(SwitchDevice):
# For this device
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
def _handle_update(self, *args, **kwargs) -> None:
"""Handle incoming webhook data."""
@ -153,15 +176,18 @@ class RachioStandbySwitch(RachioSwitch):
class RachioZone(RachioSwitch):
"""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."""
self._id = data[KEY_ID]
self._zone_name = data[KEY_NAME]
self._zone_number = data[KEY_ZONE_NUMBER]
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()
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
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 self._zone_enabled
@property
def entity_picture(self):
"""Return the entity picture to use in the frontend, if any."""
return self._entity_picture
@property
def state_attributes(self) -> dict:
"""Return the optional state attributes."""
@ -206,8 +237,18 @@ class RachioZone(RachioSwitch):
self.turn_off()
# Start this zone
self._controller.rachio.zone.start(self.zone_id, self._manual_run_time.seconds)
_LOGGER.debug("Watering %s on %s", self.name, self._controller.name)
manual_run_time = timedelta(
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:
"""Stop watering all zones."""
@ -215,8 +256,8 @@ class RachioZone(RachioSwitch):
def _poll_update(self, data=None) -> bool:
"""Poll the API to check whether the zone is running."""
schedule = self._controller.current_schedule
return self.zone_id == schedule.get(KEY_ZONE_ID)
self._current_schedule = self._controller.current_schedule
return self.zone_id == self._current_schedule.get(KEY_ZONE_ID)
def _handle_update(self, *args, **kwargs) -> None:
"""Handle incoming webhook zone data."""

View File

@ -80,6 +80,7 @@ FLOWS = [
"plex",
"point",
"ps4",
"rachio",
"rainmachine",
"ring",
"samsungtv",

View File

@ -43,6 +43,7 @@ HOMEKIT = {
"LIFX": "lifx",
"Netatmo Relay": "netatmo",
"Presence": "netatmo",
"Rachio": "rachio",
"TRADFRI": "tradfri",
"Welcome": "netatmo",
"Wemo": "wemo"

View File

@ -613,6 +613,9 @@ pyvizio==0.1.35
# homeassistant.components.html5
pywebpush==1.9.2
# homeassistant.components.rachio
rachiopy==0.1.3
# homeassistant.components.rainmachine
regenmaschine==1.5.1

View File

@ -0,0 +1 @@
"""Tests for the Rachio integration."""

View 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"}