Compare commits

...

4 Commits

Author SHA1 Message Date
Jan Čermák 139bacb814 Add test that no orphaned device is created 2026-06-05 16:25:15 +02:00
Jan Čermák 7e87ac49b0 Set device name to Raspberry Pi Firmware 2026-06-05 16:20:50 +02:00
Jan Čermák be020ada62 Fetch the firmware update data directly without coordinator 2026-06-05 16:04:30 +02:00
Jan Čermák 7ff4eafa40 Add Raspberry Pi Firmware update entity
Create update entities for the Raspberry Pi firmware if Supervisor
provides the API. The necessary OS Agent API will be added in Home
Assistant OS 18 (needs latest dev currently) and requires Supervisor
with home-assistant/supervisor#6886 (2026.06.0+, currently on beta).

On some boards that do not support the update, Supervisor creates
RPI_FIRMWARE_UPDATE_BLOCKED issue. This is currently not propagated to
UI, and shows only in the CLI resolution center. This is because the set
of boards that support the update may be extended and it'd be better to
reduce the noise. In case the update is blocked, the update entity is
simply not created now.

After a successful update, the update entity optimistically shows the
current version as the latest one and REBOOT_REQUIRED from Supervisor
prompts user to reboot the board. In case the update isn't applied for
some unknown reason, the update entity will reappear.

Refs home-assistant/operating-system#4631
2026-06-04 09:39:42 +02:00
7 changed files with 575 additions and 18 deletions
+5
View File
@@ -149,6 +149,10 @@ DATA_HASSIO_HTTP_CONFIG: HassKey[dict[str, Any]] = HassKey("hassio_http_config")
DATA_HASSIO_HOST: HassKey[str] = HassKey("hassio_host")
DATA_HASSIO_SUPERVISOR_USER: HassKey[User] = HassKey("hassio_supervisor_user")
# Supervisor `os_info.board` values for boards on which the os-agent exposes
# `io.hass.os.Boards.RaspberryPi.Firmware` (Raspberry Pi 4/5 and Yellow).
BOARDS_WITH_RASPBERRYPI_FIRMWARE = frozenset({"rpi4-64", "rpi5-64", "yellow"})
PLACEHOLDER_KEY_ADDON = "addon"
PLACEHOLDER_KEY_ADDON_INFO = "addon_info"
PLACEHOLDER_KEY_ADDON_DOCUMENTATION = "addon_documentation"
@@ -206,3 +210,4 @@ class SupervisorEntityModel(StrEnum):
SUPERVISOR = "Home Assistant Supervisor"
HOST = "Home Assistant Host"
MOUNT = "Home Assistant Mount"
RPI_FIRMWARE = "Raspberry Pi"
@@ -407,6 +407,26 @@ def async_register_os_in_dev_reg(
dev_reg.async_get_or_create(config_entry_id=entry_id, **params)
@callback
def async_register_rpi_firmware_in_dev_reg(
entry_id: str, dev_reg: dr.DeviceRegistry
) -> None:
"""Register the Raspberry Pi firmware as a device.
Nested under the OS device so the firmware update entity lives on its own
device rather than cluttering the OS one.
"""
params = DeviceInfo(
identifiers={(DOMAIN, "rpi_firmware")},
manufacturer="Raspberry Pi",
model=SupervisorEntityModel.RPI_FIRMWARE,
name="Raspberry Pi Firmware",
entry_type=dr.DeviceEntryType.SERVICE,
via_device=(DOMAIN, "OS"),
)
dev_reg.async_get_or_create(config_entry_id=entry_id, **params)
@callback
def async_register_host_in_dev_reg(
entry_id: str,
@@ -856,6 +876,7 @@ class HassioMainDataUpdateCoordinator(DataUpdateCoordinator[HassioMainData]):
# Build clean coordinator data
self.is_hass_os = info.hassos is not None
new_data = HassioMainData(
core=core_info,
supervisor=supervisor_info,
+156 -4
View File
@@ -1,10 +1,11 @@
"""Update platform for Supervisor."""
import logging
import re
from typing import Any
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import Job
from aiohasupervisor.models import Job, RaspberryPiFirmwareInfo
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from homeassistant.components.update import (
@@ -15,24 +16,60 @@ from homeassistant.components.update import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import ADDONS_COORDINATOR, ATTR_VERSION_LATEST, MAIN_COORDINATOR
from .coordinator import AddonData
from .const import (
ADDONS_COORDINATOR,
ATTR_VERSION_LATEST,
BOARDS_WITH_RASPBERRYPI_FIRMWARE,
DOMAIN,
MAIN_COORDINATOR,
)
from .coordinator import AddonData, async_register_rpi_firmware_in_dev_reg
from .entity import (
HassioAddonEntity,
HassioCoreEntity,
HassioOSEntity,
HassioSupervisorEntity,
)
from .handler import get_supervisor_client
from .jobs import JobSubscription
from .update_helper import update_addon, update_core, update_os
from .update_helper import update_addon, update_core, update_os, update_rpi_firmware
_LOGGER = logging.getLogger(__name__)
ENTITY_DESCRIPTION = UpdateEntityDescription(
translation_key="update",
key=ATTR_VERSION_LATEST,
)
RPI_FIRMWARE_RELEASE_URL = (
"https://github.com/raspberrypi/rpi-eeprom/blob/master/releases.md"
)
def _humanize_rpi_firmware_version(version: str | None) -> str | None:
"""Turn a raw firmware version into a human-readable string.
The Supervisor reports the bootloader EEPROM build as a Unix timestamp,
optionally suffixed with the VL805 EEPROM revision (`timestamp-hexstring`).
Render the timestamp as a UTC `YYYY-MM-DD` date, appending
`(VL805 hexstring)` when a VL805 revision is present.
"""
if version is None:
return None
timestamp, _, vl805 = version.partition("-")
try:
date = dt_util.utc_from_timestamp(int(timestamp)).strftime("%Y-%m-%d")
except ValueError:
return version
if vl805:
return f"{date} (VL805 {vl805})"
return date
async def async_setup_entry(
hass: HomeAssistant,
@@ -61,6 +98,25 @@ async def async_setup_entry(
)
)
# Firmware state only changes after a reboot (which restarts Core) or
# as a result of the install action. Fetch the data here and create
# the device with the RPi firmware update entity (unless the update
# is blocked).
os_info = coordinator.data.os
if os_info is not None and os_info.board in BOARDS_WITH_RASPBERRYPI_FIRMWARE:
client = get_supervisor_client(hass)
try:
rpi_firmware = await client.os.raspberry_pi_firmware_info()
except SupervisorError as err:
# Older supervisors (pre OS 18) don't expose the endpoint.
rpi_firmware = None
_LOGGER.debug("Raspberry Pi firmware info unavailable: %s", err)
if rpi_firmware is not None and not rpi_firmware.update_blocked:
async_register_rpi_firmware_in_dev_reg(
config_entry.entry_id, dr.async_get(hass)
)
entities.append(SupervisorRPiFirmwareUpdateEntity(rpi_firmware))
addons_coordinator = hass.data[ADDONS_COORDINATOR]
entities.extend(
SupervisorAddonUpdateEntity(
@@ -260,6 +316,102 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
await update_os(self.hass, version, backup)
class SupervisorRPiFirmwareUpdateEntity(UpdateEntity):
"""Update entity for the Raspberry Pi firmware (bootloader EEPROM and VL805).
Available on RPi4/RPi5/Yellow and uses `rpi-eeprom-update` via OS Agent.
To apply the update, a reboot is required - the issue is raised by
Supervisor after a successful update action.
"""
_attr_has_entity_name = True
_attr_name = None
_attr_should_poll = False
_attr_supported_features = (
UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES
)
_attr_title = "Raspberry Pi Firmware"
def __init__(self, firmware: RaspberryPiFirmwareInfo) -> None:
"""Initialize entity.
No coordinator is used. The firmware state only changes after a reboot
(which restarts Core and re-fetches at setup) or as a direct result of
the install action (re-fetched in `async_install`), so periodic polling
would never show anything new.
"""
self._firmware = firmware
self._attr_unique_id = "home_assistant_os_rpi_firmware"
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "rpi_firmware")})
@property
def installed_version(self) -> str | None:
"""Composite installed firmware version.
Once an update is applied (`update_pending`) the new version is
reported as installed so the entity reads "up to date".
REBOOT_REQUIRED is indicated after the update and actual switch to the
new version applies after the reboot.
"""
if self._firmware.update_pending:
return _humanize_rpi_firmware_version(self._firmware.latest_version)
return _humanize_rpi_firmware_version(self._firmware.current_version)
@property
def latest_version(self) -> str | None:
"""Composite available firmware version."""
return _humanize_rpi_firmware_version(self._firmware.latest_version)
@property
def entity_picture(self) -> str | None:
"""Return the icon of the entity (the HA OS device icon)."""
return "/api/brands/integration/homeassistant/icon.png?placeholder=no"
@property
def release_url(self) -> str | None:
"""Return a link to the official Raspberry Pi bootloader docs."""
return RPI_FIRMWARE_RELEASE_URL
async def async_release_notes(self) -> str | None:
"""Return the pre-install warning and reboot notice as ha-alert boxes."""
return (
"<ha-alert alert-type='warning'>"
"Do not interrupt the firmware flash. "
"Power loss during the EEPROM update can brick your device."
"</ha-alert>\n\n"
"<ha-alert alert-type='info'>"
"A reboot is required after install for the new firmware to "
"take effect."
"</ha-alert>\n"
)
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
# The flash is a single blocking host call with no progress output, so
# only a boolean in-progress state is available for the duration.
self._attr_in_progress = True
self.async_write_ha_state()
try:
await update_rpi_firmware(self.hass)
except HomeAssistantError:
self._attr_in_progress = False
self.async_write_ha_state()
raise
self._attr_in_progress = False
# The install staged/flashed the new firmware: re-fetch so the entity
# reflects `update_pending` (reads "up to date") without a coordinator.
client = get_supervisor_client(self.hass)
try:
self._firmware = await client.os.raspberry_pi_firmware_info()
except SupervisorError as err:
raise HomeAssistantError(
f"Error fetching Raspberry Pi firmware info: {err}"
) from err
self.async_write_ha_state()
class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
"""Update entity to handle updates for the Home Assistant Supervisor.
@@ -77,3 +77,18 @@ async def update_os(hass: HomeAssistant, version: str | None, backup: bool) -> N
raise HomeAssistantError(
f"Error updating Home Assistant Operating System: {err}"
) from err
async def update_rpi_firmware(hass: HomeAssistant) -> None:
"""Trigger the Raspberry Pi firmware (bootloader EEPROM and VL805) update.
The Supervisor always raises a reboot-required notice on success - the new
firmware only runs after the next reboot.
"""
client = get_supervisor_client(hass)
try:
await client.os.update_raspberry_pi_firmware()
except SupervisorError as err:
raise HomeAssistantError(
f"Error updating Raspberry Pi firmware: {err}"
) from err
+13 -1
View File
@@ -12,7 +12,11 @@ import string
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, MagicMock, patch
from aiohasupervisor import SupervisorClient, SupervisorNotFoundError
from aiohasupervisor import (
SupervisorBadRequestError,
SupervisorClient,
SupervisorNotFoundError,
)
from aiohasupervisor.addons import AddonsClient
from aiohasupervisor.backups import BackupsClient
from aiohasupervisor.discovery import DiscoveryClient
@@ -862,6 +866,10 @@ def supervisor_client() -> Generator[AsyncMock]:
)
supervisor_client.network = AsyncMock(spec=NetworkClient)
supervisor_client.os = AsyncMock(spec=OSClient)
supervisor_client.os.raspberry_pi_firmware_info.side_effect = (
# default for most targets, to be overridden in RPi FW update tests
SupervisorBadRequestError
)
supervisor_client.resolution = AsyncMock(spec=ResolutionClient)
supervisor_client.supervisor = AsyncMock(spec=SupervisorManagementClient)
supervisor_client.store = AsyncMock(spec=StoreClient)
@@ -915,6 +923,10 @@ def supervisor_client() -> Generator[AsyncMock]:
"homeassistant.components.hassio.update_helper.get_supervisor_client",
return_value=supervisor_client,
),
patch(
"homeassistant.components.hassio.update.get_supervisor_client",
return_value=supervisor_client,
),
):
yield supervisor_client
+16 -13
View File
@@ -1164,19 +1164,21 @@ async def test_coordinator_updates_stats_entities_enabled(
@pytest.mark.parametrize(
("board", "integration"),
("board", "integration", "supervisor_calls"),
[
("green", "homeassistant_green"),
("odroid-c2", "hardkernel"),
("odroid-c4", "hardkernel"),
("odroid-n2", "hardkernel"),
("odroid-xu4", "hardkernel"),
("rpi2", "raspberry_pi"),
("rpi3", "raspberry_pi"),
("rpi3-64", "raspberry_pi"),
("rpi4", "raspberry_pi"),
("rpi4-64", "raspberry_pi"),
("yellow", "homeassistant_yellow"),
("green", "homeassistant_green", 16),
("odroid-c2", "hardkernel", 16),
("odroid-c4", "hardkernel", 16),
("odroid-n2", "hardkernel", 16),
("odroid-xu4", "hardkernel", 16),
("rpi2", "raspberry_pi", 16),
("rpi3", "raspberry_pi", 16),
("rpi3-64", "raspberry_pi", 16),
("rpi4", "raspberry_pi", 16),
# rpi4-64, rpi5-64 and yellow add a raspberry_pi_firmware_info
("rpi4-64", "raspberry_pi", 17),
("rpi5-64", "raspberry_pi", 17),
("yellow", "homeassistant_yellow", 17),
],
)
async def test_setup_hardware_integration(
@@ -1185,6 +1187,7 @@ async def test_setup_hardware_integration(
os_info: AsyncMock,
board: str,
integration: str,
supervisor_calls: int,
) -> None:
"""Test setup initiates hardware integration."""
os_info.return_value = replace(os_info.return_value, board=board)
@@ -1204,7 +1207,7 @@ async def test_setup_hardware_integration(
await hass.async_block_till_done(wait_background_tasks=True)
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == supervisor_calls
assert len(mock_setup_entry.mock_calls) == 1
+349
View File
@@ -19,6 +19,7 @@ from aiohasupervisor.models import (
Job,
JobsInfo,
OSUpdate,
RaspberryPiFirmwareInfo,
StoreAddonUpdate,
)
import pytest
@@ -32,6 +33,7 @@ from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@@ -1982,3 +1984,350 @@ async def test_setting_up_core_update_when_addon_fails(
state = hass.states.get("update.home_assistant_core_update")
assert state
assert state.state == "on"
RPI_FIRMWARE_ENTITY_ID = "update.raspberry_pi_firmware"
def _set_rpi_firmware_mock(
os_info: AsyncMock,
supervisor_client: AsyncMock,
*,
board: str,
update_blocked: bool = False,
update_pending: bool = False,
current_version: str = "1765222194",
latest_version: str = "1778498402",
blocked_reason: str | None = None,
) -> None:
"""Configure the Supervisor mocks for an RPi firmware scenario."""
os_info.return_value = replace(os_info.return_value, board=board)
supervisor_client.os.raspberry_pi_firmware_info.side_effect = None
supervisor_client.os.raspberry_pi_firmware_info.return_value = (
RaspberryPiFirmwareInfo(
current_version=current_version,
latest_version=latest_version,
update_available=current_version != latest_version,
update_blocked=update_blocked,
update_pending=update_pending,
blocked_reason=blocked_reason
or ("unsupported_boot_device" if update_blocked else None),
)
)
@pytest.mark.parametrize("board", ["rpi4-64", "rpi5-64", "yellow"])
async def test_rpi_firmware_entity_registered(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
board: str,
) -> None:
"""The Raspberry Pi firmware update entity is registered on RPi4/5/Yellow."""
_set_rpi_firmware_mock(os_info, supervisor_client, board=board)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
# wait_background_tasks drains the home_assistant_yellow auto-discovery
# cascade so it doesn't leak into teardown when board == "yellow".
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get(RPI_FIRMWARE_ENTITY_ID)
assert state is not None
assert state.state == "on"
assert state.attributes["installed_version"] == "2025-12-08"
assert state.attributes["latest_version"] == "2026-05-11"
@pytest.mark.parametrize(
("current_version", "latest_version", "expected_installed", "expected_latest"),
[
pytest.param(
"1765222194",
"1778498402",
"2025-12-08",
"2026-05-11",
id="eeprom_only",
),
pytest.param(
"1765222194-000138a1",
"1778498402-000138c0",
"2025-12-08 (VL805 000138a1)",
"2026-05-11 (VL805 000138c0)",
id="with_vl805",
),
pytest.param(
"not-a-timestamp",
"1778498402",
"not-a-timestamp",
"2026-05-11",
id="unparseable_passthrough",
),
],
)
async def test_rpi_firmware_version_humanized(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
current_version: str,
latest_version: str,
expected_installed: str,
expected_latest: str,
) -> None:
"""Firmware versions are rendered as UTC dates with an optional VL805 suffix."""
_set_rpi_firmware_mock(
os_info,
supervisor_client,
board="rpi5-64",
current_version=current_version,
latest_version=latest_version,
)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
state = hass.states.get(RPI_FIRMWARE_ENTITY_ID)
assert state is not None
assert state.attributes["installed_version"] == expected_installed
assert state.attributes["latest_version"] == expected_latest
async def test_rpi_firmware_device(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
device_registry: dr.DeviceRegistry,
) -> None:
"""The firmware entity lives on its own device nested under the OS device."""
_set_rpi_firmware_mock(os_info, supervisor_client, board="rpi5-64")
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
device = device_registry.async_get_device(identifiers={(DOMAIN, "rpi_firmware")})
assert device is not None
assert device.name == "Raspberry Pi Firmware"
os_device = device_registry.async_get_device(identifiers={(DOMAIN, "OS")})
assert os_device is not None
assert device.via_device_id == os_device.id
entity_entry = er.async_get(hass).async_get(RPI_FIRMWARE_ENTITY_ID)
assert entity_entry is not None
assert entity_entry.device_id == device.id
async def test_rpi_firmware_entity_absent_on_older_supervisor(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
) -> None:
"""If the supervisor doesn't expose firmware info yet, no entity is created."""
os_info.return_value = replace(os_info.return_value, board="rpi5-64")
supervisor_client.os.raspberry_pi_firmware_info.side_effect = (
SupervisorNotFoundError("Not found")
)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID) is None
async def test_rpi_firmware_install(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
) -> None:
"""Install action delegates to the supervisor's update_raspberry_pi_firmware."""
_set_rpi_firmware_mock(os_info, supervisor_client, board="rpi5-64")
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
supervisor_client.os.update_raspberry_pi_firmware.return_value = None
await hass.services.async_call(
"update",
"install",
{"entity_id": RPI_FIRMWARE_ENTITY_ID},
blocking=True,
)
supervisor_client.os.update_raspberry_pi_firmware.assert_called_once_with()
async def test_rpi_firmware_entity_hidden_when_update_blocked(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
device_registry: dr.DeviceRegistry,
) -> None:
"""No firmware entity or device is created when the update is blocked."""
_set_rpi_firmware_mock(
os_info,
supervisor_client,
board="rpi4-64",
update_blocked=True,
)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID) is None
# The device must not be registered without an entity (no orphaned device).
assert (
device_registry.async_get_device(identifiers={(DOMAIN, "rpi_firmware")}) is None
)
async def test_rpi_firmware_release_notes_warning(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Release notes carry the pre-install firmware-flash warning."""
_set_rpi_firmware_mock(os_info, supervisor_client, board="rpi5-64")
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "update/release_notes", "entity_id": RPI_FIRMWARE_ENTITY_ID}
)
msg = await client.receive_json()
assert msg["success"]
assert "Do not interrupt" in msg["result"]
async def test_rpi_firmware_entity_up_to_date_when_update_pending(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
) -> None:
"""A pending (applied) update reads as up to date until the reboot."""
_set_rpi_firmware_mock(
os_info, supervisor_client, board="rpi5-64", update_pending=True
)
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
state = hass.states.get(RPI_FIRMWARE_ENTITY_ID)
assert state is not None
assert state.state == "off"
# The applied version is reported as installed even though the running
# firmware only changes after the reboot.
assert state.attributes["installed_version"] == "2026-05-11"
assert state.attributes["latest_version"] == "2026-05-11"
async def test_rpi_firmware_up_to_date_after_install(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
) -> None:
"""The entity reads up to date once the flash reports an update pending."""
_set_rpi_firmware_mock(os_info, supervisor_client, board="rpi5-64")
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID).state == "on"
# The supervisor reports update_pending once the flash completes; the
# post-install coordinator refresh then picks that up.
async def mark_pending() -> None:
_set_rpi_firmware_mock(
os_info, supervisor_client, board="rpi5-64", update_pending=True
)
supervisor_client.os.update_raspberry_pi_firmware.side_effect = mark_pending
await hass.services.async_call(
"update",
"install",
{"entity_id": RPI_FIRMWARE_ENTITY_ID},
blocking=True,
)
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID).state == "off"
async def test_rpi_firmware_install_sets_in_progress(
hass: HomeAssistant,
supervisor_client: AsyncMock,
os_info: AsyncMock,
) -> None:
"""The firmware entity reports in_progress while the flash runs."""
_set_rpi_firmware_mock(os_info, supervisor_client, board="rpi5-64")
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
await hass.async_block_till_done()
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID).attributes["in_progress"] is False
async def check_progress(hass: HomeAssistant) -> None:
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID).attributes["in_progress"] is True
with patch(
"homeassistant.components.hassio.update.update_rpi_firmware",
side_effect=check_progress,
) as mock_update:
await hass.services.async_call(
"update",
"install",
{"entity_id": RPI_FIRMWARE_ENTITY_ID},
blocking=True,
)
mock_update.assert_called_once()