Rachio webhooks (#15111)
* Make fewer requests to the Rachio API * BREAKING: Rewrite Rachio componentpull/15251/head
parent
136cc1d44d
commit
9db8759317
|
@ -0,0 +1,127 @@
|
|||
"""
|
||||
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/binary_sensor.rachio/
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rachio 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 homeassistant.helpers.dispatcher import dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['rachio']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Rachio binary sensors."""
|
||||
devices = []
|
||||
for controller in hass.data[DOMAIN_RACHIO].controllers:
|
||||
devices.append(RachioControllerOnlineBinarySensor(hass, controller))
|
||||
|
||||
add_devices(devices)
|
||||
_LOGGER.info("%d Rachio binary sensor(s) added", len(devices))
|
||||
|
||||
|
||||
class RachioControllerBinarySensor(BinarySensorDevice):
|
||||
"""Represent a binary sensor that reflects a Rachio state."""
|
||||
|
||||
def __init__(self, hass, controller, poll=True):
|
||||
"""Set up a new Rachio controller binary sensor."""
|
||||
self._controller = controller
|
||||
|
||||
if poll:
|
||||
self._state = self._poll_update()
|
||||
else:
|
||||
self._state = None
|
||||
|
||||
dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||
self._handle_any_update)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Declare that this entity pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the sensor has a 'true' value."""
|
||||
return self._state
|
||||
|
||||
def _handle_any_update(self, *args, **kwargs) -> None:
|
||||
"""Determine whether an update event applies to this device."""
|
||||
if args[0][KEY_DEVICE_ID] != self._controller.controller_id:
|
||||
# For another device
|
||||
return
|
||||
|
||||
# For this device
|
||||
self._handle_update()
|
||||
|
||||
@abstractmethod
|
||||
def _poll_update(self, data=None) -> bool:
|
||||
"""Request the state from the API."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Handle an update to the state of this sensor."""
|
||||
pass
|
||||
|
||||
|
||||
class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
|
||||
"""Represent a binary sensor that reflects if the controller is online."""
|
||||
|
||||
def __init__(self, hass, controller):
|
||||
"""Set up a new Rachio controller online binary sensor."""
|
||||
super().__init__(hass, controller, poll=False)
|
||||
self._state = self._poll_update(controller.init_data)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of this sensor including the controller name."""
|
||||
return "{} online".format(self._controller.name)
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'connectivity'
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the name of an icon for this sensor."""
|
||||
return 'mdi:wifi-strength-4' if self.is_on\
|
||||
else 'mdi:wifi-strength-off-outline'
|
||||
|
||||
def _poll_update(self, data=None) -> bool:
|
||||
"""Request the state from the API."""
|
||||
if data is None:
|
||||
data = self._controller.rachio.device.get(
|
||||
self._controller.controller_id)[1]
|
||||
|
||||
if data[KEY_STATUS] == STATUS_ONLINE:
|
||||
return True
|
||||
elif data[KEY_STATUS] == STATUS_OFFLINE:
|
||||
return False
|
||||
else:
|
||||
_LOGGER.warning('"%s" reported in unknown state "%s"', self.name,
|
||||
data[KEY_STATUS])
|
||||
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Handle an update to the state of this sensor."""
|
||||
if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE:
|
||||
self._state = True
|
||||
elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE:
|
||||
self._state = False
|
||||
|
||||
self.schedule_update_ha_state()
|
|
@ -0,0 +1,289 @@
|
|||
"""
|
||||
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 homeassistant.auth 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
|
||||
else:
|
||||
_LOGGER.info("%d Rachio device(s) found", len(person.controllers))
|
||||
|
||||
# Enable component
|
||||
hass.data[DOMAIN] = person
|
||||
return True
|
||||
|
||||
|
||||
class RachioPerson(object):
|
||||
"""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(object):
|
||||
"""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) -> dict or None:
|
||||
"""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)
|
|
@ -4,227 +4,239 @@ 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/switch.rachio/
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.components.rachio import (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_ZONE_STARTED,
|
||||
SUBTYPE_ZONE_STOPPED,
|
||||
SUBTYPE_ZONE_COMPLETED,
|
||||
SUBTYPE_SLEEP_MODE_ON,
|
||||
SUBTYPE_SLEEP_MODE_OFF)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||
|
||||
REQUIREMENTS = ['rachiopy==0.1.2']
|
||||
DEPENDENCIES = ['rachio']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Manual run length
|
||||
CONF_MANUAL_RUN_MINS = 'manual_run_mins'
|
||||
|
||||
DATA_RACHIO = 'rachio'
|
||||
|
||||
DEFAULT_MANUAL_RUN_MINS = 10
|
||||
|
||||
MIN_UPDATE_INTERVAL = timedelta(seconds=30)
|
||||
MIN_FORCED_UPDATE_INTERVAL = timedelta(seconds=1)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
||||
vol.Optional(CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS):
|
||||
cv.positive_int,
|
||||
})
|
||||
|
||||
ATTR_ZONE_SUMMARY = 'Summary'
|
||||
ATTR_ZONE_NUMBER = 'Zone number'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Rachio switches."""
|
||||
from rachiopy import Rachio
|
||||
manual_run_time = timedelta(minutes=config.get(CONF_MANUAL_RUN_MINS))
|
||||
_LOGGER.info("Rachio run time is %s", str(manual_run_time))
|
||||
|
||||
# Get options
|
||||
manual_run_mins = config.get(CONF_MANUAL_RUN_MINS)
|
||||
_LOGGER.debug("Rachio run time is %d min", manual_run_mins)
|
||||
# Add all zones from all controllers as switches
|
||||
devices = []
|
||||
for controller in hass.data[DOMAIN_RACHIO].controllers:
|
||||
devices.append(RachioStandbySwitch(hass, controller))
|
||||
|
||||
access_token = config.get(CONF_ACCESS_TOKEN)
|
||||
for zone in controller.list_zones():
|
||||
devices.append(RachioZone(hass, controller, zone, manual_run_time))
|
||||
|
||||
# Configure API
|
||||
_LOGGER.debug("Configuring Rachio API")
|
||||
rachio = Rachio(access_token)
|
||||
|
||||
person = None
|
||||
try:
|
||||
person = _get_person(rachio)
|
||||
except KeyError:
|
||||
_LOGGER.error(
|
||||
"Could not reach the Rachio API. Is your access token valid?")
|
||||
return
|
||||
|
||||
# Get and persist devices
|
||||
devices = _list_devices(rachio, manual_run_mins)
|
||||
if not devices:
|
||||
_LOGGER.error(
|
||||
"No Rachio devices found in account %s", person['username'])
|
||||
return
|
||||
|
||||
hass.data[DATA_RACHIO] = devices[0]
|
||||
|
||||
if len(devices) > 1:
|
||||
_LOGGER.warning("Multiple Rachio devices found in account, "
|
||||
"using %s", hass.data[DATA_RACHIO].device_id)
|
||||
else:
|
||||
_LOGGER.debug("Found Rachio device")
|
||||
|
||||
hass.data[DATA_RACHIO].update()
|
||||
add_devices(hass.data[DATA_RACHIO].list_zones())
|
||||
add_devices(devices)
|
||||
_LOGGER.info("%d Rachio switch(es) added", len(devices))
|
||||
|
||||
|
||||
def _get_person(rachio):
|
||||
"""Pull the account info of the person whose access token was provided."""
|
||||
person_id = rachio.person.getInfo()[1]['id']
|
||||
return rachio.person.get(person_id)[1]
|
||||
class RachioSwitch(SwitchDevice):
|
||||
"""Represent a Rachio state that can be toggled."""
|
||||
|
||||
def __init__(self, controller, poll=True):
|
||||
"""Initialize a new Rachio switch."""
|
||||
self._controller = controller
|
||||
|
||||
def _list_devices(rachio, manual_run_mins):
|
||||
"""Pull a list of devices on the account."""
|
||||
return [RachioIro(rachio, d['id'], manual_run_mins)
|
||||
for d in _get_person(rachio)['devices']]
|
||||
|
||||
|
||||
class RachioIro(object):
|
||||
"""Representation of a Rachio Iro."""
|
||||
|
||||
def __init__(self, rachio, device_id, manual_run_mins):
|
||||
"""Initialize a Rachio device."""
|
||||
self.rachio = rachio
|
||||
self._device_id = device_id
|
||||
self.manual_run_mins = manual_run_mins
|
||||
self._device = None
|
||||
self._running = None
|
||||
self._zones = None
|
||||
|
||||
def __str__(self):
|
||||
"""Display the device as a string."""
|
||||
return "Rachio Iro {}".format(self.serial_number)
|
||||
if poll:
|
||||
self._state = self._poll_update()
|
||||
else:
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def device_id(self):
|
||||
"""Return the Rachio API device ID."""
|
||||
return self._device['id']
|
||||
def should_poll(self) -> bool:
|
||||
"""Declare that this entity pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return the current status of the device."""
|
||||
return self._device['status']
|
||||
def name(self) -> str:
|
||||
"""Get a name for this switch."""
|
||||
return "Switch on {}".format(self._controller.name)
|
||||
|
||||
@property
|
||||
def serial_number(self):
|
||||
"""Return the serial number of the device."""
|
||||
return self._device['serialNumber']
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the switch is currently on."""
|
||||
return self._state
|
||||
|
||||
@abstractmethod
|
||||
def _poll_update(self, data=None) -> bool:
|
||||
"""Poll the API."""
|
||||
pass
|
||||
|
||||
def _handle_any_update(self, *args, **kwargs) -> None:
|
||||
"""Determine whether an update event applies to this device."""
|
||||
if args[0][KEY_DEVICE_ID] != self._controller.controller_id:
|
||||
# For another device
|
||||
return
|
||||
|
||||
# For this device
|
||||
self._handle_update(args, kwargs)
|
||||
|
||||
@abstractmethod
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Handle incoming webhook data."""
|
||||
pass
|
||||
|
||||
|
||||
class RachioStandbySwitch(RachioSwitch):
|
||||
"""Representation of a standby status/button."""
|
||||
|
||||
def __init__(self, hass, controller):
|
||||
"""Instantiate a new Rachio standby mode switch."""
|
||||
dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE,
|
||||
self._handle_any_update)
|
||||
super().__init__(controller, poll=False)
|
||||
self._poll_update(controller.init_data)
|
||||
|
||||
@property
|
||||
def is_paused(self):
|
||||
"""Return whether the device is temporarily disabled."""
|
||||
return self._device['paused']
|
||||
def name(self) -> str:
|
||||
"""Return the name of the standby switch."""
|
||||
return "{} in standby mode".format(self._controller.name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return whether the device is powered on and connected."""
|
||||
return self._device['on']
|
||||
def icon(self) -> str:
|
||||
"""Return an icon for the standby switch."""
|
||||
return "mdi:power"
|
||||
|
||||
@property
|
||||
def current_schedule(self):
|
||||
"""Return the schedule that the device is running right now."""
|
||||
return self._running
|
||||
def _poll_update(self, data=None) -> bool:
|
||||
"""Request the state from the API."""
|
||||
if data is None:
|
||||
data = self._controller.rachio.device.get(
|
||||
self._controller.controller_id)[1]
|
||||
|
||||
def list_zones(self, include_disabled=False):
|
||||
"""Return a list of the zones connected to the device, incl. data."""
|
||||
if not self._zones:
|
||||
self._zones = [RachioZone(self.rachio, self, zone['id'],
|
||||
self.manual_run_mins)
|
||||
for zone in self._device['zones']]
|
||||
return not data[KEY_ON]
|
||||
|
||||
if include_disabled:
|
||||
return self._zones
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Update the state using webhook data."""
|
||||
if args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON:
|
||||
self._state = True
|
||||
elif args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF:
|
||||
self._state = False
|
||||
|
||||
self.update(no_throttle=True)
|
||||
return [z for z in self._zones if z.is_enabled]
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@util.Throttle(MIN_UPDATE_INTERVAL, MIN_FORCED_UPDATE_INTERVAL)
|
||||
def update(self, **kwargs):
|
||||
"""Pull updated device info from the Rachio API."""
|
||||
self._device = self.rachio.device.get(self._device_id)[1]
|
||||
self._running = self.rachio.device\
|
||||
.getCurrentSchedule(self._device_id)[1]
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Put the controller in standby mode."""
|
||||
self._controller.rachio.device.off(self._controller.controller_id)
|
||||
|
||||
# Possibly update all zones
|
||||
for zone in self.list_zones(include_disabled=True):
|
||||
zone.update()
|
||||
|
||||
_LOGGER.debug("Updated %s", str(self))
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Resume controller functionality."""
|
||||
self._controller.rachio.device.on(self._controller.controller_id)
|
||||
|
||||
|
||||
class RachioZone(SwitchDevice):
|
||||
class RachioZone(RachioSwitch):
|
||||
"""Representation of one zone of sprinklers connected to the Rachio Iro."""
|
||||
|
||||
def __init__(self, rachio, device, zone_id, manual_run_mins):
|
||||
def __init__(self, hass, controller, data, manual_run_time):
|
||||
"""Initialize a new Rachio Zone."""
|
||||
self.rachio = rachio
|
||||
self._device = device
|
||||
self._zone_id = zone_id
|
||||
self._zone = None
|
||||
self._manual_run_secs = manual_run_mins * 60
|
||||
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._summary = str()
|
||||
super().__init__(controller)
|
||||
|
||||
# Listen for all zone updates
|
||||
dispatcher_connect(hass, SIGNAL_RACHIO_ZONE_UPDATE,
|
||||
self._handle_update)
|
||||
|
||||
def __str__(self):
|
||||
"""Display the zone as a string."""
|
||||
return "Rachio Zone {}".format(self.name)
|
||||
return 'Rachio Zone "{}" on {}'.format(self.name,
|
||||
str(self._controller))
|
||||
|
||||
@property
|
||||
def zone_id(self):
|
||||
def zone_id(self) -> str:
|
||||
"""How the Rachio API refers to the zone."""
|
||||
return self._zone['id']
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique string ID for the zone."""
|
||||
return '{iro}-{zone}'.format(
|
||||
iro=self._device.device_id, zone=self.zone_id)
|
||||
|
||||
@property
|
||||
def number(self):
|
||||
"""Return the physical connection of the zone pump."""
|
||||
return self._zone['zoneNumber']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the friendly name of the zone."""
|
||||
return self._zone['name']
|
||||
return self._zone_name
|
||||
|
||||
@property
|
||||
def is_enabled(self):
|
||||
def icon(self) -> str:
|
||||
"""Return the icon to display."""
|
||||
return "mdi:water"
|
||||
|
||||
@property
|
||||
def zone_is_enabled(self) -> bool:
|
||||
"""Return whether the zone is allowed to run."""
|
||||
return self._zone['enabled']
|
||||
return self._zone_enabled
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return whether the zone is currently running."""
|
||||
schedule = self._device.current_schedule
|
||||
return self.zone_id == schedule.get('zoneId')
|
||||
def state_attributes(self) -> dict:
|
||||
"""Return the optional state attributes."""
|
||||
return {
|
||||
ATTR_ZONE_NUMBER: self._zone_number,
|
||||
ATTR_ZONE_SUMMARY: self._summary,
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Pull updated zone info from the Rachio API."""
|
||||
self._zone = self.rachio.zone.get(self._zone_id)[1]
|
||||
|
||||
# Possibly update device
|
||||
self._device.update()
|
||||
|
||||
_LOGGER.debug("Updated %s", str(self))
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Start the zone."""
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Start watering this zone."""
|
||||
# Stop other zones first
|
||||
self.turn_off()
|
||||
|
||||
_LOGGER.info("Watering %s for %d s", self.name, self._manual_run_secs)
|
||||
self.rachio.zone.start(self.zone_id, self._manual_run_secs)
|
||||
# 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)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Stop all zones."""
|
||||
_LOGGER.info("Stopping watering of all zones")
|
||||
self.rachio.device.stopWater(self._device.device_id)
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Stop watering all zones."""
|
||||
self._controller.stop_watering()
|
||||
|
||||
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)
|
||||
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Handle incoming webhook zone data."""
|
||||
if args[0][KEY_ZONE_ID] != self.zone_id:
|
||||
return
|
||||
|
||||
self._summary = kwargs.get(KEY_SUMMARY, str())
|
||||
|
||||
if args[0][KEY_SUBTYPE] == SUBTYPE_ZONE_STARTED:
|
||||
self._state = True
|
||||
elif args[0][KEY_SUBTYPE] in [SUBTYPE_ZONE_STOPPED,
|
||||
SUBTYPE_ZONE_COMPLETED]:
|
||||
self._state = False
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -1163,8 +1163,8 @@ pyzabbix==0.7.4
|
|||
# homeassistant.components.sensor.qnap
|
||||
qnapstats==0.2.6
|
||||
|
||||
# homeassistant.components.switch.rachio
|
||||
rachiopy==0.1.2
|
||||
# homeassistant.components.rachio
|
||||
rachiopy==0.1.3
|
||||
|
||||
# homeassistant.components.climate.radiotherm
|
||||
radiotherm==1.3
|
||||
|
|
Loading…
Reference in New Issue