core/homeassistant/components/rachio/switch.py

388 lines
13 KiB
Python

"""Integration with the Rachio Iro sprinkler system controller."""
from abc import abstractmethod
from datetime import timedelta
import logging
from homeassistant.components.switch import SwitchDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
ATTR_ZONE_SHADE,
ATTR_ZONE_TYPE,
CONF_MANUAL_RUN_MINS,
DEFAULT_MANUAL_RUN_MINS,
DOMAIN as DOMAIN_RACHIO,
KEY_CUSTOM_CROP,
KEY_CUSTOM_SHADE,
KEY_DEVICE_ID,
KEY_DURATION,
KEY_ENABLED,
KEY_ID,
KEY_IMAGE_URL,
KEY_NAME,
KEY_ON,
KEY_SCHEDULE_ID,
KEY_SUBTYPE,
KEY_SUMMARY,
KEY_ZONE_ID,
KEY_ZONE_NUMBER,
SIGNAL_RACHIO_CONTROLLER_UPDATE,
SIGNAL_RACHIO_SCHEDULE_UPDATE,
SIGNAL_RACHIO_ZONE_UPDATE,
)
from .entity import RachioDevice
from .webhooks import (
SUBTYPE_SCHEDULE_COMPLETED,
SUBTYPE_SCHEDULE_STARTED,
SUBTYPE_SCHEDULE_STOPPED,
SUBTYPE_SLEEP_MODE_OFF,
SUBTYPE_SLEEP_MODE_ON,
SUBTYPE_ZONE_COMPLETED,
SUBTYPE_ZONE_STARTED,
SUBTYPE_ZONE_STOPPED,
)
_LOGGER = logging.getLogger(__name__)
ATTR_ZONE_SUMMARY = "Summary"
ATTR_ZONE_NUMBER = "Zone number"
ATTR_SCHEDULE_SUMMARY = "Summary"
ATTR_SCHEDULE_ENABLED = "Enabled"
ATTR_SCHEDULE_DURATION = "Duration"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Rachio switches."""
# Add all zones from all controllers as switches
entities = await hass.async_add_executor_job(_create_entities, hass, config_entry)
async_add_entities(entities)
_LOGGER.info("%d Rachio switch(es) added", len(entities))
def _create_entities(hass, config_entry):
entities = []
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:
entities.append(RachioStandbySwitch(controller))
zones = controller.list_zones()
schedules = controller.list_schedules()
current_schedule = controller.current_schedule
for zone in zones:
_LOGGER.debug("Rachio setting up zone: %s", zone)
entities.append(RachioZone(person, controller, zone, current_schedule))
for sched in schedules:
_LOGGER.debug("Added schedule: %s", sched)
entities.append(RachioSchedule(person, controller, sched, current_schedule))
return entities
class RachioSwitch(RachioDevice, SwitchDevice):
"""Represent a Rachio state that can be toggled."""
def __init__(self, controller, poll=True):
"""Initialize a new Rachio switch."""
super().__init__(controller)
if poll:
self._state = self._poll_update()
else:
self._state = None
@property
def name(self) -> str:
"""Get a name for this switch."""
return f"Switch on {self._controller.name}"
@property
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."""
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."""
class RachioStandbySwitch(RachioSwitch):
"""Representation of a standby status/button."""
def __init__(self, controller):
"""Instantiate a new Rachio standby mode switch."""
super().__init__(controller, poll=True)
self._poll_update(controller.init_data)
@property
def name(self) -> str:
"""Return the name of the standby switch."""
return f"{self._controller.name} in standby mode"
@property
def unique_id(self) -> str:
"""Return a unique id by combining controller id and purpose."""
return f"{self._controller.controller_id}-standby"
@property
def icon(self) -> str:
"""Return an icon for the standby switch."""
return "mdi:power"
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]
return not data[KEY_ON]
def _handle_update(self, *args, **kwargs) -> None:
"""Update the state using webhook data."""
if args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON:
self._state = True
elif args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF:
self._state = False
self.schedule_update_ha_state()
def turn_on(self, **kwargs) -> None:
"""Put the controller in standby mode."""
self._controller.rachio.device.off(self._controller.controller_id)
def turn_off(self, **kwargs) -> None:
"""Resume controller functionality."""
self._controller.rachio.device.on(self._controller.controller_id)
async def async_added_to_hass(self):
"""Subscribe to updates."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update
)
)
class RachioZone(RachioSwitch):
"""Representation of one zone of sprinklers connected to the Rachio Iro."""
def __init__(self, person, controller, data, current_schedule):
"""Initialize a new Rachio Zone."""
self._id = data[KEY_ID]
_LOGGER.debug("zone_data: %s", data)
self._zone_name = data[KEY_NAME]
self._zone_number = data[KEY_ZONE_NUMBER]
self._zone_enabled = data[KEY_ENABLED]
self._entity_picture = data.get(KEY_IMAGE_URL)
self._person = person
self._shade_type = data.get(KEY_CUSTOM_SHADE, {}).get(KEY_NAME)
self._zone_type = data.get(KEY_CUSTOM_CROP, {}).get(KEY_NAME)
self._summary = str()
self._current_schedule = current_schedule
super().__init__(controller, poll=False)
self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID)
self._undo_dispatcher = None
def __str__(self):
"""Display the zone as a string."""
return 'Rachio Zone "{}" on {}'.format(self.name, str(self._controller))
@property
def zone_id(self) -> str:
"""How the Rachio API refers to the zone."""
return self._id
@property
def name(self) -> str:
"""Return the friendly name of the zone."""
return self._zone_name
@property
def unique_id(self) -> str:
"""Return a unique id by combining controller id and zone number."""
return f"{self._controller.controller_id}-zone-{self.zone_id}"
@property
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
@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."""
props = {ATTR_ZONE_NUMBER: self._zone_number, ATTR_ZONE_SUMMARY: self._summary}
if self._shade_type:
props[ATTR_ZONE_SHADE] = self._shade_type
if self._zone_type:
props[ATTR_ZONE_TYPE] = self._zone_type
return props
def turn_on(self, **kwargs) -> None:
"""Start watering this zone."""
# Stop other zones first
self.turn_off()
# Start this zone
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."""
self._controller.stop_watering()
def _poll_update(self, data=None) -> bool:
"""Poll the API to check whether the zone is running."""
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."""
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()
async def async_added_to_hass(self):
"""Subscribe to updates."""
self._undo_dispatcher = async_dispatcher_connect(
self.hass, SIGNAL_RACHIO_ZONE_UPDATE, self._handle_update
)
async def async_will_remove_from_hass(self):
"""Unsubscribe from updates."""
if self._undo_dispatcher:
self._undo_dispatcher()
class RachioSchedule(RachioSwitch):
"""Representation of one fixed schedule on the Rachio Iro."""
def __init__(self, person, controller, data, current_schedule):
"""Initialize a new Rachio Schedule."""
self._id = data[KEY_ID]
self._schedule_name = data[KEY_NAME]
self._duration = data[KEY_DURATION]
self._schedule_enabled = data[KEY_ENABLED]
self._summary = data[KEY_SUMMARY]
self._current_schedule = current_schedule
super().__init__(controller, poll=False)
self._state = self.schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID)
self._undo_dispatcher = None
@property
def schedule_id(self) -> str:
"""How the Rachio API refers to the schedule."""
return self._id
@property
def name(self) -> str:
"""Return the friendly name of the schedule."""
return f"{self._schedule_name} Schedule"
@property
def unique_id(self) -> str:
"""Return a unique id by combining controller id and schedule."""
return f"{self._controller.controller_id}-schedule-{self.schedule_id}"
@property
def icon(self) -> str:
"""Return the icon to display."""
return "mdi:water"
@property
def device_state_attributes(self) -> dict:
"""Return the optional state attributes."""
return {
ATTR_SCHEDULE_SUMMARY: self._summary,
ATTR_SCHEDULE_ENABLED: self.schedule_is_enabled,
ATTR_SCHEDULE_DURATION: self._duration / 60,
}
@property
def schedule_is_enabled(self) -> bool:
"""Return whether the schedule is allowed to run."""
return self._schedule_enabled
def turn_on(self, **kwargs) -> None:
"""Start this schedule."""
self._controller.rachio.schedulerule.start(self.schedule_id)
_LOGGER.debug(
"Schedule %s started on %s", self.name, self._controller.name,
)
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 schedule is running."""
self._current_schedule = self._controller.current_schedule
return self.schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID)
def _handle_update(self, *args, **kwargs) -> None:
"""Handle incoming webhook schedule data."""
# Schedule ID not passed when running individual zones, so we catch that error
try:
if args[0][KEY_SCHEDULE_ID] == self.schedule_id:
if args[0][KEY_SUBTYPE] in [SUBTYPE_SCHEDULE_STARTED]:
self._state = True
elif args[0][KEY_SUBTYPE] in [
SUBTYPE_SCHEDULE_STOPPED,
SUBTYPE_SCHEDULE_COMPLETED,
]:
self._state = False
except KeyError:
pass
self.schedule_update_ha_state()
async def async_added_to_hass(self):
"""Subscribe to updates."""
self._undo_dispatcher = async_dispatcher_connect(
self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._handle_update
)
async def async_will_remove_from_hass(self):
"""Unsubscribe from updates."""
if self._undo_dispatcher:
self._undo_dispatcher()