diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index ac94437d76f..b6e83ff2ce2 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -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 diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index f1004d0a282..c1f204ca50e 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -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) diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 9472616faa9..d2aa4a80238 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -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] ): diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py index aa0142742df..d05d713c1eb 100644 --- a/homeassistant/components/teslemetry/models.py +++ b/homeassistant/components/teslemetry/models.py @@ -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 diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index c5ae00e02cd..4f0b136e4e8 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -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 diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index fa4419fbfcb..86ce263305d 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -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" }, diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index 0d817ad1f7e..5dd42dc0b82 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -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': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_version', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'entity_id': 'sensor.energy_site_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.44.0 eb113390', + }) +# --- +# name: test_sensors[sensor.energy_site_version-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site version', + }), + 'context': , + 'entity_id': 'sensor.energy_site_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensors[sensor.test_battery_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index 5f9d11b6818..adec3f38798 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -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