Skip to content

Commit

Permalink
Add binary sensor platform to Teslemetry (#117230)
Browse files Browse the repository at this point in the history
* Add binary sensor platform

* Add tests

* Cleanup

* Add refresh test

* Fix runtime_data after rebase

* Remove streaming strings

* test error

* updated_once

* fix updated_once

* assert_entities_alt

* Update homeassistant/components/teslemetry/binary_sensor.py

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
  • Loading branch information
Bre77 and gjohansson-ST committed May 24, 2024
1 parent 8da799e commit ad90ece
Show file tree
Hide file tree
Showing 8 changed files with 3,601 additions and 3 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/teslemetry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData

PLATFORMS: Final = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.LOCK,
Platform.SELECT,
Expand Down
271 changes: 271 additions & 0 deletions homeassistant/components/teslemetry/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
"""Binary Sensor platform for Teslemetry integration."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from itertools import chain
from typing import cast

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

from .const import TeslemetryState
from .entity import (
TeslemetryEnergyInfoEntity,
TeslemetryEnergyLiveEntity,
TeslemetryVehicleEntity,
)
from .models import TeslemetryEnergyData, TeslemetryVehicleData


@dataclass(frozen=True, kw_only=True)
class TeslemetryBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Teslemetry binary sensor entity."""

is_on: Callable[[StateType], bool] = bool


VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
TeslemetryBinarySensorEntityDescription(
key="state",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
is_on=lambda x: x == TeslemetryState.ONLINE,
),
TeslemetryBinarySensorEntityDescription(
key="charge_state_battery_heater_on",
device_class=BinarySensorDeviceClass.HEAT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="charge_state_charger_phases",
is_on=lambda x: cast(int, x) > 1,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="charge_state_preconditioning_enabled",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="climate_state_is_preconditioning",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="charge_state_scheduled_charging_pending",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="charge_state_trip_charging",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="charge_state_conn_charge_cable",
is_on=lambda x: x != "<invalid>",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
),
TeslemetryBinarySensorEntityDescription(
key="climate_state_cabin_overheat_protection_actively_cooling",
device_class=BinarySensorDeviceClass.HEAT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_dashcam_state",
device_class=BinarySensorDeviceClass.RUNNING,
is_on=lambda x: x == "Recording",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_is_user_present",
device_class=BinarySensorDeviceClass.PRESENCE,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_tpms_soft_warning_fl",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_tpms_soft_warning_fr",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_tpms_soft_warning_rl",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_tpms_soft_warning_rr",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_fd_window",
device_class=BinarySensorDeviceClass.WINDOW,
entity_category=EntityCategory.DIAGNOSTIC,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_fp_window",
device_class=BinarySensorDeviceClass.WINDOW,
entity_category=EntityCategory.DIAGNOSTIC,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_rd_window",
device_class=BinarySensorDeviceClass.WINDOW,
entity_category=EntityCategory.DIAGNOSTIC,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_rp_window",
device_class=BinarySensorDeviceClass.WINDOW,
entity_category=EntityCategory.DIAGNOSTIC,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_df",
device_class=BinarySensorDeviceClass.DOOR,
entity_category=EntityCategory.DIAGNOSTIC,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_dr",
device_class=BinarySensorDeviceClass.DOOR,
entity_category=EntityCategory.DIAGNOSTIC,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_pf",
device_class=BinarySensorDeviceClass.DOOR,
entity_category=EntityCategory.DIAGNOSTIC,
),
TeslemetryBinarySensorEntityDescription(
key="vehicle_state_pr",
device_class=BinarySensorDeviceClass.DOOR,
entity_category=EntityCategory.DIAGNOSTIC,
),
)

ENERGY_LIVE_DESCRIPTIONS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(key="backup_capable"),
BinarySensorEntityDescription(key="grid_services_active"),
)


ENERGY_INFO_DESCRIPTIONS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key="components_grid_services_enabled",
),
)


async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Teslemetry binary sensor platform from a config entry."""

async_add_entities(
chain(
( # Vehicles
TeslemetryVehicleBinarySensorEntity(vehicle, description)
for vehicle in entry.runtime_data.vehicles
for description in VEHICLE_DESCRIPTIONS
),
( # Energy Site Live
TeslemetryEnergyLiveBinarySensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites
for description in ENERGY_LIVE_DESCRIPTIONS
if energysite.info_coordinator.data.get("components_battery")
),
( # Energy Site Info
TeslemetryEnergyInfoBinarySensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites
for description in ENERGY_INFO_DESCRIPTIONS
if energysite.info_coordinator.data.get("components_battery")
),
)
)


class TeslemetryVehicleBinarySensorEntity(TeslemetryVehicleEntity, BinarySensorEntity):
"""Base class for Teslemetry vehicle binary sensors."""

entity_description: TeslemetryBinarySensorEntityDescription

def __init__(
self,
data: TeslemetryVehicleData,
description: TeslemetryBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
self.entity_description = description
super().__init__(data, description.key)

def _async_update_attrs(self) -> None:
"""Update the attributes of the binary sensor."""

if self.coordinator.updated_once:
if self._value is None:
self._attr_available = False
self._attr_is_on = None
else:
self._attr_available = True
self._attr_is_on = self.entity_description.is_on(self._value)
else:
self._attr_is_on = None


class TeslemetryEnergyLiveBinarySensorEntity(
TeslemetryEnergyLiveEntity, BinarySensorEntity
):
"""Base class for Teslemetry energy live binary sensors."""

entity_description: BinarySensorEntityDescription

def __init__(
self,
data: TeslemetryEnergyData,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
self.entity_description = description
super().__init__(data, description.key)

def _async_update_attrs(self) -> None:
"""Update the attributes of the binary sensor."""
self._attr_is_on = self._value


class TeslemetryEnergyInfoBinarySensorEntity(
TeslemetryEnergyInfoEntity, BinarySensorEntity
):
"""Base class for Teslemetry energy info binary sensors."""

entity_description: BinarySensorEntityDescription

def __init__(
self,
data: TeslemetryEnergyData,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
self.entity_description = description
super().__init__(data, description.key)

def _async_update_attrs(self) -> None:
"""Update the attributes of the binary sensor."""
self._attr_is_on = self._value
8 changes: 7 additions & 1 deletion homeassistant/components/teslemetry/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def flatten(data: dict[str, Any], parent: str | None = None) -> dict[str, Any]:
class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching data from the Teslemetry API."""

name = "Teslemetry Vehicle"
updated_once: bool

def __init__(
self, hass: HomeAssistant, api: VehicleSpecific, product: dict
Expand All @@ -62,6 +62,7 @@ def __init__(
)
self.api = api
self.data = flatten(product)
self.updated_once = False

async def _async_update_data(self) -> dict[str, Any]:
"""Update vehicle data using Teslemetry API."""
Expand All @@ -77,12 +78,15 @@ async def _async_update_data(self) -> dict[str, Any]:
except TeslaFleetError as e:
raise UpdateFailed(e.message) from e

self.updated_once = True
return flatten(data)


class TeslemetryEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching energy site live status from the Teslemetry API."""

updated_once: bool

def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None:
"""Initialize Teslemetry Energy Site Live coordinator."""
super().__init__(
Expand Down Expand Up @@ -116,6 +120,8 @@ async def _async_update_data(self) -> dict[str, Any]:
class TeslemetryEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching energy site info from the Teslemetry API."""

updated_once: bool

def __init__(self, hass: HomeAssistant, api: EnergySpecific, product: dict) -> None:
"""Initialize Teslemetry Energy Info coordinator."""
super().__init__(
Expand Down
38 changes: 38 additions & 0 deletions homeassistant/components/teslemetry/icons.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
{
"entity": {
"binary_sensor": {
"climate_state_is_preconditioning": {
"state": {
"off": "mdi:hvac-off",
"on": "mdi:hvac"
}
},
"vehicle_state_is_user_present": {
"state": {
"off": "mdi:account-remove-outline",
"on": "mdi:account"
}
},
"vehicle_state_tpms_soft_warning_fl": {
"state": {
"off": "mdi:tire",
"on": "mdi:car-tire-alert"
}
},
"vehicle_state_tpms_soft_warning_fr": {
"state": {
"off": "mdi:tire",
"on": "mdi:car-tire-alert"
}
},
"vehicle_state_tpms_soft_warning_rl": {
"state": {
"off": "mdi:tire",
"on": "mdi:car-tire-alert"
}
},
"vehicle_state_tpms_soft_warning_rr": {
"state": {
"off": "mdi:tire",
"on": "mdi:car-tire-alert"
}
}
},
"climate": {
"driver_temp": {
"state_attributes": {
Expand Down

0 comments on commit ad90ece

Please sign in to comment.