Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use EntityDescription - renault #55061

Merged
merged 12 commits into from
Aug 25, 2021
76 changes: 53 additions & 23 deletions homeassistant/components/renault/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,88 @@
"""Support for Renault binary sensors."""
from __future__ import annotations

from dataclasses import dataclass

from renault_api.kamereon.enums import ChargeState, PlugState
from renault_api.kamereon.models import KamereonVehicleBatteryStatusData

from homeassistant.components.binary_sensor import (
DEVICE_CLASS_BATTERY_CHARGING,
DEVICE_CLASS_PLUG,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType

from .const import DOMAIN
from .renault_entities import RenaultBatteryDataEntity, RenaultDataEntity
from .renault_entities import RenaultDataEntity, RenaultEntityDescription, T
from .renault_hub import RenaultHub


@dataclass
class RenaultBinarySensorRequiredKeysMixin:
"""Mixin for required keys."""

entity_class: type[RenaultBinarySensor]
on_value: StateType


@dataclass
class RenaultBinarySensorEntityDescription(
BinarySensorEntityDescription,
RenaultEntityDescription,
RenaultBinarySensorRequiredKeysMixin,
):
"""Class describing Renault binary sensor entities."""


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Renault entities from config entry."""
proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id]
entities: list[RenaultDataEntity] = []
for vehicle in proxy.vehicles.values():
if "battery" in vehicle.coordinators:
entities.append(RenaultPluggedInSensor(vehicle, "Plugged In"))
entities.append(RenaultChargingSensor(vehicle, "Charging"))
entities: list[RenaultBinarySensor] = [
description.entity_class(vehicle, description)
for vehicle in proxy.vehicles.values()
for description in BINARY_SENSOR_TYPES
if description.coordinator in vehicle.coordinators
]
async_add_entities(entities)


class RenaultPluggedInSensor(RenaultBatteryDataEntity, BinarySensorEntity):
"""Plugged In binary sensor."""
class RenaultBinarySensor(RenaultDataEntity[T], BinarySensorEntity):
"""Mixin for binary sensor specific attributes."""

_attr_device_class = DEVICE_CLASS_PLUG
entity_description: RenaultBinarySensorEntityDescription

@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
if (not self.data) or (self.data.plugStatus is None):
return None
return self.data.get_plug_status() == PlugState.PLUGGED
return self.data == self.entity_description.on_value


class RenaultChargingSensor(RenaultBatteryDataEntity, BinarySensorEntity):
"""Charging binary sensor."""

_attr_device_class = DEVICE_CLASS_BATTERY_CHARGING

@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
if (not self.data) or (self.data.chargingStatus is None):
return None
return self.data.get_charging_status() == ChargeState.CHARGE_IN_PROGRESS
BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = (
RenaultBinarySensorEntityDescription(
key="plugged_in",
coordinator="battery",
data_key="plugStatus",
device_class=DEVICE_CLASS_PLUG,
entity_class=RenaultBinarySensor[KamereonVehicleBatteryStatusData],
name="Plugged In",
on_value=PlugState.PLUGGED.value,
),
RenaultBinarySensorEntityDescription(
key="charging",
coordinator="battery",
data_key="chargingStatus",
device_class=DEVICE_CLASS_BATTERY_CHARGING,
entity_class=RenaultBinarySensor[KamereonVehicleBatteryStatusData],
name="Charging",
on_value=ChargeState.CHARGE_IN_PROGRESS.value,
),
)
126 changes: 48 additions & 78 deletions homeassistant/components/renault/renault_entities.py
Original file line number Diff line number Diff line change
@@ -1,103 +1,73 @@
"""Base classes for Renault entities."""
from __future__ import annotations

from typing import Any, Generic, Optional, TypeVar
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, Optional, TypeVar, cast

from renault_api.kamereon.enums import ChargeState, PlugState
from renault_api.kamereon.models import (
KamereonVehicleBatteryStatusData,
KamereonVehicleChargeModeData,
KamereonVehicleCockpitData,
KamereonVehicleHvacStatusData,
)
from renault_api.kamereon.models import KamereonVehicleDataAttributes

from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify

from .renault_vehicle import RenaultVehicleProxy


@dataclass
class RenaultRequiredKeysMixin:
"""Mixin for required keys."""

coordinator: str
data_key: str


@dataclass
class RenaultEntityDescription(EntityDescription, RenaultRequiredKeysMixin):
"""Class describing Renault entities."""

requires_fuel: bool | None = None


ATTR_LAST_UPDATE = "last_update"

T = TypeVar("T")
T = TypeVar("T", bound=KamereonVehicleDataAttributes)


class RenaultDataEntity(Generic[T], CoordinatorEntity[Optional[T]], Entity):
class RenaultDataEntity(CoordinatorEntity[Optional[T]], Entity):
"""Implementation of a Renault entity with a data coordinator."""

entity_description: RenaultEntityDescription

def __init__(
self, vehicle: RenaultVehicleProxy, entity_type: str, coordinator_key: str
self,
vehicle: RenaultVehicleProxy,
description: RenaultEntityDescription,
) -> None:
"""Initialise entity."""
super().__init__(vehicle.coordinators[coordinator_key])
super().__init__(vehicle.coordinators[description.coordinator])
self.vehicle = vehicle
self._entity_type = entity_type
self.entity_description = description
self._attr_device_info = self.vehicle.device_info
self._attr_name = entity_type
self._attr_unique_id = slugify(
f"{self.vehicle.details.vin}-{self._entity_type}"
)

@property
def available(self) -> bool:
"""Return if entity is available."""
# Data can succeed, but be empty
return super().available and self.coordinator.data is not None

@property
def data(self) -> T | None:
"""Return collected data."""
return self.coordinator.data


class RenaultBatteryDataEntity(RenaultDataEntity[KamereonVehicleBatteryStatusData]):
"""Implementation of a Renault entity with battery coordinator."""

def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None:
"""Initialise entity."""
super().__init__(vehicle, entity_type, "battery")
self._attr_unique_id = f"{self.vehicle.details.vin}_{description.key}".lower()

@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of this entity."""
last_update = self.data.timestamp if self.data else None
return {ATTR_LAST_UPDATE: last_update}

@property
def is_charging(self) -> bool:
"""Return charge state as boolean."""
return (
self.data is not None
and self.data.get_charging_status() == ChargeState.CHARGE_IN_PROGRESS
def data(self) -> StateType:
"""Return the state of this entity."""
if self.coordinator.data is None:
return None
return cast(
StateType, getattr(self.coordinator.data, self.entity_description.data_key)
)

@property
def is_plugged_in(self) -> bool:
"""Return plug state as boolean."""
return (
self.data is not None and self.data.get_plug_status() == PlugState.PLUGGED
)


class RenaultChargeModeDataEntity(RenaultDataEntity[KamereonVehicleChargeModeData]):
"""Implementation of a Renault entity with charge_mode coordinator."""

def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None:
"""Initialise entity."""
super().__init__(vehicle, entity_type, "charge_mode")


class RenaultCockpitDataEntity(RenaultDataEntity[KamereonVehicleCockpitData]):
"""Implementation of a Renault entity with cockpit coordinator."""

def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None:
"""Initialise entity."""
super().__init__(vehicle, entity_type, "cockpit")


class RenaultHVACDataEntity(RenaultDataEntity[KamereonVehicleHvacStatusData]):
"""Implementation of a Renault entity with hvac_status coordinator."""

def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None:
"""Initialise entity."""
super().__init__(vehicle, entity_type, "hvac_status")
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return the state attributes of this entity."""
if self.entity_description.coordinator == "battery":
last_update = (
getattr(self.coordinator.data, "timestamp")
if self.coordinator.data
else None
)
return {ATTR_LAST_UPDATE: last_update}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't include the item at all in the state attributes if the value is None.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @MartinHjelmare you told me the opposite on the original integration: an attribute should never "disappear".
So the way it is implemented is that sensor with battery coordinator will always have the attribute (sometimes with value, and sometimes None) and other coordinators do not return any attributes return None.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a link to that discussion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find it. I guess I was mistaken. I'll implement...

return None
Loading