Add API-Reference

2025-09-28 12:45:09 +00:00
parent fb9c3053e7
commit bb3471ef24

495
API-Reference.-.md Normal file

@@ -0,0 +1,495 @@
# 🛠️ API Reference
Technical documentation for developers working with AdGuard Control Hub. This covers the internal API, entity structure, and integration architecture.
## 📡 AdGuard Home API Integration
### Supported API Endpoints
The integration interacts with these AdGuard Home API endpoints:
| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/control/status` | GET | Server status and configuration |
| `/control/clients` | GET | List all configured clients |
| `/control/clients/add` | POST | Add new client |
| `/control/clients/update` | POST | Update existing client |
| `/control/clients/delete` | POST | Remove client |
| `/control/stats` | GET | DNS query statistics |
| `/control/protection` | POST | Enable/disable protection |
| `/control/blocked_services/all` | GET | Available blocked services |
| `/control/blocked_services/get` | GET | Client blocked services |
| `/control/blocked_services/update` | POST | Update blocked services |
### API Authentication
```python
# Basic authentication used for all API calls
from aiohttp import BasicAuth
auth = BasicAuth(username, password)
headers = {"Content-Type": "application/json"}
```
### Error Handling
```python
# API wrapper error handling pattern
async def _request(self, method: str, endpoint: str, data: dict = None) -> dict:
try:
async with self.session.request(method, url, json=data, headers=headers, auth=auth) as response:
response.raise_for_status()
if response.status == 204 or not response.content_length:
return {}
return await response.json()
except aiohttp.ClientError as err:
_LOGGER.error("API request failed: %s", err)
raise
except asyncio.TimeoutError:
_LOGGER.error("API request timeout")
raise
```
## 🏗️ Integration Architecture
### Component Structure
```
adguard_hub/
├── __init__.py # Main integration setup
├── api.py # AdGuard Home API wrapper
├── config_flow.py # Configuration UI flow
├── const.py # Constants and configuration
├── switch.py # Switch platform implementation
├── sensor.py # Sensor platform (optional)
├── binary_sensor.py # Binary sensor platform (optional)
├── services.py # Custom services
└── strings.json # UI strings and translations
```
### Data Flow
```mermaid
graph TD
A[Home Assistant] --> B[AdGuard Control Hub]
B --> C[Update Coordinator]
C --> D[AdGuard Home API]
D --> E[AdGuard Home Server]
C --> F[Switch Platform]
C --> G[Sensor Platform]
C --> H[Binary Sensor Platform]
F --> I[Entity Registry]
G --> I
H --> I
```
### Update Coordinator Pattern
```python
class AdGuardControlHubCoordinator(DataUpdateCoordinator):
"""Manage data fetching from AdGuard Home."""
def __init__(self, hass: HomeAssistant, api: AdGuardHomeAPI):
super().__init__(
hass,
_LOGGER,
name=f"{DOMAIN}_coordinator",
update_interval=timedelta(seconds=SCAN_INTERVAL),
)
self.api = api
async def _async_update_data(self):
"""Fetch data from AdGuard Home."""
try:
# Concurrent API calls for better performance
results = await asyncio.gather(
self.api.get_clients(),
self.api.get_statistics(),
self.api.get_status(),
return_exceptions=True,
)
# Process and return structured data
return self._process_api_results(results)
except Exception as err:
raise UpdateFailed(f"Error fetching data: {err}")
```
## 🔌 Entity System
### Entity Types and Hierarchy
```python
# Base entity class
class AdGuardBaseEntity(CoordinatorEntity):
"""Base class for AdGuard entities."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI):
super().__init__(coordinator)
self.api = api
self._attr_device_info = self._create_device_info()
def _create_device_info(self) -> dict:
"""Create device info for entity grouping."""
return {
"identifiers": {(DOMAIN, f"{self.api.host}:{self.api.port}")},
"name": f"AdGuard Control Hub ({self.api.host})",
"manufacturer": MANUFACTURER,
"model": "AdGuard Home",
"sw_version": self.coordinator.protection_status.get("version"),
}
```
### Switch Entities
```python
# Global protection switch
class AdGuardProtectionSwitch(AdGuardBaseEntity, SwitchEntity):
"""Control global AdGuard protection."""
@property
def unique_id(self) -> str:
return f"{self.api.host}_{self.api.port}_protection"
@property
def is_on(self) -> bool:
return self.coordinator.protection_status.get("protection_enabled", False)
async def async_turn_on(self, **kwargs):
await self.api.set_protection(True)
await self.coordinator.async_request_refresh()
# Client-specific switches
class AdGuardClientSwitch(AdGuardBaseEntity, SwitchEntity):
"""Control per-client protection."""
def __init__(self, coordinator, api, client_name: str):
super().__init__(coordinator, api)
self.client_name = client_name
@property
def unique_id(self) -> str:
return f"{self.api.host}_{self.api.port}_client_{self.client_name}"
```
### Sensor Entities
```python
class AdGuardStatsSensor(AdGuardBaseEntity, SensorEntity):
"""DNS statistics sensor."""
@property
def native_value(self) -> int:
return self.coordinator.statistics.get(self.stat_key, 0)
@property
def device_class(self) -> str:
return SensorDeviceClass.TIMESTAMP if "time" in self.stat_key else None
```
## 🔧 Custom Services
### Service Registration
```python
async def async_register_services(hass: HomeAssistant, api: AdGuardHomeAPI) -> None:
"""Register integration services."""
async def emergency_unblock_service(call):
"""Handle emergency unblock service."""
duration = call.data.get("duration", 300)
clients = call.data.get("clients", ["all"])
try:
if "all" in clients:
await api.set_protection(False)
# Schedule re-enabling after duration
hass.async_create_task(_restore_protection_after_delay(duration))
else:
# Handle specific clients
for client_name in clients:
await api.temporary_disable_client(client_name, duration)
except Exception as err:
_LOGGER.error("Emergency unblock failed: %s", err)
raise HomeAssistantError(f"Service call failed: {err}")
# Register service with schema validation
hass.services.async_register(
DOMAIN,
"emergency_unblock",
emergency_unblock_service,
schema=EMERGENCY_UNBLOCK_SCHEMA,
)
```
### Service Schemas
```python
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
# Service parameter validation schemas
EMERGENCY_UNBLOCK_SCHEMA = vol.Schema({
vol.Optional("duration", default=300): cv.positive_int,
vol.Optional("clients", default=["all"]): vol.All(cv.ensure_list, [cv.string]),
})
BLOCK_SERVICES_SCHEMA = vol.Schema({
vol.Required("client_name"): cv.string,
vol.Required("services"): vol.All(cv.ensure_list, [cv.string]),
vol.Optional("schedule"): vol.Schema({
vol.Required("enabled"): cv.boolean,
vol.Optional("time_zone", default="Local"): cv.string,
vol.Optional("mon"): TIME_RANGE_SCHEMA,
vol.Optional("tue"): TIME_RANGE_SCHEMA,
# ... other days
}),
})
TIME_RANGE_SCHEMA = vol.Schema({
vol.Required("start"): cv.time,
vol.Required("end"): cv.time,
})
```
## 📊 Data Models
### Client Data Structure
```python
@dataclass
class AdGuardClient:
"""Represent an AdGuard Home client."""
name: str
ids: list[str]
use_global_settings: bool = True
use_global_blocked_services: bool = True
blocked_services: list[str] = field(default_factory=list)
filtering_enabled: bool = True
parental_enabled: bool = False
safebrowsing_enabled: bool = False
safesearch_enabled: bool = False
upstream_dns: list[str] = field(default_factory=list)
@classmethod
def from_api_response(cls, data: dict) -> "AdGuardClient":
"""Create client from API response."""
return cls(
name=data.get("name", ""),
ids=data.get("ids", []),
use_global_settings=data.get("use_global_settings", True),
# ... map other fields
)
def to_api_request(self) -> dict:
"""Convert to API request format."""
return {
"name": self.name,
"ids": self.ids,
"use_global_settings": self.use_global_settings,
# ... map all fields
}
```
### Statistics Data Structure
```python
@dataclass
class AdGuardStatistics:
"""AdGuard Home DNS statistics."""
dns_queries: int
blocked_filtering: int
replaced_safebrowsing: int
replaced_safesearch: int
replaced_parental: int
avg_processing_time: float
@property
def blocked_percentage(self) -> float:
"""Calculate blocking percentage."""
if self.dns_queries == 0:
return 0.0
return (self.blocked_filtering / self.dns_queries) * 100
```
## 🔄 Configuration Flow
### Configuration Steps
```python
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle configuration flow."""
VERSION = 1
async def async_step_user(self, user_input=None):
"""Handle user input."""
errors = {}
if user_input is not None:
try:
# Validate connection
info = await self._validate_input(user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
else:
# Create config entry
return self.async_create_entry(
title=info["title"],
data=user_input
)
return self.async_show_form(
step_id="user",
data_schema=self._get_schema(user_input),
errors=errors
)
async def _validate_input(self, data: dict) -> dict:
"""Validate user input and test connection."""
api = AdGuardHomeAPI(
host=data[CONF_HOST],
port=data[CONF_PORT],
username=data.get(CONF_USERNAME),
password=data.get(CONF_PASSWORD),
ssl=data.get(CONF_SSL, False),
session=async_get_clientsession(self.hass),
)
# Test connection
if not await api.test_connection():
raise CannotConnect
# Get server info for title
status = await api.get_status()
return {"title": f"AdGuard Control Hub ({data[CONF_HOST]})"}
```
## 🧪 Testing Framework
### Unit Test Structure
```python
import pytest
from unittest.mock import AsyncMock, MagicMock
from homeassistant.core import HomeAssistant
from custom_components.adguard_hub.api import AdGuardHomeAPI
@pytest.fixture
def mock_api():
"""Mock API for testing."""
api = MagicMock(spec=AdGuardHomeAPI)
api.get_status = AsyncMock(return_value={"protection_enabled": True})
api.get_clients = AsyncMock(return_value={"clients": []})
return api
async def test_protection_switch_on(hass: HomeAssistant, mock_api):
"""Test protection switch turns on correctly."""
coordinator = AdGuardControlHubCoordinator(hass, mock_api)
switch = AdGuardProtectionSwitch(coordinator, mock_api)
await switch.async_turn_on()
mock_api.set_protection.assert_called_once_with(True)
coordinator.async_request_refresh.assert_called_once()
```
### Integration Testing
```python
async def test_integration_setup(hass: HomeAssistant):
"""Test integration setup."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 3000,
CONF_USERNAME: "admin",
CONF_PASSWORD: "password",
},
)
with patch("custom_components.adguard_hub.api.AdGuardHomeAPI.test_connection", return_value=True):
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state == config_entries.ConfigEntryState.LOADED
```
## 🔍 Debugging and Logging
### Logging Configuration
```python
import logging
_LOGGER = logging.getLogger(__name__)
# Log levels for different scenarios
_LOGGER.debug("Detailed debugging information")
_LOGGER.info("General information")
_LOGGER.warning("Warning about potential issues")
_LOGGER.error("Error occurred but recoverable")
_LOGGER.exception("Exception occurred with traceback")
```
### Performance Monitoring
```python
import time
from functools import wraps
def monitor_performance(func):
"""Decorator to monitor function performance."""
@wraps(func)
async def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = await func(*args, **kwargs)
duration = time.time() - start_time
_LOGGER.debug("%s completed in %.2fs", func.__name__, duration)
return result
except Exception as err:
duration = time.time() - start_time
_LOGGER.error("%s failed after %.2fs: %s", func.__name__, duration, err)
raise
return wrapper
```
## 📈 Metrics and Monitoring
### Integration Health Metrics
```python
class HealthMetrics:
"""Track integration health metrics."""
def __init__(self):
self.api_calls_total = 0
self.api_calls_failed = 0
self.last_successful_update = None
self.last_error = None
def record_api_call(self, success: bool, error: Exception = None):
"""Record API call metrics."""
self.api_calls_total += 1
if success:
self.last_successful_update = datetime.now()
else:
self.api_calls_failed += 1
self.last_error = str(error) if error else "Unknown error"
@property
def success_rate(self) -> float:
"""Calculate API call success rate."""
if self.api_calls_total == 0:
return 0.0
return ((self.api_calls_total - self.api_calls_failed) / self.api_calls_total) * 100
```
---
**For more technical details**, see the source code in the repository or the [Development Guide](Development) for setting up a development environment.