mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Fix bug potential in RainMachine switches by simplifying architecture (#76417)
* Fix bug potential in RainMachine switches by simplifying architecture * Better typing (per code review) * Broader error catch
This commit is contained in:
parent
27f1955f28
commit
8ea9f975fd
@ -2,12 +2,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Coroutine
|
from collections.abc import Awaitable, Callable, Coroutine
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
from regenmaschine.errors import RequestError
|
from regenmaschine.errors import RainMachineError
|
||||||
|
from typing_extensions import Concatenate, ParamSpec
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
@ -104,6 +105,27 @@ VEGETATION_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T", bound="RainMachineBaseSwitch")
|
||||||
|
_P = ParamSpec("_P")
|
||||||
|
|
||||||
|
|
||||||
|
def raise_on_request_error(
|
||||||
|
func: Callable[Concatenate[_T, _P], Awaitable[None]]
|
||||||
|
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
|
||||||
|
"""Define a decorator to raise on a request error."""
|
||||||
|
|
||||||
|
async def decorator(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||||
|
"""Decorate."""
|
||||||
|
try:
|
||||||
|
await func(self, *args, **kwargs)
|
||||||
|
except RainMachineError as err:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"Error while executing {func.__name__}: {err}",
|
||||||
|
) from err
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RainMachineSwitchDescription(
|
class RainMachineSwitchDescription(
|
||||||
SwitchEntityDescription,
|
SwitchEntityDescription,
|
||||||
@ -197,22 +219,9 @@ class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity):
|
|||||||
self._attr_is_on = False
|
self._attr_is_on = False
|
||||||
self._entry = entry
|
self._entry = entry
|
||||||
|
|
||||||
async def _async_run_api_coroutine(self, api_coro: Coroutine) -> None:
|
@callback
|
||||||
"""Await an API coroutine, handle any errors, and update as appropriate."""
|
def _update_activities(self) -> None:
|
||||||
try:
|
"""Update all activity data."""
|
||||||
resp = await api_coro
|
|
||||||
except RequestError as err:
|
|
||||||
raise HomeAssistantError(
|
|
||||||
f'Error while executing {api_coro.__name__} on "{self.name}": {err}',
|
|
||||||
) from err
|
|
||||||
|
|
||||||
if resp["statusCode"] != 0:
|
|
||||||
raise HomeAssistantError(
|
|
||||||
f'Error while executing {api_coro.__name__} on "{self.name}": {resp["message"]}',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Because of how inextricably linked programs and zones are, anytime one is
|
|
||||||
# toggled, we make sure to update the data of both coordinators:
|
|
||||||
self.hass.async_create_task(
|
self.hass.async_create_task(
|
||||||
async_update_programs_and_zones(self.hass, self._entry)
|
async_update_programs_and_zones(self.hass, self._entry)
|
||||||
)
|
)
|
||||||
@ -250,6 +259,7 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch):
|
|||||||
|
|
||||||
await self.async_turn_off_when_active(**kwargs)
|
await self.async_turn_off_when_active(**kwargs)
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_off_when_active(self, **kwargs: Any) -> None:
|
async def async_turn_off_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch off when its associated activity is active."""
|
"""Turn the switch off when its associated activity is active."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -265,6 +275,7 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch):
|
|||||||
|
|
||||||
await self.async_turn_on_when_active(**kwargs)
|
await self.async_turn_on_when_active(**kwargs)
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_on_when_active(self, **kwargs: Any) -> None:
|
async def async_turn_on_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch on when its associated activity is active."""
|
"""Turn the switch on when its associated activity is active."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -290,17 +301,17 @@ class RainMachineProgram(RainMachineActivitySwitch):
|
|||||||
"""Stop the program."""
|
"""Stop the program."""
|
||||||
await self.async_turn_off()
|
await self.async_turn_off()
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_off_when_active(self, **kwargs: Any) -> None:
|
async def async_turn_off_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch off when its associated activity is active."""
|
"""Turn the switch off when its associated activity is active."""
|
||||||
await self._async_run_api_coroutine(
|
await self._data.controller.programs.stop(self.entity_description.uid)
|
||||||
self._data.controller.programs.stop(self.entity_description.uid)
|
self._update_activities()
|
||||||
)
|
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_on_when_active(self, **kwargs: Any) -> None:
|
async def async_turn_on_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch on when its associated activity is active."""
|
"""Turn the switch on when its associated activity is active."""
|
||||||
await self._async_run_api_coroutine(
|
await self._data.controller.programs.start(self.entity_description.uid)
|
||||||
self._data.controller.programs.start(self.entity_description.uid)
|
self._update_activities()
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def update_from_latest_data(self) -> None:
|
def update_from_latest_data(self) -> None:
|
||||||
@ -332,24 +343,21 @@ class RainMachineProgram(RainMachineActivitySwitch):
|
|||||||
class RainMachineProgramEnabled(RainMachineEnabledSwitch):
|
class RainMachineProgramEnabled(RainMachineEnabledSwitch):
|
||||||
"""Define a switch to enable/disable a RainMachine program."""
|
"""Define a switch to enable/disable a RainMachine program."""
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Disable the program."""
|
"""Disable the program."""
|
||||||
tasks = [
|
tasks = [
|
||||||
self._async_run_api_coroutine(
|
self._data.controller.programs.stop(self.entity_description.uid),
|
||||||
self._data.controller.programs.stop(self.entity_description.uid)
|
self._data.controller.programs.disable(self.entity_description.uid),
|
||||||
),
|
|
||||||
self._async_run_api_coroutine(
|
|
||||||
self._data.controller.programs.disable(self.entity_description.uid)
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
self._update_activities()
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Enable the program."""
|
"""Enable the program."""
|
||||||
await self._async_run_api_coroutine(
|
await self._data.controller.programs.enable(self.entity_description.uid)
|
||||||
self._data.controller.programs.enable(self.entity_description.uid)
|
self._update_activities()
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RainMachineZone(RainMachineActivitySwitch):
|
class RainMachineZone(RainMachineActivitySwitch):
|
||||||
@ -363,20 +371,20 @@ class RainMachineZone(RainMachineActivitySwitch):
|
|||||||
"""Stop a zone."""
|
"""Stop a zone."""
|
||||||
await self.async_turn_off()
|
await self.async_turn_off()
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_off_when_active(self, **kwargs: Any) -> None:
|
async def async_turn_off_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch off when its associated activity is active."""
|
"""Turn the switch off when its associated activity is active."""
|
||||||
await self._async_run_api_coroutine(
|
await self._data.controller.zones.stop(self.entity_description.uid)
|
||||||
self._data.controller.zones.stop(self.entity_description.uid)
|
self._update_activities()
|
||||||
)
|
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_on_when_active(self, **kwargs: Any) -> None:
|
async def async_turn_on_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch on when its associated activity is active."""
|
"""Turn the switch on when its associated activity is active."""
|
||||||
await self._async_run_api_coroutine(
|
await self._data.controller.zones.start(
|
||||||
self._data.controller.zones.start(
|
self.entity_description.uid,
|
||||||
self.entity_description.uid,
|
kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]),
|
||||||
kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
self._update_activities()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def update_from_latest_data(self) -> None:
|
def update_from_latest_data(self) -> None:
|
||||||
@ -416,21 +424,18 @@ class RainMachineZone(RainMachineActivitySwitch):
|
|||||||
class RainMachineZoneEnabled(RainMachineEnabledSwitch):
|
class RainMachineZoneEnabled(RainMachineEnabledSwitch):
|
||||||
"""Define a switch to enable/disable a RainMachine zone."""
|
"""Define a switch to enable/disable a RainMachine zone."""
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Disable the zone."""
|
"""Disable the zone."""
|
||||||
tasks = [
|
tasks = [
|
||||||
self._async_run_api_coroutine(
|
self._data.controller.zones.stop(self.entity_description.uid),
|
||||||
self._data.controller.zones.stop(self.entity_description.uid)
|
self._data.controller.zones.disable(self.entity_description.uid),
|
||||||
),
|
|
||||||
self._async_run_api_coroutine(
|
|
||||||
self._data.controller.zones.disable(self.entity_description.uid)
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
self._update_activities()
|
||||||
|
|
||||||
|
@raise_on_request_error
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Enable the zone."""
|
"""Enable the zone."""
|
||||||
await self._async_run_api_coroutine(
|
await self._data.controller.zones.enable(self.entity_description.uid)
|
||||||
self._data.controller.zones.enable(self.entity_description.uid)
|
self._update_activities()
|
||||||
)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user