Fix Tesla update showing scheduled updates as installing (#158681)

This commit is contained in:
Paul Tarjan
2026-01-05 01:42:58 -10:00
committed by Bram Kragten
parent d28d55c7db
commit a697e63b8c
2 changed files with 128 additions and 25 deletions
+25 -18
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import time
from typing import Any
from tesla_fleet_api.const import Scope
@@ -24,6 +25,9 @@ SCHEDULED = "scheduled"
PARALLEL_UPDATES = 0
# Show scheduled update as installing if within this many seconds
SCHEDULED_THRESHOLD_SECONDS = 120
async def async_setup_entry(
hass: HomeAssistant,
@@ -69,12 +73,9 @@ class TeslaFleetUpdateEntity(TeslaFleetVehicleEntity, UpdateEntity):
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
# Supported Features - only show install button if update is available
# but not already scheduled
if self.scoped and self._value == AVAILABLE:
self._attr_supported_features = (
UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL
)
@@ -87,13 +88,9 @@ class TeslaFleetUpdateEntity(TeslaFleetVehicleEntity, UpdateEntity):
# 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,
# Latest Version - hide update if scheduled far in the future
if self._value in (AVAILABLE, INSTALLING, DOWNLOADING, WIFI_WAIT) or (
self._value == SCHEDULED and self._is_scheduled_soon()
):
self._attr_latest_version = self.coordinator.data[
"vehicle_state_software_update_version"
@@ -101,14 +98,24 @@ class TeslaFleetUpdateEntity(TeslaFleetVehicleEntity, UpdateEntity):
else:
self._attr_latest_version = self._attr_installed_version
# In Progress
if self._value in (
SCHEDULED,
INSTALLING,
):
# In Progress - only show as installing if actually installing or
# scheduled to start within 2 minutes
if self._value == INSTALLING:
self._attr_in_progress = True
if install_perc := self.get("vehicle_state_software_update_install_perc"):
self._attr_update_percentage = install_perc
elif self._value == SCHEDULED and self._is_scheduled_soon():
self._attr_in_progress = True
self._attr_update_percentage = None
else:
self._attr_in_progress = False
self._attr_update_percentage = None
def _is_scheduled_soon(self) -> bool:
"""Check if a scheduled update is within the threshold to start."""
scheduled_time_ms = self.get("vehicle_state_software_update_scheduled_time_ms")
if scheduled_time_ms is None:
return False
# Convert milliseconds to seconds and compare to current time
scheduled_time_sec = scheduled_time_ms / 1000
return scheduled_time_sec - time.time() < SCHEDULED_THRESHOLD_SECONDS
+103 -7
View File
@@ -1,13 +1,15 @@
"""Test the Tesla Fleet update platform."""
import copy
import time
from typing import Any
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.tesla_fleet.coordinator import VEHICLE_INTERVAL
from homeassistant.components.tesla_fleet.update import INSTALLING
from homeassistant.components.tesla_fleet.update import INSTALLING, SCHEDULED
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
@@ -19,6 +21,11 @@ from .const import COMMAND_OK, VEHICLE_DATA, VEHICLE_DATA_ALT
from tests.common import MockConfigEntry, async_fire_time_changed
def _get_software_update(data: dict[str, Any]) -> dict[str, Any]:
"""Get the software_update dict from vehicle data."""
return data["response"]["vehicle_state"]["software_update"]
async def test_update(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
@@ -70,14 +77,103 @@ async def test_update_services(
)
call.assert_called_once()
VEHICLE_INSTALLING = copy.deepcopy(VEHICLE_DATA)
VEHICLE_INSTALLING["response"]["vehicle_state"]["software_update"]["status"] = ( # type: ignore[index]
INSTALLING
)
mock_vehicle_data.return_value = VEHICLE_INSTALLING
vehicle_installing = copy.deepcopy(VEHICLE_DATA)
_get_software_update(vehicle_installing)["status"] = INSTALLING
mock_vehicle_data.return_value = vehicle_installing
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"] is True # type: ignore[union-attr]
assert state is not None
assert state.attributes["in_progress"] is True
async def test_update_scheduled_far_future_not_in_progress(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_vehicle_data: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Tests that a scheduled update far in the future is not shown as in_progress."""
await setup_platform(hass, normal_config_entry, [Platform.UPDATE])
entity_id = "update.test_update"
# Verify initial state (available) is not in_progress
state = hass.states.get(entity_id)
assert state is not None
assert state.attributes["in_progress"] is False
# Simulate update being scheduled for 1 hour in the future
vehicle_scheduled = copy.deepcopy(VEHICLE_DATA)
software_update = _get_software_update(vehicle_scheduled)
software_update["status"] = SCHEDULED
# Set scheduled time to 1 hour from now (well beyond threshold)
software_update["scheduled_time_ms"] = int((time.time() + 3600) * 1000)
mock_vehicle_data.return_value = vehicle_scheduled
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Scheduled update far in future should NOT be in_progress
state = hass.states.get(entity_id)
assert state is not None
assert state.attributes["in_progress"] is False
async def test_update_scheduled_soon_in_progress(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_vehicle_data: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Tests that a scheduled update within threshold is shown as in_progress."""
await setup_platform(hass, normal_config_entry, [Platform.UPDATE])
entity_id = "update.test_update"
# Simulate update being scheduled within threshold (1 minute from now)
vehicle_scheduled = copy.deepcopy(VEHICLE_DATA)
software_update = _get_software_update(vehicle_scheduled)
software_update["status"] = SCHEDULED
# Set scheduled time to 1 minute from now (within 2 minute threshold)
software_update["scheduled_time_ms"] = int((time.time() + 60) * 1000)
mock_vehicle_data.return_value = vehicle_scheduled
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Scheduled update within threshold should be in_progress
state = hass.states.get(entity_id)
assert state is not None
assert state.attributes["in_progress"] is True
async def test_update_scheduled_no_time_not_in_progress(
hass: HomeAssistant,
normal_config_entry: MockConfigEntry,
mock_vehicle_data: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Tests that a scheduled update without scheduled_time_ms is not in_progress."""
await setup_platform(hass, normal_config_entry, [Platform.UPDATE])
entity_id = "update.test_update"
# Simulate update being scheduled but without scheduled_time_ms
vehicle_scheduled = copy.deepcopy(VEHICLE_DATA)
_get_software_update(vehicle_scheduled)["status"] = SCHEDULED
# No scheduled_time_ms field
mock_vehicle_data.return_value = vehicle_scheduled
freezer.tick(VEHICLE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Scheduled update without time should NOT be in_progress
state = hass.states.get(entity_id)
assert state is not None
assert state.attributes["in_progress"] is False