diff --git a/homeassistant/components/renault/button.py b/homeassistant/components/renault/button.py index 71d197ac335..b34e14d365a 100644 --- a/homeassistant/components/renault/button.py +++ b/homeassistant/components/renault/button.py @@ -1,8 +1,9 @@ """Support for Renault button entities.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass +from typing import Any from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry @@ -18,7 +19,7 @@ from .renault_hub import RenaultHub class RenaultButtonRequiredKeysMixin: """Mixin for required keys.""" - async_press: Callable[[RenaultButtonEntity], Awaitable] + async_press: Callable[[RenaultButtonEntity], Coroutine[Any, Any, Any]] @dataclass @@ -56,25 +57,15 @@ class RenaultButtonEntity(RenaultEntity, ButtonEntity): await self.entity_description.async_press(self) -async def _start_charge(entity: RenaultButtonEntity) -> None: - """Start charge on the vehicle.""" - await entity.vehicle.vehicle.set_charge_start() - - -async def _start_air_conditioner(entity: RenaultButtonEntity) -> None: - """Start air conditioner on the vehicle.""" - await entity.vehicle.vehicle.set_ac_start(21, None) - - BUTTON_TYPES: tuple[RenaultButtonEntityDescription, ...] = ( RenaultButtonEntityDescription( - async_press=_start_air_conditioner, + async_press=lambda x: x.vehicle.set_ac_start(21, None), key="start_air_conditioner", icon="mdi:air-conditioner", name="Start air conditioner", ), RenaultButtonEntityDescription( - async_press=_start_charge, + async_press=lambda x: x.vehicle.set_charge_start(), key="start_charge", icon="mdi:ev-station", name="Start charge", diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 2d15e9c14a3..8aba5caa4de 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -2,22 +2,48 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass -from datetime import timedelta +from datetime import datetime, timedelta +from functools import wraps import logging -from typing import cast +from typing import Any, TypeVar, cast +from renault_api.exceptions import RenaultException from renault_api.kamereon import models from renault_api.renault_vehicle import RenaultVehicle +from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN from .renault_coordinator import RenaultDataUpdateCoordinator LOGGER = logging.getLogger(__name__) +_T = TypeVar("_T") +_P = ParamSpec("_P") + + +def with_error_wrapping( + func: Callable[Concatenate[RenaultVehicleProxy, _P], Awaitable[_T]] +) -> Callable[Concatenate[RenaultVehicleProxy, _P], Coroutine[Any, Any, _T]]: + """Catch Renault errors.""" + + @wraps(func) + async def wrapper( + self: RenaultVehicleProxy, + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _T: + """Catch RenaultException errors and raise HomeAssistantError.""" + try: + return await func(self, *args, **kwargs) + except RenaultException as err: + raise HomeAssistantError(err) from err + + return wrapper @dataclass @@ -69,11 +95,6 @@ class RenaultVehicleProxy: """Return a device description for device registry.""" return self._device_info - @property - def vehicle(self) -> RenaultVehicle: - """Return the underlying vehicle.""" - return self._vehicle - async def async_initialise(self) -> None: """Load available coordinators.""" self.coordinators = { @@ -119,6 +140,42 @@ class RenaultVehicleProxy: ) del self.coordinators[key] + @with_error_wrapping + async def set_charge_mode( + self, charge_mode: str + ) -> models.KamereonVehicleChargeModeActionData: + """Set vehicle charge mode.""" + return await self._vehicle.set_charge_mode(charge_mode) + + @with_error_wrapping + async def set_charge_start(self) -> models.KamereonVehicleChargingStartActionData: + """Start vehicle charge.""" + return await self._vehicle.set_charge_start() + + @with_error_wrapping + async def set_ac_stop(self) -> models.KamereonVehicleHvacStartActionData: + """Stop vehicle ac.""" + return await self._vehicle.set_ac_stop() + + @with_error_wrapping + async def set_ac_start( + self, temperature: float, when: datetime | None = None + ) -> models.KamereonVehicleHvacStartActionData: + """Start vehicle ac.""" + return await self._vehicle.set_ac_start(temperature, when) + + @with_error_wrapping + async def get_charging_settings(self) -> models.KamereonVehicleChargingSettingsData: + """Get vehicle charging settings.""" + return await self._vehicle.get_charging_settings() + + @with_error_wrapping + async def set_charge_schedules( + self, schedules: list[models.ChargeSchedule] + ) -> models.KamereonVehicleChargeScheduleActionData: + """Set vehicle charge schedules.""" + return await self._vehicle.set_charge_schedules(schedules) + COORDINATORS: tuple[RenaultCoordinatorDescription, ...] = ( RenaultCoordinatorDescription( diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index 1d34c9fdf2b..8fef7d9aee0 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -75,7 +75,7 @@ class RenaultSelectEntity( async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.vehicle.vehicle.set_charge_mode(option) + await self.vehicle.set_charge_mode(option) def _get_charge_mode_icon(entity: RenaultSelectEntity) -> str: diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index 23f30b2e54f..d25b73cafc2 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -74,7 +74,7 @@ def setup_services(hass: HomeAssistant) -> None: proxy = get_vehicle_proxy(service_call.data) LOGGER.debug("A/C cancel attempt") - result = await proxy.vehicle.set_ac_stop() + result = await proxy.set_ac_stop() LOGGER.debug("A/C cancel result: %s", result) async def ac_start(service_call: ServiceCall) -> None: @@ -84,21 +84,22 @@ def setup_services(hass: HomeAssistant) -> None: proxy = get_vehicle_proxy(service_call.data) LOGGER.debug("A/C start attempt: %s / %s", temperature, when) - result = await proxy.vehicle.set_ac_start(temperature, when) + result = await proxy.set_ac_start(temperature, when) LOGGER.debug("A/C start result: %s", result.raw_data) async def charge_set_schedules(service_call: ServiceCall) -> None: """Set charge schedules.""" schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES] proxy = get_vehicle_proxy(service_call.data) - charge_schedules = await proxy.vehicle.get_charging_settings() + charge_schedules = await proxy.get_charging_settings() for schedule in schedules: charge_schedules.update(schedule) if TYPE_CHECKING: assert charge_schedules.schedules is not None LOGGER.debug("Charge set schedules attempt: %s", schedules) - result = await proxy.vehicle.set_charge_schedules(charge_schedules.schedules) + result = await proxy.set_charge_schedules(charge_schedules.schedules) + LOGGER.debug("Charge set schedules result: %s", result) LOGGER.debug( "It may take some time before these changes are reflected in your vehicle" diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index 1f30c913431..d2c82a23d48 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -4,6 +4,7 @@ from datetime import datetime from unittest.mock import patch import pytest +from renault_api.exceptions import RenaultException from renault_api.kamereon import schemas from renault_api.kamereon.models import ChargeSchedule @@ -27,6 +28,7 @@ from homeassistant.const import ( ATTR_SW_VERSION, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from .const import MOCK_VEHICLES @@ -89,15 +91,12 @@ async def test_service_set_ac_cancel( with patch( "renault_api.renault_vehicle.RenaultVehicle.set_ac_stop", - return_value=( - schemas.KamereonVehicleHvacStartActionDataSchema.loads( - load_fixture("renault/action.set_ac_stop.json") - ) - ), + side_effect=RenaultException("Didn't work"), ) as mock_action: - await hass.services.async_call( - DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True - ) + with pytest.raises(HomeAssistantError, match="Didn't work"): + await hass.services.async_call( + DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True + ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == ()