Add Energy History to Tesla Fleet (#126878)

Co-authored-by: Brett Adams <Bre77@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: JEMcats <hurst-status09@icloud.com>
Co-authored-by: JEMcats <jakobmattheis@icloud.com>
This commit is contained in:
Brett Adams 2025-01-17 19:34:35 +10:00 committed by GitHub
parent b4f4b06f29
commit 689d7d3cd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1919 additions and 4 deletions

View File

@ -36,6 +36,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from .const import DOMAIN, LOGGER, MODELS from .const import DOMAIN, LOGGER, MODELS
from .coordinator import ( from .coordinator import (
TeslaFleetEnergySiteHistoryCoordinator,
TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteInfoCoordinator,
TeslaFleetEnergySiteLiveCoordinator, TeslaFleetEnergySiteLiveCoordinator,
TeslaFleetVehicleDataCoordinator, TeslaFleetVehicleDataCoordinator,
@ -176,9 +177,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
api = EnergySpecific(tesla.energy, site_id) api = EnergySpecific(tesla.energy, site_id)
live_coordinator = TeslaFleetEnergySiteLiveCoordinator(hass, api) live_coordinator = TeslaFleetEnergySiteLiveCoordinator(hass, api)
history_coordinator = TeslaFleetEnergySiteHistoryCoordinator(hass, api)
info_coordinator = TeslaFleetEnergySiteInfoCoordinator(hass, api, product) info_coordinator = TeslaFleetEnergySiteInfoCoordinator(hass, api, product)
await live_coordinator.async_config_entry_first_refresh() await live_coordinator.async_config_entry_first_refresh()
await history_coordinator.async_config_entry_first_refresh()
await info_coordinator.async_config_entry_first_refresh() await info_coordinator.async_config_entry_first_refresh()
# Create energy site model # Create energy site model
@ -211,6 +214,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
TeslaFleetEnergyData( TeslaFleetEnergyData(
api=api, api=api,
live_coordinator=live_coordinator, live_coordinator=live_coordinator,
history_coordinator=history_coordinator,
info_coordinator=info_coordinator, info_coordinator=info_coordinator,
id=site_id, id=site_id,
device=device, device=device,

View File

@ -37,6 +37,30 @@ MODELS = {
"T": "Tesla Semi", "T": "Tesla Semi",
} }
ENERGY_HISTORY_FIELDS = [
"solar_energy_exported",
"generator_energy_exported",
"grid_energy_imported",
"grid_services_energy_imported",
"grid_services_energy_exported",
"grid_energy_exported_from_solar",
"grid_energy_exported_from_generator",
"grid_energy_exported_from_battery",
"battery_energy_exported",
"battery_energy_imported_from_grid",
"battery_energy_imported_from_solar",
"battery_energy_imported_from_generator",
"consumer_energy_imported_from_grid",
"consumer_energy_imported_from_solar",
"consumer_energy_imported_from_battery",
"consumer_energy_imported_from_generator",
"total_home_usage",
"total_battery_charge",
"total_battery_discharge",
"total_solar_generation",
"total_grid_energy_exported",
]
class TeslaFleetState(StrEnum): class TeslaFleetState(StrEnum):
"""Teslemetry Vehicle States.""" """Teslemetry Vehicle States."""

View File

@ -1,10 +1,12 @@
"""Tesla Fleet Data Coordinator.""" """Tesla Fleet Data Coordinator."""
from datetime import datetime, timedelta from datetime import datetime, timedelta
from random import randint
from time import time
from typing import Any from typing import Any
from tesla_fleet_api import EnergySpecific, VehicleSpecific from tesla_fleet_api import EnergySpecific, VehicleSpecific
from tesla_fleet_api.const import VehicleDataEndpoint from tesla_fleet_api.const import TeslaEnergyPeriod, VehicleDataEndpoint
from tesla_fleet_api.exceptions import ( from tesla_fleet_api.exceptions import (
InvalidToken, InvalidToken,
LoginRequired, LoginRequired,
@ -19,7 +21,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER, TeslaFleetState from .const import ENERGY_HISTORY_FIELDS, LOGGER, TeslaFleetState
VEHICLE_INTERVAL_SECONDS = 300 VEHICLE_INTERVAL_SECONDS = 300
VEHICLE_INTERVAL = timedelta(seconds=VEHICLE_INTERVAL_SECONDS) VEHICLE_INTERVAL = timedelta(seconds=VEHICLE_INTERVAL_SECONDS)
@ -27,6 +29,7 @@ VEHICLE_WAIT = timedelta(minutes=15)
ENERGY_INTERVAL_SECONDS = 60 ENERGY_INTERVAL_SECONDS = 60
ENERGY_INTERVAL = timedelta(seconds=ENERGY_INTERVAL_SECONDS) ENERGY_INTERVAL = timedelta(seconds=ENERGY_INTERVAL_SECONDS)
ENERGY_HISTORY_INTERVAL = timedelta(minutes=5)
ENDPOINTS = [ ENDPOINTS = [
VehicleDataEndpoint.CHARGE_STATE, VehicleDataEndpoint.CHARGE_STATE,
@ -182,6 +185,61 @@ class TeslaFleetEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]])
return data return data
class TeslaFleetEnergySiteHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching energy site history import and export from the Tesla Fleet API."""
def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None:
"""Initialize Tesla Fleet Energy Site History coordinator."""
super().__init__(
hass,
LOGGER,
name=f"Tesla Fleet Energy History {api.energy_site_id}",
update_interval=timedelta(seconds=300),
)
self.api = api
self.data = {}
self.updated_once = False
async def async_config_entry_first_refresh(self) -> None:
"""Set up the data coordinator."""
await super().async_config_entry_first_refresh()
# Calculate seconds until next 5 minute period plus a random delay
delta = randint(310, 330) - (int(time()) % 300)
self.logger.debug("Scheduling next %s refresh in %s seconds", self.name, delta)
self.update_interval = timedelta(seconds=delta)
self._schedule_refresh()
self.update_interval = ENERGY_HISTORY_INTERVAL
async def _async_update_data(self) -> dict[str, Any]:
"""Update energy site history data using Tesla Fleet API."""
try:
data = (await self.api.energy_history(TeslaEnergyPeriod.DAY))["response"]
except RateLimited as e:
LOGGER.warning(
"%s rate limited, will retry in %s seconds",
self.name,
e.data.get("after"),
)
if "after" in e.data:
self.update_interval = timedelta(seconds=int(e.data["after"]))
return self.data
except (InvalidToken, OAuthExpired, LoginRequired) as e:
raise ConfigEntryAuthFailed from e
except TeslaFleetError as e:
raise UpdateFailed(e.message) from e
self.updated_once = True
# Add all time periods together
output = {key: 0 for key in ENERGY_HISTORY_FIELDS}
for period in data.get("time_series", []):
for key in ENERGY_HISTORY_FIELDS:
output[key] += period.get(key, 0)
return output
class TeslaFleetEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]]): class TeslaFleetEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching energy site info from the TeslaFleet API.""" """Class to manage fetching energy site info from the TeslaFleet API."""

View File

@ -12,6 +12,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN from .const import DOMAIN
from .coordinator import ( from .coordinator import (
TeslaFleetEnergySiteHistoryCoordinator,
TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteInfoCoordinator,
TeslaFleetEnergySiteLiveCoordinator, TeslaFleetEnergySiteLiveCoordinator,
TeslaFleetVehicleDataCoordinator, TeslaFleetVehicleDataCoordinator,
@ -24,6 +25,7 @@ class TeslaFleetEntity(
CoordinatorEntity[ CoordinatorEntity[
TeslaFleetVehicleDataCoordinator TeslaFleetVehicleDataCoordinator
| TeslaFleetEnergySiteLiveCoordinator | TeslaFleetEnergySiteLiveCoordinator
| TeslaFleetEnergySiteHistoryCoordinator
| TeslaFleetEnergySiteInfoCoordinator | TeslaFleetEnergySiteInfoCoordinator
] ]
): ):
@ -37,6 +39,7 @@ class TeslaFleetEntity(
self, self,
coordinator: TeslaFleetVehicleDataCoordinator coordinator: TeslaFleetVehicleDataCoordinator
| TeslaFleetEnergySiteLiveCoordinator | TeslaFleetEnergySiteLiveCoordinator
| TeslaFleetEnergySiteHistoryCoordinator
| TeslaFleetEnergySiteInfoCoordinator, | TeslaFleetEnergySiteInfoCoordinator,
api: VehicleSpecific | EnergySpecific, api: VehicleSpecific | EnergySpecific,
key: str, key: str,
@ -139,6 +142,21 @@ class TeslaFleetEnergyLiveEntity(TeslaFleetEntity):
super().__init__(data.live_coordinator, data.api, key) super().__init__(data.live_coordinator, data.api, key)
class TeslaFleetEnergyHistoryEntity(TeslaFleetEntity):
"""Parent class for TeslaFleet Energy Site History entities."""
def __init__(
self,
data: TeslaFleetEnergyData,
key: str,
) -> None:
"""Initialize common aspects of a Tesla Fleet Energy Site History entity."""
self._attr_unique_id = f"{data.id}-{key}"
self._attr_device_info = data.device
super().__init__(data.history_coordinator, data.api, key)
class TeslaFleetEnergyInfoEntity(TeslaFleetEntity): class TeslaFleetEnergyInfoEntity(TeslaFleetEntity):
"""Parent class for TeslaFleet Energy Site Info entities.""" """Parent class for TeslaFleet Energy Site Info entities."""

View File

@ -232,6 +232,69 @@
"island_status_unknown": "mdi:help-circle", "island_status_unknown": "mdi:help-circle",
"off_grid_intentional": "mdi:account-cancel" "off_grid_intentional": "mdi:account-cancel"
} }
},
"total_home_usage": {
"default": "mdi:home-lightning-bolt"
},
"total_battery_charge": {
"default": "mdi:battery-arrow-up"
},
"total_battery_discharge": {
"default": "mdi:battery-arrow-down"
},
"total_solar_production": {
"default": "mdi:solar-power-variant"
},
"grid_energy_imported": {
"default": "mdi:transmission-tower-import"
},
"total_grid_energy_exported": {
"default": "mdi:transmission-tower-export"
},
"solar_energy_exported": {
"default": "mdi:solar-power-variant"
},
"generator_energy_exported": {
"default": "mdi:generator-stationary"
},
"grid_services_energy_imported": {
"default": "mdi:transmission-tower-import"
},
"grid_services_energy_exported": {
"default": "mdi:transmission-tower-export"
},
"grid_energy_exported_from_solar": {
"default": "mdi:solar-power"
},
"grid_energy_exported_from_generator": {
"default": "mdi:generator-stationary"
},
"grid_energy_exported_from_battery": {
"default": "mdi:battery-arrow-down"
},
"battery_energy_exported": {
"default": "mdi:battery-arrow-down"
},
"battery_energy_imported_from_grid": {
"default": "mdi:transmission-tower-import"
},
"battery_energy_imported_from_solar": {
"default": "mdi:solar-power"
},
"battery_energy_imported_from_generator": {
"default": "mdi:generator-stationary"
},
"consumer_energy_imported_from_grid": {
"default": "mdi:transmission-tower-import"
},
"consumer_energy_imported_from_solar": {
"default": "mdi:solar-power"
},
"consumer_energy_imported_from_battery": {
"default": "mdi:home-battery"
},
"consumer_energy_imported_from_generator": {
"default": "mdi:generator-stationary"
} }
}, },
"switch": { "switch": {

View File

@ -11,6 +11,7 @@ from tesla_fleet_api.const import Scope
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from .coordinator import ( from .coordinator import (
TeslaFleetEnergySiteHistoryCoordinator,
TeslaFleetEnergySiteInfoCoordinator, TeslaFleetEnergySiteInfoCoordinator,
TeslaFleetEnergySiteLiveCoordinator, TeslaFleetEnergySiteLiveCoordinator,
TeslaFleetVehicleDataCoordinator, TeslaFleetVehicleDataCoordinator,
@ -44,6 +45,7 @@ class TeslaFleetEnergyData:
api: EnergySpecific api: EnergySpecific
live_coordinator: TeslaFleetEnergySiteLiveCoordinator live_coordinator: TeslaFleetEnergySiteLiveCoordinator
history_coordinator: TeslaFleetEnergySiteHistoryCoordinator
info_coordinator: TeslaFleetEnergySiteInfoCoordinator info_coordinator: TeslaFleetEnergySiteInfoCoordinator
id: int id: int
device: DeviceInfo device: DeviceInfo

View File

@ -35,8 +35,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 TeslaFleetConfigEntry from . import TeslaFleetConfigEntry
from .const import TeslaFleetState from .const import ENERGY_HISTORY_FIELDS, TeslaFleetState
from .entity import ( from .entity import (
TeslaFleetEnergyHistoryEntity,
TeslaFleetEnergyInfoEntity, TeslaFleetEnergyInfoEntity,
TeslaFleetEnergyLiveEntity, TeslaFleetEnergyLiveEntity,
TeslaFleetVehicleEntity, TeslaFleetVehicleEntity,
@ -415,6 +416,21 @@ WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
), ),
) )
ENERGY_HISTORY_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = tuple(
SensorEntityDescription(
key=key,
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=2,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_registry_enabled_default=(
key.startswith("total") or key == "grid_energy_imported"
),
)
for key in ENERGY_HISTORY_FIELDS
)
ENERGY_INFO_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( ENERGY_INFO_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
key="vpp_backup_reserve_percent", key="vpp_backup_reserve_percent",
@ -450,6 +466,13 @@ async def async_setup_entry(
for description in ENERGY_LIVE_DESCRIPTIONS for description in ENERGY_LIVE_DESCRIPTIONS
if description.key in energysite.live_coordinator.data if description.key in energysite.live_coordinator.data
), ),
( # Add energy site history
TeslaFleetEnergyHistorySensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites
for description in ENERGY_HISTORY_DESCRIPTIONS
if energysite.info_coordinator.data.get("components_battery")
or energysite.info_coordinator.data.get("components_solar")
),
( # Add wall connectors ( # Add wall connectors
TeslaFleetWallConnectorSensorEntity(energysite, wc["din"], description) TeslaFleetWallConnectorSensorEntity(energysite, wc["din"], description)
for energysite in entry.runtime_data.energysites for energysite in entry.runtime_data.energysites
@ -540,7 +563,25 @@ class TeslaFleetEnergyLiveSensorEntity(TeslaFleetEnergyLiveEntity, SensorEntity)
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor.""" """Update the attributes of the sensor."""
self._attr_available = not self.is_none self._attr_native_value = self._value
class TeslaFleetEnergyHistorySensorEntity(TeslaFleetEnergyHistoryEntity, SensorEntity):
"""Base class for Tesla Fleet energy site metric sensors."""
entity_description: SensorEntityDescription
def __init__(
self,
data: TeslaFleetEnergyData,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
super().__init__(data, description.key)
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_native_value = self._value self._attr_native_value = self._value

View File

@ -424,6 +424,9 @@
"off_grid_intentional": "Disconnected intentionally" "off_grid_intentional": "Disconnected intentionally"
} }
}, },
"storm_mode_active": {
"name": "Storm Watch active"
},
"vehicle_state_tpms_pressure_fl": { "vehicle_state_tpms_pressure_fl": {
"name": "Tire pressure front left" "name": "Tire pressure front left"
}, },
@ -453,6 +456,69 @@
}, },
"wall_connector_state": { "wall_connector_state": {
"name": "State code" "name": "State code"
},
"solar_energy_exported": {
"name": "Solar exported"
},
"generator_energy_exported": {
"name": "Generator exported"
},
"grid_energy_imported": {
"name": "Grid imported"
},
"grid_services_energy_imported": {
"name": "Grid services imported"
},
"grid_services_energy_exported": {
"name": "Grid services exported"
},
"grid_energy_exported_from_solar": {
"name": "Grid exported from solar"
},
"grid_energy_exported_from_generator": {
"name": "Grid exported from generator"
},
"grid_energy_exported_from_battery": {
"name": "Grid exported from battery"
},
"battery_energy_exported": {
"name": "Battery exported"
},
"battery_energy_imported_from_grid": {
"name": "Battery imported from grid"
},
"battery_energy_imported_from_solar": {
"name": "Battery imported from solar"
},
"battery_energy_imported_from_generator": {
"name": "Battery imported from generator"
},
"consumer_energy_imported_from_grid": {
"name": "Consumer imported from grid"
},
"consumer_energy_imported_from_solar": {
"name": "Consumer imported from solar"
},
"consumer_energy_imported_from_battery": {
"name": "Consumer imported from battery"
},
"consumer_energy_imported_from_generator": {
"name": "Consumer imported from generator"
},
"total_home_usage": {
"name": "Home usage"
},
"total_battery_charge": {
"name": "Battery charged"
},
"total_battery_discharge": {
"name": "Battery discharged"
},
"total_solar_generation": {
"name": "Solar generated"
},
"total_grid_energy_exported": {
"name": "Grid exported"
} }
}, },
"switch": { "switch": {

View File

@ -15,6 +15,7 @@ from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES
from .const import ( from .const import (
COMMAND_OK, COMMAND_OK,
ENERGY_HISTORY,
LIVE_STATUS, LIVE_STATUS,
PRODUCTS, PRODUCTS,
SITE_INFO, SITE_INFO,
@ -177,6 +178,16 @@ def mock_request():
yield mock_request yield mock_request
@pytest.fixture(autouse=True)
def mock_energy_history():
"""Mock Teslemetry Energy Specific site_info method."""
with patch(
"homeassistant.components.teslemetry.EnergySpecific.energy_history",
return_value=ENERGY_HISTORY,
) as mock_live_status:
yield mock_live_status
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_signed_command() -> Generator[AsyncMock]: def mock_signed_command() -> Generator[AsyncMock]:
"""Mock Tesla Fleet Api signed_command method.""" """Mock Tesla Fleet Api signed_command method."""

View File

@ -11,6 +11,7 @@ PRODUCTS = load_json_object_fixture("products.json", DOMAIN)
VEHICLE_DATA = load_json_object_fixture("vehicle_data.json", DOMAIN) VEHICLE_DATA = load_json_object_fixture("vehicle_data.json", DOMAIN)
VEHICLE_DATA_ALT = load_json_object_fixture("vehicle_data_alt.json", DOMAIN) VEHICLE_DATA_ALT = load_json_object_fixture("vehicle_data_alt.json", DOMAIN)
LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN)
ENERGY_HISTORY = load_json_object_fixture("energy_history.json", DOMAIN)
SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN) SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN)
COMMAND_OK = {"response": {"result": True, "reason": ""}} COMMAND_OK = {"response": {"result": True, "reason": ""}}

View File

@ -0,0 +1,45 @@
{
"response": {
"period": "day",
"time_series": [
{
"timestamp": "2023-06-01T01:00:00-07:00",
"solar_energy_exported": 70940,
"generator_energy_exported": 0,
"grid_energy_imported": 521,
"grid_services_energy_imported": 17.53125,
"grid_services_energy_exported": 3.80859375,
"grid_energy_exported_from_solar": 43660,
"grid_energy_exported_from_generator": 0,
"grid_energy_exported_from_battery": 19,
"battery_energy_exported": 10030,
"battery_energy_imported_from_grid": 80,
"battery_energy_imported_from_solar": 16800,
"battery_energy_imported_from_generator": 0,
"consumer_energy_imported_from_grid": 441,
"consumer_energy_imported_from_solar": 10480,
"consumer_energy_imported_from_battery": 10011,
"consumer_energy_imported_from_generator": 0
},
{
"timestamp": "2023-06-01T01:05:00-07:00",
"solar_energy_exported": 140940,
"generator_energy_exported": 1,
"grid_energy_imported": 1021,
"grid_services_energy_imported": 27.53125,
"grid_services_energy_exported": 6.80859375,
"grid_energy_exported_from_solar": 83660,
"grid_energy_exported_from_generator": 0,
"grid_energy_exported_from_battery": 29,
"battery_energy_exported": 20030,
"battery_energy_imported_from_grid": 0,
"battery_energy_imported_from_solar": 26800,
"battery_energy_imported_from_generator": 0,
"consumer_energy_imported_from_grid": 841,
"consumer_energy_imported_from_solar": 20480,
"consumer_energy_imported_from_battery": 20011,
"consumer_energy_imported_from_generator": 0
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ from tesla_fleet_api.exceptions import (
from homeassistant.components.tesla_fleet.const import AUTHORIZE_URL from homeassistant.components.tesla_fleet.const import AUTHORIZE_URL
from homeassistant.components.tesla_fleet.coordinator import ( from homeassistant.components.tesla_fleet.coordinator import (
ENERGY_HISTORY_INTERVAL,
ENERGY_INTERVAL, ENERGY_INTERVAL,
ENERGY_INTERVAL_SECONDS, ENERGY_INTERVAL_SECONDS,
VEHICLE_INTERVAL, VEHICLE_INTERVAL,
@ -317,6 +318,21 @@ async def test_energy_site_refresh_error(
assert normal_config_entry.state is state assert normal_config_entry.state is state
# Test Energy History Coordinator
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
async def test_energy_history_refresh_error(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_energy_history: AsyncMock,
side_effect: TeslaFleetError,
state: ConfigEntryState,
) -> None:
"""Test coordinator refresh with an error."""
mock_energy_history.side_effect = side_effect
await setup_platform(hass, normal_config_entry)
assert normal_config_entry.state is state
async def test_energy_live_refresh_ratelimited( async def test_energy_live_refresh_ratelimited(
hass: HomeAssistant, hass: HomeAssistant,
normal_config_entry: MockConfigEntry, normal_config_entry: MockConfigEntry,
@ -379,6 +395,39 @@ async def test_energy_info_refresh_ratelimited(
assert mock_site_info.call_count == 3 assert mock_site_info.call_count == 3
async def test_energy_history_refresh_ratelimited(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_energy_history: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test coordinator refresh handles 429."""
await setup_platform(hass, normal_config_entry)
mock_energy_history.side_effect = RateLimited(
{"after": int(ENERGY_HISTORY_INTERVAL.total_seconds() + 10)}
)
freezer.tick(ENERGY_HISTORY_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_energy_history.call_count == 2
freezer.tick(ENERGY_HISTORY_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Should not call for another 10 seconds
assert mock_energy_history.call_count == 2
freezer.tick(ENERGY_HISTORY_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_energy_history.call_count == 3
async def test_init_region_issue( async def test_init_region_issue(
hass: HomeAssistant, hass: HomeAssistant,
normal_config_entry: MockConfigEntry, normal_config_entry: MockConfigEntry,