mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Skip global ESPHome update lock when dashboard has a build queue (#176162)
This commit is contained in:
@@ -14,6 +14,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_VERSION_SUPPORTS_UPDATE = AwesomeVersion("2023.1.0")
|
||||
MIN_VERSION_SUPPORTS_BUILD_QUEUE = AwesomeVersion("2026.6.0")
|
||||
REFRESH_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
@@ -34,6 +35,7 @@ class ESPHomeDashboardCoordinator(DataUpdateCoordinator[dict[str, ConfiguredDevi
|
||||
self.url = url
|
||||
self.api = ESPHomeDashboardAPI(url, async_get_clientsession(hass))
|
||||
self.supports_update: bool | None = None
|
||||
self.supports_build_queue = False
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[str, ConfiguredDevice]:
|
||||
@@ -41,13 +43,14 @@ class ESPHomeDashboardCoordinator(DataUpdateCoordinator[dict[str, ConfiguredDevi
|
||||
devices = await self.api.get_devices()
|
||||
configured_devices = devices["configured"]
|
||||
|
||||
if (
|
||||
self.supports_update is None
|
||||
and configured_devices
|
||||
and (current_version := configured_devices[0].get("current_version"))
|
||||
if configured_devices and (
|
||||
current_version := configured_devices[0].get("current_version")
|
||||
):
|
||||
self.supports_update = (
|
||||
AwesomeVersion(current_version) > MIN_VERSION_SUPPORTS_UPDATE
|
||||
)
|
||||
version = AwesomeVersion(current_version)
|
||||
if self.supports_update is None:
|
||||
self.supports_update = version > MIN_VERSION_SUPPORTS_UPDATE
|
||||
# The dashboard has its own build queue since 2026.6.0
|
||||
# and can accept multiple compile requests at once
|
||||
self.supports_build_queue = version >= MIN_VERSION_SUPPORTS_BUILD_QUEUE
|
||||
|
||||
return {dev["name"]: dev for dev in configured_devices}
|
||||
|
||||
@@ -233,21 +233,27 @@ class ESPHomeDashboardUpdateEntity(
|
||||
|
||||
# Ensure only one OTA per device at a time
|
||||
async with self._install_lock:
|
||||
# Ensure only one compile at a time for ALL devices
|
||||
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
|
||||
coordinator = self.coordinator
|
||||
api = coordinator.api
|
||||
device = coordinator.data.get(self._device_info.name)
|
||||
assert device is not None
|
||||
configuration = device["configuration"]
|
||||
if not await api.compile(configuration):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="error_compiling",
|
||||
translation_placeholders={
|
||||
"configuration": configuration,
|
||||
},
|
||||
)
|
||||
coordinator = self.coordinator
|
||||
api = coordinator.api
|
||||
device = coordinator.data.get(self._device_info.name)
|
||||
assert device is not None
|
||||
configuration = device["configuration"]
|
||||
if coordinator.supports_build_queue:
|
||||
# The dashboard has its own build queue
|
||||
# and can handle concurrent compile requests
|
||||
compiled = await api.compile(configuration)
|
||||
else:
|
||||
# Ensure only one compile at a time for ALL devices
|
||||
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
|
||||
compiled = await api.compile(configuration)
|
||||
if not compiled:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="error_compiling",
|
||||
translation_placeholders={
|
||||
"configuration": configuration,
|
||||
},
|
||||
)
|
||||
|
||||
# If the device uses deep sleep, there's a small chance it goes
|
||||
# to sleep right after the dashboard connects but before the OTA
|
||||
|
||||
@@ -10,6 +10,7 @@ from awesomeversion.exceptions import AwesomeVersionCompareException
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.esphome.dashboard import async_get_dashboard
|
||||
from homeassistant.components.esphome.update import KEY_UPDATE_LOCK
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
@@ -811,6 +812,98 @@ async def test_attempt_to_update_twice(
|
||||
await update_task
|
||||
|
||||
|
||||
async def test_update_dashboard_with_build_queue_skips_global_lock(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
mock_dashboard: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test the global compile lock is skipped when the dashboard has a build queue."""
|
||||
mock_dashboard["configured"] = [
|
||||
{
|
||||
"name": "test",
|
||||
"current_version": "2026.6.0",
|
||||
"configuration": "test.yaml",
|
||||
}
|
||||
]
|
||||
await async_get_dashboard(hass).async_refresh()
|
||||
await mock_esphome_device(mock_client=mock_client)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Hold the global compile lock; the install must not need it
|
||||
lock = hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock())
|
||||
await lock.acquire()
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile",
|
||||
return_value=True,
|
||||
) as mock_compile,
|
||||
patch(
|
||||
"homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{ATTR_ENTITY_ID: "update.test_firmware"},
|
||||
blocking=True,
|
||||
)
|
||||
lock.release()
|
||||
|
||||
assert len(mock_compile.mock_calls) == 1
|
||||
assert mock_compile.mock_calls[0][1][0] == "test.yaml"
|
||||
|
||||
|
||||
async def test_update_dashboard_without_build_queue_waits_for_global_lock(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
mock_dashboard: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test the global compile lock still serializes installs on older dashboards."""
|
||||
mock_dashboard["configured"] = [
|
||||
{
|
||||
"name": "test",
|
||||
"current_version": "2026.5.0",
|
||||
"configuration": "test.yaml",
|
||||
}
|
||||
]
|
||||
await async_get_dashboard(hass).async_refresh()
|
||||
await mock_esphome_device(mock_client=mock_client)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
lock = hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock())
|
||||
await lock.acquire()
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile",
|
||||
return_value=True,
|
||||
) as mock_compile,
|
||||
patch(
|
||||
"homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
update_task = hass.async_create_task(
|
||||
hass.services.async_call(
|
||||
UPDATE_DOMAIN,
|
||||
SERVICE_INSTALL,
|
||||
{ATTR_ENTITY_ID: "update.test_firmware"},
|
||||
blocking=True,
|
||||
)
|
||||
)
|
||||
for _ in range(5):
|
||||
await asyncio.sleep(0)
|
||||
# The compile must be blocked on the global lock
|
||||
assert len(mock_compile.mock_calls) == 0
|
||||
lock.release()
|
||||
await update_task
|
||||
|
||||
assert len(mock_compile.mock_calls) == 1
|
||||
assert mock_compile.mock_calls[0][1][0] == "test.yaml"
|
||||
|
||||
|
||||
async def test_update_deep_sleep_already_online(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
|
||||
Reference in New Issue
Block a user