From 493859e5898b2fa8696205208f6a2e88f0cd9731 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 13 Aug 2024 18:44:12 +0200 Subject: [PATCH] Add update platform to AirGradient (#123534) --- .../components/airgradient/__init__.py | 1 + .../components/airgradient/update.py | 55 +++++++++++++++ tests/components/airgradient/conftest.py | 1 + .../airgradient/snapshots/test_update.ambr | 58 ++++++++++++++++ tests/components/airgradient/test_update.py | 69 +++++++++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 homeassistant/components/airgradient/update.py create mode 100644 tests/components/airgradient/snapshots/test_update.ambr create mode 100644 tests/components/airgradient/test_update.py diff --git a/homeassistant/components/airgradient/__init__.py b/homeassistant/components/airgradient/__init__.py index 69f1e70c6af..7ee8ac6a3c7 100644 --- a/homeassistant/components/airgradient/__init__.py +++ b/homeassistant/components/airgradient/__init__.py @@ -21,6 +21,7 @@ PLATFORMS: list[Platform] = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, ] diff --git a/homeassistant/components/airgradient/update.py b/homeassistant/components/airgradient/update.py new file mode 100644 index 00000000000..95e64930ea6 --- /dev/null +++ b/homeassistant/components/airgradient/update.py @@ -0,0 +1,55 @@ +"""Airgradient Update platform.""" + +from datetime import timedelta +from functools import cached_property + +from homeassistant.components.update import UpdateDeviceClass, UpdateEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirGradientConfigEntry, AirGradientMeasurementCoordinator +from .entity import AirGradientEntity + +SCAN_INTERVAL = timedelta(hours=1) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AirGradientConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Airgradient update platform.""" + + data = config_entry.runtime_data + + async_add_entities([AirGradientUpdate(data.measurement)], True) + + +class AirGradientUpdate(AirGradientEntity, UpdateEntity): + """Representation of Airgradient Update.""" + + _attr_device_class = UpdateDeviceClass.FIRMWARE + coordinator: AirGradientMeasurementCoordinator + + def __init__(self, coordinator: AirGradientMeasurementCoordinator) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.serial_number}-update" + + @cached_property + def should_poll(self) -> bool: + """Return True because we need to poll the latest version.""" + return True + + @property + def installed_version(self) -> str: + """Return the installed version of the entity.""" + return self.coordinator.data.firmware_version + + async def async_update(self) -> None: + """Update the entity.""" + self._attr_latest_version = ( + await self.coordinator.client.get_latest_firmware_version( + self.coordinator.serial_number + ) + ) diff --git a/tests/components/airgradient/conftest.py b/tests/components/airgradient/conftest.py index a6ee85ecbdd..1899e12c8ae 100644 --- a/tests/components/airgradient/conftest.py +++ b/tests/components/airgradient/conftest.py @@ -44,6 +44,7 @@ def mock_airgradient_client() -> Generator[AsyncMock]: client.get_config.return_value = Config.from_json( load_fixture("get_config_local.json", DOMAIN) ) + client.get_latest_firmware_version.return_value = "3.1.4" yield client diff --git a/tests/components/airgradient/snapshots/test_update.ambr b/tests/components/airgradient/snapshots/test_update.ambr new file mode 100644 index 00000000000..c639a97d5dd --- /dev/null +++ b/tests/components/airgradient/snapshots/test_update.ambr @@ -0,0 +1,58 @@ +# serializer version: 1 +# name: test_all_entities[update.airgradient_firmware-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.airgradient_firmware', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Firmware', + 'platform': 'airgradient', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '84fce612f5b8-update', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[update.airgradient_firmware-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'entity_picture': 'https://brands.home-assistant.io/_/airgradient/icon.png', + 'friendly_name': 'Airgradient Firmware', + 'in_progress': False, + 'installed_version': '3.1.1', + 'latest_version': '3.1.4', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + }), + 'context': , + 'entity_id': 'update.airgradient_firmware', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/airgradient/test_update.py b/tests/components/airgradient/test_update.py new file mode 100644 index 00000000000..020a9a82a71 --- /dev/null +++ b/tests/components/airgradient/test_update.py @@ -0,0 +1,69 @@ +"""Tests for the AirGradient update platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_OFF, STATE_ON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.airgradient.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_update_mechanism( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test update entity.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "3.1.1" + assert state.attributes["latest_version"] == "3.1.4" + mock_airgradient_client.get_latest_firmware_version.assert_called_once() + mock_airgradient_client.get_latest_firmware_version.reset_mock() + + mock_airgradient_client.get_current_measures.return_value.firmware_version = "3.1.4" + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "3.1.4" + assert state.attributes["latest_version"] == "3.1.4" + + mock_airgradient_client.get_latest_firmware_version.return_value = "3.1.5" + + freezer.tick(timedelta(minutes=59)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_airgradient_client.get_latest_firmware_version.assert_called_once() + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "3.1.4" + assert state.attributes["latest_version"] == "3.1.5"