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
This commit is contained in:
Aaron Bach 2021-10-08 12:03:47 -06:00 committed by GitHub
parent eba1d7d16a
commit 0364405595
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 191 additions and 188 deletions

View File

@ -9,16 +9,18 @@ from typing import Any
from regenmaschine import Client from regenmaschine import Client
from regenmaschine.controller import Controller from regenmaschine.controller import Controller
from regenmaschine.errors import RainMachineError from regenmaschine.errors import RainMachineError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
CONF_DEVICE_ID,
CONF_IP_ADDRESS, CONF_IP_ADDRESS,
CONF_PASSWORD, CONF_PASSWORD,
CONF_PORT, CONF_PORT,
CONF_SSL, CONF_SSL,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
@ -44,6 +46,8 @@ from .const import (
LOGGER, LOGGER,
) )
CONF_SECONDS = "seconds"
DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC"
DEFAULT_ICON = "mdi:water" DEFAULT_ICON = "mdi:water"
DEFAULT_SSL = True DEFAULT_SSL = True
@ -53,6 +57,39 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN)
PLATFORMS = ["binary_sensor", "sensor", "switch"] 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( async def async_update_programs_and_zones(
hass: HomeAssistant, entry: ConfigEntry 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)) 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 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) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: if unload_ok:
hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) 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 return unload_ok

View File

@ -1,79 +1,46 @@
# Describes the format for available RainMachine services # Describes the format for available RainMachine services
disable_program: disable_program:
name: Disable program name: Disable Program
description: Disable a program. description: Disable a program
target: target:
entity: entity:
integration: rainmachine integration: rainmachine
domain: switch domain: switch
fields:
program_id:
name: Program ID
description: The program to disable.
required: true
selector:
number:
min: 1
max: 255
disable_zone: disable_zone:
name: Disable zone name: Disable Zone
description: Disable a zone. description: Disable a zone
target: target:
entity: entity:
integration: rainmachine integration: rainmachine
domain: switch domain: switch
fields:
zone_id:
name: Zone ID
description: The zone to disable.
required: true
selector:
number:
min: 1
max: 255
enable_program: enable_program:
name: Enable program name: Enable Program
description: Enable a program. description: Enable a program
target: target:
entity: entity:
integration: rainmachine integration: rainmachine
domain: switch domain: switch
fields:
program_id:
name: Program ID
description: The program to enable.
required: true
selector:
number:
min: 1
max: 255
enable_zone: enable_zone:
name: Enable zone name: Enable Zone
description: Enable a zone. description: Enable a zone
target: target:
entity: entity:
integration: rainmachine integration: rainmachine
domain: switch domain: switch
pause_watering:
name: Pause All Watering
description: Pause all watering activities for a number of seconds
fields: fields:
zone_id: device_id:
name: Zone ID name: Controller
description: The zone to enable. description: The controller whose watering activities should be paused
required: true required: true
selector: selector:
number: device:
min: 1 integration: rainmachine
max: 255
pause_watering:
name: Pause watering
description: Pause all watering for a number of seconds.
target:
entity:
integration: rainmachine
domain: switch
fields:
seconds: seconds:
name: Seconds name: Duration
description: The time to pause. description: The amount of time (in seconds) to pause watering
required: true required: true
selector: selector:
number: number:
@ -81,40 +48,23 @@ pause_watering:
max: 86400 max: 86400
unit_of_measurement: seconds unit_of_measurement: seconds
start_program: start_program:
name: Start program name: Start Program
description: Start a program. description: Start a program
target: target:
entity: entity:
integration: rainmachine integration: rainmachine
domain: switch domain: switch
fields:
program_id:
name: Program ID
description: The program to start.
required: true
selector:
number:
min: 1
max: 255
start_zone: start_zone:
name: Start zone name: Start Zone
description: Start a zone for a set number of seconds. description: Start a zone
target: target:
entity: entity:
integration: rainmachine integration: rainmachine
domain: switch domain: switch
fields: fields:
zone_id:
name: Zone ID
description: The zone to start.
required: true
selector:
number:
min: 1
max: 255
zone_run_time: zone_run_time:
name: Zone run time name: Run Time
description: The number of seconds to run the zone. description: The amount of time (in seconds) to run the zone
default: 600 default: 600
selector: selector:
number: number:
@ -122,48 +72,38 @@ start_zone:
max: 86400 max: 86400
mode: box mode: box
stop_all: stop_all:
name: Stop all name: Stop All Watering
description: Stop all watering activities. description: Stop all watering activities
target: fields:
entity: device_id:
integration: rainmachine name: Controller
domain: switch description: The controller whose watering activities should be stopped
required: true
selector:
device:
integration: rainmachine
stop_program: stop_program:
name: Stop program name: Stop Program
description: Stop a program. description: Stop a program
target: target:
entity: entity:
integration: rainmachine integration: rainmachine
domain: switch domain: switch
fields:
program_id:
name: Program ID
description: The program to stop.
required: true
selector:
number:
min: 1
max: 255
stop_zone: stop_zone:
name: Stop zone name: Stop Zone
description: Stop a zone. description: Stop a zone
target: target:
entity: entity:
integration: rainmachine integration: rainmachine
domain: switch domain: switch
unpause_watering:
name: Unpause All Watering
description: Unpause all paused watering activities
fields: fields:
zone_id: device_id:
name: Zone ID name: Controller
description: The zone to stop. description: The controller whose watering activities should be unpaused
required: true required: true
selector: selector:
number: device:
min: 1 integration: rainmachine
max: 255
unpause_watering:
name: Unpause watering
description: Unpause all watering.
target:
entity:
integration: rainmachine
domain: switch

View File

@ -51,10 +51,6 @@ ATTR_TIME_REMAINING = "time_remaining"
ATTR_VEGETATION_TYPE = "vegetation_type" ATTR_VEGETATION_TYPE = "vegetation_type"
ATTR_ZONES = "zones" ATTR_ZONES = "zones"
CONF_PROGRAM_ID = "program_id"
CONF_SECONDS = "seconds"
CONF_ZONE_ID = "zone_id"
DEFAULT_ICON = "mdi:water" DEFAULT_ICON = "mdi:water"
DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
@ -112,9 +108,6 @@ VEGETATION_MAP = {
99: "Other", 99: "Other",
} }
SWITCH_TYPE_PROGRAM = "program"
SWITCH_TYPE_ZONE = "zone"
@dataclass @dataclass
class RainMachineSwitchDescriptionMixin: class RainMachineSwitchDescriptionMixin:
@ -136,42 +129,23 @@ async def async_setup_entry(
"""Set up RainMachine switches based on a config entry.""" """Set up RainMachine switches based on a config entry."""
platform = entity_platform.async_get_current_platform() 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 ( for service_name, schema, method in (
("disable_program", alter_program_schema, "async_disable_program"), ("disable_program", {}, "async_disable_program"),
("disable_zone", alter_zone_schema, "async_disable_zone"), ("disable_zone", {}, "async_disable_zone"),
("enable_program", alter_program_schema, "async_enable_program"), ("enable_program", {}, "async_enable_program"),
("enable_zone", alter_zone_schema, "async_enable_zone"), ("enable_zone", {}, "async_enable_zone"),
( ("start_program", {}, "async_start_program"),
"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",
),
( (
"start_zone", "start_zone",
{ {
vol.Required(CONF_ZONE_ID): cv.positive_int,
vol.Optional( vol.Optional(
CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN
): cv.positive_int, ): cv.positive_int
}, },
"async_start_zone", "async_start_zone",
), ),
("stop_all", {}, "async_stop_all"), ("stop_program", {}, "async_stop_program"),
( ("stop_zone", {}, "async_stop_zone"),
"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"),
): ):
platform.async_register_entity_service(service_name, schema, method) platform.async_register_entity_service(service_name, schema, method)
@ -187,9 +161,7 @@ async def async_setup_entry(
controller, controller,
entry, entry,
RainMachineSwitchDescription( RainMachineSwitchDescription(
key=f"RainMachineProgram_{uid}", key=f"RainMachineProgram_{uid}", name=program["name"], uid=uid
name=program["name"],
uid=uid,
), ),
) )
for uid, program in programs_coordinator.data.items() for uid, program in programs_coordinator.data.items()
@ -201,9 +173,7 @@ async def async_setup_entry(
controller, controller,
entry, entry,
RainMachineSwitchDescription( RainMachineSwitchDescription(
key=f"RainMachineZone_{uid}", key=f"RainMachineZone_{uid}", name=zone["name"], uid=uid
name=zone["name"],
uid=uid,
), ),
) )
for uid, zone in zones_coordinator.data.items() 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_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.""" """Disable a program."""
await self._controller.programs.disable(program_id) raise NotImplementedError("Service not implemented for this entity")
await async_update_programs_and_zones(self.hass, self._entry)
async def async_disable_zone(self, *, zone_id: int) -> None: async def async_disable_zone(self) -> None:
"""Disable a zone.""" """Disable a zone."""
await self._controller.zones.disable(zone_id) raise NotImplementedError("Service not implemented for this entity")
await async_update_programs_and_zones(self.hass, self._entry)
async def async_enable_program(self, *, program_id: int) -> None: async def async_enable_program(self) -> None:
"""Enable a program.""" """Enable a program."""
await self._controller.programs.enable(program_id) raise NotImplementedError("Service not implemented for this entity")
await async_update_programs_and_zones(self.hass, self._entry)
async def async_enable_zone(self, *, zone_id: int) -> None: async def async_enable_zone(self) -> None:
"""Enable a zone.""" """Enable a zone."""
await self._controller.zones.enable(zone_id) raise NotImplementedError("Service not implemented for this entity")
await async_update_programs_and_zones(self.hass, self._entry)
async def async_pause_watering(self, *, seconds: int) -> None: async def async_start_program(self) -> None:
"""Pause watering for a set number of seconds.""" """Start a program."""
await self._controller.watering.pause_all(seconds) raise NotImplementedError("Service not implemented for this entity")
await async_update_programs_and_zones(self.hass, self._entry)
async def async_start_program(self, *, program_id: int) -> None: async def async_start_zone(self, *, zone_run_time: int) -> None:
"""Start a particular program.""" """Start a zone."""
await self._controller.programs.start(program_id) raise NotImplementedError("Service not implemented for this entity")
await async_update_programs_and_zones(self.hass, self._entry)
async def async_start_zone(self, *, zone_id: int, zone_run_time: int) -> None: async def async_stop_program(self) -> 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:
"""Stop a program.""" """Stop a program."""
await self._controller.programs.stop(program_id) raise NotImplementedError("Service not implemented for this entity")
await async_update_programs_and_zones(self.hass, self._entry)
async def async_stop_zone(self, *, zone_id: int) -> None: async def async_stop_zone(self) -> None:
"""Stop a zone.""" """Stop a zone."""
await self._controller.zones.stop(zone_id) raise NotImplementedError("Service not implemented for this entity")
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)
@callback @callback
def update_from_latest_data(self) -> None: 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 a list of active zones associated with this program."""
return [z for z in self._data["wateringTimes"] if z["active"]] 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: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the program off.""" """Turn the program off."""
await self._async_run_switch_coroutine( await self._async_run_switch_coroutine(
@ -381,6 +346,25 @@ class RainMachineProgram(RainMachineSwitch):
class RainMachineZone(RainMachineSwitch): class RainMachineZone(RainMachineSwitch):
"""A RainMachine zone.""" """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: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the zone off.""" """Turn the zone off."""
await self._async_run_switch_coroutine( await self._async_run_switch_coroutine(