diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index db1aac99c47f..f119b7c11cbd 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -42,6 +42,8 @@ SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), FritzBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.7 (#70096) + entity_registry_enabled_default=False, key="firmware_update", name="Firmware Update", device_class=BinarySensorDeviceClass.UPDATE, diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index a3ba907366c8..77c5c02c2880 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -30,6 +30,7 @@ PLATFORMS = [ Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, ] CONF_OLD_DISCOVERY = "old_discovery" diff --git a/homeassistant/components/fritz/update.py b/homeassistant/components/fritz/update.py new file mode 100644 index 000000000000..620b932999e0 --- /dev/null +++ b/homeassistant/components/fritz/update.py @@ -0,0 +1,62 @@ +"""Support for AVM FRITZ!Box update platform.""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.update import UpdateEntity, UpdateEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .common import AvmWrapper, FritzBoxBaseEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up AVM FRITZ!Box update entities.""" + _LOGGER.debug("Setting up AVM FRITZ!Box update entities") + avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] + + entities = [FritzBoxUpdateEntity(avm_wrapper, entry.title)] + + async_add_entities(entities) + + +class FritzBoxUpdateEntity(FritzBoxBaseEntity, UpdateEntity): + """Mixin for update entity specific attributes.""" + + _attr_supported_features = UpdateEntityFeature.INSTALL + _attr_title = "FRITZ!OS" + + def __init__( + self, + avm_wrapper: AvmWrapper, + device_friendly_name: str, + ) -> None: + """Init FRITZ!Box connectivity class.""" + self._attr_name = f"{device_friendly_name} FRITZ!OS" + self._attr_unique_id = f"{avm_wrapper.unique_id}-update" + super().__init__(avm_wrapper, device_friendly_name) + + @property + def installed_version(self) -> str | None: + """Version currently in use.""" + return self._avm_wrapper.current_firmware + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + if self._avm_wrapper.update_available: + return self._avm_wrapper.latest_firmware + return self._avm_wrapper.current_firmware + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + await self._avm_wrapper.async_trigger_firmware_update() diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index 79d92a1e22de..39533d07a93a 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -29,6 +29,7 @@ MOCK_HOST = "fake_host" MOCK_IPS = {"fritz.box": "192.168.178.1", "printer": "192.168.178.2"} MOCK_MODELNAME = "FRITZ!Box 7530 AX" MOCK_FIRMWARE = "256.07.29" +MOCK_FIRMWARE_AVAILABLE = "256.07.50" MOCK_SERIAL_NUMBER = "fake_serial_number" MOCK_FIRMWARE_INFO = [True, "1.1.1"] MOCK_MESH_SSID = "TestSSID" diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py new file mode 100644 index 000000000000..f43b35755e84 --- /dev/null +++ b/tests/components/fritz/test_update.py @@ -0,0 +1,105 @@ +"""The tests for the Fritzbox update entity.""" + +from unittest.mock import patch + +from aiohttp import ClientSession + +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_FIRMWARE, MOCK_FIRMWARE_AVAILABLE, MOCK_USER_DATA + +from tests.common import MockConfigEntry + + +async def test_update_entities_initialized( + hass: HomeAssistant, hass_client: ClientSession, fc_class_mock, fh_class_mock +): + """Test update entities.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + updates = hass.states.async_all(UPDATE_DOMAIN) + assert len(updates) == 1 + + +async def test_update_available( + hass: HomeAssistant, hass_client: ClientSession, fc_class_mock, fh_class_mock +): + """Test update entities.""" + + with patch( + "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", + return_value=(True, MOCK_FIRMWARE_AVAILABLE), + ): + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + update = hass.states.get("update.mock_title_fritz_os") + assert update is not None + assert update.state == "on" + assert update.attributes.get("installed_version") == MOCK_FIRMWARE + assert update.attributes.get("latest_version") == MOCK_FIRMWARE_AVAILABLE + + +async def test_no_update_available( + hass: HomeAssistant, hass_client: ClientSession, fc_class_mock, fh_class_mock +): + """Test update entities.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + update = hass.states.get("update.mock_title_fritz_os") + assert update is not None + assert update.state == "off" + assert update.attributes.get("installed_version") == MOCK_FIRMWARE + assert update.attributes.get("latest_version") == MOCK_FIRMWARE + + +async def test_available_update_can_be_installed( + hass: HomeAssistant, hass_client: ClientSession, fc_class_mock, fh_class_mock +): + """Test update entities.""" + + with patch( + "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", + return_value=(True, MOCK_FIRMWARE_AVAILABLE), + ), patch( + "homeassistant.components.fritz.common.FritzBoxTools.async_trigger_firmware_update", + return_value=True, + ) as mocked_update_call: + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + update = hass.states.get("update.mock_title_fritz_os") + assert update is not None + assert update.state == "on" + + assert await hass.services.async_call( + "update", + "install", + {"entity_id": "update.mock_title_fritz_os"}, + blocking=True, + ) + assert mocked_update_call.assert_called_once