From 9180282fc6464fcf65fe981dfc73b167eeb30cda Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 16 Nov 2025 22:52:38 +0100 Subject: [PATCH] Add update entity to AdGUard Home (#156682) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/adguard/__init__.py | 2 +- homeassistant/components/adguard/update.py | 71 +++++++++ tests/components/adguard/__init__.py | 24 +++ tests/components/adguard/conftest.py | 32 ++++ .../adguard/snapshots/test_update.ambr | 61 ++++++++ tests/components/adguard/test_update.py | 138 ++++++++++++++++++ 6 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/adguard/update.py create mode 100644 tests/components/adguard/conftest.py create mode 100644 tests/components/adguard/snapshots/test_update.ambr create mode 100644 tests/components/adguard/test_update.py diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index bbc763d7ec3..cf453c75773 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -45,7 +45,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema( {vol.Optional(CONF_FORCE, default=False): cv.boolean} ) -PLATFORMS = [Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.UPDATE] type AdGuardConfigEntry = ConfigEntry[AdGuardData] diff --git a/homeassistant/components/adguard/update.py b/homeassistant/components/adguard/update.py new file mode 100644 index 00000000000..74d427e973f --- /dev/null +++ b/homeassistant/components/adguard/update.py @@ -0,0 +1,71 @@ +"""AdGuard Home Update platform.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from adguardhome import AdGuardHomeError + +from homeassistant.components.update import UpdateEntity, UpdateEntityFeature +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import AdGuardConfigEntry, AdGuardData +from .const import DOMAIN +from .entity import AdGuardHomeEntity + +SCAN_INTERVAL = timedelta(seconds=300) +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AdGuardConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up AdGuard Home update entity based on a config entry.""" + data = entry.runtime_data + + if (await data.client.update.update_available()).disabled: + return + + async_add_entities([AdGuardHomeUpdate(data, entry)], True) + + +class AdGuardHomeUpdate(AdGuardHomeEntity, UpdateEntity): + """Defines an AdGuard Home update.""" + + _attr_supported_features = UpdateEntityFeature.INSTALL + _attr_name = None + + def __init__( + self, + data: AdGuardData, + entry: AdGuardConfigEntry, + ) -> None: + """Initialize AdGuard Home update.""" + super().__init__(data, entry) + + self._attr_unique_id = "_".join( + [DOMAIN, self.adguard.host, str(self.adguard.port), "update"] + ) + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + value = await self.adguard.update.update_available() + self._attr_installed_version = self.data.version + self._attr_latest_version = value.new_version + self._attr_release_summary = value.announcement + self._attr_release_url = value.announcement_url + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install latest update.""" + try: + await self.adguard.update.begin_update() + except AdGuardHomeError as err: + raise HomeAssistantError(f"Failed to install update: {err}") from err + self.hass.config_entries.async_schedule_reload(self._entry.entry_id) diff --git a/tests/components/adguard/__init__.py b/tests/components/adguard/__init__.py index 4d8ae091dc5..a44c30e4f20 100644 --- a/tests/components/adguard/__init__.py +++ b/tests/components/adguard/__init__.py @@ -1 +1,25 @@ """Tests for the AdGuard Home integration.""" + +from homeassistant.const import CONTENT_TYPE_JSON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def setup_integration( + hass: HomeAssistant, + config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + aioclient_mock.get( + "https://127.0.0.1:3000/control/status", + json={"version": "v0.107.50"}, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/adguard/conftest.py b/tests/components/adguard/conftest.py new file mode 100644 index 00000000000..5245e3aef5c --- /dev/null +++ b/tests/components/adguard/conftest.py @@ -0,0 +1,32 @@ +"""Common fixtures for the adguard tests.""" + +import pytest + +from homeassistant.components.adguard import DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 3000, + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_SSL: True, + CONF_VERIFY_SSL: True, + }, + title="AdGuard Home", + ) diff --git a/tests/components/adguard/snapshots/test_update.ambr b/tests/components/adguard/snapshots/test_update.ambr new file mode 100644 index 00000000000..fc6af1b61ee --- /dev/null +++ b/tests/components/adguard/snapshots/test_update.ambr @@ -0,0 +1,61 @@ +# serializer version: 1 +# name: test_update[update.adguard_home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.adguard_home', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'adguard', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'adguard_127.0.0.1_3000_update', + 'unit_of_measurement': None, + }) +# --- +# name: test_update[update.adguard_home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'display_precision': 0, + 'entity_picture': 'https://brands.home-assistant.io/_/adguard/icon.png', + 'friendly_name': 'AdGuard Home', + 'in_progress': False, + 'installed_version': 'v0.107.50', + 'latest_version': 'v0.107.59', + 'release_summary': 'AdGuard Home v0.107.59 is now available!', + 'release_url': 'https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.107.59', + 'skipped_version': None, + 'supported_features': , + 'title': None, + 'update_percentage': None, + }), + 'context': , + 'entity_id': 'update.adguard_home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/adguard/test_update.py b/tests/components/adguard/test_update.py new file mode 100644 index 00000000000..deb37c16844 --- /dev/null +++ b/tests/components/adguard/test_update.py @@ -0,0 +1,138 @@ +"""Tests for the AdGuard Home update entity.""" + +from unittest.mock import patch + +from adguardhome import AdGuardHomeError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import CONTENT_TYPE_JSON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + aioclient_mock: AiohttpClientMocker, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the adguard update platform.""" + aioclient_mock.post( + "https://127.0.0.1:3000/control/version.json", + json={ + "new_version": "v0.107.59", + "announcement": "AdGuard Home v0.107.59 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.107.59", + "can_autoupdate": True, + "disabled": False, + }, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry, aioclient_mock) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_update_disabled( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the adguard update is disabled.""" + aioclient_mock.post( + "https://127.0.0.1:3000/control/version.json", + json={"disabled": True}, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry, aioclient_mock) + + assert not hass.states.async_all() + + +async def test_update_install( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the adguard update installation.""" + aioclient_mock.post( + "https://127.0.0.1:3000/control/version.json", + json={ + "new_version": "v0.107.59", + "announcement": "AdGuard Home v0.107.59 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.107.59", + "can_autoupdate": True, + "disabled": False, + }, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.post("https://127.0.0.1:3000/control/update") + + with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry, aioclient_mock) + + aioclient_mock.mock_calls.clear() + + await hass.services.async_call( + "update", + "install", + {"entity_id": "update.adguard_home"}, + blocking=True, + ) + + assert aioclient_mock.mock_calls[0][0] == "POST" + assert ( + str(aioclient_mock.mock_calls[0][1]) == "https://127.0.0.1:3000/control/update" + ) + + +async def test_update_install_failed( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the adguard update install failed.""" + aioclient_mock.post( + "https://127.0.0.1:3000/control/version.json", + json={ + "new_version": "v0.107.59", + "announcement": "AdGuard Home v0.107.59 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.107.59", + "can_autoupdate": True, + "disabled": False, + }, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.post( + "https://127.0.0.1:3000/control/update", exc=AdGuardHomeError("boom") + ) + + with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]): + await setup_integration(hass, mock_config_entry, aioclient_mock) + + aioclient_mock.mock_calls.clear() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "update", + "install", + {"entity_id": "update.adguard_home"}, + blocking=True, + ) + + assert aioclient_mock.mock_calls[0][0] == "POST" + assert ( + str(aioclient_mock.mock_calls[0][1]) == "https://127.0.0.1:3000/control/update" + )