Add energy site coordinator to Teslemetry (#117184)

* Add energy site coordinator

* Add missing string

* Add another missing string

* Aprettier
This commit is contained in:
Brett Adams 2024-05-10 20:38:20 +10:00 committed by GitHub
parent 55c4ba12f6
commit 62d70b1b10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 235 additions and 2 deletions

View File

@ -20,6 +20,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from .const import DOMAIN, MODELS
from .coordinator import (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator,
)
@ -83,6 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
site_id = product["energy_site_id"]
api = EnergySpecific(teslemetry.energy, site_id)
live_coordinator = TeslemetryEnergySiteLiveCoordinator(hass, api)
info_coordinator = TeslemetryEnergySiteInfoCoordinator(hass, api, product)
device = DeviceInfo(
identifiers={(DOMAIN, str(site_id))},
manufacturer="Tesla",
@ -94,6 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
TeslemetryEnergyData(
api=api,
live_coordinator=live_coordinator,
info_coordinator=info_coordinator,
id=site_id,
device=device,
)
@ -109,6 +112,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
energysite.live_coordinator.async_config_entry_first_refresh()
for energysite in energysites
),
*(
energysite.info_coordinator.async_config_entry_first_refresh()
for energysite in energysites
),
)
# Setup Platforms

View File

@ -111,3 +111,32 @@ class TeslemetryEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]])
}
return data
class TeslemetryEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching energy site info from the Teslemetry API."""
def __init__(self, hass: HomeAssistant, api: EnergySpecific, product: dict) -> None:
"""Initialize Teslemetry Energy Info coordinator."""
super().__init__(
hass,
LOGGER,
name="Teslemetry Energy Site Info",
update_interval=ENERGY_INFO_INTERVAL,
)
self.api = api
self.data = product
async def _async_update_data(self) -> dict[str, Any]:
"""Update energy site data using Teslemetry API."""
try:
data = (await self.api.site_info())["response"]
except InvalidToken as e:
raise ConfigEntryAuthFailed from e
except SubscriptionRequired as e:
raise ConfigEntryAuthFailed from e
except TeslaFleetError as e:
raise UpdateFailed(e.message) from e
return flatten(data)

View File

@ -13,6 +13,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, LOGGER, TeslemetryState
from .coordinator import (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator,
)
@ -21,7 +22,9 @@ from .models import TeslemetryEnergyData, TeslemetryVehicleData
class TeslemetryEntity(
CoordinatorEntity[
TeslemetryVehicleDataCoordinator | TeslemetryEnergySiteLiveCoordinator
TeslemetryVehicleDataCoordinator
| TeslemetryEnergySiteLiveCoordinator
| TeslemetryEnergySiteInfoCoordinator
]
):
"""Parent class for all Teslemetry entities."""
@ -31,7 +34,8 @@ class TeslemetryEntity(
def __init__(
self,
coordinator: TeslemetryVehicleDataCoordinator
| TeslemetryEnergySiteLiveCoordinator,
| TeslemetryEnergySiteLiveCoordinator
| TeslemetryEnergySiteInfoCoordinator,
api: VehicleSpecific | EnergySpecific,
key: str,
) -> None:
@ -172,6 +176,21 @@ class TeslemetryEnergyLiveEntity(TeslemetryEntity):
super().__init__(data.live_coordinator, data.api, key)
class TeslemetryEnergyInfoEntity(TeslemetryEntity):
"""Parent class for Teslemetry Energy Site Info Entities."""
def __init__(
self,
data: TeslemetryEnergyData,
key: str,
) -> None:
"""Initialize common aspects of a Teslemetry Energy Site Info entity."""
self._attr_unique_id = f"{data.id}-{key}"
self._attr_device_info = data.device
super().__init__(data.info_coordinator, data.api, key)
class TeslemetryWallConnectorEntity(
TeslemetryEntity, CoordinatorEntity[TeslemetryEnergySiteLiveCoordinator]
):

View File

@ -11,6 +11,7 @@ from tesla_fleet_api.const import Scope
from homeassistant.helpers.device_registry import DeviceInfo
from .coordinator import (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator,
)
@ -42,5 +43,6 @@ class TeslemetryEnergyData:
api: EnergySpecific
live_coordinator: TeslemetryEnergySiteLiveCoordinator
info_coordinator: TeslemetryEnergySiteInfoCoordinator
id: int
device: DeviceInfo

View File

@ -36,6 +36,7 @@ from homeassistant.util.variance import ignore_variance
from .const import DOMAIN
from .entity import (
TeslemetryEnergyInfoEntity,
TeslemetryEnergyLiveEntity,
TeslemetryVehicleEntity,
TeslemetryWallConnectorEntity,
@ -401,6 +402,16 @@ WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
),
)
ENERGY_INFO_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="vpp_backup_reserve_percent",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
),
SensorEntityDescription(key="version"),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@ -432,6 +443,12 @@ async def async_setup_entry(
for din in energysite.live_coordinator.data.get("wall_connectors", {})
for description in WALL_CONNECTOR_DESCRIPTIONS
),
( # Add energy site info
TeslemetryEnergyInfoSensorEntity(energysite, description)
for energysite in data.energysites
for description in ENERGY_INFO_DESCRIPTIONS
if description.key in energysite.info_coordinator.data
),
)
)
@ -527,3 +544,23 @@ class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorE
"""Update the attributes of the sensor."""
self._attr_available = not self.is_none
self._attr_native_value = self._value
class TeslemetryEnergyInfoSensorEntity(TeslemetryEnergyInfoEntity, SensorEntity):
"""Base class for Teslemetry energy site metric sensors."""
entity_description: SensorEntityDescription
def __init__(
self,
data: TeslemetryEnergyData,
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_available = not self.is_none
self._attr_native_value = self._value

View File

@ -166,9 +166,15 @@
"vehicle_state_tpms_pressure_rr": {
"name": "Tire pressure rear right"
},
"version": {
"name": "version"
},
"vin": {
"name": "Vehicle"
},
"vpp_backup_reserve_percent": {
"name": "VPP backup reserve"
},
"wall_connector_fault_state": {
"name": "Fault state code"
},

View File

@ -714,6 +714,128 @@
'state': '40.727',
})
# ---
# name: test_sensors[sensor.energy_site_version-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.energy_site_version',
'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': 'version',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'version',
'unique_id': '123456-version',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.energy_site_version-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Energy Site version',
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_version',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '23.44.0 eb113390',
})
# ---
# name: test_sensors[sensor.energy_site_version-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Energy Site version',
}),
'context': <ANY>,
'entity_id': 'sensor.energy_site_version',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '23.44.0 eb113390',
})
# ---
# 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': 'teslemetry',
'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': '0',
})
# ---
# name: test_sensors[sensor.energy_site_vpp_backup_reserve-statealt]
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': '0',
})
# ---
# name: test_sensors[sensor.test_battery_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -82,3 +82,14 @@ async def test_energy_live_refresh_error(
mock_live_status.side_effect = side_effect
entry = await setup_platform(hass)
assert entry.state is state
# Test Energy Site Coordinator
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
async def test_energy_site_refresh_error(
hass: HomeAssistant, mock_site_info, side_effect, state
) -> None:
"""Test coordinator refresh with an error."""
mock_site_info.side_effect = side_effect
entry = await setup_platform(hass)
assert entry.state is state