1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-01 11:46:40 +01:00
Files
core/tests/components/raspberry_pi/test_update.py
T
2026-06-17 23:36:01 +02:00

296 lines
10 KiB
Python

"""Test the Raspberry Pi firmware update entity."""
from collections.abc import Generator
from datetime import timedelta
from unittest.mock import AsyncMock, patch
from aiohasupervisor import SupervisorError, SupervisorNotFoundError
from aiohasupervisor.models import RaspberryPiFirmwareInfo
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN, HassioNotReadyError
from homeassistant.components.raspberry_pi.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import PLATFORM_NOT_READY_BASE_WAIT_TIME
from homeassistant.setup import async_setup_component
from tests.common import (
MockConfigEntry,
MockModule,
async_fire_time_changed,
mock_integration,
)
RPI_FIRMWARE_ENTITY_ID = "update.raspberry_pi_5_firmware"
@pytest.fixture(autouse=True)
def mock_rpi_power() -> Generator[None]:
"""Mock the rpi_power integration."""
with patch(
"homeassistant.components.rpi_power.async_setup_entry",
return_value=True,
):
yield
async def _setup_rpi(hass: HomeAssistant, board: str) -> None:
"""Set up the raspberry_pi config entry on a given board."""
mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
config_entry = MockConfigEntry(data={}, domain=DOMAIN, title="Raspberry Pi")
config_entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.raspberry_pi.get_os_info",
return_value={"board": board},
),
patch(
"homeassistant.components.raspberry_pi.update.get_os_info",
return_value={"board": board},
),
patch("homeassistant.components.rpi_power.config_flow.new_under_voltage"),
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
async def test_rpi_firmware_update_entity(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
"""The firmware update entity is created on its own RPi board device."""
supervisor_client.os.raspberry_pi_firmware_info.return_value = (
RaspberryPiFirmwareInfo(
current_version="1765222194",
latest_version="1778498402",
update_available=True,
update_blocked=False,
update_pending=False,
blocked_reason=None,
)
)
await _setup_rpi(hass, "rpi5-64")
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"
assert (
state.attributes["release_url"]
== "https://github.com/raspberrypi/rpi-eeprom/blob/master/firmware-2712/release-notes.md"
)
async def test_rpi_firmware_entity_absent_on_older_supervisor(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
"""No entity when the Supervisor doesn't expose the endpoint (404)."""
supervisor_client.os.raspberry_pi_firmware_info.side_effect = (
SupervisorNotFoundError("Not found")
)
await _setup_rpi(hass, "rpi5-64")
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID) is None
async def test_rpi_firmware_entity_absent_on_unsupported_board(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
"""No entity (or firmware probe) on boards without an EEPROM bootloader."""
await _setup_rpi(hass, "rpi3-64")
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID) is None
supervisor_client.os.raspberry_pi_firmware_info.assert_not_called()
async def test_rpi_firmware_entity_absent_when_update_blocked(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
"""No entity when the update is blocked on this boot device."""
supervisor_client.os.raspberry_pi_firmware_info.return_value = (
RaspberryPiFirmwareInfo(
current_version="1765222194",
latest_version="1778498402",
update_available=True,
update_blocked=True,
update_pending=False,
blocked_reason="unsupported_boot_device",
)
)
await _setup_rpi(hass, "rpi5-64")
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID) is None
async def test_rpi_firmware_platform_retries_when_os_info_unavailable(
hass: HomeAssistant,
supervisor_client: AsyncMock,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""The platform retries and recovers once the OS info becomes available."""
supervisor_client.os.raspberry_pi_firmware_info.return_value = (
RaspberryPiFirmwareInfo(
current_version="1765222194",
latest_version="1778498402",
update_available=True,
update_blocked=False,
update_pending=False,
blocked_reason=None,
)
)
mock_integration(hass, MockModule("hassio"))
await async_setup_component(hass, HASSIO_DOMAIN, {})
config_entry = MockConfigEntry(data={}, domain=DOMAIN, title="Raspberry Pi")
config_entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.raspberry_pi.get_os_info",
return_value={"board": "rpi5-64"},
),
patch(
"homeassistant.components.raspberry_pi.update.get_os_info",
side_effect=HassioNotReadyError,
) as mock_update_os_info,
patch("homeassistant.components.rpi_power.config_flow.new_under_voltage"),
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# First attempt fails: OS info isn't ready, so no entity is created yet.
# The PlatformNotReady message comes from the supervisor_not_ready
# translation.
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID) is None
assert "Supervisor is not ready" in caplog.text
# Once the OS info is available, the scheduled retry creates the entity.
mock_update_os_info.side_effect = None
mock_update_os_info.return_value = {"board": "rpi5-64"}
freezer.tick(timedelta(seconds=PLATFORM_NOT_READY_BASE_WAIT_TIME + 1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(RPI_FIRMWARE_ENTITY_ID) is not None
async def test_rpi_firmware_install_success(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
"""Installing reports the new version as installed once it is applied."""
supervisor_client.os.raspberry_pi_firmware_info.return_value = (
RaspberryPiFirmwareInfo(
current_version="1765222194",
latest_version="1778498402",
update_available=True,
update_blocked=False,
update_pending=False,
blocked_reason=None,
)
)
await _setup_rpi(hass, "rpi5-64")
state = hass.states.get(RPI_FIRMWARE_ENTITY_ID)
assert state is not None
assert state.state == "on"
# After the flash the Supervisor reports the update as pending (applied,
# awaiting reboot), so the entity should read "up to date".
supervisor_client.os.raspberry_pi_firmware_info.return_value = (
RaspberryPiFirmwareInfo(
current_version="1765222194",
latest_version="1778498402",
update_available=False,
update_blocked=False,
update_pending=True,
blocked_reason=None,
)
)
await hass.services.async_call(
"update",
"install",
{"entity_id": RPI_FIRMWARE_ENTITY_ID},
blocking=True,
)
supervisor_client.os.update_raspberry_pi_firmware.assert_awaited_once()
state = hass.states.get(RPI_FIRMWARE_ENTITY_ID)
assert state is not None
assert state.state == "off"
assert state.attributes["installed_version"] == "2026-05-11"
assert state.attributes["latest_version"] == "2026-05-11"
async def test_rpi_firmware_install_failure(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> None:
"""A failed update is surfaced to the user as a HomeAssistantError."""
supervisor_client.os.raspberry_pi_firmware_info.return_value = (
RaspberryPiFirmwareInfo(
current_version="1765222194",
latest_version="1778498402",
update_available=True,
update_blocked=False,
update_pending=False,
blocked_reason=None,
)
)
await _setup_rpi(hass, "rpi5-64")
supervisor_client.os.update_raspberry_pi_firmware.side_effect = SupervisorError(
"boom"
)
with pytest.raises(
HomeAssistantError, match="Error updating Raspberry Pi firmware"
):
await hass.services.async_call(
"update",
"install",
{"entity_id": RPI_FIRMWARE_ENTITY_ID},
blocking=True,
)
state = hass.states.get(RPI_FIRMWARE_ENTITY_ID)
assert state is not None
assert state.state == "on"
async def test_rpi_firmware_install_refresh_failure_keeps_previous_info(
hass: HomeAssistant,
supervisor_client: AsyncMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A failed info refresh after a successful update is logged, not raised."""
supervisor_client.os.raspberry_pi_firmware_info.return_value = (
RaspberryPiFirmwareInfo(
current_version="1765222194",
latest_version="1778498402",
update_available=True,
update_blocked=False,
update_pending=False,
blocked_reason=None,
)
)
await _setup_rpi(hass, "rpi5-64")
# The update call succeeds, but the follow-up info refresh fails.
supervisor_client.os.raspberry_pi_firmware_info.side_effect = SupervisorError(
"boom"
)
await hass.services.async_call(
"update",
"install",
{"entity_id": RPI_FIRMWARE_ENTITY_ID},
blocking=True,
)
supervisor_client.os.update_raspberry_pi_firmware.assert_awaited_once()
state = hass.states.get(RPI_FIRMWARE_ENTITY_ID)
assert state is not None
assert state.state == "on"
assert "Failed to refresh Raspberry Pi firmware info" in caplog.text