Skip to content

Commit

Permalink
Add config flow for rachio
Browse files Browse the repository at this point in the history
Also discoverable via homekit
  • Loading branch information
bdraco committed Mar 13, 2020
1 parent fbd8a56 commit 419f089
Show file tree
Hide file tree
Showing 13 changed files with 488 additions and 66 deletions.
24 changes: 24 additions & 0 deletions homeassistant/components/rachio/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"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.",
"hass_url_override": "If your instance is unaware of its actual web location (base_url).",
"manual_run_mins": "For how long, in minutes, to turn on a station when the switch is enabled."
}
}
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured"
}
}
}
130 changes: 91 additions & 39 deletions homeassistant/components/rachio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
"""Integration with the Rachio Iro sprinkler system controller."""
import asyncio
import http.client
import logging
import secrets
import ssl
from typing import Optional

from aiohttp import web
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

_LOGGER = logging.getLogger(__name__)
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,
)

DOMAIN = "rachio"
_LOGGER = logging.getLogger(__name__)

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(
Expand All @@ -39,23 +55,6 @@
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"
Expand Down Expand Up @@ -102,28 +101,62 @@
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."""

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


# Listen for incoming webhook connections
hass.http.register_view(RachioWebhookView())
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up the Rachio config entry."""

config = entry.data

# 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

# 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, config)
# Yes we really do get all these exceptions (hopefully rachiopy switches to requests)
except (http.client.HTTPException, ssl.SSLError, OSError, AssertionError) 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:
Expand All @@ -132,11 +165,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))

# 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

Expand Down Expand Up @@ -200,6 +237,8 @@ def __init__(self, hass, rachio, data, webhooks):
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
Expand Down Expand Up @@ -256,6 +295,16 @@ def controller_id(self) -> str:
"""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."""
Expand Down Expand Up @@ -307,15 +356,18 @@ class RachioWebhookView(HomeAssistantView):
url = WEBHOOK_PATH
name = url[1:].replace("/", ":")

@asyncio.coroutine
def __init__(self, entry_id):
"""Initialize the instance of the view."""
self._entry_id = entry_id

async def post(self, request) -> web.Response:
"""Handle webhook calls from the server."""
hass = request.app["hass"]
data = await request.json()

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)

Expand Down
38 changes: 30 additions & 8 deletions homeassistant/components/rachio/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,33 @@
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))
for controller in hass.data[DOMAIN_RACHIO][config_entry.entry_id].controllers:
sensor = await hass.async_add_executor_job(
RachioControllerOnlineBinarySensor, hass, controller
)
devices.append(sensor)

add_entities(devices)
async_add_entities(devices)
_LOGGER.info("%d Rachio binary sensor(s) added", len(devices))


Expand Down Expand Up @@ -70,6 +76,22 @@ def _poll_update(self, data=None) -> bool:
"""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.controller_id,
self._controller.serial_number,
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."""
Expand Down

0 comments on commit 419f089

Please sign in to comment.