mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Raise HomeAssistantError in Renault (#86071)
This commit is contained in:
parent
6c5f9c6fcb
commit
1918f21fb1
@ -1,8 +1,9 @@
|
|||||||
"""Support for Renault button entities."""
|
"""Support for Renault button entities."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Callable, Coroutine
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -18,7 +19,7 @@ from .renault_hub import RenaultHub
|
|||||||
class RenaultButtonRequiredKeysMixin:
|
class RenaultButtonRequiredKeysMixin:
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
async_press: Callable[[RenaultButtonEntity], Awaitable]
|
async_press: Callable[[RenaultButtonEntity], Coroutine[Any, Any, Any]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -56,25 +57,15 @@ class RenaultButtonEntity(RenaultEntity, ButtonEntity):
|
|||||||
await self.entity_description.async_press(self)
|
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, ...] = (
|
BUTTON_TYPES: tuple[RenaultButtonEntityDescription, ...] = (
|
||||||
RenaultButtonEntityDescription(
|
RenaultButtonEntityDescription(
|
||||||
async_press=_start_air_conditioner,
|
async_press=lambda x: x.vehicle.set_ac_start(21, None),
|
||||||
key="start_air_conditioner",
|
key="start_air_conditioner",
|
||||||
icon="mdi:air-conditioner",
|
icon="mdi:air-conditioner",
|
||||||
name="Start air conditioner",
|
name="Start air conditioner",
|
||||||
),
|
),
|
||||||
RenaultButtonEntityDescription(
|
RenaultButtonEntityDescription(
|
||||||
async_press=_start_charge,
|
async_press=lambda x: x.vehicle.set_charge_start(),
|
||||||
key="start_charge",
|
key="start_charge",
|
||||||
icon="mdi:ev-station",
|
icon="mdi:ev-station",
|
||||||
name="Start charge",
|
name="Start charge",
|
||||||
|
@ -2,22 +2,48 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable, Coroutine
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from functools import wraps
|
||||||
import logging
|
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.kamereon import models
|
||||||
from renault_api.renault_vehicle import RenaultVehicle
|
from renault_api.renault_vehicle import RenaultVehicle
|
||||||
|
from typing_extensions import Concatenate, ParamSpec
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .renault_coordinator import RenaultDataUpdateCoordinator
|
from .renault_coordinator import RenaultDataUpdateCoordinator
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
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
|
@dataclass
|
||||||
@ -69,11 +95,6 @@ class RenaultVehicleProxy:
|
|||||||
"""Return a device description for device registry."""
|
"""Return a device description for device registry."""
|
||||||
return self._device_info
|
return self._device_info
|
||||||
|
|
||||||
@property
|
|
||||||
def vehicle(self) -> RenaultVehicle:
|
|
||||||
"""Return the underlying vehicle."""
|
|
||||||
return self._vehicle
|
|
||||||
|
|
||||||
async def async_initialise(self) -> None:
|
async def async_initialise(self) -> None:
|
||||||
"""Load available coordinators."""
|
"""Load available coordinators."""
|
||||||
self.coordinators = {
|
self.coordinators = {
|
||||||
@ -119,6 +140,42 @@ class RenaultVehicleProxy:
|
|||||||
)
|
)
|
||||||
del self.coordinators[key]
|
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, ...] = (
|
COORDINATORS: tuple[RenaultCoordinatorDescription, ...] = (
|
||||||
RenaultCoordinatorDescription(
|
RenaultCoordinatorDescription(
|
||||||
|
@ -75,7 +75,7 @@ class RenaultSelectEntity(
|
|||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Change the selected option."""
|
"""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:
|
def _get_charge_mode_icon(entity: RenaultSelectEntity) -> str:
|
||||||
|
@ -74,7 +74,7 @@ def setup_services(hass: HomeAssistant) -> None:
|
|||||||
proxy = get_vehicle_proxy(service_call.data)
|
proxy = get_vehicle_proxy(service_call.data)
|
||||||
|
|
||||||
LOGGER.debug("A/C cancel attempt")
|
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)
|
LOGGER.debug("A/C cancel result: %s", result)
|
||||||
|
|
||||||
async def ac_start(service_call: ServiceCall) -> None:
|
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)
|
proxy = get_vehicle_proxy(service_call.data)
|
||||||
|
|
||||||
LOGGER.debug("A/C start attempt: %s / %s", temperature, when)
|
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)
|
LOGGER.debug("A/C start result: %s", result.raw_data)
|
||||||
|
|
||||||
async def charge_set_schedules(service_call: ServiceCall) -> None:
|
async def charge_set_schedules(service_call: ServiceCall) -> None:
|
||||||
"""Set charge schedules."""
|
"""Set charge schedules."""
|
||||||
schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES]
|
schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES]
|
||||||
proxy = get_vehicle_proxy(service_call.data)
|
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:
|
for schedule in schedules:
|
||||||
charge_schedules.update(schedule)
|
charge_schedules.update(schedule)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert charge_schedules.schedules is not None
|
assert charge_schedules.schedules is not None
|
||||||
LOGGER.debug("Charge set schedules attempt: %s", schedules)
|
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("Charge set schedules result: %s", result)
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"It may take some time before these changes are reflected in your vehicle"
|
"It may take some time before these changes are reflected in your vehicle"
|
||||||
|
@ -4,6 +4,7 @@ from datetime import datetime
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from renault_api.exceptions import RenaultException
|
||||||
from renault_api.kamereon import schemas
|
from renault_api.kamereon import schemas
|
||||||
from renault_api.kamereon.models import ChargeSchedule
|
from renault_api.kamereon.models import ChargeSchedule
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ from homeassistant.const import (
|
|||||||
ATTR_SW_VERSION,
|
ATTR_SW_VERSION,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from .const import MOCK_VEHICLES
|
from .const import MOCK_VEHICLES
|
||||||
@ -89,15 +91,12 @@ async def test_service_set_ac_cancel(
|
|||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"renault_api.renault_vehicle.RenaultVehicle.set_ac_stop",
|
"renault_api.renault_vehicle.RenaultVehicle.set_ac_stop",
|
||||||
return_value=(
|
side_effect=RenaultException("Didn't work"),
|
||||||
schemas.KamereonVehicleHvacStartActionDataSchema.loads(
|
|
||||||
load_fixture("renault/action.set_ac_stop.json")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
) as mock_action:
|
) as mock_action:
|
||||||
await hass.services.async_call(
|
with pytest.raises(HomeAssistantError, match="Didn't work"):
|
||||||
DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True
|
await hass.services.async_call(
|
||||||
)
|
DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True
|
||||||
|
)
|
||||||
assert len(mock_action.mock_calls) == 1
|
assert len(mock_action.mock_calls) == 1
|
||||||
assert mock_action.mock_calls[0][1] == ()
|
assert mock_action.mock_calls[0][1] == ()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user