From bc0412e19c394b5c6d2f5239ee4758238917477e Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 9 Jul 2026 12:29:23 +0200 Subject: [PATCH] Move Rachio services to async_setup (#175479) --- homeassistant/components/rachio/__init__.py | 13 +- homeassistant/components/rachio/device.py | 82 +----------- homeassistant/components/rachio/services.py | 135 ++++++++++++++++++++ homeassistant/components/rachio/switch.py | 59 +-------- 4 files changed, 150 insertions(+), 139 deletions(-) create mode 100644 homeassistant/components/rachio/services.py diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index ab0886096cc7..90de274fb938 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -10,9 +10,12 @@ from homeassistant.components import cloud from homeassistant.const import CONF_API_KEY, CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType -from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS +from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, DOMAIN from .device import RachioConfigEntry, RachioPerson +from .services import async_setup_services from .webhooks import ( async_get_or_create_registered_webhook_id_and_url, async_register_webhook, @@ -23,6 +26,14 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BINARY_SENSOR, Platform.CALENDAR, Platform.SWITCH] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Rachio integration.""" + async_setup_services(hass) + return True + async def async_unload_entry(hass: HomeAssistant, entry: RachioConfigEntry) -> bool: """Unload a config entry.""" diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 919f323029aa..5bf10c08f23f 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -5,16 +5,13 @@ import logging from typing import Any, override from rachiopy import Rachio -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv from .const import ( - DOMAIN, KEY_BASE_STATIONS, KEY_DEVICES, KEY_ENABLED, @@ -30,31 +27,14 @@ from .const import ( KEY_USERNAME, KEY_ZONES, LISTEN_EVENT_TYPES, - MODEL_GENERATION_1, - SERVICE_PAUSE_WATERING, - SERVICE_RESUME_WATERING, - SERVICE_STOP_WATERING, WEBHOOK_CONST_ID, ) from .coordinator import RachioScheduleUpdateCoordinator, RachioUpdateCoordinator _LOGGER = logging.getLogger(__name__) -ATTR_DEVICES = "devices" -ATTR_DURATION = "duration" PERMISSION_ERROR = "7" -PAUSE_SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_DEVICES): cv.string, - vol.Optional(ATTR_DURATION, default=60): cv.positive_int, - } -) - -RESUME_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string}) - -STOP_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string}) - type RachioConfigEntry = ConfigEntry[RachioPerson] @@ -72,66 +52,8 @@ class RachioPerson: self._base_stations: list[RachioBaseStation] = [] async def async_setup(self, hass: HomeAssistant) -> None: - """Create rachio devices and services.""" + """Create rachio devices.""" await hass.async_add_executor_job(self._setup, hass) - can_pause = False - for rachio_iro in self._controllers: - # Generation 1 controllers don't support pause or resume - if rachio_iro.model.split("_")[0] != MODEL_GENERATION_1: - can_pause = True - break - - all_controllers = [rachio_iro.name for rachio_iro in self._controllers] - - def pause_water(service: ServiceCall) -> None: - """Service to pause watering on all or specific controllers.""" - duration = service.data[ATTR_DURATION] - devices = service.data.get(ATTR_DEVICES, all_controllers) - for iro in self._controllers: - if iro.name in devices: - iro.pause_watering(duration) - - def resume_water(service: ServiceCall) -> None: - """Service to resume watering on all or specific controllers.""" - devices = service.data.get(ATTR_DEVICES, all_controllers) - for iro in self._controllers: - if iro.name in devices: - iro.resume_watering() - - def stop_water(service: ServiceCall) -> None: - """Service to stop watering on all or specific controllers.""" - devices = service.data.get(ATTR_DEVICES, all_controllers) - for iro in self._controllers: - if iro.name in devices: - iro.stop_watering() - - # If only hose timers on account, none of these services apply - if not all_controllers: - return - - hass.services.async_register( - DOMAIN, - SERVICE_STOP_WATERING, - stop_water, - schema=STOP_SERVICE_SCHEMA, - ) - - if not can_pause: - return - - hass.services.async_register( - DOMAIN, - SERVICE_PAUSE_WATERING, - pause_water, - schema=PAUSE_SERVICE_SCHEMA, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_RESUME_WATERING, - resume_water, - schema=RESUME_SERVICE_SCHEMA, - ) def _setup(self, hass: HomeAssistant) -> None: """Rachio device setup.""" diff --git a/homeassistant/components/rachio/services.py b/homeassistant/components/rachio/services.py new file mode 100644 index 000000000000..1e30e85e4645 --- /dev/null +++ b/homeassistant/components/rachio/services.py @@ -0,0 +1,135 @@ +"""Services for the Rachio integration.""" + +import logging + +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID, Platform +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import ( + config_validation as cv, + entity_registry as er, + service, +) + +from .const import ( + DOMAIN, + KEY_ID, + MODEL_GENERATION_1, + SERVICE_PAUSE_WATERING, + SERVICE_RESUME_WATERING, + SERVICE_START_MULTIPLE_ZONES, + SERVICE_STOP_WATERING, +) +from .device import RachioConfigEntry + +_LOGGER = logging.getLogger(__name__) + +ATTR_DEVICES = "devices" +ATTR_DURATION = "duration" +ATTR_SORT_ORDER = "sortOrder" + +PAUSE_SERVICE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_DEVICES): cv.string, + vol.Optional(ATTR_DURATION, default=60): cv.positive_int, + } +) + +RESUME_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string}) + +START_MULTIPLE_ZONES_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_DURATION): cv.ensure_list_csv, + } +) + +STOP_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string}) + + +def _stop_water(call: ServiceCall) -> None: + """Stop watering on all or specific controllers.""" + entry: RachioConfigEntry = service.async_get_config_entry(call.hass, DOMAIN, None) + person = entry.runtime_data + devices = call.data.get(ATTR_DEVICES, [iro.name for iro in person.controllers]) + for iro in person.controllers: + if iro.name in devices: + iro.stop_watering() + + +def _pause_water(call: ServiceCall) -> None: + """Pause watering on all or specific controllers.""" + entry: RachioConfigEntry = service.async_get_config_entry(call.hass, DOMAIN, None) + person = entry.runtime_data + devices = call.data.get(ATTR_DEVICES, [iro.name for iro in person.controllers]) + for iro in person.controllers: + if iro.name in devices and iro.model.split("_")[0] != MODEL_GENERATION_1: + iro.pause_watering(call.data[ATTR_DURATION]) + + +def _resume_water(call: ServiceCall) -> None: + """Resume watering on all or specific controllers.""" + entry: RachioConfigEntry = service.async_get_config_entry(call.hass, DOMAIN, None) + person = entry.runtime_data + devices = call.data.get(ATTR_DEVICES, [iro.name for iro in person.controllers]) + for iro in person.controllers: + if iro.name in devices and iro.model.split("_")[0] != MODEL_GENERATION_1: + iro.resume_watering() + + +def _start_multiple(call: ServiceCall) -> None: + """Start multiple zones in sequence.""" + entry: RachioConfigEntry = service.async_get_config_entry(call.hass, DOMAIN, None) + person = entry.runtime_data + entity_reg = er.async_get(call.hass) + duration = iter(call.data[ATTR_DURATION]) + default_time = call.data[ATTR_DURATION][0] + + entity_to_zone_id = { + entity_reg.async_get_entity_id( + Platform.SWITCH, + DOMAIN, + f"{controller.controller_id}-zone-{zone[KEY_ID]}", + ): zone[KEY_ID] + for controller in person.controllers + for zone in controller.list_zones() + } + + zones_list = [ + { + ATTR_ID: entity_to_zone_id[entity_id], + ATTR_DURATION: int(next(duration, default_time)) * 60, + ATTR_SORT_ORDER: count, + } + for count, entity_id in enumerate(call.data[ATTR_ENTITY_ID]) + if entity_id in entity_to_zone_id + ] + + if not zones_list: + raise HomeAssistantError("No matching zones found in given entity_ids") + + person.start_multiple_zones(zones_list) + _LOGGER.debug("Starting zone(s) %s", call.data[ATTR_ENTITY_ID]) + + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Register Rachio services.""" + + hass.services.async_register( + DOMAIN, SERVICE_STOP_WATERING, _stop_water, schema=STOP_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_PAUSE_WATERING, _pause_water, schema=PAUSE_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_RESUME_WATERING, _resume_water, schema=RESUME_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, + SERVICE_START_MULTIPLE_ZONES, + _start_multiple, + schema=START_MULTIPLE_ZONES_SCHEMA, + ) diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 146618fad2be..83025aa03c85 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -9,9 +9,7 @@ from typing import Any, override import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -22,7 +20,6 @@ from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_ti from .const import ( CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, - DOMAIN, KEY_CURRENT_STATUS, KEY_CUSTOM_CROP, KEY_CUSTOM_SHADE, @@ -45,7 +42,6 @@ from .const import ( SCHEDULE_TYPE_FIXED, SCHEDULE_TYPE_FLEX, SERVICE_SET_ZONE_MOISTURE, - SERVICE_START_MULTIPLE_ZONES, SERVICE_START_WATERING, SIGNAL_RACHIO_CONTROLLER_UPDATE, SIGNAL_RACHIO_RAIN_DELAY_UPDATE, @@ -80,7 +76,6 @@ ATTR_SCHEDULE_SUMMARY = "Summary" ATTR_SCHEDULE_ENABLED = "Enabled" ATTR_SCHEDULE_DURATION = "Duration" ATTR_SCHEDULE_TYPE = "Type" -ATTR_SORT_ORDER = "sortOrder" ATTR_WATERING_DURATION = "Watering Duration seconds" ATTR_ZONE_NUMBER = "Zone number" ATTR_ZONE_SHADE = "Shade" @@ -88,13 +83,6 @@ ATTR_ZONE_SLOPE = "Slope" ATTR_ZONE_SUMMARY = "Summary" ATTR_ZONE_TYPE = "Type" -START_MULTIPLE_ZONES_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_DURATION): cv.ensure_list_csv, - } -) - async def async_setup_entry( hass: HomeAssistant, @@ -102,47 +90,14 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Rachio switches.""" - zone_entities = [] has_flex_sched = False entities = await hass.async_add_executor_job(_create_entities, hass, config_entry) for entity in entities: - if isinstance(entity, RachioZone): - zone_entities.append(entity) if isinstance(entity, RachioSchedule) and entity.type == SCHEDULE_TYPE_FLEX: has_flex_sched = True async_add_entities(entities) - def start_multiple(service: ServiceCall) -> None: - """Service to start multiple zones in sequence.""" - zones_list = [] - person = config_entry.runtime_data - entity_id = service.data[ATTR_ENTITY_ID] - duration = iter(service.data[ATTR_DURATION]) - default_time = service.data[ATTR_DURATION][0] - entity_to_zone_id = { - entity.entity_id: entity.zone_id for entity in zone_entities - } - - for count, data in enumerate(entity_id): - if data in entity_to_zone_id: - # Time can be passed as a list per zone, - # or one time for all zones - time = int(next(duration, default_time)) * 60 - zones_list.append( - { - ATTR_ID: entity_to_zone_id.get(data), - ATTR_DURATION: time, - ATTR_SORT_ORDER: count, - } - ) - - if len(zones_list) != 0: - person.start_multiple_zones(zones_list) - _LOGGER.debug("Starting zone(s) %s", entity_id) - else: - raise HomeAssistantError("No matching zones found in given entity_ids") - platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_START_WATERING, @@ -152,18 +107,6 @@ async def async_setup_entry( "turn_on", ) - # If only hose timers on account, none of these services apply - if not zone_entities: - return - - # pylint: disable-next=home-assistant-service-registered-in-setup-entry - hass.services.async_register( - DOMAIN, - SERVICE_START_MULTIPLE_ZONES, - start_multiple, - schema=START_MULTIPLE_ZONES_SCHEMA, - ) - if has_flex_sched: platform = entity_platform.async_get_current_platform() platform.async_register_entity_service(