-
-
Notifications
You must be signed in to change notification settings - Fork 32.8k
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
cdce8p
merged 12 commits into
home-assistant:dev
from
epenet:renault-entity-description
Aug 25, 2021
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
f55412f
Cleanup sensor.py
epenet 9f327d4
Add EntityDescription
epenet f23750c
Add checks for state attributes
epenet c8f3427
Fix pylint
epenet fe6c351
Simplify checks
epenet cb9dfad
Add icon checks
epenet 398ac11
Update data type
epenet 0e8b916
Use mixin for required keys, and review class initialisation
epenet 0404a11
Add constraint to TypeVar("T")
epenet 7fc1981
Enable lambda for icon handling
epenet d75e200
Enable lambda for value handling
epenet fcc192b
Enable lambda for value handling
epenet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
return None |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 attributesreturn None
.There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...