Skip to content

Commit

Permalink
Add energy site coordinator to Teslemetry (#117184)
Browse files Browse the repository at this point in the history
* Add energy site coordinator

* Add missing string

* Add another missing string

* Aprettier
  • Loading branch information
Bre77 committed May 10, 2024
1 parent 55c4ba1 commit 62d70b1
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 2 deletions.
7 changes: 7 additions & 0 deletions homeassistant/components/teslemetry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from .const import DOMAIN, MODELS
from .coordinator import (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator,
)
Expand Down Expand Up @@ -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",
Expand All @@ -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,
)
Expand All @@ -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
Expand Down
29 changes: 29 additions & 0 deletions homeassistant/components/teslemetry/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,32 @@ async def _async_update_data(self) -> 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)
23 changes: 21 additions & 2 deletions homeassistant/components/teslemetry/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from .const import DOMAIN, LOGGER, TeslemetryState
from .coordinator import (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator,
)
Expand All @@ -21,7 +22,9 @@

class TeslemetryEntity(
CoordinatorEntity[
TeslemetryVehicleDataCoordinator | TeslemetryEnergySiteLiveCoordinator
TeslemetryVehicleDataCoordinator
| TeslemetryEnergySiteLiveCoordinator
| TeslemetryEnergySiteInfoCoordinator
]
):
"""Parent class for all Teslemetry entities."""
Expand All @@ -31,7 +34,8 @@ class TeslemetryEntity(
def __init__(
self,
coordinator: TeslemetryVehicleDataCoordinator
| TeslemetryEnergySiteLiveCoordinator,
| TeslemetryEnergySiteLiveCoordinator
| TeslemetryEnergySiteInfoCoordinator,
api: VehicleSpecific | EnergySpecific,
key: str,
) -> None:
Expand Down Expand Up @@ -172,6 +176,21 @@ def __init__(
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]
):
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/teslemetry/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from homeassistant.helpers.device_registry import DeviceInfo

from .coordinator import (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator,
TeslemetryVehicleDataCoordinator,
)
Expand Down Expand Up @@ -42,5 +43,6 @@ class TeslemetryEnergyData:

api: EnergySpecific
live_coordinator: TeslemetryEnergySiteLiveCoordinator
info_coordinator: TeslemetryEnergySiteInfoCoordinator
id: int
device: DeviceInfo
37 changes: 37 additions & 0 deletions homeassistant/components/teslemetry/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

from .const import DOMAIN
from .entity import (
TeslemetryEnergyInfoEntity,
TeslemetryEnergyLiveEntity,
TeslemetryVehicleEntity,
TeslemetryWallConnectorEntity,
Expand Down Expand Up @@ -401,6 +402,16 @@ class TeslemetryTimeEntityDescription(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
Expand Down Expand Up @@ -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
),
)
)

Expand Down Expand Up @@ -527,3 +544,23 @@ 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


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
6 changes: 6 additions & 0 deletions homeassistant/components/teslemetry/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
122 changes: 122 additions & 0 deletions tests/components/teslemetry/snapshots/test_sensor.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
11 changes: 11 additions & 0 deletions tests/components/teslemetry/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 62d70b1

Please sign in to comment.