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
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:
@@ -1 +0,0 @@
|
||||
"""Custom components for Home Assistant."""
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
@@ -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",
|
||||
]
|
||||
|
||||
|
@@ -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"
|
||||
}
|
@@ -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)
|
||||
|
@@ -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")
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user