mirror of
https://github.com/home-assistant/core.git
synced 2026-07-01 11:46:40 +01:00
296 lines
10 KiB
Python
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
|