"""Integration with the Rachio Iro sprinkler system controller.""" from abc import abstractmethod from datetime import timedelta import logging from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import as_timestamp, now from .const import ( CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, DOMAIN as DOMAIN_RACHIO, KEY_CUSTOM_CROP, KEY_CUSTOM_SHADE, KEY_CUSTOM_SLOPE, KEY_DEVICE_ID, KEY_DURATION, KEY_ENABLED, KEY_ID, KEY_IMAGE_URL, KEY_NAME, KEY_ON, KEY_RAIN_DELAY, KEY_SCHEDULE_ID, KEY_SUBTYPE, KEY_SUMMARY, KEY_ZONE_ID, KEY_ZONE_NUMBER, SIGNAL_RACHIO_CONTROLLER_UPDATE, SIGNAL_RACHIO_RAIN_DELAY_UPDATE, SIGNAL_RACHIO_SCHEDULE_UPDATE, SIGNAL_RACHIO_ZONE_UPDATE, SLOPE_FLAT, SLOPE_MODERATE, SLOPE_SLIGHT, SLOPE_STEEP, ) from .entity import RachioDevice from .webhooks import ( SUBTYPE_RAIN_DELAY_OFF, SUBTYPE_RAIN_DELAY_ON, 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_SCHEDULE_SUMMARY = "Summary" ATTR_SCHEDULE_ENABLED = "Enabled" ATTR_SCHEDULE_DURATION = "Duration" ATTR_ZONE_NUMBER = "Zone number" ATTR_ZONE_SHADE = "Shade" ATTR_ZONE_SLOPE = "Slope" ATTR_ZONE_SUMMARY = "Summary" ATTR_ZONE_TYPE = "Type" 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)) entities.append(RachioRainDelay(controller)) zones = controller.list_zones() schedules = controller.list_schedules() flex_schedules = controller.list_flex_schedules() current_schedule = controller.current_schedule for zone in zones: entities.append(RachioZone(person, controller, zone, current_schedule)) for sched in schedules + flex_schedules: entities.append(RachioSchedule(person, controller, sched, current_schedule)) _LOGGER.debug("Added %s", entities) return entities class RachioSwitch(RachioDevice, SwitchEntity): """Represent a Rachio state that can be toggled.""" def __init__(self, controller): """Initialize a new Rachio switch.""" super().__init__(controller) 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 @callback def _async_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._async_handle_update(args, kwargs) @abstractmethod def _async_handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook data.""" class RachioStandbySwitch(RachioSwitch): """Representation of a standby status/button.""" @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" @callback def _async_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.async_write_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.""" if KEY_ON in self._controller.init_data: self._state = not self._controller.init_data[KEY_ON] self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._async_handle_any_update, ) ) class RachioRainDelay(RachioSwitch): """Representation of a rain delay status/switch.""" @property def name(self) -> str: """Return the name of the switch.""" return f"{self._controller.name} rain delay" @property def unique_id(self) -> str: """Return a unique id by combining controller id and purpose.""" return f"{self._controller.controller_id}-delay" @property def icon(self) -> str: """Return an icon for rain delay.""" return "mdi:camera-timer" @callback def _async_handle_update(self, *args, **kwargs) -> None: """Update the state using webhook data.""" if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_DELAY_ON: self._state = True elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_DELAY_OFF: self._state = False self.async_write_ha_state() def turn_on(self, **kwargs) -> None: """Activate a 24 hour rain delay on the controller.""" self._controller.rachio.device.rainDelay(self._controller.controller_id, 86400) _LOGGER.debug("Starting rain delay for 24 hours") def turn_off(self, **kwargs) -> None: """Resume controller functionality.""" self._controller.rachio.device.rainDelay(self._controller.controller_id, 0) _LOGGER.debug("Canceling rain delay") async def async_added_to_hass(self): """Subscribe to updates.""" if KEY_RAIN_DELAY in self._controller.init_data: self._state = self._controller.init_data[ KEY_RAIN_DELAY ] / 1000 > as_timestamp(now()) self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_RACHIO_RAIN_DELAY_UPDATE, self._async_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] 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._slope_type = data.get(KEY_CUSTOM_SLOPE, {}).get(KEY_NAME) self._summary = "" self._current_schedule = current_schedule super().__init__(controller) 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 device_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 if self._slope_type: if self._slope_type == SLOPE_FLAT: props[ATTR_ZONE_SLOPE] = "Flat" elif self._slope_type == SLOPE_SLIGHT: props[ATTR_ZONE_SLOPE] = "Slight" elif self._slope_type == SLOPE_MODERATE: props[ATTR_ZONE_SLOPE] = "Moderate" elif self._slope_type == SLOPE_STEEP: props[ATTR_ZONE_SLOPE] = "Steep" 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() @callback def _async_handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook zone data.""" if args[0][KEY_ZONE_ID] != self.zone_id: return self._summary = args[0][KEY_SUMMARY] 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.async_write_ha_state() async def async_added_to_hass(self): """Subscribe to updates.""" self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID) self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_RACHIO_ZONE_UPDATE, self._async_handle_update ) ) 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._schedule_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) @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" if self.schedule_is_enabled else "mdi:water-off" @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: f"{round(self._duration / 60)} minutes", } @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() @callback def _async_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.async_write_ha_state() async def async_added_to_hass(self): """Subscribe to updates.""" self._state = self._schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._async_handle_update ) )