1 """Component providing support for RainMachine programs and zones."""
3 from __future__
import annotations
6 from collections.abc
import Awaitable, Callable, Coroutine
7 from dataclasses
import dataclass
8 from datetime
import datetime
9 from typing
import Any, Concatenate
11 from regenmaschine.errors
import RainMachineError
12 import voluptuous
as vol
23 from .
import RainMachineConfigEntry, RainMachineData, async_update_programs_and_zones
25 CONF_ALLOW_INACTIVE_ZONES_TO_RUN,
26 CONF_DEFAULT_ZONE_RUN_TIME,
28 CONF_USE_APP_RUN_TIMES,
30 DATA_PROVISION_SETTINGS,
31 DATA_RESTRICTIONS_UNIVERSAL,
35 from .entity
import RainMachineEntity, RainMachineEntityDescription
36 from .util
import RUN_STATE_MAP, key_exists
38 ATTR_ACTIVITY_TYPE =
"activity_type"
41 ATTR_CURRENT_CYCLE =
"current_cycle"
42 ATTR_CYCLES =
"cycles"
44 ATTR_DELAY_ON =
"delay_on"
45 ATTR_FIELD_CAPACITY =
"field_capacity"
46 ATTR_NEXT_RUN =
"next_run"
47 ATTR_NO_CYCLES =
"number_of_cycles"
48 ATTR_PRECIP_RATE =
"sprinkler_head_precipitation_rate"
49 ATTR_RESTRICTIONS =
"restrictions"
52 ATTR_SOIL_TYPE =
"soil_type"
53 ATTR_SPRINKLER_TYPE =
"sprinkler_head_type"
54 ATTR_STATUS =
"status"
55 ATTR_SUN_EXPOSURE =
"sun_exposure"
56 ATTR_VEGETATION_TYPE =
"vegetation_type"
58 ATTR_ZONE_RUN_TIME =
"zone_run_time_from_app"
60 DAYS = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"]
86 SPRINKLER_TYPE_MAP = {
92 5:
"Rotors High Rate",
96 SUN_EXPOSURE_MAP = {0:
"Not Set", 1:
"Full Sun", 2:
"Partial Shade", 3:
"Full Shade"}
101 2:
"Cool Season Grass",
107 9:
"Drought Tolerant Plants",
108 10:
"Warm Season Grass",
114 def raise_on_request_error[_T: RainMachineBaseSwitch, **_P](
115 func: Callable[Concatenate[_T, _P], Awaitable[
None]],
116 ) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any,
None]]:
117 """Define a decorator to raise on a request error."""
119 async
def decorator(self: _T, *args: _P.args, **kwargs: _P.kwargs) ->
None:
122 await func(self, *args, **kwargs)
123 except RainMachineError
as err:
125 f
"Error while executing {func.__name__}: {err}",
131 @dataclass(frozen=True, kw_only=True)
133 SwitchEntityDescription, RainMachineEntityDescription
135 """Describe a RainMachine switch."""
138 @dataclass(frozen=True, kw_only=True)
140 """Describe a RainMachine activity (program/zone) switch."""
146 @dataclass(frozen=True, kw_only=True)
148 """Describe a RainMachine restriction switch."""
153 TYPE_RESTRICTIONS_FREEZE_PROTECT_ENABLED =
"freeze_protect_enabled"
154 TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING =
"hot_days_extra_watering"
156 RESTRICTIONS_SWITCH_DESCRIPTIONS = (
158 key=TYPE_RESTRICTIONS_FREEZE_PROTECT_ENABLED,
159 translation_key=TYPE_RESTRICTIONS_FREEZE_PROTECT_ENABLED,
160 icon=
"mdi:snowflake-alert",
161 api_category=DATA_RESTRICTIONS_UNIVERSAL,
162 data_key=
"freezeProtectEnabled",
165 key=TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING,
166 translation_key=TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING,
167 icon=
"mdi:heat-wave",
168 api_category=DATA_RESTRICTIONS_UNIVERSAL,
169 data_key=
"hotDaysExtraWatering",
176 entry: RainMachineConfigEntry,
177 async_add_entities: AddEntitiesCallback,
179 """Set up RainMachine switches based on a config entry."""
180 platform = entity_platform.async_get_current_platform()
182 services: tuple[tuple[str, VolDictType |
None, str], ...] = (
183 (
"start_program",
None,
"async_start_program"),
188 CONF_DEFAULT_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN
193 (
"stop_program",
None,
"async_stop_program"),
194 (
"stop_zone",
None,
"async_stop_zone"),
196 for service_name, schema, method
in services:
197 platform.async_register_entity_service(service_name, schema, method)
199 data = entry.runtime_data
200 entities: list[RainMachineBaseSwitch] = []
202 for kind, api_category, switch_class, switch_enabled_class
in (
203 (
"program", DATA_PROGRAMS, RainMachineProgram, RainMachineProgramEnabled),
204 (
"zone", DATA_ZONES, RainMachineZone, RainMachineZoneEnabled),
206 coordinator = data.coordinators[api_category]
207 for uid, activity
in coordinator.data.items():
208 name = activity[
"name"].capitalize()
218 api_category=api_category,
227 switch_enabled_class(
231 key=f
"{kind}_{uid}_enabled",
232 name=f
"{name} enabled",
233 api_category=api_category,
241 for description
in RESTRICTIONS_SWITCH_DESCRIPTIONS:
242 coordinator = data.coordinators[description.api_category]
243 if not key_exists(coordinator.data, description.data_key):
251 """Define a base RainMachine switch."""
253 entity_description: RainMachineSwitchDescription
258 data: RainMachineData,
259 description: RainMachineSwitchDescription,
262 super().
__init__(entry, data, description)
269 """Update all activity data."""
270 self.
hasshasshass.async_create_task(
275 """Execute the start_program entity service."""
276 raise NotImplementedError(
"Service not implemented for this entity")
279 """Execute the start_zone entity service."""
280 raise NotImplementedError(
"Service not implemented for this entity")
283 """Execute the stop_program entity service."""
284 raise NotImplementedError(
"Service not implemented for this entity")
287 """Execute the stop_zone entity service."""
288 raise NotImplementedError(
"Service not implemented for this entity")
292 """Define a RainMachine switch to start/stop an activity (program or zone)."""
294 _attr_icon =
"mdi:water"
295 entity_description: RainMachineActivitySwitchDescription
300 data: RainMachineData,
301 description: RainMachineSwitchDescription,
304 super().
__init__(entry, data, description)
311 """Turn the switch off.
313 The only way this could occur is if someone rapidly turns a disabled activity
314 off right after turning it on.
317 not self.
_entry_entry_entry.options[CONF_ALLOW_INACTIVE_ZONES_TO_RUN]
318 and not self.coordinator.data[self.
entity_descriptionentity_description.uid][
"active"]
321 f
"Cannot turn off an inactive program/zone: {self.name}"
326 @raise_on_request_error
328 """Turn the switch off when its associated activity is active."""
329 raise NotImplementedError
332 """Turn the switch on."""
334 not self.
_entry_entry_entry.options[CONF_ALLOW_INACTIVE_ZONES_TO_RUN]
335 and not self.coordinator.data[self.
entity_descriptionentity_description.uid][
"active"]
340 f
"Cannot turn on an inactive program/zone: {self.name}"
345 @raise_on_request_error
347 """Turn the switch on when its associated activity is active."""
348 raise NotImplementedError
352 """Define a RainMachine switch to enable/disable an activity (program or zone)."""
354 _attr_entity_category = EntityCategory.CONFIG
355 _attr_icon =
"mdi:cog"
356 entity_description: RainMachineActivitySwitchDescription
361 data: RainMachineData,
362 description: RainMachineSwitchDescription,
365 super().
__init__(entry, data, description)
373 """Update the entity when new data is received."""
378 """Define a RainMachine program."""
381 """Start the program."""
385 """Stop the program."""
388 @raise_on_request_error
390 """Turn the switch off when its associated activity is active."""
394 @raise_on_request_error
396 """Turn the switch on when its associated activity is active."""
402 """Update the entity when new data is received."""
408 if data.get(
"nextRun")
is None:
411 next_run = datetime.strptime(
412 f
"{data['nextRun']} {data['startTime']}",
419 ATTR_NEXT_RUN: next_run,
420 ATTR_SOAK: data.get(
"soak"),
421 ATTR_STATUS: RUN_STATE_MAP[data[
"status"]],
422 ATTR_ZONES: [z
for z
in data[
"wateringTimes"]
if z[
"active"]],
428 """Define a switch to enable/disable a RainMachine program."""
430 @raise_on_request_error
432 """Disable the program."""
437 await asyncio.gather(*tasks)
440 @raise_on_request_error
442 """Enable the program."""
448 """Define a RainMachine restriction setting."""
450 _attr_entity_category = EntityCategory.CONFIG
451 entity_description: RainMachineRestrictionSwitchDescription
453 @raise_on_request_error
455 """Disable the restriction."""
456 await self.
_data_data.controller.restrictions.set_universal(
462 @raise_on_request_error
464 """Enable the restriction."""
465 await self.
_data_data.controller.restrictions.set_universal(
473 """Update the entity when new data is received."""
478 """Define a RainMachine zone."""
481 """Start a particular zone for a certain amount of time."""
488 @raise_on_request_error
490 """Turn the switch off when its associated activity is active."""
494 @raise_on_request_error
496 """Turn the switch on when its associated activity is active."""
498 duration = kwargs.get(CONF_DURATION)
508 duration = self.
_entry_entry_entry.options[CONF_DEFAULT_ZONE_RUN_TIME]
509 await self.
_data_data.controller.zones.start(
517 """Update the entity when new data is received."""
523 ATTR_CURRENT_CYCLE: data[
"cycle"],
524 ATTR_ID: data[
"uid"],
525 ATTR_NO_CYCLES: data[
"noOfCycles"],
526 ATTR_RESTRICTIONS: data[
"restriction"],
527 ATTR_SLOPE: SLOPE_TYPE_MAP.get(data[
"slope"], 99),
528 ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data[
"soil"], 99),
529 ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data[
"group_id"], 99),
530 ATTR_STATUS: RUN_STATE_MAP[data[
"state"]],
531 ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(data.get(
"sun")),
532 ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(data[
"type"], 99),
535 if "waterSense" in data:
536 if "area" in data[
"waterSense"]:
537 attrs[ATTR_AREA] = round(data[
"waterSense"][
"area"], 2)
538 if "fieldCapacity" in data[
"waterSense"]:
539 attrs[ATTR_FIELD_CAPACITY] = round(
540 data[
"waterSense"][
"fieldCapacity"], 2
542 if "precipitationRate" in data[
"waterSense"]:
543 attrs[ATTR_PRECIP_RATE] = round(
544 data[
"waterSense"][
"precipitationRate"], 2
547 if self.
_entry_entry_entry.options[CONF_USE_APP_RUN_TIMES]:
548 provision_data = self.
_data_data.coordinators[DATA_PROVISION_SETTINGS].data
549 if zone_durations := provision_data.get(
"system", {}).
get(
"zoneDuration"):
550 attrs[ATTR_ZONE_RUN_TIME] = zone_durations[
558 """Define a switch to enable/disable a RainMachine zone."""
560 @raise_on_request_error
562 """Disable the zone."""
567 await asyncio.gather(*tasks)
570 @raise_on_request_error
572 """Enable the zone."""
_attr_extra_state_attributes
None __init__(self, ConfigEntry entry, RainMachineData data, RainMachineSwitchDescription description)
None async_turn_off(self, **Any kwargs)
None async_turn_off_when_active(self, **Any kwargs)
None async_turn_on(self, **Any kwargs)
None async_turn_on_when_active(self, **Any kwargs)
None __init__(self, ConfigEntry entry, RainMachineData data, RainMachineSwitchDescription description)
None async_stop_zone(self)
None async_stop_program(self)
None async_start_zone(self, *int zone_run_time)
None _update_activities(self)
None async_start_program(self)
None update_from_latest_data(self)
None __init__(self, ConfigEntry entry, RainMachineData data, RainMachineSwitchDescription description)
None async_turn_on(self, **Any kwargs)
None async_turn_off(self, **Any kwargs)
None async_turn_on_when_active(self, **Any kwargs)
None update_from_latest_data(self)
None async_turn_off_when_active(self, **Any kwargs)
None async_start_program(self)
None async_stop_program(self)
None update_from_latest_data(self)
None async_turn_off(self, **Any kwargs)
None async_turn_on(self, **Any kwargs)
None async_turn_off(self, **Any kwargs)
None async_turn_on(self, **Any kwargs)
None update_from_latest_data(self)
None async_turn_off_when_active(self, **Any kwargs)
None async_start_zone(self, *int zone_run_time)
None async_stop_zone(self)
None async_turn_on_when_active(self, **Any kwargs)
None async_write_ha_state(self)
None async_turn_off(self, **Any kwargs)
None async_turn_on(self, **Any kwargs)
web.Response get(self, web.Request request, str config_key)
IssData update(pyiss.ISS iss)
None async_setup_entry(HomeAssistant hass, RainMachineConfigEntry entry, AddEntitiesCallback async_add_entities)
bool key_exists(dict[str, Any] data, str search_key)
None async_update_programs_and_zones(HomeAssistant hass, RainMachineConfigEntry entry)