Add update platform to Teslemetry (#118145)
* Add update platform * Add tests * updates * update test * Fix support features comment * Add assertionpull/118171/head
parent
7bbb33b415
commit
f12f82caac
|
@ -38,6 +38,7 @@ PLATFORMS: Final = [
|
|||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.UPDATE,
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -454,6 +454,11 @@
|
|||
"vehicle_state_valet_mode": {
|
||||
"name": "Valet mode"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"vehicle_state_software_update_status": {
|
||||
"name": "[%key:component::update::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
|
|
@ -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()
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
# ---
|
|
@ -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
|
Loading…
Reference in New Issue