Stream energy history totals in Teslemetry (#182529)

This commit is contained in:
Brett Adams
2026-09-25 16:21:12 +02:00
committed by GitHub
parent 94d308f631
commit cbc8db3163
14 changed files with 476 additions and 318 deletions
@@ -112,6 +112,7 @@ STREAM_TOPICS: Final = (
SseTopic.LIVE_STATUS,
SseTopic.SITE_INFO,
SseTopic.TARIFF_CONTENT_V2,
SseTopic.ENERGY_TOTALS,
)
@@ -703,10 +704,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
for vehicle in vehicles
if vehicle.poll
),
*(
energysite.info_coordinator.async_config_entry_first_refresh()
for energysite in energysites
),
*(_async_refresh_energy_site(energysite) for energysite in energysites),
)
# Setup energy devices with models, versions, and listeners
@@ -817,6 +815,8 @@ def create_handle_energy_stream_connection(
if energysite.live_coordinator is not None:
energysite.live_coordinator.async_set_update_error(error)
energysite.info_coordinator.async_set_update_error(error)
if energysite.history_coordinator is not None:
energysite.history_coordinator.async_set_update_error(error)
return handle_connection
@@ -890,14 +890,27 @@ async def _async_setup_energy_site(
)
history_coordinator = (
TeslemetryEnergyHistoryCoordinator(hass, entry, energy_site)
if powerwall
else None
TeslemetryEnergyHistoryCoordinator(hass, entry, site_id) if powerwall else None
)
if history_coordinator is not None:
entry.async_on_unload(
stream_energysite.listen_EnergyTotals(
history_coordinator.handle_stream_update
)
)
return live_coordinator, info_coordinator, history_coordinator
async def _async_refresh_energy_site(energysite: TeslemetryEnergyData) -> None:
"""Cold read the site info, then resolve the site timezone it carries."""
await energysite.info_coordinator.async_config_entry_first_refresh()
if energysite.history_coordinator is not None:
await energysite.history_coordinator.async_set_time_zone(
energysite.info_coordinator.data.get("installation_time_zone")
)
async def async_unload_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool:
"""Unload Teslemetry Config."""
unloaded = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -1,9 +1,10 @@
"""Teslemetry Data Coordinator."""
from datetime import timedelta
from dataclasses import asdict
from datetime import date, datetime, time, timedelta, tzinfo
from typing import TYPE_CHECKING, Any, override
from tesla_fleet_api.const import TeslaEnergyPeriod, VehicleDataEndpoint
from tesla_fleet_api.const import VehicleDataEndpoint
from tesla_fleet_api.exceptions import (
GatewayTimeout,
InsufficientCredits,
@@ -16,15 +17,17 @@ from tesla_fleet_api.exceptions import (
TeslaFleetError,
)
from tesla_fleet_api.teslemetry import EnergySite, Teslemetry, Vehicle
from teslemetry_stream.const import EnergyTotalsEvent
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
if TYPE_CHECKING:
from . import TeslemetryConfigEntry
from .const import DOMAIN, ENERGY_HISTORY_FIELDS, LOGGER
from .const import DOMAIN, LOGGER
from .helpers import async_update_device_sw_version, flatten
RETRY_EXCEPTIONS = (
@@ -45,9 +48,12 @@ def _get_retry_after(e: TeslaFleetError) -> float:
VEHICLE_INTERVAL = timedelta(seconds=60)
VEHICLE_WAIT = timedelta(minutes=15)
ENERGY_HISTORY_INTERVAL = timedelta(seconds=60)
METADATA_INTERVAL = timedelta(hours=1)
# Start of the day the energy history totals cover. Kept out of
# ENERGY_HISTORY_FIELDS, which is the list of keys that become sensors.
PERIOD_START = "_period_start"
# Keys within tariff_content_v2 kept as nested dicts rather than flattened,
# since entities and calendars read them as whole structures.
TARIFF_SKIP_KEYS = ["daily_charges", "demand_charges", "energy_charges", "seasons"]
@@ -330,7 +336,11 @@ class TeslemetryEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]])
class TeslemetryEnergyHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching energy site info from the Teslemetry API."""
"""Class to manage energy site history totals from the Teslemetry stream.
The server sums each day's periods itself and publishes cumulative
``energy_totals``; there is no REST poll and no local accumulation.
"""
config_entry: TeslemetryConfigEntry
@@ -338,54 +348,49 @@ class TeslemetryEnergyHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self,
hass: HomeAssistant,
config_entry: TeslemetryConfigEntry,
api: EnergySite,
site_id: int,
) -> None:
"""Initialize Teslemetry Energy Info coordinator."""
"""Initialize Teslemetry Energy History coordinator."""
super().__init__(
hass,
LOGGER,
config_entry=config_entry,
name=f"Teslemetry Energy History {api.energy_site_id}",
update_interval=ENERGY_HISTORY_INTERVAL,
name=f"Teslemetry Energy History {site_id}",
)
self.api = api
self.site_id = site_id
self.time_zone: tzinfo | None = None
self.data = {}
async def async_set_time_zone(self, name: str | None) -> None:
"""Resolve the site's installation timezone."""
if not name:
return
if (zone := await dt_util.async_get_time_zone(name)) is None:
LOGGER.warning(
"Unknown timezone %s for energy site %s, falling back to the Home Assistant timezone",
name,
self.site_id,
)
return
self.time_zone = zone
@override
async def _async_update_data(self) -> dict[str, Any]:
"""Update energy site data using Teslemetry API."""
try:
data = (await self.api.energy_history(TeslaEnergyPeriod.DAY))["response"]
except (InvalidToken, SubscriptionRequired, LoginRequired) as e:
raise ConfigEntryAuthFailed from e
except RETRY_EXCEPTIONS as e:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"message": e.message},
retry_after=_get_retry_after(e),
) from e
except TeslaFleetError as e:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"message": e.message},
) from e
"""Return the current totals; there is nothing to fetch.
if not data or not isinstance(data.get("time_series"), list):
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed_invalid_data",
)
Only reached through the generic entity update service, which must not
fail on a coordinator the stream alone feeds.
"""
return self.data
# Add all time periods together
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None)
for period in data.get("time_series", []):
for key in ENERGY_HISTORY_FIELDS:
if key in period:
if output[key] is None:
output[key] = period[key]
else:
output[key] += period[key]
return output
def handle_stream_update(self, event: EnergyTotalsEvent) -> None:
"""Handle an energy_totals document from the stream."""
data: dict[str, Any] = asdict(event.totals)
# The server finalises a day after the site's local midnight, so a late
# event must stay on the day it reports rather than the current one.
data[PERIOD_START] = datetime.combine(
date.fromisoformat(event.date),
time(),
tzinfo=self.time_zone or dt_util.get_default_time_zone(),
)
self.async_set_updated_data(data)
@@ -40,6 +40,7 @@ from homeassistant.util.variance import ignore_variance
from . import TeslemetryConfigEntry
from .const import ENERGY_HISTORY_FIELDS
from .coordinator import PERIOD_START
from .entity import (
TeslemetryEnergyHistoryEntity,
TeslemetryEnergyInfoEntity,
@@ -1653,7 +1654,7 @@ ENERGY_HISTORY_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = tuple(
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=2,
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
entity_registry_enabled_default=(
key.startswith("total") or key == "grid_energy_imported"
),
@@ -1966,6 +1967,7 @@ class TeslemetryEnergyHistorySensorEntity(TeslemetryEnergyHistoryEntity, SensorE
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_native_value = self._value
self._attr_last_reset = self.coordinator.data.get(PERIOD_START)
class TeslemetryCreditBalanceSensor(RestoreSensor):
@@ -1289,9 +1289,6 @@
"update_failed_insufficient_credits": {
"message": "Teslemetry account has insufficient command credits, pausing updates until credits are added"
},
"update_failed_invalid_data": {
"message": "Received invalid data from API"
},
"wake_up_failed": {
"message": "Failed to wake up vehicle: {message}"
},
+40 -11
View File
@@ -6,13 +6,15 @@ from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from teslemetry_stream.const import EnergyTotalsEvent
from teslemetry_stream.stream import recursive_match
from homeassistant.components.teslemetry.const import TOKEN_URL
from .const import (
COMMAND_OK,
ENERGY_HISTORY,
ENERGY_TOTALS,
ENERGY_TOTALS_DATE,
LIVE_STATUS,
METADATA,
METADATA_ENERGY,
@@ -142,16 +144,6 @@ def mock_site_info():
yield mock_live_status
@pytest.fixture(autouse=True)
def mock_energy_history():
"""Mock Teslemetry Energy Specific site_info method."""
with patch(
"tesla_fleet_api.tesla.energysite.EnergySite.energy_history",
return_value=ENERGY_HISTORY,
) as mock_live_status:
yield mock_live_status
@pytest.fixture(autouse=True)
def mock_stream_listen():
"""Mock Teslemetry Stream listen method."""
@@ -252,6 +244,43 @@ def mock_energy_info_stream() -> Generator[MagicMock]:
yield mock_listen
@pytest.fixture
def mock_energy_totals_stream() -> Generator[MagicMock]:
"""Capture the callback the integration registers for energy_totals events."""
with patch(
"teslemetry_stream.TeslemetryStreamEnergySite.listen_EnergyTotals",
) as mock_listen:
callbacks: list = []
def side_effect(callback):
callbacks.append(callback)
return MagicMock()
def send(
totals: dict[str, float | None] | None = None,
*,
date: str = ENERGY_TOTALS_DATE,
created_at: str = "2024-09-18T08:50:00.000Z",
is_cache: bool = False,
) -> None:
"""Deliver a wire event through the library's own parsing."""
event = EnergyTotalsEvent.from_dict(
{
"id": 123456,
"date": date,
"createdAt": created_at,
"isCache": is_cache,
"totals": ENERGY_TOTALS if totals is None else totals,
}
)
for callback in callbacks:
callback(event)
mock_listen.side_effect = side_effect
mock_listen.send = send
yield mock_listen
@pytest.fixture
def mock_energy_tariff_stream() -> Generator[MagicMock]:
"""Capture the callback the integration registers for tariff events."""
+33 -2
View File
@@ -29,8 +29,39 @@ SITE_INFO_WEEK_CROSSING = load_json_object_fixture(
"site_info_week_crossing.json", DOMAIN
)
SITE_INFO_MULTI_SEASON = load_json_object_fixture("site_info_multi_season.json", DOMAIN)
ENERGY_HISTORY = load_json_object_fixture("energy_history.json", DOMAIN)
ENERGY_HISTORY_EMPTY = load_json_object_fixture("energy_history_empty.json", DOMAIN)
# The site-local day the streamed energy_totals fixtures below belong to. The
# site timezone site_info.json declares is deliberately not the test machine's.
ENERGY_TOTALS_DATE = "2024-09-18"
# A valid, non-empty day: the server sums every period itself and sends 0 for a
# field that never appeared.
ENERGY_TOTALS = {
"solar_energy_exported": 724,
"generator_energy_exported": 0,
"grid_energy_imported": 0,
"grid_services_energy_imported": 0,
"grid_services_energy_exported": 0,
"grid_energy_exported_from_solar": 2,
"grid_energy_exported_from_generator": 0,
"grid_energy_exported_from_battery": 0,
"battery_energy_exported": 36,
"battery_energy_imported_from_grid": 0,
"battery_energy_imported_from_solar": 684,
"battery_energy_imported_from_generator": 0,
"consumer_energy_imported_from_grid": 0,
"consumer_energy_imported_from_solar": 38,
"consumer_energy_imported_from_battery": 36,
"consumer_energy_imported_from_generator": 0,
"total_home_usage": 74,
"total_battery_charge": 684,
"total_battery_discharge": 36,
"total_solar_generation": 724,
"total_grid_energy_exported": 2,
}
# An empty or malformed day: every field comes back null.
ENERGY_TOTALS_NULL = dict.fromkeys(ENERGY_TOTALS)
COMMAND_OK = {"response": {"result": True, "reason": ""}}
COMMAND_REASON = {"response": {"result": False, "reason": "already closed"}}
@@ -1,55 +0,0 @@
{
"response": {
"serial_number": "xxxxxx",
"period": "day",
"installation_time_zone": "Australia/Brisbane",
"time_series": [
{
"timestamp": "2024-09-18T00:00:00+10:00",
"solar_energy_exported": 0,
"generator_energy_exported": 0,
"grid_energy_imported": 0,
"grid_services_energy_imported": 0,
"grid_services_energy_exported": 0,
"grid_energy_exported_from_solar": 0,
"grid_energy_exported_from_generator": 0,
"grid_energy_exported_from_battery": 0,
"battery_energy_exported": 36,
"battery_energy_imported_from_grid": 0,
"battery_energy_imported_from_solar": 0,
"battery_energy_imported_from_generator": 0,
"consumer_energy_imported_from_grid": 0,
"consumer_energy_imported_from_solar": 0,
"consumer_energy_imported_from_battery": 36,
"consumer_energy_imported_from_generator": 0,
"raw_timestamp": "2024-09-18T00:00:00+10:00",
"total_home_usage": 36,
"total_battery_discharge": 36
},
{
"timestamp": "2024-09-18T08:45:00+10:00",
"solar_energy_exported": 724,
"generator_energy_exported": 0,
"grid_energy_imported": 0,
"grid_services_energy_imported": 0,
"grid_services_energy_exported": 0,
"grid_energy_exported_from_solar": 2,
"grid_energy_exported_from_generator": 0,
"grid_energy_exported_from_battery": 0,
"battery_energy_exported": 0,
"battery_energy_imported_from_grid": 0,
"battery_energy_imported_from_solar": 684,
"battery_energy_imported_from_generator": 0,
"consumer_energy_imported_from_grid": 0,
"consumer_energy_imported_from_solar": 38,
"consumer_energy_imported_from_battery": 0,
"consumer_energy_imported_from_generator": 0,
"raw_timestamp": "2024-09-18T08:45:00+10:00",
"total_home_usage": 38,
"total_solar_generation": 724,
"total_battery_charge": 684,
"total_grid_energy_exported": 2
}
]
}
}
@@ -1,8 +0,0 @@
{
"response": {
"serial_number": "xxxxxx",
"period": "day",
"installation_time_zone": "Australia/Brisbane",
"time_series": null
}
}
@@ -119,7 +119,7 @@
},
"nameplate_power": 15000,
"nameplate_energy": 40500,
"installation_time_zone": "",
"installation_time_zone": "Australia/Brisbane",
"max_site_meter_power_ac": 1000000000,
"min_site_meter_power_ac": -1000000000,
"tariff_content_v2": {
@@ -4,6 +4,7 @@
'energysites': list([
dict({
'history': dict({
'_period_start': '2024-09-18T00:00:00+10:00',
'battery_energy_exported': 36,
'battery_energy_imported_from_generator': 0,
'battery_energy_imported_from_grid': 0,
@@ -114,7 +115,7 @@
'default_real_mode': 'self_consumption',
'id': '1233-abcd',
'installation_date': '**REDACTED**',
'installation_time_zone': '',
'installation_time_zone': 'Australia/Brisbane',
'max_site_meter_power_ac': 1000000000,
'min_site_meter_power_ac': -1000000000,
'nameplate_energy': 40500,
@@ -6,7 +6,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -49,7 +49,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery charged',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -57,7 +58,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.684',
})
# ---
# name: test_sensors[sensor.energy_site_battery_charged-statealt]
@@ -65,7 +66,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery charged',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -83,7 +85,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -126,7 +128,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery discharged',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -134,7 +137,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.036',
})
# ---
# name: test_sensors[sensor.energy_site_battery_discharged-statealt]
@@ -142,7 +145,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery discharged',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -160,7 +164,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -203,7 +207,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -211,7 +216,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.036',
})
# ---
# name: test_sensors[sensor.energy_site_battery_exported-statealt]
@@ -219,7 +224,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -237,7 +243,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -280,7 +286,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery imported from generator',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -288,7 +295,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_battery_imported_from_generator-statealt]
@@ -296,7 +303,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery imported from generator',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -314,7 +322,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -357,7 +365,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery imported from grid',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -365,7 +374,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_battery_imported_from_grid-statealt]
@@ -373,7 +382,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery imported from grid',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -391,7 +401,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -434,7 +444,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery imported from solar',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -442,7 +453,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.684',
})
# ---
# name: test_sensors[sensor.energy_site_battery_imported_from_solar-statealt]
@@ -450,7 +461,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Battery imported from solar',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -545,7 +557,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -588,7 +600,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Consumer imported from battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -596,7 +609,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.036',
})
# ---
# name: test_sensors[sensor.energy_site_consumer_imported_from_battery-statealt]
@@ -604,7 +617,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Consumer imported from battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -622,7 +636,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -665,7 +679,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Consumer imported from generator',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -673,7 +688,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_consumer_imported_from_generator-statealt]
@@ -681,7 +696,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Consumer imported from generator',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -699,7 +715,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -742,7 +758,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Consumer imported from grid',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -750,7 +767,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_consumer_imported_from_grid-statealt]
@@ -758,7 +775,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Consumer imported from grid',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -776,7 +794,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -819,7 +837,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Consumer imported from solar',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -827,7 +846,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.038',
})
# ---
# name: test_sensors[sensor.energy_site_consumer_imported_from_solar-statealt]
@@ -835,7 +854,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Consumer imported from solar',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -930,7 +950,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -973,7 +993,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Generator exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -981,7 +1002,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_generator_exported-statealt]
@@ -989,7 +1010,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Generator exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1084,7 +1106,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1127,7 +1149,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1135,7 +1158,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.002',
})
# ---
# name: test_sensors[sensor.energy_site_grid_exported-statealt]
@@ -1143,7 +1166,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1161,7 +1185,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1204,7 +1228,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid exported from battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1212,7 +1237,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_grid_exported_from_battery-statealt]
@@ -1220,7 +1245,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid exported from battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1238,7 +1264,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1281,7 +1307,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid exported from generator',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1289,7 +1316,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_grid_exported_from_generator-statealt]
@@ -1297,7 +1324,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid exported from generator',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1315,7 +1343,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1358,7 +1386,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid exported from solar',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1366,7 +1395,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.002',
})
# ---
# name: test_sensors[sensor.energy_site_grid_exported_from_solar-statealt]
@@ -1374,7 +1403,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid exported from solar',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1392,7 +1422,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1435,7 +1465,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid imported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1443,7 +1474,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_grid_imported-statealt]
@@ -1451,7 +1482,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid imported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1546,7 +1578,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1589,7 +1621,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid services exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1597,7 +1630,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_grid_services_exported-statealt]
@@ -1605,7 +1638,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid services exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1623,7 +1657,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1666,7 +1700,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid services imported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1674,7 +1709,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.0',
})
# ---
# name: test_sensors[sensor.energy_site_grid_services_imported-statealt]
@@ -1682,7 +1717,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Grid services imported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1777,7 +1813,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1820,7 +1856,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Home usage',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1828,7 +1865,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.074',
})
# ---
# name: test_sensors[sensor.energy_site_home_usage-statealt]
@@ -1836,7 +1873,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Home usage',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -2092,7 +2130,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -2135,7 +2173,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Solar exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -2143,7 +2182,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.724',
})
# ---
# name: test_sensors[sensor.energy_site_solar_exported-statealt]
@@ -2151,7 +2190,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Solar exported',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -2169,7 +2209,7 @@
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -2212,7 +2252,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Solar generated',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -2220,7 +2261,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
'state': '0.724',
})
# ---
# name: test_sensors[sensor.energy_site_solar_generated-statealt]
@@ -2228,7 +2269,8 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'energy',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy Site Solar generated',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<SensorEntityStateAttribute.LAST_RESET: 'last_reset'>: '2024-09-18T00:00:00+10:00',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL: 'total'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
@@ -1,6 +1,6 @@
"""Test the Telemetry Diagnostics."""
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock
from freezegun.api import FrozenDateTimeFactory
from syrupy.assertion import SnapshotAssertion
@@ -21,6 +21,7 @@ async def test_diagnostics(
snapshot: SnapshotAssertion,
freezer: FrozenDateTimeFactory,
mock_legacy: AsyncMock,
mock_energy_totals_stream: MagicMock,
) -> None:
"""Test diagnostics."""
@@ -31,5 +32,8 @@ async def test_diagnostics(
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_energy_totals_stream.send()
await hass.async_block_till_done()
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry)
assert diag == snapshot
+19 -85
View File
@@ -52,7 +52,6 @@ from homeassistant.components.teslemetry.const import (
# Coordinator constants
from homeassistant.components.teslemetry.coordinator import (
ENERGY_HISTORY_INTERVAL,
INSUFFICIENT_CREDITS_RETRY_AFTER,
METADATA_INTERVAL,
VEHICLE_INTERVAL,
@@ -89,7 +88,6 @@ from homeassistant.helpers.update_coordinator import UpdateFailed
from . import mock_config_entry, setup_platform
from .const import (
CONFIG_V1,
ENERGY_HISTORY,
LIVE_STATUS,
METADATA,
METADATA_NOSCOPE,
@@ -652,54 +650,6 @@ async def test_live_status_coordinator_retry_exceptions(
assert entry.state is ConfigEntryState.LOADED
@pytest.mark.parametrize(("exception", "expected_retry_after"), RETRY_EXCEPTIONS)
async def test_energy_history_coordinator_retry_exceptions(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_energy_history: AsyncMock,
exception: TeslaFleetError,
expected_retry_after: float,
) -> None:
"""Test energy history coordinator raises UpdateFailed with retry_after."""
call_count = 0
def energy_history_side_effect(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
raise exception # First call raises exception
return ENERGY_HISTORY # Subsequent calls succeed
mock_energy_history.side_effect = energy_history_side_effect
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.LOADED
# Energy history doesn't have first_refresh during setup
assert call_count == 0
# Trigger first coordinator refresh - this will raise the exception
freezer.tick(ENERGY_HISTORY_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# API was called exactly once (no manual retry loop)
assert call_count == 1
# Entry stays loaded - UpdateFailed with retry_after doesn't break the entry
assert entry.state is ConfigEntryState.LOADED
# The coordinator staggers its scheduling deliberately, so these ticks
# bracket retry_after with a margin either side rather than sitting on it.
freezer.tick(timedelta(seconds=expected_retry_after - 2))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert call_count == 1
freezer.tick(timedelta(seconds=3))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert call_count == 2
async def test_live_status_auth_error(
hass: HomeAssistant,
) -> None:
@@ -1093,33 +1043,6 @@ async def test_live_status_coordinator_refresh_error(
assert entry.state is ConfigEntryState.LOADED
@pytest.mark.parametrize(
"side_effect",
[
[InvalidToken],
[TeslaFleetError],
[ENERGY_HISTORY, {"response": {}}],
],
)
async def test_energy_history_coordinator_refresh_errors(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_energy_history: AsyncMock,
side_effect: list,
) -> None:
"""Test energy history coordinator handles errors during refresh."""
mock_energy_history.side_effect = side_effect
entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.LOADED
freezer.tick(ENERGY_HISTORY_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
async def test_dynamic_device_discovery_triggers_reload(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
@@ -1826,6 +1749,7 @@ def test_stream_topic_allowlist() -> None:
"live_status",
"site_info",
"tariff_content_v2",
"energy_totals",
]
@@ -1835,18 +1759,22 @@ async def test_energy_stream_no_recurring_rest_polling(
mock_live_status: AsyncMock,
mock_site_info: AsyncMock,
) -> None:
"""The live/info REST cold reads happen once and do not recur."""
await setup_platform(hass, [Platform.SENSOR])
assert mock_live_status.call_count == 1
assert mock_site_info.call_count == 1
"""The live/info REST cold reads happen once, and history never reads at all."""
with patch(
"tesla_fleet_api.tesla.energysite.EnergySite.energy_history"
) as mock_energy_history:
await setup_platform(hass, [Platform.SENSOR])
assert mock_live_status.call_count == 1
assert mock_site_info.call_count == 1
# Advancing well past the old 30-second poll intervals triggers no REST reads.
freezer.tick(ENERGY_HISTORY_INTERVAL * 2)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Advancing well past the old poll intervals triggers no REST reads.
freezer.tick(timedelta(minutes=5))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_live_status.call_count == 1
assert mock_site_info.call_count == 1
mock_energy_history.assert_not_called()
async def test_energy_stream_unload_unsubscribes_and_closes_stream(
@@ -1856,6 +1784,7 @@ async def test_energy_stream_unload_unsubscribes_and_closes_stream(
live_unsub = MagicMock()
info_unsub = MagicMock()
tariff_unsub = MagicMock()
totals_unsub = MagicMock()
with (
patch(
@@ -1870,6 +1799,10 @@ async def test_energy_stream_unload_unsubscribes_and_closes_stream(
"teslemetry_stream.TeslemetryStreamEnergySite.listen_TariffContentV2",
return_value=tariff_unsub,
),
patch(
"teslemetry_stream.TeslemetryStreamEnergySite.listen_EnergyTotals",
return_value=totals_unsub,
),
patch("teslemetry_stream.TeslemetryStream.close") as mock_close,
):
entry = await setup_platform(hass, [Platform.SENSOR])
@@ -1881,6 +1814,7 @@ async def test_energy_stream_unload_unsubscribes_and_closes_stream(
live_unsub.assert_called_once()
info_unsub.assert_called_once()
tariff_unsub.assert_called_once()
totals_unsub.assert_called_once()
mock_close.assert_called_once()
+178 -15
View File
@@ -1,17 +1,24 @@
"""Test the Teslemetry sensor platform."""
from copy import deepcopy
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from teslemetry_stream import Signal
from teslemetry_stream.const import EnergyHistoryTotals
from homeassistant.components.teslemetry.const import DOMAIN
from homeassistant.components.homeassistant import (
DOMAIN as HOMEASSISTANT_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
from homeassistant.components.teslemetry.const import DOMAIN, ENERGY_HISTORY_FIELDS
from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
EntityCategory,
@@ -19,13 +26,16 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from . import assert_entities, assert_entities_alt, setup_platform
from .const import (
ENERGY_HISTORY_EMPTY,
ENERGY_TOTALS,
ENERGY_TOTALS_NULL,
LIVE_STATUS,
METADATA,
PRODUCTS,
SITE_INFO,
VEHICLE_DATA_ALT,
)
@@ -34,6 +44,12 @@ from tests.common import async_fire_time_changed
# VIN used across the Teslemetry test fixtures.
VEHICLE_VIN = "LRW3F7EK4NC700000"
ENERGY_HISTORY_ENTITY = "sensor.energy_site_battery_discharged"
# Midnight in Australia/Brisbane, the timezone site_info.json declares. Home
# Assistant runs on US/Pacific in tests, so a last_reset derived from its clock
# or from the event's created_at cannot produce this.
SITE_MIDNIGHT = "2024-09-18T00:00:00+10:00"
def _products_with_driver_assist(driver_assist: str) -> dict:
"""Return a products response with the vehicle's driver-assist capability set."""
@@ -103,6 +119,7 @@ async def test_sensors(
freezer: FrozenDateTimeFactory,
mock_vehicle_data: AsyncMock,
mock_legacy: AsyncMock,
mock_energy_totals_stream: MagicMock,
) -> None:
"""Tests that the sensor entities with the legacy polling are correct."""
@@ -112,6 +129,9 @@ async def test_sensors(
entry = await setup_platform(hass, [Platform.SENSOR])
mock_energy_totals_stream.send()
await hass.async_block_till_done()
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
# Coordinator refresh
@@ -491,26 +511,169 @@ async def test_streaming_enum_none_clears_state(
assert hass.states.get(entity_id).state == STATE_UNKNOWN
async def test_energy_history_no_time_series(
def test_energy_history_fields_match_stream_totals() -> None:
"""The sensor key list mirrors the totals the stream event carries."""
assert list(EnergyHistoryTotals.__dataclass_fields__) == ENERGY_HISTORY_FIELDS
async def test_energy_history_stream_updates(
hass: HomeAssistant,
mock_energy_totals_stream: MagicMock,
) -> None:
"""A streamed energy_totals event drives the history sensors and their last_reset."""
await setup_platform(hass, [Platform.SENSOR])
# Nothing has streamed yet, so the sensors are available but hold no value.
assert (state := hass.states.get(ENERGY_HISTORY_ENTITY))
assert state.state == STATE_UNKNOWN
assert "last_reset" not in state.attributes
mock_energy_totals_stream.send()
await hass.async_block_till_done()
assert (state := hass.states.get(ENERGY_HISTORY_ENTITY))
assert state.state == "0.036"
assert state.attributes["state_class"] == "total"
assert state.attributes["last_reset"] == SITE_MIDNIGHT
assert hass.states.get("sensor.energy_site_solar_generated").state == "0.724"
async def test_energy_history_late_event_keeps_previous_day(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_energy_history: AsyncMock,
mock_energy_totals_stream: MagicMock,
) -> None:
"""Test energy history coordinator when time_series is not a list."""
# Mock energy history to return data without time_series as a list
"""A late event finalising the previous day keeps that day's last_reset.
entry = await setup_platform(hass, [Platform.SENSOR])
assert entry.state is ConfigEntryState.LOADED
The server finalises a day after the site's local midnight. Deriving
last_reset from the current day instead would start a second cycle and bank
the whole of the previous day again.
"""
# 00:05 on the 19th in Brisbane, so "today" at the site is no longer the 18th.
freezer.move_to("2024-09-18T14:05:00+00:00")
await setup_platform(hass, [Platform.SENSOR])
entity_id = "sensor.energy_site_battery_discharged"
state = hass.states.get(entity_id)
assert state.state == STATE_UNKNOWN
mock_energy_totals_stream.send(date="2024-09-18")
await hass.async_block_till_done()
mock_energy_history.return_value = ENERGY_HISTORY_EMPTY
assert (state := hass.states.get(ENERGY_HISTORY_ENTITY))
assert state.attributes["last_reset"] == SITE_MIDNIGHT
freezer.tick(VEHICLE_INTERVAL)
async def test_energy_history_cache_snapshot_populates_sensors(
hass: HomeAssistant,
mock_energy_totals_stream: MagicMock,
) -> None:
"""The connect-time snapshot is applied exactly like a live update."""
await setup_platform(hass, [Platform.SENSOR])
mock_energy_totals_stream.send(is_cache=True)
await hass.async_block_till_done()
assert (state := hass.states.get(ENERGY_HISTORY_ENTITY))
assert state.state == "0.036"
assert state.attributes["last_reset"] == SITE_MIDNIGHT
@pytest.mark.parametrize(
("totals", "expected"),
[
pytest.param(ENERGY_TOTALS, "0.0", id="absent_field_sent_as_zero"),
pytest.param(ENERGY_TOTALS_NULL, STATE_UNKNOWN, id="empty_day_sent_as_null"),
],
)
async def test_energy_history_zero_and_null_totals(
hass: HomeAssistant,
mock_energy_totals_stream: MagicMock,
totals: dict[str, float | None],
expected: str,
) -> None:
"""A zero total reads as zero and a null total reads as unknown, never unavailable."""
await setup_platform(hass, [Platform.SENSOR])
mock_energy_totals_stream.send(totals)
await hass.async_block_till_done()
assert (state := hass.states.get("sensor.energy_site_grid_imported"))
assert state.state == expected
assert state.attributes["last_reset"] == SITE_MIDNIGHT
@pytest.mark.parametrize(
"installation_time_zone",
[pytest.param("", id="not_provided"), pytest.param("Mars/Olympus", id="unknown")],
)
async def test_energy_history_time_zone_fallback(
hass: HomeAssistant,
mock_site_info: AsyncMock,
mock_energy_totals_stream: MagicMock,
installation_time_zone: str,
) -> None:
"""Without a usable site timezone, last_reset falls back to Home Assistant's."""
site_info = deepcopy(SITE_INFO)
site_info["response"]["installation_time_zone"] = installation_time_zone
mock_site_info.side_effect = lambda: deepcopy(site_info)
await setup_platform(hass, [Platform.SENSOR])
mock_energy_totals_stream.send()
await hass.async_block_till_done()
assert (state := hass.states.get(ENERGY_HISTORY_ENTITY))
# Home Assistant runs on US/Pacific in tests, which was in PDT on this date.
assert state.attributes["last_reset"] == "2024-09-18T00:00:00-07:00"
async def test_energy_history_update_entity_service_is_a_noop(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
mock_energy_totals_stream: MagicMock,
) -> None:
"""The generic update service keeps the streamed totals instead of failing.
The coordinator has nothing to fetch, so the service must not leave the
sensors unavailable on a stream that is perfectly healthy.
"""
await setup_platform(hass, [Platform.SENSOR])
await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
mock_energy_totals_stream.send()
await hass.async_block_till_done()
await hass.services.async_call(
HOMEASSISTANT_DOMAIN,
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: ENERGY_HISTORY_ENTITY},
blocking=True,
)
# The coordinator debounces refresh requests, so let the deferred one land.
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_UNAVAILABLE
assert "NotImplementedError" not in caplog.text
assert (state := hass.states.get(ENERGY_HISTORY_ENTITY))
assert state.state == "0.036"
assert state.attributes["last_reset"] == SITE_MIDNIGHT
async def test_energy_history_unavailable_while_stream_disconnected(
hass: HomeAssistant,
mock_add_connection_listener: MagicMock,
mock_energy_totals_stream: MagicMock,
) -> None:
"""A dropped stream makes the history sensors unavailable until a snapshot returns."""
await setup_platform(hass, [Platform.SENSOR])
mock_energy_totals_stream.send()
await hass.async_block_till_done()
assert hass.states.get(ENERGY_HISTORY_ENTITY).state == "0.036"
mock_add_connection_listener.send(False)
await hass.async_block_till_done()
assert hass.states.get(ENERGY_HISTORY_ENTITY).state == STATE_UNAVAILABLE
mock_energy_totals_stream.send(is_cache=True)
await hass.async_block_till_done()
assert hass.states.get(ENERGY_HISTORY_ENTITY).state == "0.036"