fix: Fix CI/CD issues and enhance integration
Some checks failed
Integration Testing / Integration Tests (2024.12.0, 3.11) (push) Failing after 22s
Integration Testing / Integration Tests (2024.12.0, 3.12) (push) Failing after 21s
Integration Testing / Integration Tests (2024.12.0, 3.13) (push) Failing after 1m32s
Integration Testing / Integration Tests (2025.9.4, 3.11) (push) Failing after 15s
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 20s

Signed-off-by: Rafal Zielinski <sq4ind@gmail.com>
This commit is contained in:
2025-09-28 17:24:46 +01:00
parent bcec7bbf1a
commit 8281a1813d
17 changed files with 1439 additions and 276 deletions

View File

@@ -33,6 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
username=entry.data.get(CONF_USERNAME),
password=entry.data.get(CONF_PASSWORD),
ssl=entry.data.get(CONF_SSL, False),
verify_ssl=entry.data.get(CONF_VERIFY_SSL, True),
session=session,
)

View File

@@ -27,6 +27,10 @@ class AdGuardNotFoundError(AdGuardHomeError):
"""Exception for not found errors."""
class AdGuardTimeoutError(AdGuardHomeError):
"""Exception for timeout errors."""
class AdGuardHomeAPI:
"""API wrapper for AdGuard Home."""
@@ -39,6 +43,7 @@ class AdGuardHomeAPI:
ssl: bool = False,
session: Optional[aiohttp.ClientSession] = None,
timeout: int = 10,
verify_ssl: bool = True,
) -> None:
"""Initialize the API wrapper."""
self.host = host
@@ -46,6 +51,7 @@ class AdGuardHomeAPI:
self.username = username
self.password = password
self.ssl = ssl
self.verify_ssl = verify_ssl
self._session = session
self._timeout = ClientTimeout(total=timeout)
protocol = "https" if ssl else "http"
@@ -55,7 +61,11 @@ class AdGuardHomeAPI:
async def __aenter__(self):
"""Async context manager entry."""
if self._own_session:
self._session = aiohttp.ClientSession(timeout=self._timeout)
connector = aiohttp.TCPConnector(ssl=self.verify_ssl)
self._session = aiohttp.ClientSession(
timeout=self._timeout,
connector=connector
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
@@ -67,7 +77,11 @@ class AdGuardHomeAPI:
def session(self) -> aiohttp.ClientSession:
"""Get the session, creating one if needed."""
if not self._session:
self._session = aiohttp.ClientSession(timeout=self._timeout)
connector = aiohttp.TCPConnector(ssl=self.verify_ssl)
self._session = aiohttp.ClientSession(
timeout=self._timeout,
connector=connector
)
return self._session
async def _request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
@@ -81,7 +95,7 @@ class AdGuardHomeAPI:
try:
async with self.session.request(
method, url, json=data, headers=headers, auth=auth
method, url, json=data, headers=headers, auth=auth, ssl=self.verify_ssl
) as response:
if response.status == 401:
@@ -93,17 +107,19 @@ class AdGuardHomeAPI:
response.raise_for_status()
# Handle empty responses
if response.status == 204 or not response.content_length:
return {}
try:
return await response.json()
except aiohttp.ContentTypeError:
except (aiohttp.ContentTypeError, ValueError):
# If not JSON, return text response
text = await response.text()
return {"response": text}
except asyncio.TimeoutError as err:
raise AdGuardConnectionError(f"Timeout: {err}") from err
raise AdGuardTimeoutError(f"Request timeout: {err}") from err
except ClientError as err:
raise AdGuardConnectionError(f"Client error: {err}") from err
except Exception as err:
@@ -114,8 +130,8 @@ class AdGuardHomeAPI:
async def test_connection(self) -> bool:
"""Test the connection to AdGuard Home."""
try:
await self._request("GET", API_ENDPOINTS["status"])
return True
response = await self._request("GET", API_ENDPOINTS["status"])
return isinstance(response, dict) and len(response) > 0
except Exception:
return False
@@ -176,7 +192,8 @@ class AdGuardHomeAPI:
return client
return None
except Exception:
except Exception as err:
_LOGGER.error("Error getting client %s: %s", client_name, err)
return None
async def update_client_blocked_services(
@@ -192,6 +209,7 @@ class AdGuardHomeAPI:
if not client:
raise AdGuardNotFoundError(f"Client '{client_name}' not found")
# Format blocked services data according to AdGuard Home API
blocked_services_data = {
"ids": blocked_services,
"schedule": {"time_zone": "Local"}
@@ -207,6 +225,14 @@ class AdGuardHomeAPI:
return await self.update_client(update_data)
async def get_blocked_services_list(self) -> Dict[str, Any]:
"""Get list of available blocked services."""
try:
return await self._request("GET", API_ENDPOINTS["blocked_services_all"])
except Exception as err:
_LOGGER.error("Error getting blocked services list: %s", err)
return {}
async def close(self) -> None:
"""Close the API session if we own it."""
if self._own_session and self._session:

View File

@@ -1,10 +1,11 @@
"""Binary sensor platform for AdGuard Control Hub integration."""
import logging
from typing import Any
from typing import Any, Optional
from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass
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
@@ -26,15 +27,26 @@ async def async_setup_entry(
entities = [
AdGuardProtectionBinarySensor(coordinator, api),
AdGuardServerRunningBinarySensor(coordinator, api),
AdGuardSafeBrowsingBinarySensor(coordinator, api),
AdGuardParentalControlBinarySensor(coordinator, api),
AdGuardSafeSearchBinarySensor(coordinator, api),
]
async_add_entities(entities)
# Add client-specific binary sensors
for client_name in coordinator.clients.keys():
entities.extend([
AdGuardClientFilteringBinarySensor(coordinator, api, client_name),
AdGuardClientSafeBrowsingBinarySensor(coordinator, api, client_name),
])
async_add_entities(entities, update_before_add=True)
class AdGuardBaseBinarySensor(CoordinatorEntity, BinarySensorEntity):
"""Base class for AdGuard binary sensors."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI):
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self.api = api
@@ -43,21 +55,23 @@ class AdGuardBaseBinarySensor(CoordinatorEntity, BinarySensorEntity):
"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}",
}
class AdGuardProtectionBinarySensor(AdGuardBaseBinarySensor):
"""Binary sensor to show AdGuard protection status."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI):
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_protection_enabled"
self._attr_name = "AdGuard Protection Status"
self._attr_device_class = BinarySensorDeviceClass.RUNNING
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def is_on(self) -> bool | None:
def is_on(self) -> Optional[bool]:
"""Return true if protection is enabled."""
return self.coordinator.protection_status.get("protection_enabled", False)
@@ -66,6 +80,11 @@ class AdGuardProtectionBinarySensor(AdGuardBaseBinarySensor):
"""Return the icon for the binary sensor."""
return ICON_PROTECTION if self.is_on else ICON_PROTECTION_OFF
@property
def available(self) -> bool:
"""Return if sensor 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."""
@@ -74,4 +93,205 @@ class AdGuardProtectionBinarySensor(AdGuardBaseBinarySensor):
"dns_port": status.get("dns_port", "N/A"),
"version": status.get("version", "N/A"),
"running": status.get("running", False),
"dhcp_available": status.get("dhcp_available", False),
}
class AdGuardServerRunningBinarySensor(AdGuardBaseBinarySensor):
"""Binary sensor to show if AdGuard server is running."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_server_running"
self._attr_name = "AdGuard Server Running"
self._attr_device_class = BinarySensorDeviceClass.RUNNING
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def is_on(self) -> Optional[bool]:
"""Return true if server is running."""
return self.coordinator.protection_status.get("running", False)
@property
def icon(self) -> str:
"""Return the icon for the binary sensor."""
return "mdi:server" if self.is_on else "mdi:server-off"
@property
def available(self) -> bool:
"""Return if sensor is available."""
return self.coordinator.last_update_success and bool(self.coordinator.protection_status)
class AdGuardSafeBrowsingBinarySensor(AdGuardBaseBinarySensor):
"""Binary sensor to show SafeBrowsing status."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_safebrowsing_enabled"
self._attr_name = "AdGuard SafeBrowsing"
self._attr_device_class = BinarySensorDeviceClass.SAFETY
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def is_on(self) -> Optional[bool]:
"""Return true if SafeBrowsing is enabled."""
return self.coordinator.protection_status.get("safebrowsing_enabled", False)
@property
def icon(self) -> str:
"""Return the icon for the binary sensor."""
return "mdi:shield-check" if self.is_on else "mdi:shield-off"
@property
def available(self) -> bool:
"""Return if sensor is available."""
return self.coordinator.last_update_success and bool(self.coordinator.protection_status)
class AdGuardParentalControlBinarySensor(AdGuardBaseBinarySensor):
"""Binary sensor to show Parental Control status."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_parental_enabled"
self._attr_name = "AdGuard Parental Control"
self._attr_device_class = BinarySensorDeviceClass.SAFETY
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def is_on(self) -> Optional[bool]:
"""Return true if Parental Control is enabled."""
return self.coordinator.protection_status.get("parental_enabled", False)
@property
def icon(self) -> str:
"""Return the icon for the binary sensor."""
return "mdi:account-child" if self.is_on else "mdi:account-child-outline"
@property
def available(self) -> bool:
"""Return if sensor is available."""
return self.coordinator.last_update_success and bool(self.coordinator.protection_status)
class AdGuardSafeSearchBinarySensor(AdGuardBaseBinarySensor):
"""Binary sensor to show Safe Search status."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_safesearch_enabled"
self._attr_name = "AdGuard Safe Search"
self._attr_device_class = BinarySensorDeviceClass.SAFETY
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def is_on(self) -> Optional[bool]:
"""Return true if Safe Search is enabled."""
return self.coordinator.protection_status.get("safesearch_enabled", False)
@property
def icon(self) -> str:
"""Return the icon for the binary sensor."""
return "mdi:shield-search" if self.is_on else "mdi:magnify"
@property
def available(self) -> bool:
"""Return if sensor is available."""
return self.coordinator.last_update_success and bool(self.coordinator.protection_status)
class AdGuardClientFilteringBinarySensor(AdGuardBaseBinarySensor):
"""Binary sensor to show client-specific filtering status."""
def __init__(
self,
coordinator: AdGuardControlHubCoordinator,
api: AdGuardHomeAPI,
client_name: str,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator, api)
self.client_name = client_name
self._attr_unique_id = f"{api.host}_{api.port}_client_{client_name}_filtering"
self._attr_name = f"AdGuard {client_name} Filtering"
self._attr_device_class = BinarySensorDeviceClass.RUNNING
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def is_on(self) -> Optional[bool]:
"""Return true if client filtering is enabled."""
client = self.coordinator.clients.get(self.client_name, {})
return client.get("filtering_enabled", True)
@property
def icon(self) -> str:
"""Return the icon for the binary sensor."""
return "mdi:filter" if self.is_on else "mdi:filter-off"
@property
def available(self) -> bool:
"""Return if sensor is available."""
return (
self.coordinator.last_update_success
and self.client_name in self.coordinator.clients
)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
client = self.coordinator.clients.get(self.client_name, {})
return {
"client_ids": client.get("ids", []),
"use_global_settings": client.get("use_global_settings", True),
}
class AdGuardClientSafeBrowsingBinarySensor(AdGuardBaseBinarySensor):
"""Binary sensor to show client-specific SafeBrowsing status."""
def __init__(
self,
coordinator: AdGuardControlHubCoordinator,
api: AdGuardHomeAPI,
client_name: str,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator, api)
self.client_name = client_name
self._attr_unique_id = f"{api.host}_{api.port}_client_{client_name}_safebrowsing"
self._attr_name = f"AdGuard {client_name} SafeBrowsing"
self._attr_device_class = BinarySensorDeviceClass.SAFETY
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def is_on(self) -> Optional[bool]:
"""Return true if client SafeBrowsing is enabled."""
client = self.coordinator.clients.get(self.client_name, {})
return client.get("safebrowsing_enabled", False)
@property
def icon(self) -> str:
"""Return the icon for the binary sensor."""
return "mdi:shield-account" if self.is_on else "mdi:shield-account-outline"
@property
def available(self) -> bool:
"""Return if sensor is available."""
return (
self.coordinator.last_update_success
and self.client_name in self.coordinator.clients
)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
client = self.coordinator.clients.get(self.client_name, {})
return {
"parental_enabled": client.get("parental_enabled", False),
"safesearch_enabled": client.get("safesearch_enabled", False),
}

View File

@@ -1,6 +1,7 @@
"""Config flow for AdGuard Control Hub integration."""
import asyncio
import logging
import re
from typing import Any, Dict, Optional
import voluptuous as vol
@@ -10,7 +11,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from .api import AdGuardHomeAPI, AdGuardConnectionError, AdGuardAuthError
from .api import AdGuardHomeAPI, AdGuardConnectionError, AdGuardAuthError, AdGuardTimeoutError
from .const import (
CONF_SSL,
CONF_VERIFY_SSL,
@@ -32,16 +33,33 @@ STEP_USER_DATA_SCHEMA = vol.Schema({
})
async def validate_input(hass, data: Dict[str, Any]) -> Dict[str, Any]:
"""Validate the user input allows us to connect."""
host = data[CONF_HOST].strip()
def validate_host(host: str) -> str:
"""Validate and clean host input."""
host = host.strip()
if not host:
raise InvalidHost("Host cannot be empty")
# Remove protocol if present
if host.startswith(("http://", "https://")):
host = host.split("://", 1)[1]
data[CONF_HOST] = host
# Remove path if present
if "/" in host:
host = host.split("/", 1)[0]
return host
async def validate_input(hass, data: Dict[str, Any]) -> Dict[str, Any]:
"""Validate the user input allows us to connect."""
# Validate and clean host
try:
host = validate_host(data[CONF_HOST])
data[CONF_HOST] = host
except InvalidHost:
raise
# Validate port
port = data[CONF_PORT]
if not (1 <= port <= 65535):
raise InvalidPort("Port must be between 1 and 65535")
@@ -54,6 +72,7 @@ async def validate_input(hass, data: Dict[str, Any]) -> Dict[str, Any]:
username=data.get(CONF_USERNAME),
password=data.get(CONF_PASSWORD),
ssl=data.get(CONF_SSL, False),
verify_ssl=data.get(CONF_VERIFY_SSL, True),
session=session,
timeout=10,
)
@@ -72,6 +91,7 @@ async def validate_input(hass, data: Dict[str, Any]) -> Dict[str, Any]:
"host": host,
}
except Exception:
# If we can't get status but connection works, still proceed
return {
"title": f"AdGuard Control Hub ({host})",
"version": "unknown",
@@ -80,6 +100,8 @@ async def validate_input(hass, data: Dict[str, Any]) -> Dict[str, Any]:
except AdGuardAuthError as err:
raise InvalidAuth from err
except AdGuardTimeoutError as err:
raise Timeout from err
except AdGuardConnectionError as err:
if "timeout" in str(err).lower():
raise Timeout from err
@@ -87,6 +109,7 @@ async def validate_input(hass, data: Dict[str, Any]) -> Dict[str, Any]:
except asyncio.TimeoutError as err:
raise Timeout from err
except Exception as err:
_LOGGER.exception("Unexpected error during validation")
raise CannotConnect from err

View File

@@ -4,6 +4,7 @@ from typing import Final
# Integration details
DOMAIN: Final = "adguard_hub"
MANUFACTURER: Final = "AdGuard Control Hub"
INTEGRATION_NAME: Final = "AdGuard Control Hub"
# Configuration
CONF_SSL: Final = "ssl"
@@ -27,17 +28,19 @@ API_ENDPOINTS: Final = {
"status": "/control/status",
"clients": "/control/clients",
"clients_add": "/control/clients/add",
"clients_update": "/control/clients/update",
"clients_update": "/control/clients/update",
"clients_delete": "/control/clients/delete",
"blocked_services_all": "/control/blocked_services/all",
"protection": "/control/protection",
"stats": "/control/stats",
"rewrite": "/control/rewrite/list",
"querylog": "/control/querylog",
}
# Available blocked services
# Available blocked services (common ones)
BLOCKED_SERVICES: Final = {
"youtube": "YouTube",
"facebook": "Facebook",
"facebook": "Facebook",
"netflix": "Netflix",
"gaming": "Gaming Services",
"instagram": "Instagram",
@@ -52,6 +55,19 @@ BLOCKED_SERVICES: Final = {
"whatsapp": "WhatsApp",
"telegram": "Telegram",
"discord": "Discord",
"amazon": "Amazon",
"ebay": "eBay",
"skype": "Skype",
"zoom": "Zoom",
"tinder": "Tinder",
"pinterest": "Pinterest",
"linkedin": "LinkedIn",
"dailymotion": "Dailymotion",
"vimeo": "Vimeo",
"viber": "Viber",
"wechat": "WeChat",
"ok": "Odnoklassniki",
"vk": "VKontakte",
}
# Service attributes
@@ -59,9 +75,22 @@ ATTR_CLIENT_NAME: Final = "client_name"
ATTR_SERVICES: Final = "services"
ATTR_DURATION: Final = "duration"
ATTR_CLIENTS: Final = "clients"
ATTR_ENABLED: Final = "enabled"
# Icons
ICON_PROTECTION: Final = "mdi:shield"
ICON_PROTECTION_OFF: Final = "mdi:shield-off"
ICON_CLIENT: Final = "mdi:devices"
ICON_STATISTICS: Final = "mdi:chart-line"
ICON_BLOCKED: Final = "mdi:shield-check"
ICON_QUERIES: Final = "mdi:dns"
ICON_PERCENTAGE: Final = "mdi:percent"
ICON_CLIENTS: Final = "mdi:account-multiple"
# Service names
SERVICE_BLOCK_SERVICES: Final = "block_services"
SERVICE_UNBLOCK_SERVICES: Final = "unblock_services"
SERVICE_EMERGENCY_UNBLOCK: Final = "emergency_unblock"
SERVICE_ADD_CLIENT: Final = "add_client"
SERVICE_REMOVE_CLIENT: Final = "remove_client"
SERVICE_REFRESH_DATA: Final = "refresh_data"

View File

@@ -10,5 +10,5 @@
"requirements": [
"aiohttp>=3.8.0"
],
"version": "1.0.0"
"version": "1.0.1"
}

View File

@@ -1,17 +1,18 @@
"""Sensor platform for AdGuard Control Hub integration."""
import logging
from typing import Any
from typing import Any, Optional
from homeassistant.components.sensor import SensorEntity, SensorStateClass
from homeassistant.components.sensor import SensorEntity, SensorStateClass, SensorDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE
from homeassistant.const import PERCENTAGE, UnitOfTime
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 . import AdGuardControlHubCoordinator
from .api import AdGuardHomeAPI
from .const import DOMAIN, MANUFACTURER, ICON_STATISTICS
from .const import DOMAIN, MANUFACTURER, ICON_STATISTICS, ICON_BLOCKED, ICON_QUERIES, ICON_PERCENTAGE, ICON_CLIENTS
_LOGGER = logging.getLogger(__name__)
@@ -30,15 +31,17 @@ async def async_setup_entry(
AdGuardBlockedCounterSensor(coordinator, api),
AdGuardBlockingPercentageSensor(coordinator, api),
AdGuardClientCountSensor(coordinator, api),
AdGuardProcessingTimeSensor(coordinator, api),
AdGuardFilteringRulesSensor(coordinator, api),
]
async_add_entities(entities)
async_add_entities(entities, update_before_add=True)
class AdGuardBaseSensor(CoordinatorEntity, SensorEntity):
"""Base class for AdGuard sensors."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI):
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.api = api
@@ -47,61 +50,91 @@ class AdGuardBaseSensor(CoordinatorEntity, SensorEntity):
"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 sensor is available."""
return self.coordinator.last_update_success and bool(self.coordinator.statistics)
class AdGuardQueriesCounterSensor(AdGuardBaseSensor):
"""Sensor to track DNS queries count."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI):
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_dns_queries"
self._attr_name = "AdGuard DNS Queries"
self._attr_icon = ICON_STATISTICS
self._attr_icon = ICON_QUERIES
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
self._attr_native_unit_of_measurement = "queries"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def native_value(self):
def native_value(self) -> Optional[int]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
return stats.get("num_dns_queries", 0)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
stats = self.coordinator.statistics
return {
"queries_today": stats.get("num_dns_queries_today", 0),
"replaced_safebrowsing": stats.get("num_replaced_safebrowsing", 0),
"replaced_parental": stats.get("num_replaced_parental", 0),
"replaced_safesearch": stats.get("num_replaced_safesearch", 0),
}
class AdGuardBlockedCounterSensor(AdGuardBaseSensor):
"""Sensor to track blocked queries count."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI):
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_blocked_queries"
self._attr_name = "AdGuard Blocked Queries"
self._attr_icon = ICON_STATISTICS
self._attr_icon = ICON_BLOCKED
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
self._attr_native_unit_of_measurement = "queries"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def native_value(self):
def native_value(self) -> Optional[int]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
return stats.get("num_blocked_filtering", 0)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
stats = self.coordinator.statistics
return {
"blocked_today": stats.get("num_blocked_filtering_today", 0),
"malware_phishing": stats.get("num_replaced_safebrowsing", 0),
"adult_websites": stats.get("num_replaced_parental", 0),
}
class AdGuardBlockingPercentageSensor(AdGuardBaseSensor):
"""Sensor to track blocking percentage."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI):
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_blocking_percentage"
self._attr_name = "AdGuard Blocking Percentage"
self._attr_icon = ICON_STATISTICS
self._attr_icon = ICON_PERCENTAGE
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = PERCENTAGE
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def native_value(self):
def native_value(self) -> Optional[float]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
total_queries = stats.get("num_dns_queries", 0)
@@ -117,16 +150,75 @@ class AdGuardBlockingPercentageSensor(AdGuardBaseSensor):
class AdGuardClientCountSensor(AdGuardBaseSensor):
"""Sensor to track active clients count."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI):
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_clients_count"
self._attr_name = "AdGuard Clients Count"
self._attr_icon = ICON_STATISTICS
self._attr_icon = ICON_CLIENTS
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = "clients"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def native_value(self):
def native_value(self) -> Optional[int]:
"""Return the state of the sensor."""
return len(self.coordinator.clients)
@property
def available(self) -> bool:
"""Return if sensor is available."""
return self.coordinator.last_update_success
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
clients = self.coordinator.clients
protected_clients = sum(1 for c in clients.values() if c.get("filtering_enabled", True))
return {
"protected_clients": protected_clients,
"unprotected_clients": len(clients) - protected_clients,
"client_names": list(clients.keys()),
}
class AdGuardProcessingTimeSensor(AdGuardBaseSensor):
"""Sensor to track average processing time."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_avg_processing_time"
self._attr_name = "AdGuard Average Processing Time"
self._attr_icon = "mdi:speedometer"
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = UnitOfTime.MILLISECONDS
self._attr_entity_category = EntityCategory.DIAGNOSTIC
self._attr_device_class = SensorDeviceClass.DURATION
@property
def native_value(self) -> Optional[float]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
avg_time = stats.get("avg_processing_time", 0)
return round(avg_time, 2) if avg_time else 0
class AdGuardFilteringRulesSensor(AdGuardBaseSensor):
"""Sensor to track number of filtering rules."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_filtering_rules"
self._attr_name = "AdGuard Filtering Rules"
self._attr_icon = "mdi:filter"
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = "rules"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def native_value(self) -> Optional[int]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
return stats.get("filtering_rules_count", 0)

View File

@@ -7,7 +7,7 @@ import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from .api import AdGuardHomeAPI
from .api import AdGuardHomeAPI, AdGuardHomeError
from .const import (
DOMAIN,
BLOCKED_SERVICES,
@@ -15,47 +15,101 @@ from .const import (
ATTR_SERVICES,
ATTR_DURATION,
ATTR_CLIENTS,
ATTR_ENABLED,
SERVICE_BLOCK_SERVICES,
SERVICE_UNBLOCK_SERVICES,
SERVICE_EMERGENCY_UNBLOCK,
SERVICE_ADD_CLIENT,
SERVICE_REMOVE_CLIENT,
SERVICE_REFRESH_DATA,
)
_LOGGER = logging.getLogger(__name__)
# Service schemas
SCHEMA_BLOCK_SERVICES = vol.Schema({
vol.Required(ATTR_CLIENT_NAME): cv.string,
vol.Required(ATTR_SERVICES): vol.All(cv.ensure_list, [vol.In(BLOCKED_SERVICES.keys())]),
})
SCHEMA_UNBLOCK_SERVICES = vol.Schema({
vol.Required(ATTR_CLIENT_NAME): cv.string,
vol.Required(ATTR_SERVICES): vol.All(cv.ensure_list, [vol.In(BLOCKED_SERVICES.keys())]),
})
SCHEMA_EMERGENCY_UNBLOCK = vol.Schema({
vol.Required(ATTR_DURATION): cv.positive_int,
vol.Optional(ATTR_CLIENTS, default=["all"]): vol.All(cv.ensure_list, [cv.string]),
})
SCHEMA_ADD_CLIENT = vol.Schema({
vol.Required("name"): cv.string,
vol.Required("ids"): vol.All(cv.ensure_list, [cv.string]),
vol.Optional("filtering_enabled", default=True): cv.boolean,
vol.Optional("safebrowsing_enabled", default=False): cv.boolean,
vol.Optional("parental_enabled", default=False): cv.boolean,
vol.Optional("safesearch_enabled", default=False): cv.boolean,
vol.Optional("use_global_blocked_services", default=True): cv.boolean,
vol.Optional("blocked_services", default=[]): vol.All(cv.ensure_list, [cv.string]),
})
SCHEMA_REMOVE_CLIENT = vol.Schema({
vol.Required("name"): cv.string,
})
SCHEMA_REFRESH_DATA = vol.Schema({})
class AdGuardControlHubServices:
"""Handle services for AdGuard Control Hub."""
def __init__(self, hass: HomeAssistant):
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the services."""
self.hass = hass
def register_services(self) -> None:
"""Register all services."""
self.hass.services.register(
DOMAIN, "block_services", self.block_services, schema=SCHEMA_BLOCK_SERVICES
)
self.hass.services.register(
DOMAIN, "unblock_services", self.unblock_services, schema=SCHEMA_BLOCK_SERVICES
)
self.hass.services.register(
DOMAIN, "emergency_unblock", self.emergency_unblock, schema=SCHEMA_EMERGENCY_UNBLOCK
)
_LOGGER.debug("Registering AdGuard Control Hub services")
services = [
(SERVICE_BLOCK_SERVICES, self.block_services, SCHEMA_BLOCK_SERVICES),
(SERVICE_UNBLOCK_SERVICES, self.unblock_services, SCHEMA_UNBLOCK_SERVICES),
(SERVICE_EMERGENCY_UNBLOCK, self.emergency_unblock, SCHEMA_EMERGENCY_UNBLOCK),
(SERVICE_ADD_CLIENT, self.add_client, SCHEMA_ADD_CLIENT),
(SERVICE_REMOVE_CLIENT, self.remove_client, SCHEMA_REMOVE_CLIENT),
(SERVICE_REFRESH_DATA, self.refresh_data, SCHEMA_REFRESH_DATA),
]
for service_name, service_func, schema in services:
if not self.hass.services.has_service(DOMAIN, service_name):
self.hass.services.register(DOMAIN, service_name, service_func, schema=schema)
_LOGGER.debug("Registered service: %s", service_name)
def unregister_services(self) -> None:
"""Unregister all services."""
services = ["block_services", "unblock_services", "emergency_unblock"]
_LOGGER.debug("Unregistering AdGuard Control Hub services")
for service in services:
if self.hass.services.has_service(DOMAIN, service):
self.hass.services.remove(DOMAIN, service)
services = [
SERVICE_BLOCK_SERVICES,
SERVICE_UNBLOCK_SERVICES,
SERVICE_EMERGENCY_UNBLOCK,
SERVICE_ADD_CLIENT,
SERVICE_REMOVE_CLIENT,
SERVICE_REFRESH_DATA,
]
for service_name in services:
if self.hass.services.has_service(DOMAIN, service_name):
self.hass.services.remove(DOMAIN, service_name)
_LOGGER.debug("Unregistered service: %s", service_name)
def _get_api_instances(self) -> list[AdGuardHomeAPI]:
"""Get all API instances."""
apis = []
for entry_data in self.hass.data.get(DOMAIN, {}).values():
if isinstance(entry_data, dict) and "api" in entry_data:
apis.append(entry_data["api"])
return apis
async def block_services(self, call: ServiceCall) -> None:
"""Block services for a specific client."""
@@ -64,36 +118,62 @@ class AdGuardControlHubServices:
_LOGGER.info("Blocking services %s for client %s", services, client_name)
for entry_data in self.hass.data[DOMAIN].values():
api: AdGuardHomeAPI = entry_data["api"]
success_count = 0
for api in self._get_api_instances():
try:
client = await api.get_client_by_name(client_name)
if client:
current_blocked = client.get("blocked_services", {})
current_services = current_blocked.get("ids", []) if isinstance(current_blocked, dict) else current_blocked or []
if isinstance(current_blocked, dict):
current_services = current_blocked.get("ids", [])
else:
current_services = current_blocked or []
updated_services = list(set(current_services + services))
await api.update_client_blocked_services(client_name, updated_services)
success_count += 1
_LOGGER.info("Successfully blocked services for %s", client_name)
else:
_LOGGER.warning("Client %s not found", client_name)
except AdGuardHomeError as err:
_LOGGER.error("AdGuard error blocking services for %s: %s", client_name, err)
except Exception as err:
_LOGGER.error("Failed to block services for %s: %s", client_name, err)
_LOGGER.exception("Unexpected error blocking services for %s: %s", client_name, err)
if success_count == 0:
_LOGGER.error("Failed to block services for %s on any instance", client_name)
async def unblock_services(self, call: ServiceCall) -> None:
"""Unblock services for a specific client."""
client_name = call.data[ATTR_CLIENT_NAME]
services = call.data[ATTR_SERVICES]
for entry_data in self.hass.data[DOMAIN].values():
api: AdGuardHomeAPI = entry_data["api"]
_LOGGER.info("Unblocking services %s for client %s", services, client_name)
success_count = 0
for api in self._get_api_instances():
try:
client = await api.get_client_by_name(client_name)
if client:
current_blocked = client.get("blocked_services", {})
current_services = current_blocked.get("ids", []) if isinstance(current_blocked, dict) else current_blocked or []
if isinstance(current_blocked, dict):
current_services = current_blocked.get("ids", [])
else:
current_services = current_blocked or []
updated_services = [s for s in current_services if s not in services]
await api.update_client_blocked_services(client_name, updated_services)
success_count += 1
_LOGGER.info("Successfully unblocked services for %s", client_name)
else:
_LOGGER.warning("Client %s not found", client_name)
except AdGuardHomeError as err:
_LOGGER.error("AdGuard error unblocking services for %s: %s", client_name, err)
except Exception as err:
_LOGGER.error("Failed to unblock services for %s: %s", client_name, err)
_LOGGER.exception("Unexpected error unblocking services for %s: %s", client_name, err)
if success_count == 0:
_LOGGER.error("Failed to unblock services for %s on any instance", client_name)
async def emergency_unblock(self, call: ServiceCall) -> None:
"""Emergency unblock - temporarily disable protection."""
@@ -102,20 +182,95 @@ class AdGuardControlHubServices:
_LOGGER.warning("Emergency unblock activated for %s seconds", duration)
for entry_data in self.hass.data[DOMAIN].values():
api: AdGuardHomeAPI = entry_data["api"]
for api in self._get_api_instances():
try:
if "all" in clients:
await api.set_protection(False)
_LOGGER.warning("Protection disabled for %s:%s", api.host, api.port)
# Re-enable after duration
async def delayed_enable():
async def delayed_enable(api_instance: AdGuardHomeAPI):
await asyncio.sleep(duration)
try:
await api.set_protection(True)
_LOGGER.info("Emergency unblock expired - protection re-enabled")
await api_instance.set_protection(True)
_LOGGER.info("Emergency unblock expired - protection re-enabled for %s:%s",
api_instance.host, api_instance.port)
except Exception as err:
_LOGGER.error("Failed to re-enable protection: %s", err)
_LOGGER.error("Failed to re-enable protection for %s:%s: %s",
api_instance.host, api_instance.port, err)
asyncio.create_task(delayed_enable())
asyncio.create_task(delayed_enable(api))
else:
# Individual client emergency unblock
for client_name in clients:
if client_name == "all":
continue
try:
client = await api.get_client_by_name(client_name)
if client:
update_data = {
"name": client_name,
"data": {**client, "filtering_enabled": False}
}
await api.update_client(update_data)
_LOGGER.info("Emergency unblock applied to client %s", client_name)
except Exception as err:
_LOGGER.error("Failed to emergency unblock client %s: %s", client_name, err)
except AdGuardHomeError as err:
_LOGGER.error("AdGuard error during emergency unblock: %s", err)
except Exception as err:
_LOGGER.error("Failed to execute emergency unblock: %s", err)
_LOGGER.exception("Unexpected error during emergency unblock: %s", err)
async def add_client(self, call: ServiceCall) -> None:
"""Add a new client."""
client_data = dict(call.data)
_LOGGER.info("Adding new client: %s", client_data.get("name"))
success_count = 0
for api in self._get_api_instances():
try:
await api.add_client(client_data)
success_count += 1
_LOGGER.info("Successfully added client: %s", client_data.get("name"))
except AdGuardHomeError as err:
_LOGGER.error("AdGuard error adding client: %s", err)
except Exception as err:
_LOGGER.exception("Unexpected error adding client: %s", err)
if success_count == 0:
_LOGGER.error("Failed to add client %s on any instance", client_data.get("name"))
async def remove_client(self, call: ServiceCall) -> None:
"""Remove a client."""
client_name = call.data.get("name")
_LOGGER.info("Removing client: %s", client_name)
success_count = 0
for api in self._get_api_instances():
try:
await api.delete_client(client_name)
success_count += 1
_LOGGER.info("Successfully removed client: %s", client_name)
except AdGuardHomeError as err:
_LOGGER.error("AdGuard error removing client: %s", err)
except Exception as err:
_LOGGER.exception("Unexpected error removing client: %s", err)
if success_count == 0:
_LOGGER.error("Failed to remove client %s on any instance", client_name)
async def refresh_data(self, call: ServiceCall) -> None:
"""Refresh data for all coordinators."""
_LOGGER.info("Manually refreshing AdGuard Control Hub data")
for entry_data in self.hass.data.get(DOMAIN, {}).values():
if isinstance(entry_data, dict) and "coordinator" in entry_data:
coordinator = entry_data["coordinator"]
try:
await coordinator.async_request_refresh()
_LOGGER.debug("Refreshed coordinator data")
except Exception as err:
_LOGGER.error("Failed to refresh coordinator: %s", err)

View File

@@ -7,21 +7,22 @@
"data": {
"host": "Host",
"port": "Port",
"username": "Username",
"password": "Password",
"username": "Username (optional)",
"password": "Password (optional)",
"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"
"cannot_connect": "Failed to connect to AdGuard Home. Please check the host and port.",
"invalid_auth": "Invalid username or password. Please verify your credentials.",
"invalid_host": "Invalid host format. Please enter a valid hostname or IP address.",
"timeout": "Connection timeout. Please check your network connection and try again.",
"unknown": "Unexpected error occurred. Please check your configuration and try again."
},
"abort": {
"already_configured": "AdGuard Control Hub is already configured"
"already_configured": "AdGuard Control Hub is already configured for this host and port"
}
}
}

View File

@@ -1,15 +1,16 @@
"""Switch platform for AdGuard Control Hub integration."""
import logging
from typing import Any
from typing import Any, Optional
from homeassistant.components.switch import SwitchEntity
from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass
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 . import AdGuardControlHubCoordinator
from .api import AdGuardHomeAPI
from .api import AdGuardHomeAPI, AdGuardHomeError
from .const import DOMAIN, ICON_PROTECTION, ICON_PROTECTION_OFF, ICON_CLIENT, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
@@ -30,13 +31,13 @@ async def async_setup_entry(
for client_name in coordinator.clients.keys():
entities.append(AdGuardClientSwitch(coordinator, api, client_name))
async_add_entities(entities)
async_add_entities(entities, update_before_add=True)
class AdGuardBaseSwitch(CoordinatorEntity, SwitchEntity):
"""Base class for AdGuard switches."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI):
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the switch."""
super().__init__(coordinator)
self.api = api
@@ -45,20 +46,28 @@ class AdGuardBaseSwitch(CoordinatorEntity, SwitchEntity):
"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):
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
@property
def is_on(self) -> bool | None:
def is_on(self) -> Optional[bool]:
"""Return true if protection is enabled."""
return self.coordinator.protection_status.get("protection_enabled", False)
@@ -67,23 +76,47 @@ class AdGuardProtectionSwitch(AdGuardBaseSwitch):
"""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", []),
}
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on AdGuard protection."""
try:
await self.api.set_protection(True)
await self.coordinator.async_request_refresh()
except Exception as err:
_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
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off AdGuard protection."""
try:
await self.api.set_protection(False)
await self.coordinator.async_request_refresh()
except Exception as err:
_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
class AdGuardClientSwitch(AdGuardBaseSwitch):
@@ -94,20 +127,49 @@ class AdGuardClientSwitch(AdGuardBaseSwitch):
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}"
self._attr_name = f"AdGuard {client_name}"
self._attr_icon = ICON_CLIENT
self._attr_device_class = SwitchDeviceClass.SWITCH
self._attr_entity_category = EntityCategory.CONFIG
@property
def is_on(self) -> bool | None:
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
)
@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 []
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,
}
async def async_turn_on(self, **kwargs: Any) -> None:
"""Enable protection for this client."""
try:
@@ -119,9 +181,15 @@ class AdGuardClientSwitch(AdGuardBaseSwitch):
}
await self.api.update_client(update_data)
await self.coordinator.async_request_refresh()
except Exception as err:
_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
async def async_turn_off(self, **kwargs: Any) -> None:
"""Disable protection for this client."""
@@ -134,6 +202,12 @@ class AdGuardClientSwitch(AdGuardBaseSwitch):
}
await self.api.update_client(update_data)
await self.coordinator.async_request_refresh()
except Exception as err:
_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