Skip to content

Commit 1a23610

Browse files
authored
Add update platform to Tesla Fleet (home-assistant#156908)
1 parent 0c9e92f commit 1a23610

File tree

5 files changed

+324
-0
lines changed

5 files changed

+324
-0
lines changed

homeassistant/components/tesla_fleet/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
Platform.SELECT,
5151
Platform.SENSOR,
5252
Platform.SWITCH,
53+
Platform.UPDATE,
5354
]
5455

5556
type TeslaFleetConfigEntry = ConfigEntry[TeslaFleetData]

homeassistant/components/tesla_fleet/strings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,11 @@
576576
"vehicle_state_valet_mode": {
577577
"name": "Valet mode"
578578
}
579+
},
580+
"update": {
581+
"vehicle_state_software_update_status": {
582+
"name": "[%key:component::update::title%]"
583+
}
579584
}
580585
},
581586
"exceptions": {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Update platform for Tesla Fleet integration."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from tesla_fleet_api.const import Scope
8+
from tesla_fleet_api.tesla.vehicle.fleet import VehicleFleet
9+
10+
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
11+
from homeassistant.core import HomeAssistant
12+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
13+
14+
from . import TeslaFleetConfigEntry
15+
from .entity import TeslaFleetVehicleEntity
16+
from .helpers import handle_vehicle_command
17+
from .models import TeslaFleetVehicleData
18+
19+
AVAILABLE = "available"
20+
DOWNLOADING = "downloading"
21+
INSTALLING = "installing"
22+
WIFI_WAIT = "downloading_wifi_wait"
23+
SCHEDULED = "scheduled"
24+
25+
PARALLEL_UPDATES = 0
26+
27+
28+
async def async_setup_entry(
29+
hass: HomeAssistant,
30+
entry: TeslaFleetConfigEntry,
31+
async_add_entities: AddConfigEntryEntitiesCallback,
32+
) -> None:
33+
"""Set up the Tesla Fleet update platform from a config entry."""
34+
35+
async_add_entities(
36+
TeslaFleetUpdateEntity(vehicle, entry.runtime_data.scopes)
37+
for vehicle in entry.runtime_data.vehicles
38+
)
39+
40+
41+
class TeslaFleetUpdateEntity(TeslaFleetVehicleEntity, UpdateEntity):
42+
"""Tesla Fleet Update entity."""
43+
44+
_attr_supported_features = UpdateEntityFeature.PROGRESS
45+
api: VehicleFleet
46+
47+
def __init__(
48+
self,
49+
data: TeslaFleetVehicleData,
50+
scopes: list[Scope],
51+
) -> None:
52+
"""Initialize the Update."""
53+
self.scoped = Scope.VEHICLE_CMDS in scopes
54+
super().__init__(
55+
data,
56+
"vehicle_state_software_update_status",
57+
)
58+
59+
async def async_install(
60+
self, version: str | None, backup: bool, **kwargs: Any
61+
) -> None:
62+
"""Install an update."""
63+
self.raise_for_read_only(Scope.VEHICLE_CMDS)
64+
65+
await handle_vehicle_command(self.api.schedule_software_update(offset_sec=0))
66+
self._attr_in_progress = True
67+
self.async_write_ha_state()
68+
69+
def _async_update_attrs(self) -> None:
70+
"""Update the attributes of the entity."""
71+
72+
# Supported Features
73+
if self.scoped and self._value in (
74+
AVAILABLE,
75+
SCHEDULED,
76+
):
77+
# Only allow install when an update has been fully downloaded
78+
self._attr_supported_features = (
79+
UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL
80+
)
81+
else:
82+
self._attr_supported_features = UpdateEntityFeature.PROGRESS
83+
84+
# Installed Version
85+
self._attr_installed_version = self.get("vehicle_state_car_version")
86+
if self._attr_installed_version is not None:
87+
# Remove build from version
88+
self._attr_installed_version = self._attr_installed_version.split(" ")[0]
89+
90+
# Latest Version
91+
if self._value in (
92+
AVAILABLE,
93+
SCHEDULED,
94+
INSTALLING,
95+
DOWNLOADING,
96+
WIFI_WAIT,
97+
):
98+
self._attr_latest_version = self.coordinator.data[
99+
"vehicle_state_software_update_version"
100+
]
101+
else:
102+
self._attr_latest_version = self._attr_installed_version
103+
104+
# In Progress
105+
if self._value in (
106+
SCHEDULED,
107+
INSTALLING,
108+
):
109+
self._attr_in_progress = True
110+
if install_perc := self.get("vehicle_state_software_update_install_perc"):
111+
self._attr_update_percentage = install_perc
112+
else:
113+
self._attr_in_progress = False
114+
self._attr_update_percentage = None
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# serializer version: 1
2+
# name: test_update[update.test_update-entry]
3+
EntityRegistryEntrySnapshot({
4+
'aliases': set({
5+
}),
6+
'area_id': None,
7+
'capabilities': None,
8+
'config_entry_id': <ANY>,
9+
'config_subentry_id': <ANY>,
10+
'device_class': None,
11+
'device_id': <ANY>,
12+
'disabled_by': None,
13+
'domain': 'update',
14+
'entity_category': <EntityCategory.CONFIG: 'config'>,
15+
'entity_id': 'update.test_update',
16+
'has_entity_name': True,
17+
'hidden_by': None,
18+
'icon': None,
19+
'id': <ANY>,
20+
'labels': set({
21+
}),
22+
'name': None,
23+
'options': dict({
24+
}),
25+
'original_device_class': None,
26+
'original_icon': None,
27+
'original_name': 'Update',
28+
'platform': 'tesla_fleet',
29+
'previous_unique_id': None,
30+
'suggested_object_id': None,
31+
'supported_features': <UpdateEntityFeature: 5>,
32+
'translation_key': 'vehicle_state_software_update_status',
33+
'unique_id': 'LRWXF7EK4KC700000-vehicle_state_software_update_status',
34+
'unit_of_measurement': None,
35+
})
36+
# ---
37+
# name: test_update[update.test_update-state]
38+
StateSnapshot({
39+
'attributes': ReadOnlyDict({
40+
'auto_update': False,
41+
'display_precision': 0,
42+
'entity_picture': 'https://brands.home-assistant.io/_/tesla_fleet/icon.png',
43+
'friendly_name': 'Test Update',
44+
'in_progress': False,
45+
'installed_version': '2023.44.30.8',
46+
'latest_version': '2024.12.0.0',
47+
'release_summary': None,
48+
'release_url': None,
49+
'skipped_version': None,
50+
'supported_features': <UpdateEntityFeature: 5>,
51+
'title': None,
52+
'update_percentage': None,
53+
}),
54+
'context': <ANY>,
55+
'entity_id': 'update.test_update',
56+
'last_changed': <ANY>,
57+
'last_reported': <ANY>,
58+
'last_updated': <ANY>,
59+
'state': 'on',
60+
})
61+
# ---
62+
# name: test_update_alt[update.test_update-entry]
63+
EntityRegistryEntrySnapshot({
64+
'aliases': set({
65+
}),
66+
'area_id': None,
67+
'capabilities': None,
68+
'config_entry_id': <ANY>,
69+
'config_subentry_id': <ANY>,
70+
'device_class': None,
71+
'device_id': <ANY>,
72+
'disabled_by': None,
73+
'domain': 'update',
74+
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
75+
'entity_id': 'update.test_update',
76+
'has_entity_name': True,
77+
'hidden_by': None,
78+
'icon': None,
79+
'id': <ANY>,
80+
'labels': set({
81+
}),
82+
'name': None,
83+
'options': dict({
84+
}),
85+
'original_device_class': None,
86+
'original_icon': None,
87+
'original_name': 'Update',
88+
'platform': 'tesla_fleet',
89+
'previous_unique_id': None,
90+
'suggested_object_id': None,
91+
'supported_features': <UpdateEntityFeature: 4>,
92+
'translation_key': 'vehicle_state_software_update_status',
93+
'unique_id': 'LRWXF7EK4KC700000-vehicle_state_software_update_status',
94+
'unit_of_measurement': None,
95+
})
96+
# ---
97+
# name: test_update_alt[update.test_update-state]
98+
StateSnapshot({
99+
'attributes': ReadOnlyDict({
100+
'auto_update': False,
101+
'display_precision': 0,
102+
'entity_picture': 'https://brands.home-assistant.io/_/tesla_fleet/icon.png',
103+
'friendly_name': 'Test Update',
104+
'in_progress': False,
105+
'installed_version': '2023.44.30.8',
106+
'latest_version': '2023.44.30.8',
107+
'release_summary': None,
108+
'release_url': None,
109+
'skipped_version': None,
110+
'supported_features': <UpdateEntityFeature: 4>,
111+
'title': None,
112+
'update_percentage': None,
113+
}),
114+
'context': <ANY>,
115+
'entity_id': 'update.test_update',
116+
'last_changed': <ANY>,
117+
'last_reported': <ANY>,
118+
'last_updated': <ANY>,
119+
'state': 'off',
120+
})
121+
# ---
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Test the Tesla Fleet update platform."""
2+
3+
import copy
4+
from unittest.mock import AsyncMock, patch
5+
6+
from freezegun.api import FrozenDateTimeFactory
7+
from syrupy.assertion import SnapshotAssertion
8+
9+
from homeassistant.components.tesla_fleet.coordinator import VEHICLE_INTERVAL
10+
from homeassistant.components.tesla_fleet.update import INSTALLING
11+
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL
12+
from homeassistant.const import ATTR_ENTITY_ID, Platform
13+
from homeassistant.core import HomeAssistant
14+
from homeassistant.helpers import entity_registry as er
15+
16+
from . import assert_entities, setup_platform
17+
from .const import COMMAND_OK, VEHICLE_DATA, VEHICLE_DATA_ALT
18+
19+
from tests.common import MockConfigEntry, async_fire_time_changed
20+
21+
22+
async def test_update(
23+
hass: HomeAssistant,
24+
snapshot: SnapshotAssertion,
25+
entity_registry: er.EntityRegistry,
26+
normal_config_entry: MockConfigEntry,
27+
) -> None:
28+
"""Tests that the update entities are correct."""
29+
30+
await setup_platform(hass, normal_config_entry, [Platform.UPDATE])
31+
assert_entities(hass, normal_config_entry.entry_id, entity_registry, snapshot)
32+
33+
34+
async def test_update_alt(
35+
hass: HomeAssistant,
36+
snapshot: SnapshotAssertion,
37+
entity_registry: er.EntityRegistry,
38+
normal_config_entry: MockConfigEntry,
39+
mock_vehicle_data: AsyncMock,
40+
) -> None:
41+
"""Tests that the update entities are correct."""
42+
43+
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
44+
await setup_platform(hass, normal_config_entry, [Platform.UPDATE])
45+
assert_entities(hass, normal_config_entry.entry_id, entity_registry, snapshot)
46+
47+
48+
async def test_update_services(
49+
hass: HomeAssistant,
50+
normal_config_entry: MockConfigEntry,
51+
mock_vehicle_data: AsyncMock,
52+
freezer: FrozenDateTimeFactory,
53+
snapshot: SnapshotAssertion,
54+
) -> None:
55+
"""Tests that the update services work."""
56+
57+
await setup_platform(hass, normal_config_entry, [Platform.UPDATE])
58+
59+
entity_id = "update.test_update"
60+
61+
with patch(
62+
"tesla_fleet_api.tesla.VehicleFleet.schedule_software_update",
63+
return_value=COMMAND_OK,
64+
) as call:
65+
await hass.services.async_call(
66+
UPDATE_DOMAIN,
67+
SERVICE_INSTALL,
68+
{ATTR_ENTITY_ID: entity_id},
69+
blocking=True,
70+
)
71+
call.assert_called_once()
72+
73+
VEHICLE_INSTALLING = copy.deepcopy(VEHICLE_DATA)
74+
VEHICLE_INSTALLING["response"]["vehicle_state"]["software_update"]["status"] = ( # type: ignore[index]
75+
INSTALLING
76+
)
77+
mock_vehicle_data.return_value = VEHICLE_INSTALLING
78+
freezer.tick(VEHICLE_INTERVAL)
79+
async_fire_time_changed(hass)
80+
await hass.async_block_till_done()
81+
82+
state = hass.states.get(entity_id)
83+
assert state.attributes["in_progress"] is True # type: ignore[union-attr]

0 commit comments

Comments
 (0)