mirror of
https://github.com/home-assistant/core.git
synced 2026-07-07 14:56:25 +01:00
d766aae436
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: frenck <195327+frenck@users.noreply.github.com>
151 lines
5.0 KiB
Python
151 lines
5.0 KiB
Python
"""Support for Renault number entities."""
|
|
|
|
from collections.abc import Callable, Coroutine
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from renault_api.kamereon.models import (
|
|
KamereonVehicleBatterySocData,
|
|
KamereonVehicleDataAttributes,
|
|
)
|
|
|
|
from homeassistant.components.number import (
|
|
NumberDeviceClass,
|
|
NumberEntity,
|
|
NumberEntityDescription,
|
|
NumberMode,
|
|
)
|
|
from homeassistant.const import PERCENTAGE
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ServiceValidationError
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
|
|
from . import RenaultConfigEntry
|
|
from .const import DOMAIN
|
|
from .entity import RenaultDataEntity, RenaultDataEntityDescription
|
|
|
|
# Coordinator is used to centralize the data updates
|
|
# but renault servers are unreliable and it's safer to queue action calls
|
|
PARALLEL_UPDATES = 1
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class RenaultNumberEntityDescription[T: KamereonVehicleDataAttributes](
|
|
NumberEntityDescription, RenaultDataEntityDescription
|
|
):
|
|
"""Class describing Renault number entities."""
|
|
|
|
value_fn: Callable[[RenaultNumberEntity[T]], float | None]
|
|
update_fn: Callable[[RenaultNumberEntity[T], float], Coroutine[Any, Any, None]]
|
|
|
|
|
|
async def _set_charge_limit_min(
|
|
entity: RenaultNumberEntity[KamereonVehicleBatterySocData], value: float
|
|
) -> None:
|
|
"""Set the minimum SOC.
|
|
|
|
The target SOC is required to set the minimum SOC, so we need to fetch it first.
|
|
"""
|
|
if (target_soc := entity.coordinator.data.socTarget) is None:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="battery_soc_unavailable",
|
|
)
|
|
await _set_charge_limits(entity, min_soc=round(value), target_soc=target_soc)
|
|
|
|
|
|
async def _set_charge_limit_target(
|
|
entity: RenaultNumberEntity[KamereonVehicleBatterySocData], value: float
|
|
) -> None:
|
|
"""Set the target SOC.
|
|
|
|
The minimum SOC is required to set the target SOC, so we need to fetch it first.
|
|
"""
|
|
if (min_soc := entity.coordinator.data.socMin) is None:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="battery_soc_unavailable",
|
|
)
|
|
await _set_charge_limits(entity, min_soc=min_soc, target_soc=round(value))
|
|
|
|
|
|
async def _set_charge_limits(
|
|
entity: RenaultNumberEntity[KamereonVehicleBatterySocData],
|
|
min_soc: int,
|
|
target_soc: int,
|
|
) -> None:
|
|
"""Set the minimum and target SOC.
|
|
|
|
Optimistically update local coordinator data so the new
|
|
limits are reflected immediately without a remote refresh,
|
|
as Renault servers may still cache old values.
|
|
"""
|
|
await entity.vehicle.set_battery_soc(min_soc=min_soc, target_soc=target_soc)
|
|
|
|
entity.coordinator.data.socMin = min_soc
|
|
entity.coordinator.data.socTarget = target_soc
|
|
entity.coordinator.assumed_state = True
|
|
entity.coordinator.async_set_updated_data(entity.coordinator.data)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: RenaultConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Renault entities from config entry."""
|
|
entities: list[RenaultNumberEntity] = [
|
|
RenaultNumberEntity(vehicle, description)
|
|
for vehicle in config_entry.runtime_data.vehicles.values()
|
|
for description in NUMBER_TYPES
|
|
if description.coordinator in vehicle.coordinators
|
|
]
|
|
async_add_entities(entities)
|
|
|
|
|
|
class RenaultNumberEntity[T: KamereonVehicleDataAttributes](
|
|
RenaultDataEntity[T], NumberEntity
|
|
):
|
|
"""Mixin for number specific attributes."""
|
|
|
|
entity_description: RenaultNumberEntityDescription[T]
|
|
|
|
@property
|
|
def native_value(self) -> float | None:
|
|
"""Return the entity value to represent the entity state."""
|
|
return self.entity_description.value_fn(self)
|
|
|
|
async def async_set_native_value(self, value: float) -> None:
|
|
"""Update the current value."""
|
|
await self.entity_description.update_fn(self, value)
|
|
|
|
|
|
NUMBER_TYPES: tuple[RenaultNumberEntityDescription, ...] = (
|
|
RenaultNumberEntityDescription[KamereonVehicleBatterySocData](
|
|
key="charge_limit_min",
|
|
coordinator="battery_soc",
|
|
update_fn=_set_charge_limit_min,
|
|
device_class=NumberDeviceClass.BATTERY,
|
|
native_min_value=15,
|
|
native_max_value=45,
|
|
native_step=5,
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
mode=NumberMode.SLIDER,
|
|
translation_key="charge_limit_min",
|
|
value_fn=lambda entity: entity.coordinator.data.socMin,
|
|
),
|
|
RenaultNumberEntityDescription[KamereonVehicleBatterySocData](
|
|
key="charge_limit_target",
|
|
coordinator="battery_soc",
|
|
update_fn=_set_charge_limit_target,
|
|
device_class=NumberDeviceClass.BATTERY,
|
|
native_min_value=55,
|
|
native_max_value=100,
|
|
native_step=5,
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
mode=NumberMode.SLIDER,
|
|
translation_key="charge_limit_target",
|
|
value_fn=lambda entity: entity.coordinator.data.socTarget,
|
|
),
|
|
)
|