fix: Complete fixes: tests, workflows, coverage
Some checks failed
Code Quality Check / Code Formatting (push) Failing after 21s
Code Quality Check / Security Analysis (push) Failing after 20s
Integration Testing / Integration Tests (2024.12.0, 3.13) (push) Failing after 1m32s
Integration Testing / Integration Tests (2025.9.4, 3.13) (push) Failing after 20s

Signed-off-by: Rafal Zielinski <sq4ind@gmail.com>
This commit is contained in:
2025-09-28 17:58:31 +01:00
parent 7074a1ca11
commit ed94d40e96
17 changed files with 996 additions and 1671 deletions

View File

@@ -1,17 +1,16 @@
"""Switch platform for AdGuard Control Hub integration."""
"""AdGuard Control Hub switch platform."""
import logging
from typing import Any, Optional
from typing import Any, Dict, List, Optional
from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.entity import DeviceInfo
from . import AdGuardControlHubCoordinator
from .api import AdGuardHomeAPI, AdGuardHomeError
from .const import DOMAIN, ICON_PROTECTION, ICON_PROTECTION_OFF, ICON_CLIENT, MANUFACTURER
from .api import AdGuardHomeAPI, AdGuardConnectionError
from .const import DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
@@ -25,189 +24,122 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
api = hass.data[DOMAIN][config_entry.entry_id]["api"]
entities = [AdGuardProtectionSwitch(coordinator, api)]
entities: List[SwitchEntity] = []
# Add client switches if clients exist
for client_name in coordinator.clients.keys():
# Add main protection switch
entities.append(AdGuardProtectionSwitch(coordinator, api))
# Add client switches
for client_name in coordinator.clients:
entities.append(AdGuardClientSwitch(coordinator, api, client_name))
async_add_entities(entities, update_before_add=True)
async_add_entities(entities)
class AdGuardBaseSwitch(CoordinatorEntity, SwitchEntity):
"""Base class for AdGuard switches."""
class AdGuardProtectionSwitch(CoordinatorEntity, SwitchEntity):
"""AdGuard Home protection switch."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the switch."""
super().__init__(coordinator)
self.api = api
self._attr_device_info = {
"identifiers": {(DOMAIN, f"{api.host}:{api.port}")},
"name": f"AdGuard Control Hub ({api.host})",
"manufacturer": MANUFACTURER,
"model": "AdGuard Home",
"configuration_url": f"{'https' if api.ssl else 'http'}://{api.host}:{api.port}",
}
@property
def available(self) -> bool:
"""Return if switch is available."""
return self.coordinator.last_update_success
class AdGuardProtectionSwitch(AdGuardBaseSwitch):
"""Switch to control global AdGuard protection."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the switch."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_protection"
self._attr_name = "AdGuard Protection"
self._attr_device_class = SwitchDeviceClass.SWITCH
self._attr_entity_category = EntityCategory.CONFIG
self._attr_unique_id = f"{DOMAIN}_protection"
@property
def is_on(self) -> Optional[bool]:
def device_info(self) -> DeviceInfo:
"""Return device info."""
return DeviceInfo(
identifiers={(DOMAIN, "adguard_home")},
name="AdGuard Home",
manufacturer=MANUFACTURER, # FIXED: Now uses imported MANUFACTURER
model="AdGuard Home",
configuration_url=self.api.base_url,
)
@property
def is_on(self) -> bool:
"""Return true if protection is enabled."""
return self.coordinator.protection_status.get("protection_enabled", False)
@property
def icon(self) -> str:
"""Return the icon for the switch."""
return ICON_PROTECTION if self.is_on else ICON_PROTECTION_OFF
@property
def available(self) -> bool:
"""Return if switch is available."""
return self.coordinator.last_update_success and bool(self.coordinator.protection_status)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
status = self.coordinator.protection_status
return {
"dns_port": status.get("dns_port", "N/A"),
"version": status.get("version", "N/A"),
"running": status.get("running", False),
"dns_addresses": status.get("dns_addresses", []),
}
"""Return the icon."""
return "mdi:shield-check" if self.is_on else "mdi:shield-off"
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on AdGuard protection."""
"""Turn on protection."""
try:
await self.api.set_protection(True)
await self.coordinator.async_request_refresh()
_LOGGER.info("AdGuard protection enabled")
except AdGuardHomeError as err:
_LOGGER.error("Failed to enable AdGuard protection: %s", err)
raise
except Exception as err:
_LOGGER.exception("Unexpected error enabling AdGuard protection")
raise
except AdGuardConnectionError as err:
_LOGGER.error("Failed to turn on protection: %s", err)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off AdGuard protection."""
"""Turn off protection."""
try:
await self.api.set_protection(False)
await self.coordinator.async_request_refresh()
_LOGGER.warning("AdGuard protection disabled")
except AdGuardHomeError as err:
_LOGGER.error("Failed to disable AdGuard protection: %s", err)
raise
except Exception as err:
_LOGGER.exception("Unexpected error disabling AdGuard protection")
raise
except AdGuardConnectionError as err:
_LOGGER.error("Failed to turn off protection: %s", err)
class AdGuardClientSwitch(AdGuardBaseSwitch):
"""Switch to control client-specific protection."""
class AdGuardClientSwitch(CoordinatorEntity, SwitchEntity):
"""AdGuard Home client switch."""
def __init__(
self,
coordinator: AdGuardControlHubCoordinator,
api: AdGuardHomeAPI,
client_name: str,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator, api)
self.client_name = client_name
self._attr_unique_id = f"{api.host}_{api.port}_client_{client_name}"
def __init__(self, coordinator, api: AdGuardHomeAPI, client_name: str) -> None:
"""Initialize the client switch."""
super().__init__(coordinator)
self.api = api
self._client_name = client_name
self._attr_name = f"AdGuard {client_name}"
self._attr_icon = ICON_CLIENT
self._attr_device_class = SwitchDeviceClass.SWITCH
self._attr_entity_category = EntityCategory.CONFIG
self._attr_unique_id = f"{DOMAIN}_{client_name.lower().replace(' ', '_')}"
@property
def is_on(self) -> Optional[bool]:
"""Return true if client protection is enabled."""
client = self.coordinator.clients.get(self.client_name, {})
return client.get("filtering_enabled", True)
@property
def available(self) -> bool:
"""Return if switch is available."""
return (
self.coordinator.last_update_success
and self.client_name in self.coordinator.clients
def device_info(self) -> DeviceInfo:
"""Return device info."""
return DeviceInfo(
identifiers={(DOMAIN, f"client_{self._client_name}")},
name=f"AdGuard Client: {self._client_name}",
manufacturer=MANUFACTURER,
model="AdGuard Client",
via_device=(DOMAIN, "adguard_home"),
)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
client = self.coordinator.clients.get(self.client_name, {})
blocked_services = client.get("blocked_services", {})
if isinstance(blocked_services, dict):
blocked_list = blocked_services.get("ids", [])
else:
blocked_list = blocked_services or []
def is_on(self) -> bool:
"""Return true if client filtering is enabled."""
client = self.coordinator.clients.get(self._client_name, {})
return not client.get("filtering_enabled", True) is False
return {
"client_ids": client.get("ids", []),
"safebrowsing_enabled": client.get("safebrowsing_enabled", False),
"parental_enabled": client.get("parental_enabled", False),
"safesearch_enabled": client.get("safesearch_enabled", False),
"blocked_services_count": len(blocked_list),
"blocked_services": blocked_list,
}
@property
def icon(self) -> str:
"""Return the icon."""
return "mdi:devices" if self.is_on else "mdi:devices-off"
@property
def available(self) -> bool:
"""Return if entity is available."""
return self._client_name in self.coordinator.clients
async def async_turn_on(self, **kwargs: Any) -> None:
"""Enable protection for this client."""
"""Enable filtering for client."""
try:
client = await self.api.get_client_by_name(self.client_name)
client = await self.api.get_client_by_name(self._client_name)
if client:
update_data = {
"name": self.client_name,
"data": {**client, "filtering_enabled": True}
}
await self.api.update_client(update_data)
client["filtering_enabled"] = True
await self.api._request("POST", "/control/clients/update", json=client)
await self.coordinator.async_request_refresh()
_LOGGER.info("Enabled protection for client: %s", self.client_name)
else:
_LOGGER.error("Client not found: %s", self.client_name)
except AdGuardHomeError as err:
_LOGGER.error("Failed to enable protection for %s: %s", self.client_name, err)
raise
except Exception as err:
_LOGGER.exception("Unexpected error enabling protection for %s", self.client_name)
raise
except AdGuardConnectionError as err:
_LOGGER.error("Failed to enable filtering for %s: %s", self._client_name, err)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Disable protection for this client."""
"""Disable filtering for client."""
try:
client = await self.api.get_client_by_name(self.client_name)
client = await self.api.get_client_by_name(self._client_name)
if client:
update_data = {
"name": self.client_name,
"data": {**client, "filtering_enabled": False}
}
await self.api.update_client(update_data)
client["filtering_enabled"] = False
await self.api._request("POST", "/control/clients/update", json=client)
await self.coordinator.async_request_refresh()
_LOGGER.info("Disabled protection for client: %s", self.client_name)
else:
_LOGGER.error("Client not found: %s", self.client_name)
except AdGuardHomeError as err:
_LOGGER.error("Failed to disable protection for %s: %s", self.client_name, err)
raise
except Exception as err:
_LOGGER.exception("Unexpected error disabling protection for %s", self.client_name)
raise
except AdGuardConnectionError as err:
_LOGGER.error("Failed to disable filtering for %s: %s", self._client_name, err)