1 """Integration with the Rachio Iro sprinkler system controller."""
3 from abc
import abstractmethod
4 from contextlib
import suppress
5 from datetime
import timedelta
9 import voluptuous
as vol
25 DEFAULT_MANUAL_RUN_MINS,
26 DOMAIN
as DOMAIN_RACHIO,
50 SERVICE_SET_ZONE_MOISTURE,
51 SERVICE_START_MULTIPLE_ZONES,
52 SERVICE_START_WATERING,
53 SIGNAL_RACHIO_CONTROLLER_UPDATE,
54 SIGNAL_RACHIO_RAIN_DELAY_UPDATE,
55 SIGNAL_RACHIO_SCHEDULE_UPDATE,
56 SIGNAL_RACHIO_ZONE_UPDATE,
62 from .device
import RachioPerson
63 from .entity
import RachioDevice, RachioHoseTimerEntity
64 from .webhooks
import (
65 SUBTYPE_RAIN_DELAY_OFF,
66 SUBTYPE_RAIN_DELAY_ON,
67 SUBTYPE_SCHEDULE_COMPLETED,
68 SUBTYPE_SCHEDULE_STARTED,
69 SUBTYPE_SCHEDULE_STOPPED,
70 SUBTYPE_SLEEP_MODE_OFF,
71 SUBTYPE_SLEEP_MODE_ON,
72 SUBTYPE_ZONE_COMPLETED,
78 _LOGGER = logging.getLogger(__name__)
80 ATTR_DURATION =
"duration"
81 ATTR_PERCENT =
"percent"
82 ATTR_SCHEDULE_SUMMARY =
"Summary"
83 ATTR_SCHEDULE_ENABLED =
"Enabled"
84 ATTR_SCHEDULE_DURATION =
"Duration"
85 ATTR_SCHEDULE_TYPE =
"Type"
86 ATTR_SORT_ORDER =
"sortOrder"
87 ATTR_WATERING_DURATION =
"Watering Duration seconds"
88 ATTR_ZONE_NUMBER =
"Zone number"
89 ATTR_ZONE_SHADE =
"Shade"
90 ATTR_ZONE_SLOPE =
"Slope"
91 ATTR_ZONE_SUMMARY =
"Summary"
92 ATTR_ZONE_TYPE =
"Type"
94 START_MULTIPLE_ZONES_SCHEMA = vol.Schema(
96 vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
97 vol.Required(ATTR_DURATION): cv.ensure_list_csv,
104 config_entry: ConfigEntry,
105 async_add_entities: AddEntitiesCallback,
107 """Set up the Rachio switches."""
109 has_flex_sched =
False
110 entities = await hass.async_add_executor_job(_create_entities, hass, config_entry)
111 for entity
in entities:
112 if isinstance(entity, RachioZone):
113 zone_entities.append(entity)
114 if isinstance(entity, RachioSchedule)
and entity.type == SCHEDULE_TYPE_FLEX:
115 has_flex_sched =
True
119 def start_multiple(service: ServiceCall) ->
None:
120 """Service to start multiple zones in sequence."""
122 person = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
123 entity_id = service.data[ATTR_ENTITY_ID]
124 duration = iter(service.data[ATTR_DURATION])
125 default_time = service.data[ATTR_DURATION][0]
126 entity_to_zone_id = {
127 entity.entity_id: entity.zone_id
for entity
in zone_entities
130 for count, data
in enumerate(entity_id):
131 if data
in entity_to_zone_id:
134 time =
int(next(duration, default_time)) * 60
137 ATTR_ID: entity_to_zone_id.get(data),
139 ATTR_SORT_ORDER: count,
143 if len(zones_list) != 0:
144 person.start_multiple_zones(zones_list)
145 _LOGGER.debug(
"Starting zone(s) %s", entity_id)
149 platform = entity_platform.async_get_current_platform()
150 platform.async_register_entity_service(
151 SERVICE_START_WATERING,
153 vol.Optional(ATTR_DURATION): cv.positive_int,
159 if not zone_entities:
162 hass.services.async_register(
164 SERVICE_START_MULTIPLE_ZONES,
166 schema=START_MULTIPLE_ZONES_SCHEMA,
170 platform = entity_platform.async_get_current_platform()
171 platform.async_register_entity_service(
172 SERVICE_SET_ZONE_MOISTURE,
173 {vol.Required(ATTR_PERCENT): cv.positive_int},
174 "set_moisture_percent",
179 entities: list[Entity] = []
180 person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id]
183 for controller
in person.controllers:
186 zones = controller.list_zones()
187 schedules = controller.list_schedules()
188 flex_schedules = controller.list_flex_schedules()
189 current_schedule = controller.current_schedule
191 RachioZone(person, controller, zone, current_schedule)
for zone
in zones
195 for schedule
in schedules + flex_schedules
198 RachioValve(person, base_station, valve, base_station.status_coordinator)
199 for base_station
in person.base_stations
200 for valve
in base_station.status_coordinator.data.values()
206 """Represent a Rachio state that can be toggled."""
210 """Determine whether an update event applies to this device."""
211 if args[0][KEY_DEVICE_ID] != self.
_controller_controller.controller_id:
220 """Handle incoming webhook data."""
223 class RachioStandbySwitch(RachioSwitch):
224 """Representation of a standby status/button."""
226 _attr_has_entity_name =
True
227 _attr_translation_key =
"standby"
231 """Return a unique id by combining controller id and purpose."""
232 return f
"{self._controller.controller_id}-standby"
236 """Update the state using webhook data."""
237 if args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON:
239 elif args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF:
245 """Put the controller in standby mode."""
249 """Resume controller functionality."""
253 """Subscribe to updates."""
254 if KEY_ON
in self.
_controller_controller.init_data:
260 SIGNAL_RACHIO_CONTROLLER_UPDATE,
267 """Representation of a rain delay status/switch."""
269 _attr_has_entity_name =
True
270 _attr_translation_key =
"rain_delay"
273 """Set up a Rachio rain delay switch."""
279 """Return a unique id by combining controller id and purpose."""
280 return f
"{self._controller.controller_id}-delay"
284 """Update the state using webhook data."""
289 if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_DELAY_ON:
291 _LOGGER.debug(
"Rain delay expires at %s", endtime)
293 assert endtime
is not None
297 elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_DELAY_OFF:
304 """Trigger when a rain delay expires."""
310 """Activate a 24 hour rain delay on the controller."""
312 _LOGGER.debug(
"Starting rain delay for 24 hours")
315 """Resume controller functionality."""
317 _LOGGER.debug(
"Canceling rain delay")
320 """Subscribe to updates."""
321 if KEY_RAIN_DELAY
in self.
_controller_controller.init_data:
329 self.
_controller_controller.init_data[KEY_RAIN_DELAY] / 1000
331 _LOGGER.debug(
"Re-setting rain delay timer for %s", delay_end)
339 SIGNAL_RACHIO_RAIN_DELAY_UPDATE,
346 """Representation of one zone of sprinklers connected to the Rachio Iro."""
348 _attr_icon =
"mdi:water"
350 def __init__(self, person, controller, data, current_schedule) -> None:
351 """Initialize a new Rachio Zone."""
352 self.
idid = data[KEY_ID]
367 """Display the zone as a string."""
368 return f
'Rachio Zone "{self.name}" on {self._controller!s}'
372 """How the Rachio API refers to the zone."""
377 """Return whether the zone is allowed to run."""
382 """Return the optional state attributes."""
383 props = {ATTR_ZONE_NUMBER: self.
_zone_number_zone_number, ATTR_ZONE_SUMMARY: self.
_summary_summary}
385 props[ATTR_ZONE_SHADE] = self.
_shade_type_shade_type
387 props[ATTR_ZONE_TYPE] = self.
_zone_type_zone_type
390 props[ATTR_ZONE_SLOPE] =
"Flat"
392 props[ATTR_ZONE_SLOPE] =
"Slight"
393 elif self.
_slope_type_slope_type == SLOPE_MODERATE:
394 props[ATTR_ZONE_SLOPE] =
"Moderate"
396 props[ATTR_ZONE_SLOPE] =
"Steep"
400 """Start watering this zone."""
405 if ATTR_DURATION
in kwargs:
406 manual_run_time =
timedelta(minutes=kwargs[ATTR_DURATION])
409 minutes=self.
_person_person.config_entry.options.get(
410 CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
416 "Watering %s on %s for %s",
419 str(manual_run_time),
423 """Stop watering all zones."""
427 """Set the zone moisture percent."""
428 _LOGGER.debug(
"Setting %s moisture to %s percent", self.
namename, percent)
429 self.
_controller_controller.rachio.zone.set_moisture_percent(self.
idid, percent / 100)
433 """Handle incoming webhook zone data."""
437 self.
_summary_summary = args[0][KEY_SUMMARY]
439 if args[0][KEY_SUBTYPE] == SUBTYPE_ZONE_STARTED:
441 elif args[0][KEY_SUBTYPE]
in [
442 SUBTYPE_ZONE_STOPPED,
443 SUBTYPE_ZONE_COMPLETED,
451 """Subscribe to updates."""
462 """Representation of one fixed schedule on the Rachio Iro."""
464 def __init__(self, person, controller, data, current_schedule) -> None:
465 """Initialize a new Rachio Schedule."""
470 self.
typetype = data.get(KEY_TYPE, SCHEDULE_TYPE_FIXED)
473 f
"{controller.controller_id}-schedule-{self._schedule_id}"
480 """Return the icon to display."""
485 """Return the optional state attributes."""
487 ATTR_SCHEDULE_SUMMARY: self.
_summary_summary,
489 ATTR_SCHEDULE_DURATION: f
"{round(self._duration / 60)} minutes",
490 ATTR_SCHEDULE_TYPE: self.
typetype,
495 """Return whether the schedule is allowed to run."""
499 """Start this schedule."""
502 "Schedule %s started on %s",
508 """Stop watering all zones."""
513 """Handle incoming webhook schedule data."""
515 with suppress(KeyError):
516 if args[0][KEY_SCHEDULE_ID] == self.
_schedule_id_schedule_id:
517 if args[0][KEY_SUBTYPE]
in [SUBTYPE_SCHEDULE_STARTED]:
519 elif args[0][KEY_SUBTYPE]
in [
520 SUBTYPE_SCHEDULE_STOPPED,
521 SUBTYPE_SCHEDULE_COMPLETED,
528 """Subscribe to updates."""
541 """Representation of one smart hose timer valve."""
545 def __init__(self, person, base, data, coordinator) -> None:
546 """Initialize a new smart hose valve."""
553 """Turn on this valve."""
554 if ATTR_DURATION
in kwargs:
555 manual_run_time =
timedelta(minutes=kwargs[ATTR_DURATION])
558 minutes=self.
_person_person.config_entry.options.get(
559 CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
563 self.
_base_base.start_watering(self.
idid, manual_run_time.seconds)
566 _LOGGER.debug(
"Starting valve %s for %s", self.
_name_name,
str(manual_run_time))
569 """Turn off this valve."""
570 self.
_base_base.stop_watering(self.
idid)
573 _LOGGER.debug(
"Stopping watering on valve %s", self.
_name_name)
577 """Handle updated coordinator data."""
578 data = self.coordinator.data[self.
idid]
None turn_on(self, **Any kwargs)
None async_added_to_hass(self)
None __init__(self, controller)
None turn_off(self, **Any kwargs)
None _async_handle_update(self, *args, **kwargs)
None _delay_expiration(self, *args)
None turn_off(self, **Any kwargs)
bool schedule_is_enabled(self)
None async_added_to_hass(self)
None turn_on(self, **Any kwargs)
dict[str, Any] extra_state_attributes(self)
None _async_handle_update(self, *args, **kwargs)
None __init__(self, person, controller, data, current_schedule)
None _async_handle_update(self, *args, **kwargs)
None turn_on(self, **Any kwargs)
None turn_off(self, **Any kwargs)
None async_added_to_hass(self)
None _async_handle_any_update(self, *args, **kwargs)
None _async_handle_update(self, *args, **kwargs)
None turn_on(self, **Any kwargs)
None __init__(self, person, base, data, coordinator)
None turn_off(self, **Any kwargs)
None turn_off(self, **Any kwargs)
None __init__(self, person, controller, data, current_schedule)
bool zone_is_enabled(self)
None set_moisture_percent(self, percent)
None async_added_to_hass(self)
dict[str, Any] extra_state_attributes(self)
None _async_handle_update(self, *args, **kwargs)
None turn_on(self, **Any kwargs)
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
None schedule_update_ha_state(self, bool force_refresh=False)
str|UndefinedType|None name(self)
None turn_off(self, **Any kwargs)
datetime|None parse_datetime(str|None value)
web.Response get(self, web.Request request, str config_key)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
list[Entity] _create_entities(HomeAssistant hass, ConfigEntry config_entry)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
CALLBACK_TYPE async_track_point_in_utc_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
datetime now(HomeAssistant hass)
float as_timestamp(dt.datetime|str dt_value)