mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 22:27:07 +00:00
Add sensor platform to Teslemetry (#109088)
* Adding Energy * Adding Energy * Work in progress * Add fixtures * Add product info * Add sensors * Add icons * Update metadata * Use SensorEntityDescription for Energy * Use ENERGY_STORAGE * Add tests * Fix coverage * Update wall connector precision and units * Change devices * Fix serial number * Add icons and VIN to wall connector * Fix serial number again * Update snapshots * Use timestamp for minutes to arrival * Cleanup snapshot * Improvements * Update fixture * Add "code" to translations * Add "code" to snapshot * Use async_add_entities once * Disable a bunch of sensors * Ruff * Improve fixture and test coverage * Regen Snapshots * Add init to coordinator
This commit is contained in:
parent
b195c3fa7b
commit
b5528de807
@ -2,7 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from tesla_fleet_api import Teslemetry, VehicleSpecific
|
from tesla_fleet_api import EnergySpecific, Teslemetry, VehicleSpecific
|
||||||
from tesla_fleet_api.exceptions import InvalidToken, PaymentRequired, TeslaFleetError
|
from tesla_fleet_api.exceptions import InvalidToken, PaymentRequired, TeslaFleetError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -12,12 +12,13 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
from .coordinator import TeslemetryVehicleDataCoordinator
|
from .coordinator import (
|
||||||
from .models import TeslemetryVehicleData
|
TeslemetryEnergyDataCoordinator,
|
||||||
|
TeslemetryVehicleDataCoordinator,
|
||||||
|
)
|
||||||
|
from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
|
||||||
|
|
||||||
PLATFORMS: Final = [
|
PLATFORMS: Final = [Platform.CLIMATE, Platform.SENSOR]
|
||||||
Platform.CLIMATE,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
@ -42,29 +43,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
raise ConfigEntryNotReady from e
|
raise ConfigEntryNotReady from e
|
||||||
|
|
||||||
# Create array of classes
|
# Create array of classes
|
||||||
data = []
|
vehicles: list[TeslemetryVehicleData] = []
|
||||||
|
energysites: list[TeslemetryEnergyData] = []
|
||||||
for product in products:
|
for product in products:
|
||||||
if "vin" not in product:
|
if "vin" in product:
|
||||||
continue
|
|
||||||
vin = product["vin"]
|
vin = product["vin"]
|
||||||
|
|
||||||
api = VehicleSpecific(teslemetry.vehicle, vin)
|
api = VehicleSpecific(teslemetry.vehicle, vin)
|
||||||
coordinator = TeslemetryVehicleDataCoordinator(hass, api)
|
coordinator = TeslemetryVehicleDataCoordinator(hass, api)
|
||||||
data.append(
|
vehicles.append(
|
||||||
TeslemetryVehicleData(
|
TeslemetryVehicleData(
|
||||||
api=api,
|
api=api,
|
||||||
coordinator=coordinator,
|
coordinator=coordinator,
|
||||||
vin=vin,
|
vin=vin,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
elif "energy_site_id" in product:
|
||||||
|
site_id = product["energy_site_id"]
|
||||||
|
api = EnergySpecific(teslemetry.energy, site_id)
|
||||||
|
energysites.append(
|
||||||
|
TeslemetryEnergyData(
|
||||||
|
api=api,
|
||||||
|
coordinator=TeslemetryEnergyDataCoordinator(hass, api),
|
||||||
|
id=site_id,
|
||||||
|
info=product,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Do all coordinator first refresh simultaneously
|
# Do all coordinator first refreshes simultaneously
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*(vehicle.coordinator.async_config_entry_first_refresh() for vehicle in data)
|
*(
|
||||||
|
vehicle.coordinator.async_config_entry_first_refresh()
|
||||||
|
for vehicle in vehicles
|
||||||
|
),
|
||||||
|
*(
|
||||||
|
energysite.coordinator.async_config_entry_first_refresh()
|
||||||
|
for energysite in energysites
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Setup Platforms
|
# Setup Platforms
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = TeslemetryData(
|
||||||
|
vehicles, energysites
|
||||||
|
)
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -26,7 +26,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER)
|
TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER)
|
||||||
for vehicle in data
|
for vehicle in data.vehicles
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from tesla_fleet_api import VehicleSpecific
|
from tesla_fleet_api import EnergySpecific, VehicleSpecific
|
||||||
from tesla_fleet_api.exceptions import TeslaFleetError, VehicleOffline
|
from tesla_fleet_api.exceptions import TeslaFleetError, VehicleOffline
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -14,19 +14,29 @@ from .const import LOGGER, TeslemetryState
|
|||||||
SYNC_INTERVAL = 60
|
SYNC_INTERVAL = 60
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
class TeslemetryDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
"""Class to manage fetching data from the Teslemetry API."""
|
"""Base class for Teslemetry Data Coordinators."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, api: VehicleSpecific) -> None:
|
name: str
|
||||||
"""Initialize Teslemetry Data Update Coordinator."""
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, api: VehicleSpecific | EnergySpecific
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Teslemetry Vehicle Update Coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
name="Teslemetry Vehicle",
|
name=self.name,
|
||||||
update_interval=timedelta(seconds=SYNC_INTERVAL),
|
update_interval=timedelta(seconds=SYNC_INTERVAL),
|
||||||
)
|
)
|
||||||
self.api = api
|
self.api = api
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryVehicleDataCoordinator(TeslemetryDataCoordinator):
|
||||||
|
"""Class to manage fetching data from the Teslemetry API."""
|
||||||
|
|
||||||
|
name = "Teslemetry Vehicle"
|
||||||
|
|
||||||
async def async_config_entry_first_refresh(self) -> None:
|
async def async_config_entry_first_refresh(self) -> None:
|
||||||
"""Perform first refresh."""
|
"""Perform first refresh."""
|
||||||
try:
|
try:
|
||||||
@ -65,3 +75,24 @@ class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
else:
|
else:
|
||||||
result[key] = value
|
result[key] = value
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryEnergyDataCoordinator(TeslemetryDataCoordinator):
|
||||||
|
"""Class to manage fetching data from the Teslemetry API."""
|
||||||
|
|
||||||
|
name = "Teslemetry Energy Site"
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
|
"""Update energy site data using Teslemetry API."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await self.api.live_status()
|
||||||
|
except TeslaFleetError as e:
|
||||||
|
raise UpdateFailed(e.message) from e
|
||||||
|
|
||||||
|
# Convert Wall Connectors from array to dict
|
||||||
|
data["response"]["wall_connectors"] = {
|
||||||
|
wc["din"]: wc for wc in data["response"].get("wall_connectors", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
return data["response"]
|
||||||
|
@ -10,12 +10,15 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN, MODELS, TeslemetryState
|
from .const import DOMAIN, MODELS, TeslemetryState
|
||||||
from .coordinator import TeslemetryVehicleDataCoordinator
|
from .coordinator import (
|
||||||
from .models import TeslemetryVehicleData
|
TeslemetryEnergyDataCoordinator,
|
||||||
|
TeslemetryVehicleDataCoordinator,
|
||||||
|
)
|
||||||
|
from .models import TeslemetryEnergyData, TeslemetryVehicleData
|
||||||
|
|
||||||
|
|
||||||
class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator]):
|
class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator]):
|
||||||
"""Parent class for Teslemetry Entities."""
|
"""Parent class for Teslemetry Vehicle Entities."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
@ -74,3 +77,65 @@ class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator
|
|||||||
for key, value in args:
|
for key, value in args:
|
||||||
self.coordinator.data[key] = value
|
self.coordinator.data[key] = value
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryEnergyEntity(CoordinatorEntity[TeslemetryEnergyDataCoordinator]):
|
||||||
|
"""Parent class for Teslemetry Energy Entities."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
energysite: TeslemetryEnergyData,
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize common aspects of a Teslemetry entity."""
|
||||||
|
super().__init__(energysite.coordinator)
|
||||||
|
self.key = key
|
||||||
|
self.api = energysite.api
|
||||||
|
|
||||||
|
self._attr_translation_key = key
|
||||||
|
self._attr_unique_id = f"{energysite.id}-{key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, str(energysite.id))},
|
||||||
|
manufacturer="Tesla",
|
||||||
|
configuration_url="https://teslemetry.com/console",
|
||||||
|
name=self.coordinator.data.get("site_name", "Energy Site"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, key: str | None = None, default: Any | None = None) -> Any:
|
||||||
|
"""Return a specific value from coordinator data."""
|
||||||
|
return self.coordinator.data.get(key or self.key, default)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryWallConnectorEntity(CoordinatorEntity[TeslemetryEnergyDataCoordinator]):
|
||||||
|
"""Parent class for Teslemetry Wall Connector Entities."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
energysite: TeslemetryEnergyData,
|
||||||
|
din: str,
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize common aspects of a Teslemetry entity."""
|
||||||
|
super().__init__(energysite.coordinator)
|
||||||
|
self.din = din
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
self._attr_translation_key = key
|
||||||
|
self._attr_unique_id = f"{energysite.id}-{din}-{key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, din)},
|
||||||
|
manufacturer="Tesla",
|
||||||
|
configuration_url="https://teslemetry.com/console",
|
||||||
|
name="Wall Connector",
|
||||||
|
via_device=(DOMAIN, str(energysite.id)),
|
||||||
|
serial_number=din.split("-")[-1],
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _value(self) -> int:
|
||||||
|
"""Return a specific wall connector value from coordinator data."""
|
||||||
|
return self.coordinator.data["wall_connectors"][self.din].get(self.key)
|
||||||
|
@ -4,9 +4,20 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from tesla_fleet_api import VehicleSpecific
|
from tesla_fleet_api import EnergySpecific, VehicleSpecific
|
||||||
|
|
||||||
from .coordinator import TeslemetryVehicleDataCoordinator
|
from .coordinator import (
|
||||||
|
TeslemetryEnergyDataCoordinator,
|
||||||
|
TeslemetryVehicleDataCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TeslemetryData:
|
||||||
|
"""Data for the Teslemetry integration."""
|
||||||
|
|
||||||
|
vehicles: list[TeslemetryVehicleData]
|
||||||
|
energysites: list[TeslemetryEnergyData]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -17,3 +28,13 @@ class TeslemetryVehicleData:
|
|||||||
coordinator: TeslemetryVehicleDataCoordinator
|
coordinator: TeslemetryVehicleDataCoordinator
|
||||||
vin: str
|
vin: str
|
||||||
wakelock = asyncio.Lock()
|
wakelock = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TeslemetryEnergyData:
|
||||||
|
"""Data for a vehicle in the Teslemetry integration."""
|
||||||
|
|
||||||
|
api: EnergySpecific
|
||||||
|
coordinator: TeslemetryEnergyDataCoordinator
|
||||||
|
id: int
|
||||||
|
info: dict[str, str]
|
||||||
|
461
homeassistant/components/teslemetry/sensor.py
Normal file
461
homeassistant/components/teslemetry/sensor.py
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
"""Sensor platform for Teslemetry integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import (
|
||||||
|
PERCENTAGE,
|
||||||
|
EntityCategory,
|
||||||
|
UnitOfElectricCurrent,
|
||||||
|
UnitOfElectricPotential,
|
||||||
|
UnitOfEnergy,
|
||||||
|
UnitOfLength,
|
||||||
|
UnitOfPower,
|
||||||
|
UnitOfPressure,
|
||||||
|
UnitOfSpeed,
|
||||||
|
UnitOfTemperature,
|
||||||
|
UnitOfTime,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .entity import (
|
||||||
|
TeslemetryEnergyEntity,
|
||||||
|
TeslemetryVehicleEntity,
|
||||||
|
TeslemetryWallConnectorEntity,
|
||||||
|
)
|
||||||
|
from .models import TeslemetryEnergyData, TeslemetryVehicleData
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def minutes_to_datetime(value: StateType) -> datetime | None:
|
||||||
|
"""Convert relative minutes into absolute datetime."""
|
||||||
|
if isinstance(value, (int, float)) and value > 0:
|
||||||
|
return dt_util.now() + timedelta(minutes=value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TeslemetrySensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes Teslemetry Sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[StateType], StateType | datetime] = lambda x: x
|
||||||
|
|
||||||
|
|
||||||
|
VEHICLE_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = (
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="charge_state_usable_battery_level",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="charge_state_charge_energy_added",
|
||||||
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
|
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
|
device_class=SensorDeviceClass.ENERGY,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="charge_state_charger_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="charge_state_charger_voltage",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||||
|
device_class=SensorDeviceClass.VOLTAGE,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="charge_state_charger_actual_current",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||||
|
device_class=SensorDeviceClass.CURRENT,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="charge_state_charge_rate",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||||
|
device_class=SensorDeviceClass.SPEED,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="charge_state_minutes_to_full_charge",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=minutes_to_datetime,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="charge_state_battery_range",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfLength.MILES,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="drive_state_speed",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||||
|
device_class=SensorDeviceClass.SPEED,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="drive_state_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="drive_state_shift_state",
|
||||||
|
icon="mdi:car-shift-pattern",
|
||||||
|
options=["p", "d", "r", "n"],
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
value_fn=lambda x: x.lower() if isinstance(x, str) else x,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="vehicle_state_odometer",
|
||||||
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
|
native_unit_of_measurement=UnitOfLength.MILES,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="vehicle_state_tpms_pressure_fl",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="vehicle_state_tpms_pressure_fr",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="vehicle_state_tpms_pressure_rl",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="vehicle_state_tpms_pressure_rr",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="climate_state_inside_temp",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="climate_state_outside_temp",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="climate_state_driver_temp_setting",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="climate_state_passenger_temp_setting",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="drive_state_active_route_traffic_minutes_delay",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="drive_state_active_route_energy_at_arrival",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="drive_state_active_route_miles_to_arrival",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfLength.MILES,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="drive_state_active_route_minutes_to_arrival",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
value_fn=minutes_to_datetime,
|
||||||
|
),
|
||||||
|
TeslemetrySensorEntityDescription(
|
||||||
|
key="drive_state_active_route_destination",
|
||||||
|
icon="mdi:map-marker",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
ENERGY_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="solar_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
icon="mdi:solar-power",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="energy_left",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||||
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
icon="mdi:battery",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="total_pack_energy",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||||
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
icon="mdi:battery-high",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="percentage_charged",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="battery_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
icon="mdi:home-battery",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="load_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
icon="mdi:power-plug",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="grid_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
icon="mdi:transmission-tower",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="grid_services_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
icon="mdi:transmission-tower",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="generator_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
icon="mdi:generator-stationary",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="wall_connector_state",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
icon="mdi:ev-station",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="wall_connector_fault_state",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
icon="mdi:ev-station",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="wall_connector_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
icon="mdi:ev-station",
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="vin",
|
||||||
|
icon="mdi:car-electric",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Teslemetry sensor platform from a config entry."""
|
||||||
|
data = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
chain(
|
||||||
|
( # Add vehicles
|
||||||
|
TeslemetryVehicleSensorEntity(vehicle, description)
|
||||||
|
for vehicle in data.vehicles
|
||||||
|
for description in VEHICLE_DESCRIPTIONS
|
||||||
|
),
|
||||||
|
( # Add energy sites
|
||||||
|
TeslemetryEnergySensorEntity(energysite, description)
|
||||||
|
for energysite in data.energysites
|
||||||
|
for description in ENERGY_DESCRIPTIONS
|
||||||
|
if description.key in energysite.coordinator.data
|
||||||
|
),
|
||||||
|
( # Add wall connectors
|
||||||
|
TeslemetryWallConnectorSensorEntity(energysite, din, description)
|
||||||
|
for energysite in data.energysites
|
||||||
|
for din in energysite.coordinator.data.get("wall_connectors", {})
|
||||||
|
for description in WALL_CONNECTOR_DESCRIPTIONS
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity):
|
||||||
|
"""Base class for Teslemetry vehicle metric sensors."""
|
||||||
|
|
||||||
|
entity_description: TeslemetrySensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
vehicle: TeslemetryVehicleData,
|
||||||
|
description: TeslemetrySensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(vehicle, description.key)
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType | datetime:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.entity_description.value_fn(self.get())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if sensor is available."""
|
||||||
|
return super().available and self.get() is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryEnergySensorEntity(TeslemetryEnergyEntity, SensorEntity):
|
||||||
|
"""Base class for Teslemetry energy site metric sensors."""
|
||||||
|
|
||||||
|
entity_description: SensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
energysite: TeslemetryEnergyData,
|
||||||
|
description: SensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(energysite, description.key)
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.get()
|
||||||
|
|
||||||
|
|
||||||
|
class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorEntity):
|
||||||
|
"""Base class for Teslemetry energy site metric sensors."""
|
||||||
|
|
||||||
|
entity_description: SensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
energysite: TeslemetryEnergyData,
|
||||||
|
din: str,
|
||||||
|
description: SensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(
|
||||||
|
energysite,
|
||||||
|
din,
|
||||||
|
description.key,
|
||||||
|
)
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self._value
|
@ -30,6 +30,128 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"sensor": {
|
||||||
|
"charge_state_usable_battery_level": {
|
||||||
|
"name": "Battery level"
|
||||||
|
},
|
||||||
|
"charge_state_charge_energy_added": {
|
||||||
|
"name": "Charge energy added"
|
||||||
|
},
|
||||||
|
"charge_state_charger_power": {
|
||||||
|
"name": "Charger power"
|
||||||
|
},
|
||||||
|
"charge_state_charger_voltage": {
|
||||||
|
"name": "Charger voltage"
|
||||||
|
},
|
||||||
|
"charge_state_charger_actual_current": {
|
||||||
|
"name": "Charger current"
|
||||||
|
},
|
||||||
|
"charge_state_charge_rate": {
|
||||||
|
"name": "Charge rate"
|
||||||
|
},
|
||||||
|
"charge_state_battery_range": {
|
||||||
|
"name": "Battery range"
|
||||||
|
},
|
||||||
|
"charge_state_minutes_to_full_charge": {
|
||||||
|
"name": "Time to full charge"
|
||||||
|
},
|
||||||
|
"drive_state_speed": {
|
||||||
|
"name": "Speed"
|
||||||
|
},
|
||||||
|
"drive_state_power": {
|
||||||
|
"name": "Power"
|
||||||
|
},
|
||||||
|
"drive_state_shift_state": {
|
||||||
|
"name": "Shift state",
|
||||||
|
"state": {
|
||||||
|
"p": "Park",
|
||||||
|
"d": "Drive",
|
||||||
|
"r": "Reverse",
|
||||||
|
"n": "Neutral"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vehicle_state_odometer": {
|
||||||
|
"name": "Odometer"
|
||||||
|
},
|
||||||
|
"vehicle_state_tpms_pressure_fl": {
|
||||||
|
"name": "Tire pressure front left"
|
||||||
|
},
|
||||||
|
"vehicle_state_tpms_pressure_fr": {
|
||||||
|
"name": "Tire pressure front right"
|
||||||
|
},
|
||||||
|
"vehicle_state_tpms_pressure_rl": {
|
||||||
|
"name": "Tire pressure rear left"
|
||||||
|
},
|
||||||
|
"vehicle_state_tpms_pressure_rr": {
|
||||||
|
"name": "Tire pressure rear right"
|
||||||
|
},
|
||||||
|
"climate_state_inside_temp": {
|
||||||
|
"name": "Inside temperature"
|
||||||
|
},
|
||||||
|
"climate_state_outside_temp": {
|
||||||
|
"name": "Outside temperature"
|
||||||
|
},
|
||||||
|
"climate_state_driver_temp_setting": {
|
||||||
|
"name": "Driver temperature setting"
|
||||||
|
},
|
||||||
|
"climate_state_passenger_temp_setting": {
|
||||||
|
"name": "Passenger temperature setting"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_traffic_minutes_delay": {
|
||||||
|
"name": "Traffic delay"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_energy_at_arrival": {
|
||||||
|
"name": "State of charge at arrival"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_miles_to_arrival": {
|
||||||
|
"name": "Distance to arrival"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_minutes_to_arrival": {
|
||||||
|
"name": "Time to arrival"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_destination": {
|
||||||
|
"name": "Destination"
|
||||||
|
},
|
||||||
|
"solar_power": {
|
||||||
|
"name": "Solar power"
|
||||||
|
},
|
||||||
|
"energy_left": {
|
||||||
|
"name": "Energy left"
|
||||||
|
},
|
||||||
|
"total_pack_energy": {
|
||||||
|
"name": "Total pack energy"
|
||||||
|
},
|
||||||
|
"percentage_charged": {
|
||||||
|
"name": "Percentage charged"
|
||||||
|
},
|
||||||
|
"battery_power": {
|
||||||
|
"name": "Battery power"
|
||||||
|
},
|
||||||
|
"load_power": {
|
||||||
|
"name": "Load power"
|
||||||
|
},
|
||||||
|
"grid_power": {
|
||||||
|
"name": "Grid power"
|
||||||
|
},
|
||||||
|
"grid_services_power": {
|
||||||
|
"name": "Grid services power"
|
||||||
|
},
|
||||||
|
"generator_power": {
|
||||||
|
"name": "Generator power"
|
||||||
|
},
|
||||||
|
"wall_connector_state": {
|
||||||
|
"name": "State code"
|
||||||
|
},
|
||||||
|
"wall_connector_fault_state": {
|
||||||
|
"name": "Fault state code"
|
||||||
|
},
|
||||||
|
"wall_connector_power": {
|
||||||
|
"name": "Power"
|
||||||
|
},
|
||||||
|
"vin": {
|
||||||
|
"name": "Vehicle"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
"""Fixtures for Tessie."""
|
"""Fixtures for Tessie."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from .const import PRODUCTS, RESPONSE_OK, VEHICLE_DATA, WAKE_UP_ONLINE
|
from .const import LIVE_STATUS, PRODUCTS, RESPONSE_OK, VEHICLE_DATA, WAKE_UP_ONLINE
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@ -55,3 +56,13 @@ def mock_request():
|
|||||||
return_value=RESPONSE_OK,
|
return_value=RESPONSE_OK,
|
||||||
) as mock_request:
|
) as mock_request:
|
||||||
yield mock_request
|
yield mock_request
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_live_status():
|
||||||
|
"""Mock Teslemetry Energy Specific live_status method."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.teslemetry.EnergySpecific.live_status",
|
||||||
|
side_effect=lambda: deepcopy(LIVE_STATUS),
|
||||||
|
) as mock_live_status:
|
||||||
|
yield mock_live_status
|
||||||
|
@ -12,5 +12,6 @@ WAKE_UP_ASLEEP = {"response": {"state": TeslemetryState.ASLEEP}, "error": None}
|
|||||||
|
|
||||||
PRODUCTS = load_json_object_fixture("products.json", DOMAIN)
|
PRODUCTS = load_json_object_fixture("products.json", DOMAIN)
|
||||||
VEHICLE_DATA = load_json_object_fixture("vehicle_data.json", DOMAIN)
|
VEHICLE_DATA = load_json_object_fixture("vehicle_data.json", DOMAIN)
|
||||||
|
LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN)
|
||||||
|
|
||||||
RESPONSE_OK = {"response": {}, "error": None}
|
RESPONSE_OK = {"response": {}, "error": None}
|
||||||
|
33
tests/components/teslemetry/fixtures/live_status.json
Normal file
33
tests/components/teslemetry/fixtures/live_status.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"solar_power": 1185,
|
||||||
|
"energy_left": 38896.47368421053,
|
||||||
|
"total_pack_energy": 40727,
|
||||||
|
"percentage_charged": 95.50537403739663,
|
||||||
|
"backup_capable": true,
|
||||||
|
"battery_power": 5060,
|
||||||
|
"load_power": 6245,
|
||||||
|
"grid_status": "Active",
|
||||||
|
"grid_services_active": false,
|
||||||
|
"grid_power": 0,
|
||||||
|
"grid_services_power": 0,
|
||||||
|
"generator_power": 0,
|
||||||
|
"island_status": "on_grid",
|
||||||
|
"storm_mode_active": false,
|
||||||
|
"timestamp": "2024-01-01T00:00:00+00:00",
|
||||||
|
"wall_connectors": [
|
||||||
|
{
|
||||||
|
"din": "abd-123",
|
||||||
|
"wall_connector_state": 2,
|
||||||
|
"wall_connector_fault_state": 2,
|
||||||
|
"wall_connector_power": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"din": "bcd-234",
|
||||||
|
"wall_connector_state": 2,
|
||||||
|
"wall_connector_fault_state": 2,
|
||||||
|
"wall_connector_power": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -71,28 +71,50 @@
|
|||||||
"release_notes_supported": true
|
"release_notes_supported": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"energy_site_id": 2345,
|
"energy_site_id": 123456,
|
||||||
"resource_type": "wall_connector",
|
"resource_type": "battery",
|
||||||
"id": "ID1234",
|
"site_name": "Energy Site",
|
||||||
"asset_site_id": "abcdef",
|
"id": "ABC123",
|
||||||
"warp_site_number": "ID1234",
|
"gateway_id": "ABC123",
|
||||||
|
"asset_site_id": "c0ffee",
|
||||||
|
"warp_site_number": "GA123456",
|
||||||
|
"energy_left": 23286.105263157893,
|
||||||
|
"total_pack_energy": 40804,
|
||||||
|
"percentage_charged": 57.068192488868476,
|
||||||
|
"battery_type": "ac_powerwall",
|
||||||
|
"backup_capable": true,
|
||||||
|
"battery_power": 14990,
|
||||||
"go_off_grid_test_banner_enabled": null,
|
"go_off_grid_test_banner_enabled": null,
|
||||||
"storm_mode_enabled": null,
|
"storm_mode_enabled": true,
|
||||||
"powerwall_onboarding_settings_set": null,
|
"powerwall_onboarding_settings_set": true,
|
||||||
"powerwall_tesla_electric_interested_in": null,
|
"powerwall_tesla_electric_interested_in": null,
|
||||||
"vpp_tour_enabled": null,
|
"vpp_tour_enabled": null,
|
||||||
"sync_grid_alert_enabled": false,
|
"sync_grid_alert_enabled": true,
|
||||||
"breaker_alert_enabled": false,
|
"breaker_alert_enabled": true,
|
||||||
"components": {
|
"components": {
|
||||||
"battery": false,
|
"battery": true,
|
||||||
"solar": false,
|
"battery_type": "ac_powerwall",
|
||||||
"grid": false,
|
"solar": true,
|
||||||
"load_meter": false,
|
"solar_type": "pv_panel",
|
||||||
|
"grid": true,
|
||||||
|
"load_meter": true,
|
||||||
|
"market_type": "residential",
|
||||||
"wall_connectors": [
|
"wall_connectors": [
|
||||||
{ "device_id": "abcdef", "din": "12345", "is_active": true }
|
{
|
||||||
|
"device_id": "abc-123",
|
||||||
|
"din": "123-abc",
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"device_id": "bcd-234",
|
||||||
|
"din": "234-bcd",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"features": {}
|
"features": {
|
||||||
|
"rate_plan_manager_no_pricing_constraint": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"count": 2
|
"count": 2
|
||||||
|
87
tests/components/teslemetry/fixtures/site_info.json
Normal file
87
tests/components/teslemetry/fixtures/site_info.json
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"id": "1233-abcd",
|
||||||
|
"site_name": "Site",
|
||||||
|
"backup_reserve_percent": 0,
|
||||||
|
"default_real_mode": "self_consumption",
|
||||||
|
"installation_date": "2022-01-01T00:00:00+00:00",
|
||||||
|
"user_settings": {
|
||||||
|
"go_off_grid_test_banner_enabled": false,
|
||||||
|
"storm_mode_enabled": true,
|
||||||
|
"powerwall_onboarding_settings_set": true,
|
||||||
|
"powerwall_tesla_electric_interested_in": false,
|
||||||
|
"vpp_tour_enabled": true,
|
||||||
|
"sync_grid_alert_enabled": true,
|
||||||
|
"breaker_alert_enabled": false
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"solar": true,
|
||||||
|
"solar_type": "pv_panel",
|
||||||
|
"battery": true,
|
||||||
|
"grid": true,
|
||||||
|
"backup": true,
|
||||||
|
"gateway": "teg",
|
||||||
|
"load_meter": true,
|
||||||
|
"tou_capable": true,
|
||||||
|
"storm_mode_capable": true,
|
||||||
|
"flex_energy_request_capable": false,
|
||||||
|
"car_charging_data_supported": false,
|
||||||
|
"off_grid_vehicle_charging_reserve_supported": false,
|
||||||
|
"vehicle_charging_performance_view_enabled": false,
|
||||||
|
"vehicle_charging_solar_offset_view_enabled": false,
|
||||||
|
"battery_solar_offset_view_enabled": true,
|
||||||
|
"solar_value_enabled": true,
|
||||||
|
"energy_value_header": "Energy Value",
|
||||||
|
"energy_value_subheader": "Estimated Value",
|
||||||
|
"energy_service_self_scheduling_enabled": true,
|
||||||
|
"show_grid_import_battery_source_cards": true,
|
||||||
|
"set_islanding_mode_enabled": true,
|
||||||
|
"wifi_commissioning_enabled": true,
|
||||||
|
"backup_time_remaining_enabled": true,
|
||||||
|
"battery_type": "ac_powerwall",
|
||||||
|
"configurable": true,
|
||||||
|
"grid_services_enabled": false,
|
||||||
|
"wall_connectors": [
|
||||||
|
{
|
||||||
|
"device_id": "123abc",
|
||||||
|
"din": "abc123",
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"device_id": "234bcd",
|
||||||
|
"din": "bcd234",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"disallow_charge_from_grid_with_solar_installed": true,
|
||||||
|
"customer_preferred_export_rule": "pv_only",
|
||||||
|
"net_meter_mode": "battery_ok",
|
||||||
|
"system_alerts_enabled": true
|
||||||
|
},
|
||||||
|
"version": "23.44.0 eb113390",
|
||||||
|
"battery_count": 3,
|
||||||
|
"tou_settings": {
|
||||||
|
"optimization_strategy": "economics",
|
||||||
|
"schedule": [
|
||||||
|
{
|
||||||
|
"target": "off_peak",
|
||||||
|
"week_days": [1, 0],
|
||||||
|
"start_seconds": 0,
|
||||||
|
"end_seconds": 3600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "peak",
|
||||||
|
"week_days": [1, 0],
|
||||||
|
"start_seconds": 3600,
|
||||||
|
"end_seconds": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nameplate_power": 15000,
|
||||||
|
"nameplate_energy": 40500,
|
||||||
|
"installation_time_zone": "",
|
||||||
|
"max_site_meter_power_ac": 1000000000,
|
||||||
|
"min_site_meter_power_ac": -1000000000,
|
||||||
|
"vpp_backup_reserve_percent": 0
|
||||||
|
}
|
||||||
|
}
|
@ -112,10 +112,20 @@
|
|||||||
"wiper_blade_heater": false
|
"wiper_blade_heater": false
|
||||||
},
|
},
|
||||||
"drive_state": {
|
"drive_state": {
|
||||||
"active_route_latitude": -27.855946,
|
"active_route_latitude": 30.2226265,
|
||||||
"active_route_longitude": 153.345056,
|
"active_route_longitude": -97.6236871,
|
||||||
|
"active_route_miles_to_arrival": 0.039491,
|
||||||
|
"active_route_minutes_to_arrival": 0.103577,
|
||||||
"active_route_traffic_minutes_delay": 0,
|
"active_route_traffic_minutes_delay": 0,
|
||||||
"power": 0,
|
"gps_as_of": 1701129612,
|
||||||
|
"heading": 185,
|
||||||
|
"latitude": -30.222626,
|
||||||
|
"longitude": -97.6236871,
|
||||||
|
"native_latitude": -30.222626,
|
||||||
|
"native_location_supported": 1,
|
||||||
|
"native_longitude": -97.6236871,
|
||||||
|
"native_type": "wgs",
|
||||||
|
"power": -7,
|
||||||
"shift_state": null,
|
"shift_state": null,
|
||||||
"speed": null,
|
"speed": null,
|
||||||
"timestamp": 1705707520649
|
"timestamp": 1705707520649
|
||||||
|
2208
tests/components/teslemetry/snapshots/test_sensor.ambr
Normal file
2208
tests/components/teslemetry/snapshots/test_sensor.ambr
Normal file
File diff suppressed because it is too large
Load Diff
@ -55,10 +55,10 @@ async def test_other_failure(hass: HomeAssistant, mock_products) -> None:
|
|||||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
# Coordinator
|
# Vehicle Coordinator
|
||||||
|
|
||||||
|
|
||||||
async def test_first_refresh(
|
async def test_vehicle_first_refresh(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_wake_up,
|
mock_wake_up,
|
||||||
mock_vehicle_data,
|
mock_vehicle_data,
|
||||||
@ -88,14 +88,14 @@ async def test_first_refresh(
|
|||||||
mock_vehicle_data.assert_called_once()
|
mock_vehicle_data.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
async def test_first_refresh_error(hass: HomeAssistant, mock_wake_up) -> None:
|
async def test_vehicle_first_refresh_error(hass: HomeAssistant, mock_wake_up) -> None:
|
||||||
"""Test first coordinator refresh with an error."""
|
"""Test first coordinator refresh with an error."""
|
||||||
mock_wake_up.side_effect = TeslaFleetError
|
mock_wake_up.side_effect = TeslaFleetError
|
||||||
entry = await setup_platform(hass)
|
entry = await setup_platform(hass)
|
||||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
async def test_refresh_offline(
|
async def test_vehicle_refresh_offline(
|
||||||
hass: HomeAssistant, mock_vehicle_data, freezer: FrozenDateTimeFactory
|
hass: HomeAssistant, mock_vehicle_data, freezer: FrozenDateTimeFactory
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test coordinator refresh with an error."""
|
"""Test coordinator refresh with an error."""
|
||||||
@ -111,8 +111,18 @@ async def test_refresh_offline(
|
|||||||
mock_vehicle_data.assert_called_once()
|
mock_vehicle_data.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
async def test_refresh_error(hass: HomeAssistant, mock_vehicle_data) -> None:
|
async def test_vehicle_refresh_error(hass: HomeAssistant, mock_vehicle_data) -> None:
|
||||||
"""Test coordinator refresh with an error."""
|
"""Test coordinator refresh with an error."""
|
||||||
mock_vehicle_data.side_effect = TeslaFleetError
|
mock_vehicle_data.side_effect = TeslaFleetError
|
||||||
entry = await setup_platform(hass)
|
entry = await setup_platform(hass)
|
||||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
# Test Energy Coordinator
|
||||||
|
|
||||||
|
|
||||||
|
async def test_energy_refresh_error(hass: HomeAssistant, mock_live_status) -> None:
|
||||||
|
"""Test coordinator refresh with an error."""
|
||||||
|
mock_live_status.side_effect = TeslaFleetError
|
||||||
|
entry = await setup_platform(hass)
|
||||||
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
26
tests/components/teslemetry/test_sensor.py
Normal file
26
tests/components/teslemetry/test_sensor.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"""Test the Teslemetry sensor platform."""
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import assert_entities, setup_platform
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Tests that the sensor entities are correct."""
|
||||||
|
|
||||||
|
freezer.move_to("2024-01-01 00:00:00+00:00")
|
||||||
|
|
||||||
|
entry = await setup_platform(hass, [Platform.SENSOR])
|
||||||
|
|
||||||
|
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
|
Loading…
x
Reference in New Issue
Block a user