Add update platform to Teslemetry (#118145)

* Add update platform

* Add tests

* updates

* update test

* Fix support features comment

* Add assertion
pull/118171/head
Brett Adams 2024-05-26 21:04:02 +10:00 committed by GitHub
parent 7bbb33b415
commit f12f82caac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 319 additions and 6 deletions

View File

@ -38,6 +38,7 @@ PLATFORMS: Final = [
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]

View File

@ -454,6 +454,11 @@
"vehicle_state_valet_mode": {
"name": "Valet mode"
}
},
"update": {
"vehicle_state_software_update_status": {
"name": "[%key:component::update::title%]"
}
}
},
"exceptions": {

View File

@ -0,0 +1,105 @@
"""Update platform for Teslemetry integration."""
from __future__ import annotations
from typing import Any, cast
from tesla_fleet_api.const import Scope
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import TeslemetryVehicleEntity
from .models import TeslemetryVehicleData
AVAILABLE = "available"
DOWNLOADING = "downloading"
INSTALLING = "installing"
WIFI_WAIT = "downloading_wifi_wait"
SCHEDULED = "scheduled"
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Teslemetry update platform from a config entry."""
async_add_entities(
TeslemetryUpdateEntity(vehicle, entry.runtime_data.scopes)
for vehicle in entry.runtime_data.vehicles
)
class TeslemetryUpdateEntity(TeslemetryVehicleEntity, UpdateEntity):
"""Teslemetry Updates entity."""
def __init__(
self,
data: TeslemetryVehicleData,
scopes: list[Scope],
) -> None:
"""Initialize the Update."""
self.scoped = Scope.VEHICLE_CMDS in scopes
super().__init__(
data,
"vehicle_state_software_update_status",
)
def _async_update_attrs(self) -> None:
"""Update the attributes of the entity."""
# Supported Features
if self.scoped and self._value in (
AVAILABLE,
SCHEDULED,
):
# Only allow install when an update has been fully downloaded
self._attr_supported_features = (
UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL
)
else:
self._attr_supported_features = UpdateEntityFeature.PROGRESS
# Installed Version
self._attr_installed_version = self.get("vehicle_state_car_version")
if self._attr_installed_version is not None:
# Remove build from version
self._attr_installed_version = self._attr_installed_version.split(" ")[0]
# Latest Version
if self._value in (
AVAILABLE,
SCHEDULED,
INSTALLING,
DOWNLOADING,
WIFI_WAIT,
):
self._attr_latest_version = self.coordinator.data[
"vehicle_state_software_update_version"
]
else:
self._attr_latest_version = self._attr_installed_version
# In Progress
if self._value in (
SCHEDULED,
INSTALLING,
):
self._attr_in_progress = (
cast(int, self.get("vehicle_state_software_update_install_perc"))
or True
)
else:
self._attr_in_progress = False
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
self.raise_for_scope()
await self.wake_up_if_asleep()
await self.handle_command(self.api.schedule_software_update(offset_sec=60))
self._attr_in_progress = True
self.async_write_ha_state()

View File

@ -237,11 +237,11 @@
"service_mode": false,
"service_mode_plus": false,
"software_update": {
"download_perc": 0,
"download_perc": 100,
"expected_duration_sec": 2700,
"install_perc": 1,
"status": "",
"version": " "
"status": "available",
"version": "2024.12.0.0"
},
"speed_limit_mode": {
"active": false,

View File

@ -390,11 +390,11 @@
'vehicle_state_sentry_mode_available': True,
'vehicle_state_service_mode': False,
'vehicle_state_service_mode_plus': False,
'vehicle_state_software_update_download_perc': 0,
'vehicle_state_software_update_download_perc': 100,
'vehicle_state_software_update_expected_duration_sec': 2700,
'vehicle_state_software_update_install_perc': 1,
'vehicle_state_software_update_status': '',
'vehicle_state_software_update_version': ' ',
'vehicle_state_software_update_status': 'available',
'vehicle_state_software_update_version': '2024.12.0.0',
'vehicle_state_speed_limit_mode_active': False,
'vehicle_state_speed_limit_mode_current_limit_mph': 69,
'vehicle_state_speed_limit_mode_max_limit_mph': 120,

View File

@ -0,0 +1,113 @@
# serializer version: 1
# name: test_update[update.test_update-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'update',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'update.test_update',
'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': 'Update',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': <UpdateEntityFeature: 5>,
'translation_key': 'vehicle_state_software_update_status',
'unique_id': 'VINVINVIN-vehicle_state_software_update_status',
'unit_of_measurement': None,
})
# ---
# name: test_update[update.test_update-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'entity_picture': 'https://brands.home-assistant.io/_/teslemetry/icon.png',
'friendly_name': 'Test Update',
'in_progress': False,
'installed_version': '2023.44.30.8',
'latest_version': '2024.12.0.0',
'release_summary': None,
'release_url': None,
'skipped_version': None,
'supported_features': <UpdateEntityFeature: 5>,
'title': None,
}),
'context': <ANY>,
'entity_id': 'update.test_update',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_update_alt[update.test_update-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'update',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'update.test_update',
'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': 'Update',
'platform': 'teslemetry',
'previous_unique_id': None,
'supported_features': <UpdateEntityFeature: 4>,
'translation_key': 'vehicle_state_software_update_status',
'unique_id': 'VINVINVIN-vehicle_state_software_update_status',
'unit_of_measurement': None,
})
# ---
# name: test_update_alt[update.test_update-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'auto_update': False,
'entity_picture': 'https://brands.home-assistant.io/_/teslemetry/icon.png',
'friendly_name': 'Test Update',
'in_progress': False,
'installed_version': '2023.44.30.8',
'latest_version': '2023.44.30.8',
'release_summary': None,
'release_url': None,
'skipped_version': None,
'supported_features': <UpdateEntityFeature: 4>,
'title': None,
}),
'context': <ANY>,
'entity_id': 'update.test_update',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -0,0 +1,89 @@
"""Test the Teslemetry update platform."""
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
from syrupy import SnapshotAssertion
from tesla_fleet_api.exceptions import VehicleOffline
from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL
from homeassistant.components.teslemetry.update import INSTALLING
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import assert_entities, setup_platform
from .const import COMMAND_OK, VEHICLE_DATA, VEHICLE_DATA_ALT
from tests.common import async_fire_time_changed
async def test_update(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Tests that the update entities are correct."""
entry = await setup_platform(hass, [Platform.UPDATE])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
async def test_update_alt(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_vehicle_data,
) -> None:
"""Tests that the update entities are correct."""
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
entry = await setup_platform(hass, [Platform.UPDATE])
assert_entities(hass, entry.entry_id, entity_registry, snapshot)
async def test_update_offline(
hass: HomeAssistant,
mock_vehicle_data,
) -> None:
"""Tests that the update entities are correct when offline."""
mock_vehicle_data.side_effect = VehicleOffline
await setup_platform(hass, [Platform.UPDATE])
state = hass.states.get("update.test_update")
assert state.state == STATE_UNKNOWN
async def test_update_services(
hass: HomeAssistant,
mock_vehicle_data,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
) -> None:
"""Tests that the update services work."""
await setup_platform(hass, [Platform.UPDATE])
entity_id = "update.test_update"
with patch(
"homeassistant.components.teslemetry.VehicleSpecific.schedule_software_update",
return_value=COMMAND_OK,
) as call:
await hass.services.async_call(
UPDATE_DOMAIN,
SERVICE_INSTALL,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
call.assert_called_once()
VEHICLE_DATA["response"]["vehicle_state"]["software_update"]["status"] = INSTALLING
mock_vehicle_data.return_value = VEHICLE_DATA
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.attributes["in_progress"] == 1