From 03644055957219c01608f6c89af87980694bed9e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 8 Oct 2021 12:03:47 -0600 Subject: [PATCH] Reorganize RainMachine services (#57145) * Reorganize RainMachine services * Code review * Ensure integration services aren't tied to a particular config entry * Cleanup * linting * Code review * Code review * Code review * Code review --- .../components/rainmachine/__init__.py | 81 +++++++++- .../components/rainmachine/services.yaml | 152 ++++++------------ .../components/rainmachine/switch.py | 146 ++++++++--------- 3 files changed, 191 insertions(+), 188 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index d5eceab05fc..0d25b6c41c7 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -9,16 +9,18 @@ from typing import Any from regenmaschine import Client from regenmaschine.controller import Controller from regenmaschine.errors import RainMachineError +import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, + CONF_DEVICE_ID, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr @@ -44,6 +46,8 @@ from .const import ( LOGGER, ) +CONF_SECONDS = "seconds" + DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True @@ -53,6 +57,39 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] +SERVICE_NAME_PAUSE_WATERING = "pause_watering" +SERVICE_NAME_STOP_ALL = "stop_all" +SERVICE_NAME_UNPAUSE_WATERING = "unpause_watering" + +SERVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + } +) + +SERVICE_PAUSE_WATERING_SCHEMA = SERVICE_SCHEMA.extend( + { + vol.Required(CONF_SECONDS): cv.positive_int, + } +) + + +@callback +def async_get_controller_for_service_call( + hass: HomeAssistant, call: ServiceCall +) -> Controller: + """Get the controller related to a service call (by device ID).""" + controllers: dict[str, Controller] = hass.data[DOMAIN][DATA_CONTROLLER] + device_id = call.data[CONF_DEVICE_ID] + device_registry = dr.async_get(hass) + + if device_entry := device_registry.async_get(device_id): + for entry_id in device_entry.config_entries: + if entry_id in controllers: + return controllers[entry_id] + + raise ValueError(f"No controller for device ID: {device_id}") + async def async_update_programs_and_zones( hass: HomeAssistant, entry: ConfigEntry @@ -158,6 +195,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + async def async_pause_watering(call: ServiceCall) -> None: + """Pause watering for a set number of seconds.""" + controller = async_get_controller_for_service_call(hass, call) + await controller.watering.pause_all(call.data[CONF_SECONDS]) + await async_update_programs_and_zones(hass, entry) + + async def async_stop_all(call: ServiceCall) -> None: + """Stop all watering.""" + controller = async_get_controller_for_service_call(hass, call) + await controller.watering.stop_all() + await async_update_programs_and_zones(hass, entry) + + async def async_unpause_watering(call: ServiceCall) -> None: + """Unpause watering.""" + controller = async_get_controller_for_service_call(hass, call) + await controller.watering.unpause_all() + await async_update_programs_and_zones(hass, entry) + + for service_name, schema, method in ( + ( + SERVICE_NAME_PAUSE_WATERING, + SERVICE_PAUSE_WATERING_SCHEMA, + async_pause_watering, + ), + (SERVICE_NAME_STOP_ALL, SERVICE_SCHEMA, async_stop_all), + (SERVICE_NAME_UNPAUSE_WATERING, SERVICE_SCHEMA, async_unpause_watering), + ): + if hass.services.has_service(DOMAIN, service_name): + continue + hass.services.async_register(DOMAIN, service_name, method, schema=schema) + return True @@ -166,6 +234,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + + if len(hass.config_entries.async_entries(DOMAIN)) == 1: + # If this is the last instance of RainMachine, deregister any services defined + # during integration setup: + for service_name in ( + SERVICE_NAME_PAUSE_WATERING, + SERVICE_NAME_STOP_ALL, + SERVICE_NAME_UNPAUSE_WATERING, + ): + hass.services.async_remove(DOMAIN, service_name) + return unload_ok diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index c12e0938059..fc6a71f8842 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -1,79 +1,46 @@ # Describes the format for available RainMachine services disable_program: - name: Disable program - description: Disable a program. + name: Disable Program + description: Disable a program target: entity: integration: rainmachine domain: switch - fields: - program_id: - name: Program ID - description: The program to disable. - required: true - selector: - number: - min: 1 - max: 255 disable_zone: - name: Disable zone - description: Disable a zone. + name: Disable Zone + description: Disable a zone target: entity: integration: rainmachine domain: switch - fields: - zone_id: - name: Zone ID - description: The zone to disable. - required: true - selector: - number: - min: 1 - max: 255 enable_program: - name: Enable program - description: Enable a program. + name: Enable Program + description: Enable a program target: entity: integration: rainmachine domain: switch - fields: - program_id: - name: Program ID - description: The program to enable. - required: true - selector: - number: - min: 1 - max: 255 enable_zone: - name: Enable zone - description: Enable a zone. + name: Enable Zone + description: Enable a zone target: entity: integration: rainmachine domain: switch +pause_watering: + name: Pause All Watering + description: Pause all watering activities for a number of seconds fields: - zone_id: - name: Zone ID - description: The zone to enable. + device_id: + name: Controller + description: The controller whose watering activities should be paused required: true selector: - number: - min: 1 - max: 255 -pause_watering: - name: Pause watering - description: Pause all watering for a number of seconds. - target: - entity: - integration: rainmachine - domain: switch - fields: + device: + integration: rainmachine seconds: - name: Seconds - description: The time to pause. + name: Duration + description: The amount of time (in seconds) to pause watering required: true selector: number: @@ -81,40 +48,23 @@ pause_watering: max: 86400 unit_of_measurement: seconds start_program: - name: Start program - description: Start a program. + name: Start Program + description: Start a program target: entity: integration: rainmachine domain: switch - fields: - program_id: - name: Program ID - description: The program to start. - required: true - selector: - number: - min: 1 - max: 255 start_zone: - name: Start zone - description: Start a zone for a set number of seconds. + name: Start Zone + description: Start a zone target: entity: integration: rainmachine domain: switch fields: - zone_id: - name: Zone ID - description: The zone to start. - required: true - selector: - number: - min: 1 - max: 255 zone_run_time: - name: Zone run time - description: The number of seconds to run the zone. + name: Run Time + description: The amount of time (in seconds) to run the zone default: 600 selector: number: @@ -122,48 +72,38 @@ start_zone: max: 86400 mode: box stop_all: - name: Stop all - description: Stop all watering activities. - target: - entity: - integration: rainmachine - domain: switch + name: Stop All Watering + description: Stop all watering activities + fields: + device_id: + name: Controller + description: The controller whose watering activities should be stopped + required: true + selector: + device: + integration: rainmachine stop_program: - name: Stop program - description: Stop a program. + name: Stop Program + description: Stop a program target: entity: integration: rainmachine domain: switch - fields: - program_id: - name: Program ID - description: The program to stop. - required: true - selector: - number: - min: 1 - max: 255 stop_zone: - name: Stop zone - description: Stop a zone. + name: Stop Zone + description: Stop a zone target: entity: integration: rainmachine domain: switch +unpause_watering: + name: Unpause All Watering + description: Unpause all paused watering activities fields: - zone_id: - name: Zone ID - description: The zone to stop. + device_id: + name: Controller + description: The controller whose watering activities should be unpaused required: true selector: - number: - min: 1 - max: 255 -unpause_watering: - name: Unpause watering - description: Unpause all watering. - target: - entity: - integration: rainmachine - domain: switch + device: + integration: rainmachine diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index a4d4bce2383..bf84bc1f360 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -51,10 +51,6 @@ ATTR_TIME_REMAINING = "time_remaining" ATTR_VEGETATION_TYPE = "vegetation_type" ATTR_ZONES = "zones" -CONF_PROGRAM_ID = "program_id" -CONF_SECONDS = "seconds" -CONF_ZONE_ID = "zone_id" - DEFAULT_ICON = "mdi:water" DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] @@ -112,9 +108,6 @@ VEGETATION_MAP = { 99: "Other", } -SWITCH_TYPE_PROGRAM = "program" -SWITCH_TYPE_ZONE = "zone" - @dataclass class RainMachineSwitchDescriptionMixin: @@ -136,42 +129,23 @@ async def async_setup_entry( """Set up RainMachine switches based on a config entry.""" platform = entity_platform.async_get_current_platform() - alter_program_schema = {vol.Required(CONF_PROGRAM_ID): cv.positive_int} - alter_zone_schema = {vol.Required(CONF_ZONE_ID): cv.positive_int} - for service_name, schema, method in ( - ("disable_program", alter_program_schema, "async_disable_program"), - ("disable_zone", alter_zone_schema, "async_disable_zone"), - ("enable_program", alter_program_schema, "async_enable_program"), - ("enable_zone", alter_zone_schema, "async_enable_zone"), - ( - "pause_watering", - {vol.Required(CONF_SECONDS): cv.positive_int}, - "async_pause_watering", - ), - ( - "start_program", - {vol.Required(CONF_PROGRAM_ID): cv.positive_int}, - "async_start_program", - ), + ("disable_program", {}, "async_disable_program"), + ("disable_zone", {}, "async_disable_zone"), + ("enable_program", {}, "async_enable_program"), + ("enable_zone", {}, "async_enable_zone"), + ("start_program", {}, "async_start_program"), ( "start_zone", { - vol.Required(CONF_ZONE_ID): cv.positive_int, vol.Optional( CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN - ): cv.positive_int, + ): cv.positive_int }, "async_start_zone", ), - ("stop_all", {}, "async_stop_all"), - ( - "stop_program", - {vol.Required(CONF_PROGRAM_ID): cv.positive_int}, - "async_stop_program", - ), - ("stop_zone", {vol.Required(CONF_ZONE_ID): cv.positive_int}, "async_stop_zone"), - ("unpause_watering", {}, "async_unpause_watering"), + ("stop_program", {}, "async_stop_program"), + ("stop_zone", {}, "async_stop_zone"), ): platform.async_register_entity_service(service_name, schema, method) @@ -187,9 +161,7 @@ async def async_setup_entry( controller, entry, RainMachineSwitchDescription( - key=f"RainMachineProgram_{uid}", - name=program["name"], - uid=uid, + key=f"RainMachineProgram_{uid}", name=program["name"], uid=uid ), ) for uid, program in programs_coordinator.data.items() @@ -201,9 +173,7 @@ async def async_setup_entry( controller, entry, RainMachineSwitchDescription( - key=f"RainMachineZone_{uid}", - name=zone["name"], - uid=uid, + key=f"RainMachineZone_{uid}", name=zone["name"], uid=uid ), ) for uid, zone in zones_coordinator.data.items() @@ -267,60 +237,37 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): async_update_programs_and_zones(self.hass, self._entry) ) - async def async_disable_program(self, *, program_id: int) -> None: + async def async_disable_program(self) -> None: """Disable a program.""" - await self._controller.programs.disable(program_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_disable_zone(self, *, zone_id: int) -> None: + async def async_disable_zone(self) -> None: """Disable a zone.""" - await self._controller.zones.disable(zone_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_enable_program(self, *, program_id: int) -> None: + async def async_enable_program(self) -> None: """Enable a program.""" - await self._controller.programs.enable(program_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_enable_zone(self, *, zone_id: int) -> None: + async def async_enable_zone(self) -> None: """Enable a zone.""" - await self._controller.zones.enable(zone_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_pause_watering(self, *, seconds: int) -> None: - """Pause watering for a set number of seconds.""" - await self._controller.watering.pause_all(seconds) - await async_update_programs_and_zones(self.hass, self._entry) + async def async_start_program(self) -> None: + """Start a program.""" + raise NotImplementedError("Service not implemented for this entity") - async def async_start_program(self, *, program_id: int) -> None: - """Start a particular program.""" - await self._controller.programs.start(program_id) - await async_update_programs_and_zones(self.hass, self._entry) + async def async_start_zone(self, *, zone_run_time: int) -> None: + """Start a zone.""" + raise NotImplementedError("Service not implemented for this entity") - async def async_start_zone(self, *, zone_id: int, zone_run_time: int) -> None: - """Start a particular zone for a certain amount of time.""" - await self._controller.zones.start(zone_id, zone_run_time) - await async_update_programs_and_zones(self.hass, self._entry) - - async def async_stop_all(self) -> None: - """Stop all watering.""" - await self._controller.watering.stop_all() - await async_update_programs_and_zones(self.hass, self._entry) - - async def async_stop_program(self, *, program_id: int) -> None: + async def async_stop_program(self) -> None: """Stop a program.""" - await self._controller.programs.stop(program_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_stop_zone(self, *, zone_id: int) -> None: + async def async_stop_zone(self) -> None: """Stop a zone.""" - await self._controller.zones.stop(zone_id) - await async_update_programs_and_zones(self.hass, self._entry) - - async def async_unpause_watering(self) -> None: - """Unpause watering.""" - await self._controller.watering.unpause_all() - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") @callback def update_from_latest_data(self) -> None: @@ -337,6 +284,24 @@ class RainMachineProgram(RainMachineSwitch): """Return a list of active zones associated with this program.""" return [z for z in self._data["wateringTimes"] if z["active"]] + async def async_disable_program(self) -> None: + """Disable a program.""" + await self._controller.programs.disable(self.entity_description.uid) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_enable_program(self) -> None: + """Enable a program.""" + await self._controller.programs.enable(self.entity_description.uid) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_start_program(self) -> None: + """Start a program.""" + await self.async_turn_on() + + async def async_stop_program(self) -> None: + """Stop a program.""" + await self.async_turn_off() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the program off.""" await self._async_run_switch_coroutine( @@ -381,6 +346,25 @@ class RainMachineProgram(RainMachineSwitch): class RainMachineZone(RainMachineSwitch): """A RainMachine zone.""" + async def async_disable_zone(self) -> None: + """Disable a zone.""" + await self._controller.zones.disable(self.entity_description.uid) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_enable_zone(self) -> None: + """Enable a zone.""" + await self._controller.zones.enable(self.entity_description.uid) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_start_zone(self, *, zone_run_time: int) -> None: + """Start a particular zone for a certain amount of time.""" + await self._controller.zones.start(self.entity_description.uid, zone_run_time) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_stop_zone(self) -> None: + """Stop a zone.""" + await self.async_turn_off() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self._async_run_switch_coroutine(