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

View File

@ -111,3 +111,32 @@ class TeslemetryEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]])
} }
return data 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 .const import DOMAIN, LOGGER, TeslemetryState
from .coordinator import ( from .coordinator import (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator, TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator, TeslemetryVehicleDataCoordinator,
) )
@ -21,7 +22,9 @@ from .models import TeslemetryEnergyData, TeslemetryVehicleData
class TeslemetryEntity( class TeslemetryEntity(
CoordinatorEntity[ CoordinatorEntity[
TeslemetryVehicleDataCoordinator | TeslemetryEnergySiteLiveCoordinator TeslemetryVehicleDataCoordinator
| TeslemetryEnergySiteLiveCoordinator
| TeslemetryEnergySiteInfoCoordinator
] ]
): ):
"""Parent class for all Teslemetry entities.""" """Parent class for all Teslemetry entities."""
@ -31,7 +34,8 @@ class TeslemetryEntity(
def __init__( def __init__(
self, self,
coordinator: TeslemetryVehicleDataCoordinator coordinator: TeslemetryVehicleDataCoordinator
| TeslemetryEnergySiteLiveCoordinator, | TeslemetryEnergySiteLiveCoordinator
| TeslemetryEnergySiteInfoCoordinator,
api: VehicleSpecific | EnergySpecific, api: VehicleSpecific | EnergySpecific,
key: str, key: str,
) -> None: ) -> None:
@ -172,6 +176,21 @@ class TeslemetryEnergyLiveEntity(TeslemetryEntity):
super().__init__(data.live_coordinator, data.api, key) 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( class TeslemetryWallConnectorEntity(
TeslemetryEntity, CoordinatorEntity[TeslemetryEnergySiteLiveCoordinator] TeslemetryEntity, CoordinatorEntity[TeslemetryEnergySiteLiveCoordinator]
): ):

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 (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator, TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator, TeslemetryVehicleDataCoordinator,
) )
@ -42,5 +43,6 @@ class TeslemetryEnergyData:
api: EnergySpecific api: EnergySpecific
live_coordinator: TeslemetryEnergySiteLiveCoordinator live_coordinator: TeslemetryEnergySiteLiveCoordinator
info_coordinator: TeslemetryEnergySiteInfoCoordinator
id: int id: int
device: DeviceInfo device: DeviceInfo

View File

@ -36,6 +36,7 @@ from homeassistant.util.variance import ignore_variance
from .const import DOMAIN from .const import DOMAIN
from .entity import ( from .entity import (
TeslemetryEnergyInfoEntity,
TeslemetryEnergyLiveEntity, TeslemetryEnergyLiveEntity,
TeslemetryVehicleEntity, TeslemetryVehicleEntity,
TeslemetryWallConnectorEntity, 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( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 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 din in energysite.live_coordinator.data.get("wall_connectors", {})
for description in WALL_CONNECTOR_DESCRIPTIONS 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.""" """Update the attributes of the sensor."""
self._attr_available = not self.is_none self._attr_available = not self.is_none
self._attr_native_value = self._value 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": { "vehicle_state_tpms_pressure_rr": {
"name": "Tire pressure rear right" "name": "Tire pressure rear right"
}, },
"version": {
"name": "version"
},
"vin": { "vin": {
"name": "Vehicle" "name": "Vehicle"
}, },
"vpp_backup_reserve_percent": {
"name": "VPP backup reserve"
},
"wall_connector_fault_state": { "wall_connector_fault_state": {
"name": "Fault state code" "name": "Fault state code"
}, },

View File

@ -714,6 +714,128 @@
'state': '40.727', '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] # name: test_sensors[sensor.test_battery_level-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -82,3 +82,14 @@ async def test_energy_live_refresh_error(
mock_live_status.side_effect = side_effect mock_live_status.side_effect = side_effect
entry = await setup_platform(hass) entry = await setup_platform(hass)
assert entry.state is state 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