""" Integration with the Rachio Iro sprinkler system controller. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/rachio/ """ import asyncio import logging from aiohttp import web import voluptuous as vol from typing import Optional from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send REQUIREMENTS = ['rachiopy==0.1.3'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'rachio' CONF_CUSTOM_URL = 'hass_url_override' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_CUSTOM_URL): cv.string, }) }, 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' # Device webhook values TYPE_CONTROLLER_STATUS = 'DEVICE_STATUS' SUBTYPE_OFFLINE = 'OFFLINE' SUBTYPE_ONLINE = 'ONLINE' SUBTYPE_OFFLINE_NOTIFICATION = 'OFFLINE_NOTIFICATION' SUBTYPE_COLD_REBOOT = 'COLD_REBOOT' SUBTYPE_SLEEP_MODE_ON = 'SLEEP_MODE_ON' SUBTYPE_SLEEP_MODE_OFF = 'SLEEP_MODE_OFF' SUBTYPE_BROWNOUT_VALVE = 'BROWNOUT_VALVE' SUBTYPE_RAIN_SENSOR_DETECTION_ON = 'RAIN_SENSOR_DETECTION_ON' SUBTYPE_RAIN_SENSOR_DETECTION_OFF = 'RAIN_SENSOR_DETECTION_OFF' SUBTYPE_RAIN_DELAY_ON = 'RAIN_DELAY_ON' SUBTYPE_RAIN_DELAY_OFF = 'RAIN_DELAY_OFF' # Schedule webhook values TYPE_SCHEDULE_STATUS = 'SCHEDULE_STATUS' SUBTYPE_SCHEDULE_STARTED = 'SCHEDULE_STARTED' SUBTYPE_SCHEDULE_STOPPED = 'SCHEDULE_STOPPED' SUBTYPE_SCHEDULE_COMPLETED = 'SCHEDULE_COMPLETED' SUBTYPE_WEATHER_NO_SKIP = 'WEATHER_INTELLIGENCE_NO_SKIP' SUBTYPE_WEATHER_SKIP = 'WEATHER_INTELLIGENCE_SKIP' SUBTYPE_WEATHER_CLIMATE_SKIP = 'WEATHER_INTELLIGENCE_CLIMATE_SKIP' SUBTYPE_WEATHER_FREEZE = 'WEATHER_INTELLIGENCE_FREEZE' # Zone webhook values TYPE_ZONE_STATUS = 'ZONE_STATUS' SUBTYPE_ZONE_STARTED = 'ZONE_STARTED' SUBTYPE_ZONE_STOPPED = 'ZONE_STOPPED' SUBTYPE_ZONE_COMPLETED = 'ZONE_COMPLETED' SUBTYPE_ZONE_CYCLING = 'ZONE_CYCLING' SUBTYPE_ZONE_CYCLING_COMPLETED = 'ZONE_CYCLING_COMPLETED' # Webhook callbacks LISTEN_EVENT_TYPES = ['DEVICE_STATUS_EVENT', 'ZONE_STATUS_EVENT'] WEBHOOK_CONST_ID = 'homeassistant.rachio:' WEBHOOK_PATH = URL_API + DOMAIN SIGNAL_RACHIO_UPDATE = DOMAIN + '_update' SIGNAL_RACHIO_CONTROLLER_UPDATE = SIGNAL_RACHIO_UPDATE + '_controller' 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.""" from rachiopy import Rachio # Listen for incoming webhook connections hass.http.register_view(RachioWebhookView()) # Configure API api_key = config[DOMAIN].get(CONF_API_KEY) rachio = Rachio(api_key) # Get the URL of this server custom_url = config[DOMAIN].get(CONF_CUSTOM_URL) hass_url = hass.config.api.base_url if custom_url is None else custom_url rachio.webhook_auth = generate_secret() rachio.webhook_url = hass_url + WEBHOOK_PATH # Get the API user try: person = RachioPerson(hass, rachio) except AssertionError as error: _LOGGER.error("Could not reach the Rachio API: %s", error) return False # Check for Rachio controller devices if not person.controllers: _LOGGER.error("No Rachio devices found in account %s", person.username) return False _LOGGER.info("%d Rachio device(s) found", len(person.controllers)) # Enable component hass.data[DOMAIN] = person return True class RachioPerson: """Represent a Rachio user.""" def __init__(self, hass, rachio): """Create an object from the provided API instance.""" # Use API token to get user ID self._hass = hass self.rachio = rachio response = rachio.person.getInfo() assert int(response[0][KEY_STATUS]) == 200, "API key error" self._id = response[1][KEY_ID] # Use user ID to get user data data = rachio.person.get(self._id) assert int(data[0][KEY_STATUS]) == 200, "User ID error" self.username = data[1][KEY_USERNAME] self._controllers = [RachioIro(self._hass, self.rachio, controller) for controller in data[1][KEY_DEVICES]] _LOGGER.info('Using Rachio API as user "%s"', self.username) @property def user_id(self) -> str: """Get the user ID as defined by the Rachio API.""" return self._id @property def controllers(self) -> list: """Get a list of controllers managed by this account.""" return self._controllers class RachioIro: """Represent a Rachio Iro.""" def __init__(self, hass, rachio, data): """Initialize a Rachio device.""" self.hass = hass self.rachio = rachio self._id = data[KEY_ID] self._name = data[KEY_NAME] self._zones = data[KEY_ZONES] self._init_data = data _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) # Listen for all updates self._init_webhooks() def _init_webhooks(self) -> None: """Start getting updates from the Rachio API.""" current_webhook_id = None # First delete any old webhooks that may have stuck around def _deinit_webhooks(event) -> None: """Stop getting updates from the Rachio API.""" webhooks = self.rachio.notification.getDeviceWebhook( self.controller_id)[1] for webhook in webhooks: if webhook[KEY_EXTERNAL_ID].startswith(WEBHOOK_CONST_ID) or\ webhook[KEY_ID] == current_webhook_id: self.rachio.notification.deleteWebhook(webhook[KEY_ID]) _deinit_webhooks(None) # Choose which events to listen for and get their IDs event_types = [] for event_type in self.rachio.notification.getWebhookEventType()[1]: if event_type[KEY_NAME] in LISTEN_EVENT_TYPES: event_types.append({"id": event_type[KEY_ID]}) # Register to listen to these events from the device url = self.rachio.webhook_url auth = WEBHOOK_CONST_ID + self.rachio.webhook_auth new_webhook = self.rachio.notification.postWebhook(self.controller_id, auth, url, event_types) # Save ID for deletion at shutdown current_webhook_id = new_webhook[1][KEY_ID] self.hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _deinit_webhooks) def __str__(self) -> str: """Display the controller as a string.""" return 'Rachio controller "{}"'.format(self.name) @property def controller_id(self) -> str: """Return the Rachio API controller ID.""" return self._id @property def name(self) -> str: """Return the user-defined name of the controller.""" return self._name @property def current_schedule(self) -> str: """Return the schedule that the device is running right now.""" return self.rachio.device.getCurrentSchedule(self.controller_id)[1] @property def init_data(self) -> dict: """Return the information used to set up the controller.""" return self._init_data def list_zones(self, include_disabled=False) -> list: """Return a list of the zone dicts connected to the device.""" # All zones if include_disabled: return self._zones # Only enabled zones return [z for z in self._zones if z[KEY_ENABLED]] def get_zone(self, zone_id) -> Optional[dict]: """Return the zone with the given ID.""" for zone in self.list_zones(include_disabled=True): if zone[KEY_ID] == zone_id: return zone return None def stop_watering(self) -> None: """Stop watering all zones connected to this controller.""" self.rachio.device.stopWater(self.controller_id) _LOGGER.info("Stopped watering of all zones on %s", str(self)) class RachioWebhookView(HomeAssistantView): """Provide a page for the server to call.""" SIGNALS = { TYPE_CONTROLLER_STATUS: SIGNAL_RACHIO_CONTROLLER_UPDATE, TYPE_SCHEDULE_STATUS: SIGNAL_RACHIO_SCHEDULE_UPDATE, TYPE_ZONE_STATUS: SIGNAL_RACHIO_ZONE_UPDATE, } requires_auth = False # Handled separately url = WEBHOOK_PATH name = url[1:].replace('/', ':') # pylint: disable=no-self-use @asyncio.coroutine 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 except (AssertionError, IndexError): return web.Response(status=web.HTTPForbidden.status_code) update_type = data[KEY_TYPE] if update_type in self.SIGNALS: async_dispatcher_send(hass, self.SIGNALS[update_type], data) return web.Response(status=web.HTTPNoContent.status_code)