refactor: another refactor
Some checks failed
Integration Testing / Integration Tests (2024.12.0, 3.11) (push) Failing after 27s
Integration Testing / Integration Tests (2024.12.0, 3.12) (push) Failing after 56s
Integration Testing / Integration Tests (2024.12.0, 3.13) (push) Failing after 1m38s
Integration Testing / Integration Tests (2025.9.4, 3.11) (push) Failing after 19s
Integration Testing / Integration Tests (2025.9.4, 3.12) (push) Failing after 20s
Integration Testing / Integration Tests (2025.9.4, 3.13) (push) Failing after 25s
Code Quality Check / Code Quality Analysis (push) Failing after 20s
Code Quality Check / Security Analysis (push) Failing after 21s

Signed-off-by: Rafal Zielinski <sq4ind@gmail.com>
This commit is contained in:
2025-09-28 17:01:21 +01:00
parent 1aad59c582
commit 554b8ac16b
25 changed files with 464 additions and 543 deletions

View File

@@ -1 +0,0 @@
"""Custom components for Home Assistant."""

View File

@@ -6,7 +6,7 @@ Transform your AdGuard Home into a smart network management powerhouse.
import asyncio
import logging
from datetime import timedelta
from typing import Dict, Any
from typing import Any, Dict
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
@@ -15,8 +15,8 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .api import AdGuardHomeAPI, AdGuardConnectionError
from .const import DOMAIN, PLATFORMS, SCAN_INTERVAL, CONF_SSL, CONF_VERIFY_SSL
from .api import AdGuardHomeAPI, AdGuardConnectionError, AdGuardHomeError
from .const import CONF_SSL, CONF_VERIFY_SSL, DOMAIN, PLATFORMS, SCAN_INTERVAL
from .services import AdGuardControlHubServices
_LOGGER = logging.getLogger(__name__)
@@ -43,19 +43,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.info(
"Successfully connected to AdGuard Home at %s:%s",
entry.data[CONF_HOST],
entry.data[CONF_HOST],
entry.data[CONF_PORT]
)
except Exception as err:
except AdGuardHomeError as err:
_LOGGER.error("Failed to connect to AdGuard Home: %s", err)
raise ConfigEntryNotReady(f"Unable to connect: {err}") from err
except Exception as err:
_LOGGER.exception("Unexpected error connecting to AdGuard Home")
raise ConfigEntryNotReady(f"Unexpected error: {err}") from err
# Create update coordinator
coordinator = AdGuardControlHubCoordinator(hass, api)
try:
await coordinator.async_config_entry_first_refresh()
except Exception as err:
except UpdateFailed as err:
_LOGGER.error("Failed to perform initial data refresh: %s", err)
raise ConfigEntryNotReady(f"Failed to fetch initial data: {err}") from err
@@ -72,17 +75,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except Exception as err:
_LOGGER.error("Failed to set up platforms: %s", err)
# Clean up on failure
hass.data[DOMAIN].pop(entry.entry_id)
hass.data[DOMAIN].pop(entry.entry_id, None)
raise ConfigEntryNotReady(f"Failed to set up platforms: {err}") from err
# Register services (only once)
if not hass.services.has_service(DOMAIN, "block_services"):
services_key = f"{DOMAIN}_services"
if services_key not in hass.data:
services = AdGuardControlHubServices(hass)
services.register_services()
hass.data.setdefault(f"{DOMAIN}_services", services)
hass.data[services_key] = services
_LOGGER.info("AdGuard Control Hub setup complete for %s:%s",
entry.data[CONF_HOST], entry.data[CONF_PORT])
_LOGGER.info("AdGuard Control Hub setup complete")
return True
@@ -92,14 +95,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok:
# Remove this entry's data
hass.data[DOMAIN].pop(entry.entry_id)
hass.data[DOMAIN].pop(entry.entry_id, None)
# Unregister services if this was the last entry
if not hass.data[DOMAIN]:
services = hass.data.get(f"{DOMAIN}_services")
services_key = f"{DOMAIN}_services"
services = hass.data.get(services_key)
if services:
services.unregister_services()
hass.data.pop(f"{DOMAIN}_services", None)
hass.data.pop(services_key, None)
hass.data.pop(DOMAIN, None)
return unload_ok
@@ -108,7 +112,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
class AdGuardControlHubCoordinator(DataUpdateCoordinator):
"""AdGuard Control Hub data update coordinator."""
def __init__(self, hass: HomeAssistant, api: AdGuardHomeAPI):
def __init__(self, hass: HomeAssistant, api: AdGuardHomeAPI) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
@@ -134,7 +138,7 @@ class AdGuardControlHubCoordinator(DataUpdateCoordinator):
results = await asyncio.gather(*tasks, return_exceptions=True)
clients, statistics, status = results
# Update stored data (use empty dict if fetch failed)
# Update stored data (use previous data if fetch failed)
if not isinstance(clients, Exception):
self._clients = {
client["name"]: client

View File

@@ -13,22 +13,18 @@ _LOGGER = logging.getLogger(__name__)
class AdGuardHomeError(Exception):
"""Base exception for AdGuard Home API."""
pass
class AdGuardConnectionError(AdGuardHomeError):
"""Exception for connection errors."""
pass
class AdGuardAuthError(AdGuardHomeError):
"""Exception for authentication errors."""
pass
class AdGuardNotFoundError(AdGuardHomeError):
"""Exception for not found errors."""
pass
class AdGuardHomeAPI:
@@ -43,7 +39,7 @@ class AdGuardHomeAPI:
ssl: bool = False,
session: Optional[aiohttp.ClientSession] = None,
timeout: int = 10,
):
) -> None:
"""Initialize the API wrapper."""
self.host = host
self.port = port
@@ -107,11 +103,13 @@ class AdGuardHomeAPI:
return {"response": text}
except asyncio.TimeoutError as err:
raise AdGuardConnectionError(f"Timeout: {err}")
raise AdGuardConnectionError(f"Timeout: {err}") from err
except ClientError as err:
raise AdGuardConnectionError(f"Client error: {err}")
raise AdGuardConnectionError(f"Client error: {err}") from err
except Exception as err:
raise AdGuardHomeError(f"Unexpected error: {err}")
if isinstance(err, AdGuardHomeError):
raise
raise AdGuardHomeError(f"Unexpected error: {err}") from err
async def test_connection(self) -> bool:
"""Test the connection to AdGuard Home."""

View File

@@ -1,6 +1,7 @@
"""Constants for the AdGuard Control Hub integration."""
from typing import Final
# Integration details
DOMAIN: Final = "adguard_hub"
MANUFACTURER: Final = "AdGuard Control Hub"
@@ -17,7 +18,7 @@ SCAN_INTERVAL: Final = 30
# Platforms
PLATFORMS: Final = [
"switch",
"binary_sensor",
"binary_sensor",
"sensor",
]

View File

@@ -1,14 +1,14 @@
{
"domain": "adguard_hub",
"name": "AdGuard Control Hub",
"codeowners": ["@sq4ind"],
"config_flow": true,
"dependencies": [],
"documentation": "https://git.sq4ind.eu/sq4ind/adguard-control-hub",
"integration_type": "hub",
"iot_class": "local_polling",
"requirements": [
"aiohttp>=3.8.0"
],
"version": "1.0.0"
"domain": "adguard_hub",
"name": "AdGuard Control Hub",
"codeowners": ["@sq4ind"],
"config_flow": true,
"dependencies": [],
"documentation": "https://git.sq4ind.eu/sq4ind/adguard-control-hub",
"integration_type": "hub",
"iot_class": "local_polling",
"requirements": [
"aiohttp>=3.8.0"
],
"version": "1.0.0"
}

View File

@@ -63,7 +63,7 @@ class AdGuardQueriesCounterSensor(AdGuardBaseSensor):
self._attr_native_unit_of_measurement = "queries"
@property
def native_value(self) -> int | None:
def native_value(self):
"""Return the state of the sensor."""
stats = self.coordinator.statistics
return stats.get("num_dns_queries", 0)
@@ -77,12 +77,12 @@ class AdGuardBlockedCounterSensor(AdGuardBaseSensor):
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_blocked_queries"
self._attr_name = "AdGuard Blocked Queries"
self._attr_icon = "mdi:shield-check"
self._attr_icon = ICON_STATISTICS
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
self._attr_native_unit_of_measurement = "queries"
@property
def native_value(self) -> int | None:
def native_value(self):
"""Return the state of the sensor."""
stats = self.coordinator.statistics
return stats.get("num_blocked_filtering", 0)
@@ -96,19 +96,19 @@ class AdGuardBlockingPercentageSensor(AdGuardBaseSensor):
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_blocking_percentage"
self._attr_name = "AdGuard Blocking Percentage"
self._attr_icon = "mdi:percent"
self._attr_icon = ICON_STATISTICS
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = PERCENTAGE
@property
def native_value(self) -> float | None:
def native_value(self):
"""Return the state of the sensor."""
stats = self.coordinator.statistics
total_queries = stats.get("num_dns_queries", 0)
blocked_queries = stats.get("num_blocked_filtering", 0)
if total_queries == 0:
return 0
return 0.0
percentage = (blocked_queries / total_queries) * 100
return round(percentage, 2)
@@ -122,11 +122,11 @@ class AdGuardClientCountSensor(AdGuardBaseSensor):
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_clients_count"
self._attr_name = "AdGuard Clients Count"
self._attr_icon = "mdi:account-multiple"
self._attr_icon = ICON_STATISTICS
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = "clients"
@property
def native_value(self) -> int | None:
def native_value(self):
"""Return the state of the sensor."""
return len(self.coordinator.clients)

View File

@@ -48,16 +48,10 @@ class AdGuardControlHubServices:
self.hass.services.register(
DOMAIN, "emergency_unblock", self.emergency_unblock, schema=SCHEMA_EMERGENCY_UNBLOCK
)
self.hass.services.register(DOMAIN, "add_client", self.add_client)
self.hass.services.register(DOMAIN, "remove_client", self.remove_client)
self.hass.services.register(DOMAIN, "bulk_update_clients", self.bulk_update_clients)
def unregister_services(self) -> None:
"""Unregister all services."""
services = [
"block_services", "unblock_services", "emergency_unblock",
"add_client", "remove_client", "bulk_update_clients"
]
services = ["block_services", "unblock_services", "emergency_unblock"]
for service in services:
if self.hass.services.has_service(DOMAIN, service):
@@ -125,29 +119,3 @@ class AdGuardControlHubServices:
asyncio.create_task(delayed_enable())
except Exception as err:
_LOGGER.error("Failed to execute emergency unblock: %s", err)
async def add_client(self, call: ServiceCall) -> None:
"""Add a new client."""
client_data = dict(call.data)
for entry_data in self.hass.data[DOMAIN].values():
api: AdGuardHomeAPI = entry_data["api"]
try:
await api.add_client(client_data)
_LOGGER.info("Successfully added client: %s", client_data.get("name"))
except Exception as err:
_LOGGER.error("Failed to add client: %s", err)
async def remove_client(self, call: ServiceCall) -> None:
"""Remove a client."""
client_name = call.data.get("name")
for entry_data in self.hass.data[DOMAIN].values():
api: AdGuardHomeAPI = entry_data["api"]
try:
await api.delete_client(client_name)
_LOGGER.info("Successfully removed client: %s", client_name)
except Exception as err:
_LOGGER.error("Failed to remove client: %s", err)
async def bulk_update_clients(self, call: ServiceCall) -> None:
"""Bulk update clients."""
_LOGGER.info("Bulk update clients called")

View File

@@ -1,27 +1,27 @@
{
"config": {
"step": {
"user": {
"title": "AdGuard Control Hub",
"description": "Configure your AdGuard Home connection",
"data": {
"host": "Host",
"port": "Port",
"username": "Username",
"password": "Password",
"ssl": "Use SSL",
"verify_ssl": "Verify SSL Certificate"
}
}
},
"error": {
"cannot_connect": "Failed to connect to AdGuard Home",
"invalid_auth": "Invalid username or password",
"timeout": "Connection timeout",
"unknown": "Unexpected error occurred"
},
"abort": {
"already_configured": "AdGuard Control Hub is already configured"
"config": {
"step": {
"user": {
"title": "AdGuard Control Hub",
"description": "Configure your AdGuard Home connection",
"data": {
"host": "Host",
"port": "Port",
"username": "Username",
"password": "Password",
"ssl": "Use SSL",
"verify_ssl": "Verify SSL Certificate"
}
}
},
"error": {
"cannot_connect": "Failed to connect to AdGuard Home",
"invalid_auth": "Invalid username or password",
"timeout": "Connection timeout",
"unknown": "Unexpected error occurred"
},
"abort": {
"already_configured": "AdGuard Control Hub is already configured"
}
}
}