Add new coordinators to Tessie (#118452)

* WIP

* wip

* Add energy classes

* Add basis for Testing

* Bump Library

* fix case

* bump library

* bump library again

* bump library for teslemetry

* reorder

* Fix super

* Update strings.json

* Tests

* Small tweaks

* Bump

* Bump teslemetry

* Remove version

* Add WC states

* Bump to match dev

* Review feedback

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Review feedback

* Review feedback 1

* Review feedback 2

* TessieWallConnectorStates Enum

* fixes

* Fix translations and value

* Update homeassistant/components/tessie/strings.json

* Update homeassistant/components/tessie/strings.json

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Brett Adams 2024-06-26 22:23:06 +10:00 committed by GitHub
parent fd67fe417e
commit ec16fc235b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1910 additions and 98 deletions

View file

@ -1,9 +1,12 @@
"""Tessie integration.""" """Tessie integration."""
import asyncio
from http import HTTPStatus from http import HTTPStatus
import logging import logging
from aiohttp import ClientError, ClientResponseError from aiohttp import ClientError, ClientResponseError
from tesla_fleet_api import EnergySpecific, Tessie
from tesla_fleet_api.exceptions import TeslaFleetError
from tessie_api import get_state_of_all_vehicles from tessie_api import get_state_of_all_vehicles
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -14,8 +17,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from .const import DOMAIN, MODELS from .const import DOMAIN, MODELS
from .coordinator import TessieStateUpdateCoordinator from .coordinator import (
from .models import TessieData, TessieVehicleData TessieEnergySiteInfoCoordinator,
TessieEnergySiteLiveCoordinator,
TessieStateUpdateCoordinator,
)
from .models import TessieData, TessieEnergyData, TessieVehicleData
PLATFORMS = [ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
@ -40,10 +47,11 @@ type TessieConfigEntry = ConfigEntry[TessieData]
async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bool:
"""Set up Tessie config.""" """Set up Tessie config."""
api_key = entry.data[CONF_ACCESS_TOKEN] api_key = entry.data[CONF_ACCESS_TOKEN]
session = async_get_clientsession(hass)
try: try:
state_of_all_vehicles = await get_state_of_all_vehicles( state_of_all_vehicles = await get_state_of_all_vehicles(
session=async_get_clientsession(hass), session=session,
api_key=api_key, api_key=api_key,
only_active=True, only_active=True,
) )
@ -84,7 +92,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
if vehicle["last_state"] is not None if vehicle["last_state"] is not None
] ]
entry.runtime_data = TessieData(vehicles=vehicles) # Energy Sites
tessie = Tessie(session, api_key)
try:
products = (await tessie.products())["response"]
except TeslaFleetError as e:
raise ConfigEntryNotReady from e
energysites: list[TessieEnergyData] = []
for product in products:
if "energy_site_id" in product:
site_id = product["energy_site_id"]
api = EnergySpecific(tessie.energy, site_id)
energysites.append(
TessieEnergyData(
api=api,
id=site_id,
live_coordinator=TessieEnergySiteLiveCoordinator(hass, api),
info_coordinator=TessieEnergySiteInfoCoordinator(hass, api),
device=DeviceInfo(
identifiers={(DOMAIN, str(site_id))},
manufacturer="Tesla",
name=product.get("site_name", "Energy Site"),
),
)
)
# Populate coordinator data before forwarding to platforms
await asyncio.gather(
*(
energysite.live_coordinator.async_config_entry_first_refresh()
for energysite in energysites
),
*(
energysite.info_coordinator.async_config_entry_first_refresh()
for energysite in energysites
),
)
entry.runtime_data = TessieData(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

View file

@ -79,3 +79,18 @@ TessieChargeStates = {
"Disconnected": "disconnected", "Disconnected": "disconnected",
"NoPower": "no_power", "NoPower": "no_power",
} }
class TessieWallConnectorStates(IntEnum):
"""Tessie Wall Connector states."""
BOOTING = 0
CHARGING = 1
DISCONNECTED = 2
CONNECTED = 4
SCHEDULED = 5
NEGOTIATING = 6
ERROR = 7
CHARGING_FINISHED = 8
WAITING_CAR = 9
CHARGING_REDUCED = 10

View file

@ -6,21 +6,37 @@ import logging
from typing import Any from typing import Any
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from tesla_fleet_api import EnergySpecific
from tesla_fleet_api.exceptions import InvalidToken, MissingToken, TeslaFleetError
from tessie_api import get_state, get_status from tessie_api import get_state, get_status
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import TessieStatus from .const import TessieStatus
# This matches the update interval Tessie performs server side # This matches the update interval Tessie performs server side
TESSIE_SYNC_INTERVAL = 10 TESSIE_SYNC_INTERVAL = 10
TESSIE_FLEET_API_SYNC_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def flatten(data: dict[str, Any], parent: str | None = None) -> dict[str, Any]:
"""Flatten the data structure."""
result = {}
for key, value in data.items():
if parent:
key = f"{parent}_{key}"
if isinstance(value, dict):
result.update(flatten(value, key))
else:
result[key] = value
return result
class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching data from the Tessie API.""" """Class to manage fetching data from the Tessie API."""
@ -41,7 +57,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self.api_key = api_key self.api_key = api_key
self.vin = vin self.vin = vin
self.session = async_get_clientsession(hass) self.session = async_get_clientsession(hass)
self.data = self._flatten(data) self.data = flatten(data)
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update vehicle data using Tessie API.""" """Update vehicle data using Tessie API."""
@ -68,18 +84,61 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
raise ConfigEntryAuthFailed from e raise ConfigEntryAuthFailed from e
raise raise
return self._flatten(vehicle) return flatten(vehicle)
def _flatten(
self, data: dict[str, Any], parent: str | None = None class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]):
) -> dict[str, Any]: """Class to manage fetching energy site live status from the Tessie API."""
"""Flatten the data structure."""
result = {} def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None:
for key, value in data.items(): """Initialize Tessie Energy Site Live coordinator."""
if parent: super().__init__(
key = f"{parent}_{key}" hass,
if isinstance(value, dict): _LOGGER,
result.update(self._flatten(value, key)) name="Tessie Energy Site Live",
else: update_interval=TESSIE_FLEET_API_SYNC_INTERVAL,
result[key] = value )
return result self.api = api
async def _async_update_data(self) -> dict[str, Any]:
"""Update energy site data using Tessie API."""
try:
data = (await self.api.live_status())["response"]
except (InvalidToken, MissingToken) as e:
raise ConfigEntryAuthFailed from e
except TeslaFleetError as e:
raise UpdateFailed(e.message) from e
# Convert Wall Connectors from array to dict
data["wall_connectors"] = {
wc["din"]: wc for wc in (data.get("wall_connectors") or [])
}
return data
class TessieEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching energy site info from the Tessie API."""
def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None:
"""Initialize Tessie Energy Info coordinator."""
super().__init__(
hass,
_LOGGER,
name="Tessie Energy Site Info",
update_interval=TESSIE_FLEET_API_SYNC_INTERVAL,
)
self.api = api
async def _async_update_data(self) -> dict[str, Any]:
"""Update energy site data using Tessie API."""
try:
data = (await self.api.site_info())["response"]
except (InvalidToken, MissingToken) as e:
raise ConfigEntryAuthFailed from e
except TeslaFleetError as e:
raise UpdateFailed(e.message) from e
return flatten(data)

View file

@ -1,36 +1,47 @@
"""Tessie parent entity class.""" """Tessie parent entity class."""
from abc import abstractmethod
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import Any from typing import Any
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
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 from .const import DOMAIN
from .coordinator import TessieStateUpdateCoordinator from .coordinator import (
from .models import TessieVehicleData TessieEnergySiteInfoCoordinator,
TessieEnergySiteLiveCoordinator,
TessieStateUpdateCoordinator,
)
from .models import TessieEnergyData, TessieVehicleData
class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): class TessieBaseEntity(
"""Parent class for Tessie Entities.""" CoordinatorEntity[
TessieStateUpdateCoordinator
| TessieEnergySiteInfoCoordinator
| TessieEnergySiteLiveCoordinator
]
):
"""Parent class for Tessie entities."""
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__( def __init__(
self, self,
vehicle: TessieVehicleData, coordinator: TessieStateUpdateCoordinator
| TessieEnergySiteInfoCoordinator
| TessieEnergySiteLiveCoordinator,
key: str, key: str,
) -> None: ) -> None:
"""Initialize common aspects of a Tessie entity.""" """Initialize common aspects of a Tessie entity."""
super().__init__(vehicle.data_coordinator)
self.vin = vehicle.vin
self.key = key
self.key = key
self._attr_translation_key = key self._attr_translation_key = key
self._attr_unique_id = f"{vehicle.vin}-{key}" super().__init__(coordinator)
self._attr_device_info = vehicle.device
@property @property
def _value(self) -> Any: def _value(self) -> Any:
@ -41,15 +52,53 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]):
"""Return a specific value from coordinator data.""" """Return a specific value from coordinator data."""
return self.coordinator.data.get(key or self.key, default) return self.coordinator.data.get(key or self.key, default)
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._async_update_attrs()
super()._handle_coordinator_update()
@abstractmethod
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
class TessieEntity(TessieBaseEntity):
"""Parent class for Tessie vehicle entities."""
def __init__(
self,
vehicle: TessieVehicleData,
key: str,
) -> None:
"""Initialize common aspects of a Tessie vehicle entity."""
self.vin = vehicle.vin
self._session = vehicle.data_coordinator.session
self._api_key = vehicle.data_coordinator.api_key
self._attr_unique_id = f"{vehicle.vin}-{key}"
self._attr_device_info = vehicle.device
super().__init__(vehicle.data_coordinator, key)
@property
def _value(self) -> Any:
"""Return value from coordinator data."""
return self.coordinator.data.get(self.key)
def set(self, *args: Any) -> None:
"""Set a value in coordinator data."""
for key, value in args:
self.coordinator.data[key] = value
self.async_write_ha_state()
async def run( async def run(
self, func: Callable[..., Awaitable[dict[str, Any]]], **kargs: Any self, func: Callable[..., Awaitable[dict[str, Any]]], **kargs: Any
) -> None: ) -> None:
"""Run a tessie_api function and handle exceptions.""" """Run a tessie_api function and handle exceptions."""
try: try:
response = await func( response = await func(
session=self.coordinator.session, session=self._session,
vin=self.vin, vin=self.vin,
api_key=self.coordinator.api_key, api_key=self._api_key,
**kargs, **kargs,
) )
except ClientResponseError as e: except ClientResponseError as e:
@ -63,8 +112,55 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]):
translation_placeholders={"name": name}, translation_placeholders={"name": name},
) )
def set(self, *args: Any) -> None: def _async_update_attrs(self) -> None:
"""Set a value in coordinator data.""" """Update the attributes of the entity."""
for key, value in args: # Not used in this class yet
self.coordinator.data[key] = value
self.async_write_ha_state()
class TessieEnergyEntity(TessieBaseEntity):
"""Parent class for Tessie energy site entities."""
def __init__(
self,
data: TessieEnergyData,
coordinator: TessieEnergySiteInfoCoordinator | TessieEnergySiteLiveCoordinator,
key: str,
) -> None:
"""Initialize common aspects of a Tessie energy site entity."""
self._attr_unique_id = f"{data.id}-{key}"
self._attr_device_info = data.device
super().__init__(coordinator, key)
class TessieWallConnectorEntity(TessieBaseEntity):
"""Parent class for Tessie wall connector entities."""
def __init__(
self,
data: TessieEnergyData,
din: str,
key: str,
) -> None:
"""Initialize common aspects of a Teslemetry entity."""
self.din = din
self._attr_unique_id = f"{data.id}-{din}-{key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, din)},
manufacturer="Tesla",
name="Wall Connector",
via_device=(DOMAIN, str(data.id)),
serial_number=din.split("-")[-1],
)
super().__init__(data.live_coordinator, key)
@property
def _value(self) -> int:
"""Return a specific wall connector value from coordinator data."""
return (
self.coordinator.data.get("wall_connectors", {})
.get(self.din, {})
.get(self.key)
)

View file

@ -189,6 +189,42 @@
}, },
"drive_state_active_route_destination": { "drive_state_active_route_destination": {
"default": "mdi:map-marker" "default": "mdi:map-marker"
},
"battery_power": {
"default": "mdi:home-battery"
},
"energy_left": {
"default": "mdi:battery"
},
"generator_power": {
"default": "mdi:generator-stationary"
},
"grid_power": {
"default": "mdi:transmission-tower"
},
"grid_services_power": {
"default": "mdi:transmission-tower"
},
"load_power": {
"default": "mdi:power-plug"
},
"solar_power": {
"default": "mdi:solar-power"
},
"total_pack_energy": {
"default": "mdi:battery-high"
},
"vin": {
"default": "mdi:car-electric"
},
"wall_connector_fault_state": {
"default": "mdi:ev-station"
},
"wall_connector_power": {
"default": "mdi:ev-station"
},
"wall_connector_state": {
"default": "mdi:ev-station"
} }
}, },
"switch": { "switch": {

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tessie", "documentation": "https://www.home-assistant.io/integrations/tessie",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tessie"], "loggers": ["tessie"],
"requirements": ["tessie-api==0.0.9"] "requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.1"]
} }

View file

@ -4,9 +4,15 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from tesla_fleet_api import EnergySpecific
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from .coordinator import TessieStateUpdateCoordinator from .coordinator import (
TessieEnergySiteInfoCoordinator,
TessieEnergySiteLiveCoordinator,
TessieStateUpdateCoordinator,
)
@dataclass @dataclass
@ -14,6 +20,18 @@ class TessieData:
"""Data for the Tessie integration.""" """Data for the Tessie integration."""
vehicles: list[TessieVehicleData] vehicles: list[TessieVehicleData]
energysites: list[TessieEnergyData]
@dataclass
class TessieEnergyData:
"""Data for a Energy Site in the Tessie integration."""
api: EnergySpecific
live_coordinator: TessieEnergySiteLiveCoordinator
info_coordinator: TessieEnergySiteInfoCoordinator
id: int
device: DeviceInfo
@dataclass @dataclass

View file

@ -5,6 +5,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from itertools import chain
from typing import cast from typing import cast
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -33,9 +34,9 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.variance import ignore_variance from homeassistant.util.variance import ignore_variance
from . import TessieConfigEntry from . import TessieConfigEntry
from .const import TessieChargeStates from .const import TessieChargeStates, TessieWallConnectorStates
from .entity import TessieEntity from .entity import TessieEnergyEntity, TessieEntity, TessieWallConnectorEntity
from .models import TessieVehicleData from .models import TessieEnergyData, TessieVehicleData
@callback @callback
@ -257,6 +258,115 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
), ),
) )
ENERGY_LIVE_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
TessieSensorEntityDescription(
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,
),
TessieSensorEntityDescription(
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,
),
TessieSensorEntityDescription(
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,
entity_registry_enabled_default=False,
),
TessieSensorEntityDescription(
key="percentage_charged",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
suggested_display_precision=2,
),
TessieSensorEntityDescription(
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,
),
TessieSensorEntityDescription(
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,
),
TessieSensorEntityDescription(
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,
),
TessieSensorEntityDescription(
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,
),
TessieSensorEntityDescription(
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,
entity_registry_enabled_default=False,
),
)
WALL_CONNECTOR_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
TessieSensorEntityDescription(
key="wall_connector_state",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.ENUM,
value_fn=lambda x: TessieWallConnectorStates(cast(int, x)).name.lower(),
options=[state.name.lower() for state in TessieWallConnectorStates],
),
TessieSensorEntityDescription(
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,
),
TessieSensorEntityDescription(
key="vin",
),
)
ENERGY_INFO_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
TessieSensorEntityDescription(
key="vpp_backup_reserve_percent",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
),
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -264,17 +374,38 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Tessie sensor platform from a config entry.""" """Set up the Tessie sensor platform from a config entry."""
data = entry.runtime_data
async_add_entities( async_add_entities(
TessieSensorEntity(vehicle, description) chain(
for vehicle in data.vehicles ( # Add vehicles
for description in DESCRIPTIONS TessieVehicleSensorEntity(vehicle, description)
for vehicle in entry.runtime_data.vehicles
for description in DESCRIPTIONS
),
( # Add energy site info
TessieEnergyInfoSensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites
for description in ENERGY_INFO_DESCRIPTIONS
if description.key in energysite.info_coordinator.data
),
( # Add energy site live
TessieEnergyLiveSensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites
for description in ENERGY_LIVE_DESCRIPTIONS
if description.key in energysite.live_coordinator.data
),
( # Add wall connectors
TessieWallConnectorSensorEntity(energysite, din, description)
for energysite in entry.runtime_data.energysites
for din in energysite.live_coordinator.data.get("wall_connectors", {})
for description in WALL_CONNECTOR_DESCRIPTIONS
),
)
) )
class TessieSensorEntity(TessieEntity, SensorEntity): class TessieVehicleSensorEntity(TessieEntity, SensorEntity):
"""Base class for Tessie metric sensors.""" """Base class for Tessie sensor entities."""
entity_description: TessieSensorEntityDescription entity_description: TessieSensorEntityDescription
@ -284,8 +415,8 @@ class TessieSensorEntity(TessieEntity, SensorEntity):
description: TessieSensorEntityDescription, description: TessieSensorEntityDescription,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(vehicle, description.key)
self.entity_description = description self.entity_description = description
super().__init__(vehicle, description.key)
@property @property
def native_value(self) -> StateType | datetime: def native_value(self) -> StateType | datetime:
@ -296,3 +427,68 @@ class TessieSensorEntity(TessieEntity, SensorEntity):
def available(self) -> bool: def available(self) -> bool:
"""Return if sensor is available.""" """Return if sensor is available."""
return super().available and self.entity_description.available_fn(self.get()) return super().available and self.entity_description.available_fn(self.get())
class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity):
"""Base class for Tessie energy site sensor entity."""
entity_description: TessieSensorEntityDescription
def __init__(
self,
data: TessieEnergyData,
description: TessieSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
super().__init__(data, data.live_coordinator, description.key)
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = self._value is not None
self._attr_native_value = self.entity_description.value_fn(self._value)
class TessieEnergyInfoSensorEntity(TessieEnergyEntity, SensorEntity):
"""Base class for Tessie energy site sensor entity."""
entity_description: TessieSensorEntityDescription
def __init__(
self,
data: TessieEnergyData,
description: TessieSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
super().__init__(data, data.info_coordinator, description.key)
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = self._value is not None
self._attr_native_value = self._value
class TessieWallConnectorSensorEntity(TessieWallConnectorEntity, SensorEntity):
"""Base class for Tessie wall connector sensor entity."""
entity_description: TessieSensorEntityDescription
def __init__(
self,
data: TessieEnergyData,
din: str,
description: TessieSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
super().__init__(
data,
din,
description.key,
)
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = self._value is not None
self._attr_native_value = self.entity_description.value_fn(self._value)

View file

@ -167,6 +167,60 @@
}, },
"drive_state_active_route_destination": { "drive_state_active_route_destination": {
"name": "Destination" "name": "Destination"
},
"battery_power": {
"name": "Battery power"
},
"energy_left": {
"name": "Energy left"
},
"generator_power": {
"name": "Generator power"
},
"grid_power": {
"name": "Grid power"
},
"grid_services_power": {
"name": "Grid services power"
},
"load_power": {
"name": "Load power"
},
"percentage_charged": {
"name": "Percentage charged"
},
"solar_power": {
"name": "Solar power"
},
"total_pack_energy": {
"name": "Total pack energy"
},
"vin": {
"name": "Vehicle"
},
"vpp_backup_reserve_percent": {
"name": "VPP backup reserve"
},
"wall_connector_fault_state": {
"name": "Fault state code"
},
"wall_connector_power": {
"name": "Power"
},
"wall_connector_state": {
"name": "State",
"state": {
"booting": "Booting",
"charging": "[%key:component::tessie::entity::sensor::charge_state_charging_state::state::charging%]",
"disconnected": "[%key:common::state::disconnected%]",
"connected": "[%key:common::state::connected%]",
"scheduled": "Scheduled",
"negotiating": "Negotiating",
"error": "Error",
"charging_finished": "Charging finished",
"waiting_car": "Waiting car",
"charging_reduced": "Charging reduced"
}
} }
}, },
"cover": { "cover": {

View file

@ -2713,6 +2713,7 @@ temperusb==1.6.1
# tensorflow==2.5.0 # tensorflow==2.5.0
# homeassistant.components.teslemetry # homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==0.6.1 tesla-fleet-api==0.6.1
# homeassistant.components.powerwall # homeassistant.components.powerwall

View file

@ -2111,6 +2111,7 @@ temescal==0.5
temperusb==1.6.1 temperusb==1.6.1
# homeassistant.components.teslemetry # homeassistant.components.teslemetry
# homeassistant.components.tessie
tesla-fleet-api==0.6.1 tesla-fleet-api==0.6.1
# homeassistant.components.powerwall # homeassistant.components.powerwall

View file

@ -16,6 +16,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from tests.common import MockConfigEntry, load_json_object_fixture from tests.common import MockConfigEntry, load_json_object_fixture
# Tessie library
TEST_STATE_OF_ALL_VEHICLES = load_json_object_fixture("vehicles.json", DOMAIN) TEST_STATE_OF_ALL_VEHICLES = load_json_object_fixture("vehicles.json", DOMAIN)
TEST_VEHICLE_STATE_ONLINE = load_json_object_fixture("online.json", DOMAIN) TEST_VEHICLE_STATE_ONLINE = load_json_object_fixture("online.json", DOMAIN)
TEST_VEHICLE_STATUS_AWAKE = {"status": TessieStatus.AWAKE} TEST_VEHICLE_STATUS_AWAKE = {"status": TessieStatus.AWAKE}
@ -47,6 +48,13 @@ ERROR_VIRTUAL_KEY = ClientResponseError(
) )
ERROR_CONNECTION = ClientConnectionError() ERROR_CONNECTION = ClientConnectionError()
# Fleet API library
PRODUCTS = load_json_object_fixture("products.json", DOMAIN)
LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN)
SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN)
RESPONSE_OK = {"response": {}, "error": None}
COMMAND_OK = {"response": {"result": True, "reason": ""}}
async def setup_platform( async def setup_platform(
hass: HomeAssistant, platforms: list[Platform] | UndefinedType = UNDEFINED hass: HomeAssistant, platforms: list[Platform] | UndefinedType = UNDEFINED

View file

@ -2,16 +2,23 @@
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 .common import ( from .common import (
COMMAND_OK,
LIVE_STATUS,
PRODUCTS,
SITE_INFO,
TEST_STATE_OF_ALL_VEHICLES, TEST_STATE_OF_ALL_VEHICLES,
TEST_VEHICLE_STATE_ONLINE, TEST_VEHICLE_STATE_ONLINE,
TEST_VEHICLE_STATUS_AWAKE, TEST_VEHICLE_STATUS_AWAKE,
) )
# Tessie
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_get_state(): def mock_get_state():
@ -41,3 +48,43 @@ def mock_get_state_of_all_vehicles():
return_value=TEST_STATE_OF_ALL_VEHICLES, return_value=TEST_STATE_OF_ALL_VEHICLES,
) as mock_get_state_of_all_vehicles: ) as mock_get_state_of_all_vehicles:
yield mock_get_state_of_all_vehicles yield mock_get_state_of_all_vehicles
# Fleet API
@pytest.fixture(autouse=True)
def mock_products():
"""Mock Tesla Fleet Api products method."""
with patch(
"homeassistant.components.tessie.Tessie.products", return_value=PRODUCTS
) as mock_products:
yield mock_products
@pytest.fixture(autouse=True)
def mock_request():
"""Mock Tesla Fleet API request method."""
with patch(
"homeassistant.components.tessie.Tessie._request",
return_value=COMMAND_OK,
) as mock_request:
yield mock_request
@pytest.fixture(autouse=True)
def mock_live_status():
"""Mock Tesla Fleet API EnergySpecific live_status method."""
with patch(
"homeassistant.components.tessie.EnergySpecific.live_status",
side_effect=lambda: deepcopy(LIVE_STATUS),
) as mock_live_status:
yield mock_live_status
@pytest.fixture(autouse=True)
def mock_site_info():
"""Mock Tesla Fleet API EnergySpecific site_info method."""
with patch(
"homeassistant.components.tessie.EnergySpecific.site_info",
side_effect=lambda: deepcopy(SITE_INFO),
) as mock_live_status:
yield mock_live_status

View 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
}
]
}
}

View file

@ -0,0 +1,121 @@
{
"response": [
{
"id": 1234,
"user_id": 1234,
"vehicle_id": 1234,
"vin": "LRWXF7EK4KC700000",
"color": null,
"access_type": "OWNER",
"display_name": "Test",
"option_codes": null,
"cached_data": null,
"granular_access": { "hide_private": false },
"tokens": ["abc", "def"],
"state": "asleep",
"in_service": false,
"id_s": "1234",
"calendar_enabled": true,
"api_version": 71,
"backseat_token": null,
"backseat_token_updated_at": null,
"ble_autopair_enrolled": false,
"vehicle_config": {
"aux_park_lamps": "Eu",
"badge_version": 1,
"can_accept_navigation_requests": true,
"can_actuate_trunks": true,
"car_special_type": "base",
"car_type": "model3",
"charge_port_type": "CCS",
"cop_user_set_temp_supported": false,
"dashcam_clip_save_supported": true,
"default_charge_to_max": false,
"driver_assist": "TeslaAP3",
"ece_restrictions": false,
"efficiency_package": "M32021",
"eu_vehicle": true,
"exterior_color": "DeepBlue",
"exterior_trim": "Black",
"exterior_trim_override": "",
"has_air_suspension": false,
"has_ludicrous_mode": false,
"has_seat_cooling": false,
"headlamp_type": "Global",
"interior_trim_type": "White2",
"key_version": 2,
"motorized_charge_port": true,
"paint_color_override": "0,9,25,0.7,0.04",
"performance_package": "Base",
"plg": true,
"pws": true,
"rear_drive_unit": "PM216MOSFET",
"rear_seat_heaters": 1,
"rear_seat_type": 0,
"rhd": true,
"roof_color": "RoofColorGlass",
"seat_type": null,
"spoiler_type": "None",
"sun_roof_installed": null,
"supports_qr_pairing": false,
"third_row_seats": "None",
"timestamp": 1705701487912,
"trim_badging": "74d",
"use_range_badging": true,
"utc_offset": 36000,
"webcam_selfie_supported": true,
"webcam_supported": true,
"wheel_type": "Pinwheel18CapKit"
},
"command_signing": "allowed",
"release_notes_supported": true
},
{
"energy_site_id": 123456,
"resource_type": "battery",
"site_name": "Energy Site",
"id": "ABC123",
"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,
"storm_mode_enabled": true,
"powerwall_onboarding_settings_set": true,
"powerwall_tesla_electric_interested_in": null,
"vpp_tour_enabled": null,
"sync_grid_alert_enabled": true,
"breaker_alert_enabled": true,
"components": {
"battery": true,
"battery_type": "ac_powerwall",
"solar": true,
"solar_type": "pv_panel",
"grid": true,
"load_meter": true,
"market_type": "residential",
"wall_connectors": [
{
"device_id": "abc-123",
"din": "123-abc",
"is_active": true
},
{
"device_id": "bcd-234",
"din": "234-bcd",
"is_active": true
}
]
},
"features": {
"rate_plan_manager_no_pricing_constraint": true
}
}
],
"count": 2
}

View file

@ -0,0 +1,125 @@
{
"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": true,
"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,
"gateways": [
{
"device_id": "gateway-id",
"din": "gateway-din",
"serial_number": "CN00000000J50D",
"part_number": "1152100-14-J",
"part_type": 10,
"part_name": "Tesla Backup Gateway 2",
"is_active": true,
"site_id": "1234-abcd",
"firmware_version": "24.4.0 0fe780c9",
"updated_datetime": "2024-05-14T00:00:00.000Z"
}
],
"batteries": [
{
"device_id": "battery-1-id",
"din": "battery-1-din",
"serial_number": "TG000000001DA5",
"part_number": "3012170-10-B",
"part_type": 2,
"part_name": "Powerwall 2",
"nameplate_max_charge_power": 5000,
"nameplate_max_discharge_power": 5000,
"nameplate_energy": 13500
},
{
"device_id": "battery-2-id",
"din": "battery-2-din",
"serial_number": "TG000000002DA5",
"part_number": "3012170-05-C",
"part_type": 2,
"part_name": "Powerwall 2",
"nameplate_max_charge_power": 5000,
"nameplate_max_discharge_power": 5000,
"nameplate_energy": 13500
}
],
"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": 2,
"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
}
}

View file

@ -1,4 +1,562 @@
# serializer version: 1 # serializer version: 1
# name: test_sensors[sensor.energy_site_battery_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energy_site_battery_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Battery power',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'battery_power',
'unique_id': '123456-battery_power',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_sensors[sensor.energy_site_battery_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Energy Site Battery power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_battery_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.energy_site_energy_left-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.energy_site_energy_left',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
'original_icon': None,
'original_name': 'Energy left',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'energy_left',
'unique_id': '123456-energy_left',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_sensors[sensor.energy_site_energy_left-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy_storage',
'friendly_name': 'Energy Site Energy left',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_energy_left',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.energy_site_generator_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energy_site_generator_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Generator power',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'generator_power',
'unique_id': '123456-generator_power',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_sensors[sensor.energy_site_generator_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Energy Site Generator power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_generator_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.energy_site_grid_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energy_site_grid_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Grid power',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'grid_power',
'unique_id': '123456-grid_power',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_sensors[sensor.energy_site_grid_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Energy Site Grid power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_grid_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.energy_site_grid_services_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energy_site_grid_services_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Grid services power',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'grid_services_power',
'unique_id': '123456-grid_services_power',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_sensors[sensor.energy_site_grid_services_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Energy Site Grid services power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_grid_services_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.energy_site_load_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energy_site_load_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Load power',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'load_power',
'unique_id': '123456-load_power',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_sensors[sensor.energy_site_load_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Energy Site Load power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_load_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.energy_site_percentage_charged-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energy_site_percentage_charged',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Percentage charged',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'percentage_charged',
'unique_id': '123456-percentage_charged',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[sensor.energy_site_percentage_charged-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Energy Site Percentage charged',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_percentage_charged',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.energy_site_solar_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energy_site_solar_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Solar power',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'solar_power',
'unique_id': '123456-solar_power',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_sensors[sensor.energy_site_solar_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Energy Site Solar power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_solar_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.energy_site_total_pack_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.energy_site_total_pack_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
'original_icon': None,
'original_name': 'Total pack energy',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'total_pack_energy',
'unique_id': '123456-total_pack_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_sensors[sensor.energy_site_total_pack_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy_storage',
'friendly_name': 'Energy Site Total pack energy',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_total_pack_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.energy_site_vpp_backup_reserve-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.energy_site_vpp_backup_reserve',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'VPP backup reserve',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'vpp_backup_reserve_percent',
'unique_id': '123456-vpp_backup_reserve_percent',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[sensor.energy_site_vpp_backup_reserve-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Energy Site VPP backup reserve',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_vpp_backup_reserve',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.test_battery_level-entry] # name: test_sensors[sensor.test_battery_level-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -592,42 +1150,6 @@
'state': 'Giga Texas', 'state': 'Giga Texas',
}) })
# --- # ---
# name: test_sensors[sensor.test_distance-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': <RegistryEntryDisabler.INTEGRATION: 'integration'>,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_distance',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
}),
}),
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None,
'original_name': 'Distance',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'charge_state_est_battery_range',
'unique_id': 'VINVINVIN-charge_state_est_battery_range',
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
})
# ---
# name: test_sensors[sensor.test_distance_to_arrival-entry] # name: test_sensors[sensor.test_distance_to_arrival-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -1544,3 +2066,353 @@
'state': '0', 'state': '0',
}) })
# --- # ---
# name: test_sensors[sensor.wall_connector_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.wall_connector_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'wall_connector_power',
'unique_id': '123456-abd-123-wall_connector_power',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_sensors[sensor.wall_connector_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Wall Connector Power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.wall_connector_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.wall_connector_power_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.wall_connector_power_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'wall_connector_power',
'unique_id': '123456-bcd-234-wall_connector_power',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_sensors[sensor.wall_connector_power_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Wall Connector Power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.wall_connector_power_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.wall_connector_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'booting',
'charging',
'disconnected',
'connected',
'scheduled',
'negotiating',
'error',
'charging_finished',
'waiting_car',
'charging_reduced',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.wall_connector_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'State',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'wall_connector_state',
'unique_id': '123456-abd-123-wall_connector_state',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.wall_connector_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Wall Connector State',
'options': list([
'booting',
'charging',
'disconnected',
'connected',
'scheduled',
'negotiating',
'error',
'charging_finished',
'waiting_car',
'charging_reduced',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.wall_connector_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.wall_connector_state_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'booting',
'charging',
'disconnected',
'connected',
'scheduled',
'negotiating',
'error',
'charging_finished',
'waiting_car',
'charging_reduced',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.wall_connector_state_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'State',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'wall_connector_state',
'unique_id': '123456-bcd-234-wall_connector_state',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.wall_connector_state_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Wall Connector State',
'options': list([
'booting',
'charging',
'disconnected',
'connected',
'scheduled',
'negotiating',
'error',
'charging_finished',
'waiting_car',
'charging_reduced',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.wall_connector_state_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.wall_connector_vehicle-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.wall_connector_vehicle',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Vehicle',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'vin',
'unique_id': '123456-abd-123-vin',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.wall_connector_vehicle-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Wall Connector Vehicle',
}),
'context': <ANY>,
'entity_id': 'sensor.wall_connector_vehicle',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[sensor.wall_connector_vehicle_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.wall_connector_vehicle_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Vehicle',
'platform': 'tessie',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'vin',
'unique_id': '123456-bcd-234-vin',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.wall_connector_vehicle_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Wall Connector Vehicle',
}),
'context': <ANY>,
'entity_id': 'sensor.wall_connector_vehicle_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View file

@ -2,11 +2,17 @@
from datetime import timedelta from datetime import timedelta
from freezegun.api import FrozenDateTimeFactory
from tesla_fleet_api.exceptions import Forbidden, InvalidToken
from homeassistant.components.tessie import PLATFORMS from homeassistant.components.tessie import PLATFORMS
from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL from homeassistant.components.tessie.coordinator import (
TESSIE_FLEET_API_SYNC_INTERVAL,
TESSIE_SYNC_INTERVAL,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow
from .common import ( from .common import (
ERROR_AUTH, ERROR_AUTH,
@ -22,60 +28,124 @@ WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL)
async def test_coordinator_online( async def test_coordinator_online(
hass: HomeAssistant, mock_get_state, mock_get_status hass: HomeAssistant, mock_get_state, mock_get_status, freezer: FrozenDateTimeFactory
) -> None: ) -> None:
"""Tests that the coordinator handles online vehicles.""" """Tests that the coordinator handles online vehicles."""
await setup_platform(hass, PLATFORMS) await setup_platform(hass, PLATFORMS)
async_fire_time_changed(hass, utcnow() + WAIT) freezer.tick(WAIT)
async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_get_status.assert_called_once() mock_get_status.assert_called_once()
mock_get_state.assert_called_once() mock_get_state.assert_called_once()
assert hass.states.get("binary_sensor.test_status").state == STATE_ON assert hass.states.get("binary_sensor.test_status").state == STATE_ON
async def test_coordinator_asleep(hass: HomeAssistant, mock_get_status) -> None: async def test_coordinator_asleep(
hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory
) -> None:
"""Tests that the coordinator handles asleep vehicles.""" """Tests that the coordinator handles asleep vehicles."""
await setup_platform(hass, [Platform.BINARY_SENSOR]) await setup_platform(hass, [Platform.BINARY_SENSOR])
mock_get_status.return_value = TEST_VEHICLE_STATUS_ASLEEP mock_get_status.return_value = TEST_VEHICLE_STATUS_ASLEEP
async_fire_time_changed(hass, utcnow() + WAIT) freezer.tick(WAIT)
async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_get_status.assert_called_once() mock_get_status.assert_called_once()
assert hass.states.get("binary_sensor.test_status").state == STATE_OFF assert hass.states.get("binary_sensor.test_status").state == STATE_OFF
async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_status) -> None: async def test_coordinator_clienterror(
hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory
) -> None:
"""Tests that the coordinator handles client errors.""" """Tests that the coordinator handles client errors."""
mock_get_status.side_effect = ERROR_UNKNOWN mock_get_status.side_effect = ERROR_UNKNOWN
await setup_platform(hass, [Platform.BINARY_SENSOR]) await setup_platform(hass, [Platform.BINARY_SENSOR])
async_fire_time_changed(hass, utcnow() + WAIT) freezer.tick(WAIT)
async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_get_status.assert_called_once() mock_get_status.assert_called_once()
assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE
async def test_coordinator_auth(hass: HomeAssistant, mock_get_status) -> None: async def test_coordinator_auth(
"""Tests that the coordinator handles timeout errors.""" hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory
) -> None:
"""Tests that the coordinator handles auth errors."""
mock_get_status.side_effect = ERROR_AUTH mock_get_status.side_effect = ERROR_AUTH
await setup_platform(hass, [Platform.BINARY_SENSOR]) await setup_platform(hass, [Platform.BINARY_SENSOR])
async_fire_time_changed(hass, utcnow() + WAIT) freezer.tick(WAIT)
async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_get_status.assert_called_once() mock_get_status.assert_called_once()
async def test_coordinator_connection(hass: HomeAssistant, mock_get_status) -> None: async def test_coordinator_connection(
hass: HomeAssistant, mock_get_status, freezer: FrozenDateTimeFactory
) -> None:
"""Tests that the coordinator handles connection errors.""" """Tests that the coordinator handles connection errors."""
mock_get_status.side_effect = ERROR_CONNECTION mock_get_status.side_effect = ERROR_CONNECTION
await setup_platform(hass, [Platform.BINARY_SENSOR]) await setup_platform(hass, [Platform.BINARY_SENSOR])
async_fire_time_changed(hass, utcnow() + WAIT) freezer.tick(WAIT)
async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_get_status.assert_called_once() mock_get_status.assert_called_once()
assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE
async def test_coordinator_live_error(
hass: HomeAssistant, mock_live_status, freezer: FrozenDateTimeFactory
) -> None:
"""Tests that the energy live coordinator handles fleet errors."""
await setup_platform(hass, [Platform.SENSOR])
mock_live_status.reset_mock()
mock_live_status.side_effect = Forbidden
freezer.tick(TESSIE_FLEET_API_SYNC_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_live_status.assert_called_once()
assert hass.states.get("sensor.energy_site_solar_power").state == STATE_UNAVAILABLE
async def test_coordinator_info_error(
hass: HomeAssistant, mock_site_info, freezer: FrozenDateTimeFactory
) -> None:
"""Tests that the energy info coordinator handles fleet errors."""
await setup_platform(hass, [Platform.SENSOR])
mock_site_info.reset_mock()
mock_site_info.side_effect = Forbidden
freezer.tick(TESSIE_FLEET_API_SYNC_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_site_info.assert_called_once()
assert (
hass.states.get("sensor.energy_site_vpp_backup_reserve").state
== STATE_UNAVAILABLE
)
async def test_coordinator_live_reauth(hass: HomeAssistant, mock_live_status) -> None:
"""Tests that the energy live coordinator handles auth errors."""
mock_live_status.side_effect = InvalidToken
entry = await setup_platform(hass, [Platform.SENSOR])
assert entry.state is ConfigEntryState.SETUP_ERROR
async def test_coordinator_info_reauth(hass: HomeAssistant, mock_site_info) -> None:
"""Tests that the energy info coordinator handles auth errors."""
mock_site_info.side_effect = InvalidToken
entry = await setup_platform(hass, [Platform.SENSOR])
assert entry.state is ConfigEntryState.SETUP_ERROR

View file

@ -1,5 +1,9 @@
"""Test the Tessie init.""" """Test the Tessie init."""
from unittest.mock import patch
from tesla_fleet_api.exceptions import TeslaFleetError
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -44,3 +48,13 @@ async def test_connection_failure(
mock_get_state_of_all_vehicles.side_effect = ERROR_CONNECTION mock_get_state_of_all_vehicles.side_effect = ERROR_CONNECTION
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_fleet_error(hass: HomeAssistant) -> None:
"""Test init with a fleet error."""
with patch(
"homeassistant.components.tessie.Tessie.products", side_effect=TeslaFleetError
):
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.SETUP_RETRY