Initial commit
Some checks failed
Tests / test (3.13) (push) Failing after 23s
Tests / lint (push) Failing after 20s
Tests / hacs (push) Failing after 52s

Signed-off-by: Rafal Zielinski <sq4ind@gmail.com>
This commit is contained in:
2025-10-02 16:00:15 +01:00
commit d4cdcc04c0
25 changed files with 2650 additions and 0 deletions

BIN
.gitea/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,54 @@
name: Release
on:
release:
types: [published]
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements_test.txt
- name: Run tests
run: |
python -m pytest tests/ -v
- name: Update version in manifest
run: |
python -c "
import json
import os
with open('custom_components/adguard_control_hub/manifest.json', 'r') as f:
manifest = json.load(f)
manifest['version'] = os.environ['GITHUB_REF_NAME'].lstrip('v')
with open('custom_components/adguard_control_hub/manifest.json', 'w') as f:
json.dump(manifest, f, indent=2)
"
- name: Create ZIP archive
run: |
cd custom_components/adguard_control_hub/
zip -r ../../adguard-control-hub.zip .
cd ../..
- name: Upload release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./adguard-control-hub.zip
asset_name: adguard-control-hub.zip
asset_content_type: application/zip

80
.gitea/workflows/test.yml Normal file
View File

@@ -0,0 +1,80 @@
name: Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.13']
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements_test.txt
- name: Run pytest
run: |
python -m pytest tests/ -v --tb=short
- name: Validate manifest
run: |
python -c "import json; json.load(open('custom_components/adguard_control_hub/manifest.json'))"
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install black isort flake8 mypy
- name: Run black
run: |
black --check --diff custom_components/
- name: Run isort
run: |
isort --check-only --diff custom_components/
- name: Run flake8
run: |
flake8 custom_components/ --max-line-length=88 --extend-ignore=E203,W503
- name: Run mypy
run: |
mypy custom_components/
continue-on-error: true
hacs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: HACS validation
uses: hacs/action@main
with:
category: integration

127
README.md Normal file
View File

@@ -0,0 +1,127 @@
# AdGuard Control Hub
[![GitHub Release][releases-shield]][releases]
[![GitHub Activity][commits-shield]][commits]
[![License][license-shield]](LICENSE)
[![hacs][hacsbadge]][hacs]
A comprehensive Home Assistant integration for managing your AdGuard Home instance with advanced control and monitoring capabilities.
## Features
### Switches
- **AdGuard Protection**: Master switch that controls all AdGuard features
- **DNS Filtering**: Enables DNS filtering using blocklists
- **Safe Browsing**: Blocks known phishing and malware sites
- **Parental Control**: Blocks adult content
- **Safe Search**: Enforces safe search on search engines
- **Query Log**: Records DNS queries for statistics
### Sensors
- **DNS Queries**: Total number of DNS queries processed
- **Blocked Queries**: Number of queries blocked by filtering
- **Blocked Percentage**: Percentage of queries that were blocked
- **Active Filter Rules**: Number of active filtering rules loaded
- **Average Processing Time**: Average DNS query processing time
### Binary Sensors
- **AdGuard Home Running**: Shows if AdGuard Home is responsive
## Installation
### HACS (Recommended)
1. Install [HACS](https://hacs.xyz/) if you haven't already
2. In HACS, go to "Integrations"
3. Click the "+" button and search for "AdGuard Control Hub"
4. Click "Install"
5. Restart Home Assistant
### Manual Installation
1. Download the latest release from the [releases page](https://github.com/your-username/adguard-control-hub/releases)
2. Extract the `adguard-control-hub.zip` file
3. Copy the `custom_components/adguard_control_hub` folder to your Home Assistant's `custom_components` directory
4. Restart Home Assistant
## Configuration
1. In Home Assistant, go to **Settings****Devices & Services**
2. Click **"+ ADD INTEGRATION"**
3. Search for "AdGuard Control Hub" and select it
4. Fill in your AdGuard Home connection details:
- **Host**: IP address or hostname of your AdGuard Home instance
- **Port**: Port number (default: 3000)
- **Username**: Admin username (if authentication is enabled)
- **Password**: Admin password (if authentication is enabled)
- **Use HTTPS**: Enable if using HTTPS
- **Verify SSL**: Verify SSL certificates
## Requirements
- AdGuard Home v0.107.0 or newer
- Home Assistant 2023.5.0 or newer
- Network access to your AdGuard Home instance
## Supported AdGuard Home Versions
This integration has been tested with:
- AdGuard Home v0.107.50+
- AdGuard Home v0.108.x
- AdGuard Home v0.109.x
## Troubleshooting
### Connection Issues
- Ensure AdGuard Home is running and accessible from Home Assistant
- Check firewall settings on the AdGuard Home host
- Verify the correct port is being used
- Test connection manually using curl: `curl http://your-adguard-ip:port/control/status`
### Authentication Issues
- Verify username and password are correct
- Ensure the user account has admin privileges in AdGuard Home
- Try connecting without authentication first to isolate the issue
### Feature Not Working
- Check AdGuard Home logs for any error messages
- Verify the specific feature is enabled in AdGuard Home
- Some features require specific AdGuard Home configurations
## Development
### Setting up Development Environment
1. Clone the repository
2. Install development dependencies: `pip install -r requirements_test.txt`
3. Run tests: `pytest tests/`
4. Run linting: `black custom_components/ && isort custom_components/`
### Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests for new functionality
5. Ensure all tests pass
6. Submit a pull request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Acknowledgments
- AdGuard Team for creating AdGuard Home
- Home Assistant community for the integration framework
- All contributors and testers
---
[releases-shield]: https://img.shields.io/github/release/your-username/adguard-control-hub.svg?style=for-the-badge
[releases]: https://github.com/your-username/adguard-control-hub/releases
[commits-shield]: https://img.shields.io/github/commit-activity/y/your-username/adguard-control-hub.svg?style=for-the-badge
[commits]: https://github.com/your-username/adguard-control-hub/commits/main
[license-shield]: https://img.shields.io/github/license/your-username/adguard-control-hub.svg?style=for-the-badge
[hacs]: https://github.com/hacs/integration
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge

View File

@@ -0,0 +1,60 @@
"""The AdGuard Control Hub integration."""
from __future__ import annotations
import logging
from datetime import timedelta
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import AdGuardDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.SWITCH,
Platform.SENSOR,
Platform.BINARY_SENSOR,
]
SCAN_INTERVAL = timedelta(seconds=30)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AdGuard Control Hub from a config entry."""
session = async_get_clientsession(hass)
coordinator = AdGuardDataUpdateCoordinator(
hass,
session,
entry.data,
SCAN_INTERVAL,
)
# Test connection
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)

View File

@@ -0,0 +1,154 @@
"""AdGuard Home API client."""
from __future__ import annotations
import asyncio
import logging
from typing import Any
import aiohttp
import async_timeout
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
API_CLIENTS,
API_DNS_CONFIG,
API_STATUS,
API_STATS,
)
_LOGGER = logging.getLogger(__name__)
class AdGuardHomeApiError(Exception):
"""Exception to indicate a general API error."""
class AdGuardHomeConnectionError(AdGuardHomeApiError):
"""Exception to indicate a connection error."""
class AdGuardHomeAuthError(AdGuardHomeApiError):
"""Exception to indicate an authentication error."""
class AdGuardHomeAPI:
"""AdGuard Home API client."""
def __init__(
self,
session: aiohttp.ClientSession,
host: str,
port: int,
username: str | None = None,
password: str | None = None,
ssl: bool = False,
verify_ssl: bool = True,
) -> None:
"""Initialize the API client."""
self._session = session
self._host = host
self._port = port
self._username = username
self._password = password
self._ssl = ssl
self._verify_ssl = verify_ssl
protocol = "https" if ssl else "http"
self._base_url = f"{protocol}://{host}:{port}"
async def _request(
self,
method: str,
endpoint: str,
data: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Make a request to the AdGuard Home API."""
url = f"{self._base_url}{endpoint}"
auth = None
if self._username and self._password:
auth = aiohttp.BasicAuth(self._username, self._password)
headers = {"Content-Type": "application/json"}
try:
async with async_timeout.timeout(10):
async with self._session.request(
method,
url,
json=data,
auth=auth,
headers=headers,
ssl=self._verify_ssl,
) as response:
if response.status == 401:
raise AdGuardHomeAuthError("Authentication failed")
if response.status == 403:
raise AdGuardHomeAuthError("Access forbidden")
if response.status not in (200, 204):
text = await response.text()
raise AdGuardHomeApiError(
f"Request failed with status {response.status}: {text}"
)
if response.status == 204:
return {}
return await response.json()
except asyncio.TimeoutError as err:
raise AdGuardHomeConnectionError("Timeout connecting to AdGuard Home") from err
except aiohttp.ClientError as err:
raise AdGuardHomeConnectionError(f"Error connecting to AdGuard Home: {err}") from err
async def get_status(self) -> dict[str, Any]:
"""Get AdGuard Home status."""
return await self._request("GET", API_STATUS)
async def get_stats(self) -> dict[str, Any]:
"""Get AdGuard Home statistics."""
return await self._request("GET", API_STATS)
async def get_clients(self) -> dict[str, Any]:
"""Get AdGuard Home clients."""
return await self._request("GET", API_CLIENTS)
async def set_protection(self, enabled: bool) -> None:
"""Enable or disable protection."""
data = {"protection_enabled": enabled}
await self._request("POST", API_DNS_CONFIG, data)
async def set_filtering(self, enabled: bool) -> None:
"""Enable or disable filtering."""
data = {"filtering_enabled": enabled}
await self._request("POST", API_DNS_CONFIG, data)
async def set_safebrowsing(self, enabled: bool) -> None:
"""Enable or disable safe browsing."""
data = {"safebrowsing_enabled": enabled}
await self._request("POST", API_DNS_CONFIG, data)
async def set_parental_control(self, enabled: bool) -> None:
"""Enable or disable parental control."""
data = {"parental_enabled": enabled}
await self._request("POST", API_DNS_CONFIG, data)
async def set_safe_search(self, enabled: bool) -> None:
"""Enable or disable safe search."""
data = {"safesearch_enabled": enabled}
await self._request("POST", API_DNS_CONFIG, data)
async def set_query_log(self, enabled: bool) -> None:
"""Enable or disable query log."""
data = {"querylog_enabled": enabled}
await self._request("POST", API_DNS_CONFIG, data)
async def test_connection(self) -> bool:
"""Test connection to AdGuard Home."""
try:
await self.get_status()
return True
except AdGuardHomeApiError:
return False

View File

@@ -0,0 +1,80 @@
"""AdGuard Control Hub binary sensor platform."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AdGuardDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AdGuard Control Hub binary sensor based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
entities = [
AdGuardBinarySensor(
coordinator,
entry,
"running",
"AdGuard Home Running",
"mdi:shield-check-outline",
),
]
async_add_entities(entities)
class AdGuardBinarySensor(CoordinatorEntity[AdGuardDataUpdateCoordinator], BinarySensorEntity):
"""Representation of an AdGuard Control Hub binary sensor."""
def __init__(
self,
coordinator: AdGuardDataUpdateCoordinator,
entry: ConfigEntry,
sensor_type: str,
name: str,
icon: str,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self._sensor_type = sensor_type
self._entry = entry
self._attr_name = name
self._attr_icon = icon
self._attr_unique_id = f"{entry.entry_id}_{sensor_type}"
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this AdGuard Home instance."""
return DeviceInfo(
identifiers={(DOMAIN, self._entry.entry_id)},
name="AdGuard Control Hub",
manufacturer="AdGuard",
model="AdGuard Home",
sw_version=self.coordinator.data.get("status", {}).get("version"),
configuration_url=f"http://{self._entry.data['host']}:{self._entry.data['port']}",
)
@property
def is_on(self) -> bool | None:
"""Return True if the binary sensor is on."""
if self.coordinator.data is None:
return None
# If we can fetch data, AdGuard Home is running
return self.coordinator.data.get("status") is not None

View File

@@ -0,0 +1,100 @@
"""Config flow for AdGuard Control Hub integration."""
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .api import AdGuardHomeAPI, AdGuardHomeApiError, AdGuardHomeAuthError, AdGuardHomeConnectionError
from .const import CONF_SSL, CONF_VERIFY_SSL, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
vol.Optional(CONF_USERNAME): str,
vol.Optional(CONF_PASSWORD): str,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
}
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
session = async_get_clientsession(hass)
api = AdGuardHomeAPI(
session,
data[CONF_HOST],
data[CONF_PORT],
data.get(CONF_USERNAME),
data.get(CONF_PASSWORD),
data.get(CONF_SSL, DEFAULT_SSL),
data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
)
try:
status = await api.get_status()
return {"title": f"AdGuard Home ({data[CONF_HOST]})", "version": status.get("version", "Unknown")}
except AdGuardHomeConnectionError as err:
raise CannotConnect from err
except AdGuardHomeAuthError as err:
raise InvalidAuth from err
except AdGuardHomeApiError as err:
raise CannotConnect from err
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for AdGuard Control Hub."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors = {}
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
# Create unique ID from host and port
unique_id = f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@@ -0,0 +1,108 @@
"""Constants for the AdGuard Control Hub integration."""
from homeassistant.const import Platform
DOMAIN = "adguard_control_hub"
DEFAULT_NAME = "AdGuard Control Hub"
DEFAULT_PORT = 3000
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
# Configuration
CONF_HOST = "host"
CONF_PORT = "port"
CONF_USERNAME = "username"
CONF_PASSWORD = "password"
CONF_SSL = "ssl"
CONF_VERIFY_SSL = "verify_ssl"
# AdGuard API endpoints
API_STATUS = "/control/status"
API_DNS_CONFIG = "/control/dns_config"
API_STATS = "/control/stats"
API_CLIENTS = "/control/clients"
API_REWRITE = "/control/rewrite"
API_FILTERING = "/control/filtering"
# Switch types
SWITCH_PROTECTION = "protection"
SWITCH_FILTERING = "filtering"
SWITCH_SAFEBROWSING = "safebrowsing"
SWITCH_PARENTAL = "parental"
SWITCH_SAFESEARCH = "safesearch"
SWITCH_QUERY_LOG = "query_log"
SWITCHES = {
SWITCH_PROTECTION: {
"name": "AdGuard Protection",
"icon": "mdi:shield-check",
"api_key": "protection_enabled",
},
SWITCH_FILTERING: {
"name": "DNS Filtering",
"icon": "mdi:filter",
"api_key": "filtering_enabled",
},
SWITCH_SAFEBROWSING: {
"name": "Safe Browsing",
"icon": "mdi:shield-bug",
"api_key": "safebrowsing_enabled",
},
SWITCH_PARENTAL: {
"name": "Parental Control",
"icon": "mdi:account-child-circle",
"api_key": "parental_enabled",
},
SWITCH_SAFESEARCH: {
"name": "Safe Search",
"icon": "mdi:shield-search",
"api_key": "safesearch_enabled",
},
SWITCH_QUERY_LOG: {
"name": "Query Log",
"icon": "mdi:file-document-multiple",
"api_key": "querylog_enabled",
},
}
# Sensor types
SENSOR_DNS_QUERIES = "dns_queries"
SENSOR_BLOCKED_QUERIES = "blocked_queries"
SENSOR_BLOCKED_PERCENTAGE = "blocked_percentage"
SENSOR_ACTIVE_FILTERS = "active_filters"
SENSOR_AVG_PROCESSING_TIME = "avg_processing_time"
SENSORS = {
SENSOR_DNS_QUERIES: {
"name": "DNS Queries",
"icon": "mdi:dns",
"unit": "queries",
"api_key": "num_dns_queries",
},
SENSOR_BLOCKED_QUERIES: {
"name": "Blocked Queries",
"icon": "mdi:shield-check",
"unit": "queries",
"api_key": "num_blocked_filtering",
},
SENSOR_BLOCKED_PERCENTAGE: {
"name": "Blocked Percentage",
"icon": "mdi:percent",
"unit": "%",
"api_key": "blocked_percentage",
},
SENSOR_ACTIVE_FILTERS: {
"name": "Active Filter Rules",
"icon": "mdi:filter-check",
"unit": "rules",
"api_key": "num_replaced_safebrowsing",
},
SENSOR_AVG_PROCESSING_TIME: {
"name": "Average Processing Time",
"icon": "mdi:clock-fast",
"unit": "ms",
"api_key": "avg_processing_time",
},
}
# Update interval
DEFAULT_SCAN_INTERVAL = 30

View File

@@ -0,0 +1,100 @@
"""AdGuard Control Hub data update coordinator."""
from __future__ import annotations
import logging
from datetime import timedelta
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .api import AdGuardHomeAPI, AdGuardHomeApiError, AdGuardHomeAuthError, AdGuardHomeConnectionError
from .const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, DOMAIN
_LOGGER = logging.getLogger(__name__)
class AdGuardDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching AdGuard Home data from the API."""
def __init__(
self,
hass: HomeAssistant,
session,
config: dict[str, Any],
update_interval: timedelta,
) -> None:
"""Initialize the data update coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=update_interval,
)
self.api = AdGuardHomeAPI(
session,
config[CONF_HOST],
config[CONF_PORT],
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
config.get(CONF_SSL, False),
config.get(CONF_VERIFY_SSL, True),
)
self._config = config
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
try:
# Get status and stats data
status_data = await self.api.get_status()
stats_data = await self.api.get_stats()
clients_data = await self.api.get_clients()
# Combine all data
data = {
"status": status_data,
"stats": stats_data,
"clients": clients_data,
}
# Calculate blocked percentage
if "stats" in data and "num_dns_queries" in data["stats"]:
queries = data["stats"].get("num_dns_queries", 0)
blocked = data["stats"].get("num_blocked_filtering", 0)
if queries > 0:
data["stats"]["blocked_percentage"] = round((blocked / queries) * 100, 2)
else:
data["stats"]["blocked_percentage"] = 0.0
return data
except AdGuardHomeAuthError as err:
raise ConfigEntryAuthFailed("Authentication failed") from err
except (AdGuardHomeConnectionError, AdGuardHomeApiError) as err:
raise UpdateFailed(f"Error communicating with AdGuard Home: {err}") from err
async def async_set_switch(self, switch_type: str, state: bool) -> None:
"""Set switch state."""
try:
if switch_type == "protection":
await self.api.set_protection(state)
elif switch_type == "filtering":
await self.api.set_filtering(state)
elif switch_type == "safebrowsing":
await self.api.set_safebrowsing(state)
elif switch_type == "parental":
await self.api.set_parental_control(state)
elif switch_type == "safesearch":
await self.api.set_safe_search(state)
elif switch_type == "query_log":
await self.api.set_query_log(state)
# Refresh data after changing state
await self.async_request_refresh()
except (AdGuardHomeConnectionError, AdGuardHomeApiError) as err:
raise UpdateFailed(f"Error setting switch {switch_type}: {err}") from err

View File

@@ -0,0 +1,16 @@
{
"domain": "adguard_control_hub",
"name": "AdGuard Control Hub",
"version": "1.0.0",
"documentation": "https://github.com/your-username/adguard-control-hub",
"issue_tracker": "https://github.com/your-username/adguard-control-hub/issues",
"dependencies": [],
"codeowners": [
"@your-username"
],
"requirements": [
"aiohttp>=3.8.0"
],
"config_flow": true,
"iot_class": "local_polling"
}

View File

@@ -0,0 +1,98 @@
"""AdGuard Control Hub sensor platform."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.sensor import SensorEntity, SensorStateClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, SENSORS
from .coordinator import AdGuardDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AdGuard Control Hub sensor based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
entities = []
for sensor_type, sensor_info in SENSORS.items():
entities.append(
AdGuardSensor(
coordinator,
entry,
sensor_type,
sensor_info,
)
)
async_add_entities(entities)
class AdGuardSensor(CoordinatorEntity[AdGuardDataUpdateCoordinator], SensorEntity):
"""Representation of an AdGuard Control Hub sensor."""
def __init__(
self,
coordinator: AdGuardDataUpdateCoordinator,
entry: ConfigEntry,
sensor_type: str,
sensor_info: dict[str, Any],
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._sensor_type = sensor_type
self._sensor_info = sensor_info
self._entry = entry
self._attr_name = sensor_info["name"]
self._attr_icon = sensor_info["icon"]
self._attr_native_unit_of_measurement = sensor_info.get("unit")
self._attr_unique_id = f"{entry.entry_id}_{sensor_type}"
# Set state class for numeric sensors
if sensor_info.get("unit") in ["queries", "rules", "ms"]:
self._attr_state_class = SensorStateClass.MEASUREMENT
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this AdGuard Home instance."""
return DeviceInfo(
identifiers={(DOMAIN, self._entry.entry_id)},
name="AdGuard Control Hub",
manufacturer="AdGuard",
model="AdGuard Home",
sw_version=self.coordinator.data.get("status", {}).get("version"),
configuration_url=f"http://{self._entry.data['host']}:{self._entry.data['port']}",
)
@property
def native_value(self) -> str | int | float | None:
"""Return the native value of the sensor."""
if self.coordinator.data is None:
return None
stats_data = self.coordinator.data.get("stats", {})
api_key = self._sensor_info["api_key"]
value = stats_data.get(api_key)
# Handle special cases
if self._sensor_type == "blocked_percentage":
return value
elif self._sensor_type == "avg_processing_time":
# Convert to milliseconds if needed
if value is not None:
return round(value, 2)
return value

View File

@@ -0,0 +1,26 @@
{
"config": {
"step": {
"user": {
"title": "AdGuard Control Hub Setup",
"description": "Configure connection to your AdGuard Home instance",
"data": {
"host": "Host",
"port": "Port",
"username": "Username (optional)",
"password": "Password (optional)",
"ssl": "Use HTTPS",
"verify_ssl": "Verify SSL certificate"
}
}
},
"error": {
"cannot_connect": "Failed to connect to AdGuard Home",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error occurred"
},
"abort": {
"already_configured": "AdGuard Home instance already configured"
}
}
}

View File

@@ -0,0 +1,91 @@
"""AdGuard Control Hub switch platform."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, SWITCHES
from .coordinator import AdGuardDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AdGuard Control Hub switch based on a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
entities = []
for switch_type, switch_info in SWITCHES.items():
entities.append(
AdGuardSwitch(
coordinator,
entry,
switch_type,
switch_info,
)
)
async_add_entities(entities)
class AdGuardSwitch(CoordinatorEntity[AdGuardDataUpdateCoordinator], SwitchEntity):
"""Representation of an AdGuard Control Hub switch."""
def __init__(
self,
coordinator: AdGuardDataUpdateCoordinator,
entry: ConfigEntry,
switch_type: str,
switch_info: dict[str, Any],
) -> None:
"""Initialize the switch."""
super().__init__(coordinator)
self._switch_type = switch_type
self._switch_info = switch_info
self._entry = entry
self._attr_name = switch_info["name"]
self._attr_icon = switch_info["icon"]
self._attr_unique_id = f"{entry.entry_id}_{switch_type}"
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this AdGuard Home instance."""
return DeviceInfo(
identifiers={(DOMAIN, self._entry.entry_id)},
name="AdGuard Control Hub",
manufacturer="AdGuard",
model="AdGuard Home",
sw_version=self.coordinator.data.get("status", {}).get("version"),
configuration_url=f"http://{self._entry.data['host']}:{self._entry.data['port']}",
)
@property
def is_on(self) -> bool | None:
"""Return True if entity is on."""
if self.coordinator.data is None:
return None
status_data = self.coordinator.data.get("status", {})
api_key = self._switch_info["api_key"]
return status_data.get(api_key, False)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
await self.coordinator.async_set_switch(self._switch_type, True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
await self.coordinator.async_set_switch(self._switch_type, False)

View File

@@ -0,0 +1,36 @@
{
"config": {
"step": {
"user": {
"title": "AdGuard Control Hub Setup",
"description": "Configure connection to your AdGuard Home instance",
"data": {
"host": "Host",
"port": "Port",
"username": "Username (optional)",
"password": "Password (optional)",
"ssl": "Use HTTPS",
"verify_ssl": "Verify SSL certificate"
}
}
},
"error": {
"cannot_connect": "Failed to connect to AdGuard Home",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error occurred"
},
"abort": {
"already_configured": "AdGuard Home instance already configured"
}
},
"options": {
"step": {
"init": {
"title": "AdGuard Control Hub Options",
"data": {
"scan_interval": "Update interval (seconds)"
}
}
}
}
}

9
hacs.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "AdGuard Control Hub",
"render_readme": true,
"domains": [
"switch",
"sensor",
"binary_sensor"
]
}

5
requirements_test.txt Normal file
View File

@@ -0,0 +1,5 @@
pytest>=7.4.0
pytest-homeassistant-custom-component>=0.13.0
pytest-asyncio>=0.21.1
aiohttp>=3.8.0
voluptuous>=0.13.1

69
tests/conftest.py Normal file
View File

@@ -0,0 +1,69 @@
"""Test configuration for AdGuard Control Hub integration."""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from custom_components.adguard_control_hub.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
DOMAIN,
)
@pytest.fixture
def mock_adguard_api():
"""Mock AdGuard API client."""
mock_api = MagicMock()
mock_api.get_status = AsyncMock(return_value={
"version": "0.107.50",
"protection_enabled": True,
"filtering_enabled": True,
"safebrowsing_enabled": True,
"parental_enabled": True,
"safesearch_enabled": True,
"querylog_enabled": True,
})
mock_api.get_stats = AsyncMock(return_value={
"num_dns_queries": 1000,
"num_blocked_filtering": 200,
"avg_processing_time": 1.5,
"num_replaced_safebrowsing": 1500,
})
mock_api.get_clients = AsyncMock(return_value={
"clients": [],
"auto_clients": [],
})
mock_api.set_protection = AsyncMock()
mock_api.set_filtering = AsyncMock()
mock_api.set_safebrowsing = AsyncMock()
mock_api.set_parental_control = AsyncMock()
mock_api.set_safe_search = AsyncMock()
mock_api.set_query_log = AsyncMock()
mock_api.test_connection = AsyncMock(return_value=True)
return mock_api
@pytest.fixture
def mock_config_entry():
"""Mock config entry."""
return {
CONF_HOST: "192.168.1.100",
CONF_PORT: 3000,
CONF_USERNAME: "admin",
CONF_PASSWORD: "password",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
}
@pytest.fixture
def mock_setup_entry():
"""Mock setup entry."""
with patch("custom_components.adguard_control_hub.AdGuardHomeAPI") as mock_api_class:
yield mock_api_class

104
tests/test_config_flow.py Normal file
View File

@@ -0,0 +1,104 @@
"""Test AdGuard Control Hub config flow."""
import pytest
from unittest.mock import AsyncMock, patch
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant
from custom_components.adguard_control_hub.config_flow import (
CannotConnect,
InvalidAuth,
)
from custom_components.adguard_control_hub.const import CONF_SSL, CONF_VERIFY_SSL, DOMAIN
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] is None
async def test_form_valid_connection(hass: HomeAssistant, mock_adguard_api) -> None:
"""Test we get the form with valid connection."""
with patch(
"custom_components.adguard_control_hub.config_flow.AdGuardHomeAPI"
) as mock_api_class:
mock_api_class.return_value = mock_adguard_api
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "192.168.1.100",
CONF_PORT: 3000,
CONF_USERNAME: "admin",
CONF_PASSWORD: "password",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "AdGuard Home (192.168.1.100)"
assert result2["data"] == {
CONF_HOST: "192.168.1.100",
CONF_PORT: 3000,
CONF_USERNAME: "admin",
CONF_PASSWORD: "password",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
}
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"""Test we handle cannot connect error."""
with patch(
"custom_components.adguard_control_hub.config_flow.validate_input",
side_effect=CannotConnect,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "192.168.1.100",
CONF_PORT: 3000,
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
"""Test we handle invalid auth error."""
with patch(
"custom_components.adguard_control_hub.config_flow.validate_input",
side_effect=InvalidAuth,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "192.168.1.100",
CONF_PORT: 3000,
CONF_USERNAME: "admin",
CONF_PASSWORD: "wrong",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"}

47
tests/test_init.py Normal file
View File

@@ -0,0 +1,47 @@
"""Test AdGuard Control Hub initialization."""
from unittest.mock import patch
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from custom_components.adguard_control_hub.const import DOMAIN
async def test_setup_entry(hass: HomeAssistant, mock_config_entry, mock_adguard_api) -> None:
"""Test successful setup of entry."""
with patch(
"custom_components.adguard_control_hub.AdGuardHomeAPI"
) as mock_api_class:
mock_api_class.return_value = mock_adguard_api
entry = hass.config_entries.async_add(mock_config_entry)
with patch("custom_components.adguard_control_hub.PLATFORMS", []):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED
assert DOMAIN in hass.data
async def test_unload_entry(hass: HomeAssistant, mock_config_entry, mock_adguard_api) -> None:
"""Test successful unload of entry."""
with patch(
"custom_components.adguard_control_hub.AdGuardHomeAPI"
) as mock_api_class:
mock_api_class.return_value = mock_adguard_api
entry = hass.config_entries.async_add(mock_config_entry)
with patch("custom_components.adguard_control_hub.PLATFORMS", []):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.NOT_LOADED

527
wiki/Advanced-Usage.md Normal file
View File

@@ -0,0 +1,527 @@
# Advanced Usage
This guide covers advanced features, automations, and customizations for AdGuard Control Hub.
## Lovelace Dashboard Cards
Create informative dashboard cards to monitor and control your AdGuard Home instance.
### Protection Status Card
```yaml
type: entities
title: AdGuard Protection
entities:
- switch.adguard_protection
- switch.adguard_dns_filtering
- switch.adguard_safe_browsing
- switch.adguard_parental_control
- switch.adguard_safe_search
- switch.adguard_query_log
show_header_toggle: true
state_color: true
```
### Statistics Overview Card
```yaml
type: glance
title: AdGuard Statistics
entities:
- entity: sensor.adguard_dns_queries
name: Total Queries
- entity: sensor.adguard_blocked_queries
name: Blocked
- entity: sensor.adguard_blocked_percentage
name: Blocked %
- entity: sensor.adguard_average_processing_time
name: Avg Time
columns: 2
show_state: true
show_name: true
```
### Historical Chart Card
```yaml
type: history-graph
title: DNS Activity (24h)
entities:
- sensor.adguard_dns_queries
- sensor.adguard_blocked_queries
hours_to_show: 24
refresh_interval: 60
```
### Protection Control Card
```yaml
type: custom:mushroom-chips-card
chips:
- type: entity
entity: switch.adguard_protection
icon_color: green
tap_action:
action: toggle
- type: entity
entity: switch.adguard_parental_control
icon_color: blue
tap_action:
action: toggle
- type: entity
entity: binary_sensor.adguard_home_running
icon_color: red
```
## Automations
### Basic Automations
#### Enable Parental Controls During School Hours
```yaml
automation:
- alias: "AdGuard: Enable Parental Controls - School Hours"
trigger:
- platform: time
at: "08:00:00"
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
action:
- service: switch.turn_on
target:
entity_id: switch.adguard_parental_control
- service: notify.mobile_app_your_phone
data:
title: "AdGuard"
message: "Parental controls enabled for school hours"
- alias: "AdGuard: Disable Parental Controls - After School"
trigger:
- platform: time
at: "16:00:00"
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
action:
- service: switch.turn_off
target:
entity_id: switch.adguard_parental_control
- service: notify.mobile_app_your_phone
data:
title: "AdGuard"
message: "Parental controls disabled - school day ended"
```
#### Temporarily Disable Filtering
```yaml
automation:
- alias: "AdGuard: Temporary Disable Protection"
trigger:
- platform: event
event_type: call_service
event_data:
domain: script
service: disable_adguard_temporarily
action:
- service: switch.turn_off
target:
entity_id: switch.adguard_protection
- delay: "00:10:00" # 10 minutes
- service: switch.turn_on
target:
entity_id: switch.adguard_protection
- service: notify.mobile_app_your_phone
data:
title: "AdGuard"
message: "Protection re-enabled after temporary disable"
script:
disable_adguard_temporarily:
alias: "Disable AdGuard for 10 minutes"
sequence:
- event: call_service
event_data:
domain: script
service: disable_adguard_temporarily
```
### Advanced Automations
#### Dynamic Protection Based on Network Activity
```yaml
automation:
- alias: "AdGuard: High Activity Alert"
trigger:
- platform: numeric_state
entity_id: sensor.adguard_dns_queries
above: 5000
for: "00:05:00"
action:
- service: notify.mobile_app_your_phone
data:
title: "AdGuard Alert"
message: "High DNS activity detected: {{ states('sensor.adguard_dns_queries') }} queries"
- service: switch.turn_on
target:
entity_id: switch.adguard_safe_browsing
- service: switch.turn_on
target:
entity_id: switch.adguard_dns_filtering
- alias: "AdGuard: Performance Monitoring"
trigger:
- platform: numeric_state
entity_id: sensor.adguard_average_processing_time
above: 100
for: "00:02:00"
action:
- service: persistent_notification.create
data:
title: "AdGuard Performance Warning"
message: |
DNS response time is high: {{ states('sensor.adguard_average_processing_time') }}ms
Current queries: {{ states('sensor.adguard_dns_queries') }}
Active rules: {{ states('sensor.adguard_active_filter_rules') }}
notification_id: adguard_performance
```
#### Guest Network Protection
```yaml
automation:
- alias: "AdGuard: Guest Network - Enable Strict Filtering"
trigger:
- platform: device_tracker
entity_id: device_tracker.guest_device
to: "home"
action:
- service: switch.turn_on
target:
entity_id:
- switch.adguard_safe_browsing
- switch.adguard_parental_control
- switch.adguard_safe_search
- service: notify.mobile_app_your_phone
data:
title: "AdGuard"
message: "Guest device connected - strict filtering enabled"
- alias: "AdGuard: Guest Network - Restore Normal Settings"
trigger:
- platform: device_tracker
entity_id: device_tracker.guest_device
to: "not_home"
for: "00:05:00"
action:
- service: switch.turn_off
target:
entity_id: switch.adguard_parental_control
- service: notify.mobile_app_your_phone
data:
title: "AdGuard"
message: "Guest device disconnected - normal filtering restored"
```
### Monitoring and Alerting
#### AdGuard Home Downtime Alert
```yaml
automation:
- alias: "AdGuard: Downtime Alert"
trigger:
- platform: state
entity_id: binary_sensor.adguard_home_running
to: "off"
for: "00:02:00"
action:
- service: notify.mobile_app_your_phone
data:
title: "AdGuard Home Down"
message: "AdGuard Home is not responding. Check the service status."
data:
priority: high
color: red
- alias: "AdGuard: Service Restored"
trigger:
- platform: state
entity_id: binary_sensor.adguard_home_running
to: "on"
condition:
- condition: state
entity_id: binary_sensor.adguard_home_running
state: "off"
for: "00:01:00"
action:
- service: notify.mobile_app_your_phone
data:
title: "AdGuard Home Restored"
message: "AdGuard Home is responding again."
data:
color: green
```
## Scripts
### Quick Control Scripts
#### Emergency Bypass Script
```yaml
script:
adguard_emergency_bypass:
alias: "AdGuard Emergency Bypass"
description: "Quickly disable all protection for troubleshooting"
sequence:
- service: switch.turn_off
target:
entity_id: switch.adguard_protection
- service: timer.start
target:
entity_id: timer.adguard_bypass
data:
duration: "00:15:00"
- service: persistent_notification.create
data:
title: "AdGuard Bypass Active"
message: "All protection disabled for 15 minutes"
notification_id: adguard_bypass
timer:
adguard_bypass:
duration: "00:15:00"
restore: true
```
#### Weekly Statistics Report
```yaml
script:
adguard_weekly_report:
alias: "AdGuard Weekly Report"
sequence:
- service: notify.mobile_app_your_phone
data:
title: "AdGuard Weekly Report"
message: |
Total Queries: {{ states('sensor.adguard_dns_queries') }}
Blocked: {{ states('sensor.adguard_blocked_queries') }}
Block Rate: {{ states('sensor.adguard_blocked_percentage') }}%
Avg Response Time: {{ states('sensor.adguard_average_processing_time') }}ms
Filter Rules: {{ states('sensor.adguard_active_filter_rules') }}
automation:
- alias: "AdGuard: Weekly Report"
trigger:
- platform: time
at: "09:00:00"
condition:
- condition: time
weekday:
- sun
action:
- service: script.adguard_weekly_report
```
## Custom Templates
### Template Sensors
#### Protection Status Summary
```yaml
template:
- sensor:
- name: "AdGuard Protection Summary"
state: >
{% set protection = states('switch.adguard_protection') %}
{% set filtering = states('switch.adguard_dns_filtering') %}
{% set safebrowsing = states('switch.adguard_safe_browsing') %}
{% set parental = states('switch.adguard_parental_control') %}
{% if protection == 'off' %}
Disabled
{% else %}
{% set active = [filtering, safebrowsing, parental] | select('eq', 'on') | list | length %}
{% if active == 3 %}
Full Protection
{% elif active == 0 %}
Basic Protection
{% else %}
Partial Protection ({{ active }}/3)
{% endif %}
{% endif %}
icon: >
{% set protection = states('switch.adguard_protection') %}
{% if protection == 'off' %}
mdi:shield-off
{% else %}
mdi:shield-check
{% endif %}
```
#### Daily Block Statistics
```yaml
template:
- sensor:
- name: "AdGuard Daily Block Rate"
state: >
{% set queries = states('sensor.adguard_dns_queries') | int(0) %}
{% set blocked = states('sensor.adguard_blocked_queries') | int(0) %}
{% if queries > 0 %}
{{ ((blocked / queries) * 100) | round(1) }}
{% else %}
0
{% endif %}
unit_of_measurement: "%"
device_class: None
attributes:
daily_queries: "{{ states('sensor.adguard_dns_queries') }}"
daily_blocked: "{{ states('sensor.adguard_blocked_queries') }}"
effectiveness: >
{% set rate = states('sensor.adguard_daily_block_rate') | float(0) %}
{% if rate > 20 %}
High
{% elif rate > 10 %}
Medium
{% elif rate > 5 %}
Low
{% else %}
Very Low
{% endif %}
```
## Node-RED Integration
### Flow Examples
#### AdGuard Control Flow
```json
[
{
"id": "adguard-control",
"type": "api-call-service",
"name": "Toggle AdGuard Protection",
"service_domain": "switch",
"service": "toggle",
"entityId": "switch.adguard_protection",
"data": {},
"wires": [["notification-node"]]
},
{
"id": "notification-node",
"type": "api-call-service",
"name": "Send Notification",
"service_domain": "notify",
"service": "mobile_app_your_phone",
"data": {
"title": "AdGuard",
"message": "Protection toggled via Node-RED"
}
}
]
```
## API Integration
### REST Commands
Add REST commands to your configuration.yaml for direct API access:
```yaml
rest_command:
adguard_enable_protection:
url: "http://192.168.1.100:3000/control/dns_config"
method: POST
headers:
Content-Type: application/json
Authorization: "Basic {{ ('admin:password') | b64encode }}"
payload: '{"protection_enabled": true}'
adguard_get_status:
url: "http://192.168.1.100:3000/control/status"
method: GET
headers:
Authorization: "Basic {{ ('admin:password') | b64encode }}"
```
## Troubleshooting Advanced Features
### Automation Issues
**Automations not triggering:**
1. Check automation conditions and triggers
2. Verify entity IDs are correct
3. Check timezone settings
4. Review automation traces in Developer Tools
**State changes not detected:**
1. Verify update intervals
2. Check network connectivity
3. Review entity history
4. Confirm AdGuard Home is responding
### Template Issues
**Templates showing unavailable:**
1. Check template syntax
2. Verify referenced entities exist
3. Test templates in Developer Tools → Template
4. Review Home Assistant logs for template errors
### Performance Optimization
**High resource usage:**
1. Reduce automation frequency
2. Optimize template sensors
3. Use conditions to prevent unnecessary actions
4. Monitor Home Assistant performance
**Slow response times:**
1. Check AdGuard Home performance
2. Verify network latency
3. Reduce update frequency if needed
4. Monitor integration logs
## Best Practices
### Security
- Use HTTPS when possible
- Store credentials in secrets.yaml
- Limit API access to required functions
- Regular security updates
### Reliability
- Add error handling to automations
- Use conditions to prevent conflicts
- Monitor integration health
- Have fallback procedures
### Performance
- Balance update frequency with resources
- Use efficient templates
- Avoid excessive API calls
- Monitor system resources
## Next Steps
- [Review troubleshooting guide](Troubleshooting.md)
- [Learn about API endpoints](API-Reference.md)
- [Contribute to development](Development.md)

244
wiki/Configuration.md Normal file
View File

@@ -0,0 +1,244 @@
# Configuration Guide
This guide covers all configuration options for AdGuard Control Hub integration.
## Initial Setup
### Basic Configuration
When adding the integration for the first time, you'll need to provide:
| Field | Description | Required | Default |
|-------|-------------|----------|---------|
| **Host** | IP address or hostname of AdGuard Home | ✅ | - |
| **Port** | Port number AdGuard Home is running on | ✅ | 3000 |
| **Username** | Admin username (if authentication enabled) | ❌ | - |
| **Password** | Admin password (if authentication enabled) | ❌ | - |
| **Use HTTPS** | Enable HTTPS connection | ❌ | false |
| **Verify SSL** | Verify SSL certificates | ❌ | true |
### Step-by-Step Configuration
1. **Navigate to Integration Setup**
- Go to **Settings****Devices & Services**
- Click **"+ ADD INTEGRATION"**
- Search for **"AdGuard Control Hub"**
2. **Enter Connection Details**
```
Host: 192.168.1.100
Port: 3000
Username: admin (optional)
Password: your-password (optional)
Use HTTPS: ☐ (check if using HTTPS)
Verify SSL: ☑ (recommended)
```
3. **Test Connection**
- Click **"SUBMIT"**
- Integration will test the connection
- If successful, you'll see a success message
4. **Complete Setup**
- Integration will create all entities
- Device will appear in **Devices & Services**
## Advanced Configuration
### HTTPS Setup
If your AdGuard Home uses HTTPS:
1. **Enable HTTPS in AdGuard Home**
- Open AdGuard Home web interface
- Go to **Settings** → **Encryption**
- Configure SSL certificate
2. **Configure Integration**
- Check **"Use HTTPS"** during setup
- Use HTTPS port (usually 443 or custom)
3. **SSL Certificate Verification**
- **Verify SSL: ON** - Recommended for production
- **Verify SSL: OFF** - Only for self-signed certificates
### Authentication Configuration
AdGuard Home supports optional authentication:
#### Without Authentication
- Leave **Username** and **Password** empty
- AdGuard Home must have authentication disabled
#### With Authentication
- Enter your AdGuard Home admin credentials
- User must have admin privileges
- Credentials are stored securely in Home Assistant
### Network Configuration Examples
#### Local Network
```
Host: 192.168.1.100
Port: 3000
Use HTTPS: false
```
#### Remote Server with SSL
```
Host: adguard.example.com
Port: 443
Use HTTPS: true
Verify SSL: true
Username: admin
Password: secure-password
```
#### Docker Installation
```
Host: adguard-home.local
Port: 3000
Use HTTPS: false
```
## Multiple Instances
You can configure multiple AdGuard Home instances:
1. **Add each instance separately**
- Each gets its own configuration entry
- Entities are prefixed with instance name
- All instances appear as separate devices
2. **Naming Convention**
- Integration automatically names based on hostname
- Example: "AdGuard Home (192.168.1.100)"
- Example: "AdGuard Home (adguard.example.com)"
## Entity Configuration
### Entity Naming
Entities are automatically created with descriptive names:
**Switches:**
- `switch.adguard_protection`
- `switch.adguard_dns_filtering`
- `switch.adguard_safe_browsing`
- `switch.adguard_parental_control`
- `switch.adguard_safe_search`
- `switch.adguard_query_log`
**Sensors:**
- `sensor.adguard_dns_queries`
- `sensor.adguard_blocked_queries`
- `sensor.adguard_blocked_percentage`
- `sensor.adguard_active_filter_rules`
- `sensor.adguard_average_processing_time`
**Binary Sensors:**
- `binary_sensor.adguard_home_running`
### Customizing Entities
You can customize entity names and icons:
1. **Entity Settings**
- Go to **Settings** → **Devices & Services**
- Find AdGuard Control Hub device
- Click on any entity
- Click gear icon (⚙️) to edit
2. **Available Customizations**
- Entity ID
- Friendly name
- Icon
- Device class
- Area assignment
## Update Interval
The integration updates every 30 seconds by default. This provides a good balance between responsiveness and resource usage.
**Note:** Query log must be enabled in AdGuard Home for statistics to update. Disabling query log will stop sensor updates.
## Options Configuration
Currently, there are no additional options to configure after initial setup. All configuration is done during the initial integration setup.
## Configuration Troubleshooting
### Connection Issues
**Cannot connect to AdGuard Home:**
1. Verify AdGuard Home is running
2. Check network connectivity: `ping your-adguard-ip`
3. Test API manually: `curl http://your-adguard-ip:port/control/status`
4. Verify firewall allows connection
**SSL/HTTPS Issues:**
1. Verify certificate is valid
2. Check certificate matches hostname
3. Try with SSL verification disabled temporarily
4. Ensure correct port for HTTPS
### Authentication Issues
**Authentication failed:**
1. Verify credentials in AdGuard Home
2. Check user has admin privileges
3. Test login in AdGuard Home web interface
4. Ensure password doesn't contain special characters that need escaping
**Forbidden access:**
1. User account may not have sufficient privileges
2. Check AdGuard Home access control settings
3. Verify IP is not blocked by AdGuard Home
### Entity Issues
**Entities not created:**
1. Check Home Assistant logs for errors
2. Verify AdGuard Home API is responding
3. Restart Home Assistant
4. Check entity registry for conflicts
**Entities showing unavailable:**
1. Check AdGuard Home is running
2. Verify network connectivity
3. Check authentication status
4. Review integration logs
## Configuration Files
### Example YAML (for reference only)
*Configuration is done via UI, not YAML*
```yaml
# This is for reference only - actual configuration is done via UI
adguard_control_hub:
host: 192.168.1.100
port: 3000
username: admin
password: !secret adguard_password
ssl: false
verify_ssl: true
```
### Secrets Management
Store sensitive information in `secrets.yaml`:
```yaml
# secrets.yaml
adguard_password: "your-secure-password"
adguard_host: "192.168.1.100"
```
## Next Steps
After configuration:
- [Explore available features](Features.md)
- [Set up Lovelace cards](Advanced-Usage.md#lovelace-cards)
- [Create automations](Advanced-Usage.md#automations)
- [Monitor performance](Features.md#sensors)

294
wiki/Features.md Normal file
View File

@@ -0,0 +1,294 @@
# Features Overview
AdGuard Control Hub provides comprehensive control and monitoring of your AdGuard Home instance through Home Assistant.
## Switches
All switches provide instant control over AdGuard Home's protection features with real-time status updates.
### AdGuard Protection
- **Entity ID:** `switch.adguard_protection`
- **Function:** Master switch that controls all AdGuard features
- **Icon:** `mdi:shield-check`
**Behavior:**
- **ON:** All AdGuard features are active
- **OFF:** Bypasses all AdGuard features (DNS queries pass through without filtering)
- Acts as a master override for all other protection features
**Use Cases:**
- Temporarily disable all filtering for troubleshooting
- Emergency bypass for important downloads
- Scheduled maintenance windows
### DNS Filtering
- **Entity ID:** `switch.adguard_dns_filtering`
- **Function:** Enables DNS filtering using configured blocklists
- **Icon:** `mdi:filter`
**Behavior:**
- **ON:** DNS queries are filtered against blocklists
- **OFF:** DNS filtering is disabled, but other features remain active
- Only affects blocklist-based filtering
**Use Cases:**
- Disable filtering for specific time periods
- Allow access to falsely blocked domains temporarily
- Maintenance of filter lists
### Safe Browsing
- **Entity ID:** `switch.adguard_safe_browsing`
- **Function:** Blocks known phishing and malware sites
- **Icon:** `mdi:shield-bug`
**Behavior:**
- **ON:** Queries to malicious domains are blocked
- **OFF:** Safe browsing protection is disabled
- Uses AdGuard's malware and phishing database
**Use Cases:**
- Enhanced security for family networks
- Protection for less tech-savvy users
- Corporate security policies
### Parental Control
- **Entity ID:** `switch.adguard_parental_control`
- **Function:** Blocks adult content and inappropriate websites
- **Icon:** `mdi:account-child-circle`
**Behavior:**
- **ON:** Adult content domains are blocked
- **OFF:** Parental controls are disabled
- Blocks based on content category classification
**Use Cases:**
- Protect children from inappropriate content
- School or educational environments
- Time-based content filtering
### Safe Search
- **Entity ID:** `switch.adguard_safe_search`
- **Function:** Enforces safe search on major search engines
- **Icon:** `mdi:shield-search`
**Behavior:**
- **ON:** Forces safe search mode on Google, Bing, Yandex, etc.
- **OFF:** Search engines operate in normal mode
- Redirects search queries to safe search variants
**Use Cases:**
- Additional layer of content protection
- Compliance with content policies
- Educational institution requirements
### Query Log
- **Entity ID:** `switch.adguard_query_log`
- **Function:** Records DNS queries for statistics and analysis
- **Icon:** `mdi:file-document-multiple`
**Behavior:**
- **ON:** All DNS queries are logged and stored
- **OFF:** Query logging is disabled, statistics stop updating
- Required for sensor data and analytics
**Use Cases:**
- Monitor network activity
- Troubleshoot DNS issues
- Privacy mode (disable logging)
## Sensors
Sensors provide real-time statistics and monitoring data from your AdGuard Home instance.
### DNS Queries
- **Entity ID:** `sensor.adguard_dns_queries`
- **Unit:** queries
- **Function:** Total number of DNS queries processed
- **Icon:** `mdi:dns`
**Data Points:**
- Total queries since last statistics reset
- Updates every 30 seconds
- Includes all query types (A, AAAA, CNAME, etc.)
**Usage:**
- Monitor network activity levels
- Track DNS server load
- Historical trending
### Blocked Queries
- **Entity ID:** `sensor.adguard_blocked_queries`
- **Unit:** queries
- **Function:** Number of queries blocked by filtering
- **Icon:** `mdi:shield-check`
**Data Points:**
- Queries blocked by all filtering mechanisms
- Includes blocklist, parental control, and safe browsing blocks
- Updates every 30 seconds
**Usage:**
- Measure filtering effectiveness
- Identify potential threats blocked
- Compare blocking trends
### Blocked Percentage
- **Entity ID:** `sensor.adguard_blocked_percentage`
- **Unit:** %
- **Function:** Percentage of queries that were blocked
- **Icon:** `mdi:percent`
**Calculation:**
- `(Blocked Queries / Total Queries) × 100`
- Automatically calculated by integration
- Provides ratio view of filtering activity
**Usage:**
- Quick assessment of filtering impact
- Performance monitoring
- Historical comparison
### Active Filter Rules
- **Entity ID:** `sensor.adguard_active_filter_rules`
- **Unit:** rules
- **Function:** Number of active filtering rules loaded
- **Icon:** `mdi:filter-check`
**Data Points:**
- Total rules from all enabled filter lists
- Includes custom rules
- Updated when filter lists are refreshed
**Usage:**
- Monitor filter list size and complexity
- Track filter performance impact
- Troubleshoot over-blocking
### Average Processing Time
- **Entity ID:** `sensor.adguard_average_processing_time`
- **Unit:** ms
- **Function:** Average DNS query processing time
- **Icon:** `mdi:clock-fast`
**Measurement:**
- Time from query receipt to response
- Rolling average over recent queries
- Includes upstream server response time
**Usage:**
- Monitor DNS performance
- Identify performance bottlenecks
- Compare different upstream servers
## Binary Sensors
Binary sensors provide simple on/off status information.
### AdGuard Home Running
- **Entity ID:** `binary_sensor.adguard_home_running`
- **Function:** Shows if AdGuard Home is responsive
- **Icon:** `mdi:shield-check-outline`
**States:**
- **ON:** AdGuard Home is responding to API requests
- **OFF:** AdGuard Home is unreachable or not responding
- **UNAVAILABLE:** Integration cannot determine status
**Usage:**
- Monitor AdGuard Home availability
- Trigger alerts for downtime
- Automation conditions
## Device Information
The integration creates a single device representing your AdGuard Home instance:
**Device Attributes:**
- **Name:** "AdGuard Control Hub"
- **Manufacturer:** "AdGuard"
- **Model:** "AdGuard Home"
- **Software Version:** Detected from AdGuard Home API
- **Configuration URL:** Direct link to AdGuard Home web interface
**Device Identifiers:**
- Unique identifier based on host and port
- Allows multiple instances without conflicts
- Persistent across restarts
## Entity Attributes
Each entity provides additional information in its attributes:
### Switch Attributes
```yaml
friendly_name: "AdGuard Protection"
device_class: switch
icon: "mdi:shield-check"
state: "on"
```
### Sensor Attributes
```yaml
friendly_name: "DNS Queries"
device_class: null
unit_of_measurement: "queries"
state_class: "measurement"
icon: "mdi:dns"
state: 1247
```
## Feature Dependencies
Understanding feature relationships:
1. **Query Log Dependency**
- Most sensors require query log to be enabled
- Disabling query log stops statistics updates
- Binary sensor still works without query log
2. **Master Protection Switch**
- When protection is OFF, individual switches show their configured state
- But filtering is bypassed regardless of individual switch states
- Protection switch overrides all other filtering
3. **Network Connectivity**
- All features require network access to AdGuard Home
- Connection loss makes all entities unavailable
- Integration automatically reconnects when connection is restored
## Feature Limitations
**Current Limitations:**
- Client-specific controls not yet implemented
- Custom filter list management not available
- DNS rewrite rules not supported
- DHCP settings not exposed
**Planned Features:**
- Individual client management
- Custom filtering rules via HA
- DNS rewrite configuration
- Advanced statistics and reporting
## Performance Considerations
**Update Frequency:**
- Default: 30-second intervals
- Balances responsiveness vs. resource usage
- Configurable in future versions
**Resource Usage:**
- Minimal CPU impact on AdGuard Home
- Uses standard AdGuard Home API endpoints
- No additional logging or processing required
**Network Traffic:**
- Small API requests every 30 seconds
- Typical response size: < 1KB per request
- Negligible bandwidth impact
## Next Steps
- [Set up advanced automations](Advanced-Usage.md)
- [Troubleshoot issues](Troubleshooting.md)
- [Learn about the API](API-Reference.md)

82
wiki/Home.md Normal file
View File

@@ -0,0 +1,82 @@
# AdGuard Control Hub Wiki
Welcome to the AdGuard Control Hub integration wiki! This comprehensive guide will help you get the most out of your AdGuard Home integration with Home Assistant.
## Quick Navigation
- [Installation Guide](Installation.md) - Step-by-step installation instructions
- [Configuration](Configuration.md) - Detailed configuration options
- [Features Overview](Features.md) - Complete feature documentation
- [API Reference](API-Reference.md) - AdGuard Home API information
- [Troubleshooting](Troubleshooting.md) - Common issues and solutions
- [Advanced Usage](Advanced-Usage.md) - Automation examples and advanced features
- [Development](Development.md) - Contributing and development setup
## What is AdGuard Control Hub?
AdGuard Control Hub is a comprehensive Home Assistant integration that provides complete control over your AdGuard Home DNS server. It allows you to:
- Monitor DNS statistics and performance
- Control filtering and protection features
- Manage clients and their settings
- Create powerful automations based on DNS activity
- View detailed analytics and reports
## Key Features
### 🛡️ Complete Protection Control
- Master protection switch
- DNS filtering management
- Safe browsing controls
- Parental control settings
- Safe search enforcement
- Query logging control
### 📊 Comprehensive Monitoring
- Real-time DNS query statistics
- Blocked query tracking
- Performance metrics
- Filter rule counts
- Client activity monitoring
### 🏠 Home Assistant Integration
- Native Home Assistant entities
- Lovelace dashboard cards
- Automation support
- Service calls
- Event triggering
### 🔧 Easy Configuration
- GUI-based setup flow
- Automatic discovery
- Connection validation
- Error handling
- Secure credential storage
## Getting Started
1. **Prerequisites**: Ensure you have AdGuard Home v0.107.0+ running
2. **Installation**: Follow the [Installation Guide](Installation.md)
3. **Configuration**: Set up your connection using the [Configuration Guide](Configuration.md)
4. **Features**: Explore available features in the [Features Overview](Features.md)
## Support
If you encounter any issues:
1. Check the [Troubleshooting Guide](Troubleshooting.md)
2. Review the [GitHub Issues](https://github.com/your-username/adguard-control-hub/issues)
3. Join the discussion in [GitHub Discussions](https://github.com/your-username/adguard-control-hub/discussions)
## Contributing
We welcome contributions! See the [Development Guide](Development.md) for information on:
- Setting up a development environment
- Running tests
- Submitting pull requests
- Reporting bugs
---
*This integration is not officially affiliated with AdGuard. AdGuard Home is a trademark of AdGuard Software Ltd.*

139
wiki/Installation.md Normal file
View File

@@ -0,0 +1,139 @@
# Installation Guide
This guide covers all installation methods for AdGuard Control Hub integration.
## Prerequisites
Before installing AdGuard Control Hub, ensure you have:
- **Home Assistant** 2023.5.0 or newer
- **AdGuard Home** v0.107.0 or newer
- Network connectivity between Home Assistant and AdGuard Home
- (Optional) Admin credentials for AdGuard Home if authentication is enabled
## Installation Methods
### Method 1: HACS Installation (Recommended)
HACS (Home Assistant Community Store) is the easiest way to install and manage custom integrations.
#### Step 1: Install HACS
If you don't have HACS installed:
1. Follow the [official HACS installation guide](https://hacs.xyz/docs/setup/prerequisites)
2. Restart Home Assistant after installation
#### Step 2: Add AdGuard Control Hub
1. Open Home Assistant web interface
2. Navigate to **HACS****Integrations**
3. Click the **"+ EXPLORE & DOWNLOAD REPOSITORIES"** button
4. Search for **"AdGuard Control Hub"**
5. Click on the integration and then **"DOWNLOAD"**
6. Select the latest version and click **"DOWNLOAD"**
7. Restart Home Assistant
#### Step 3: Add Integration
1. Navigate to **Settings****Devices & Services**
2. Click **"+ ADD INTEGRATION"**
3. Search for **"AdGuard Control Hub"**
4. Follow the configuration steps
### Method 2: Manual Installation
For users who prefer manual installation or cannot use HACS.
#### Step 1: Download Integration
1. Go to the [latest release page](https://github.com/your-username/adguard-control-hub/releases/latest)
2. Download the `adguard-control-hub.zip` file
3. Extract the ZIP file
#### Step 2: Copy Files
1. Copy the extracted `custom_components/adguard_control_hub` folder
2. Place it in your Home Assistant's `config/custom_components/` directory
3. The final structure should look like:
```
config/
└── custom_components/
└── adguard_control_hub/
├── __init__.py
├── manifest.json
├── config_flow.py
└── ... (other files)
```
#### Step 3: Restart and Configure
1. Restart Home Assistant
2. Navigate to **Settings****Devices & Services**
3. Click **"+ ADD INTEGRATION"**
4. Search for **"AdGuard Control Hub"**
5. Follow the configuration steps
## Verification
After installation, verify everything is working:
1. Check **Settings****Devices & Services** for the AdGuard Control Hub integration
2. Verify entities are created under the AdGuard device
3. Test switching protection on/off
4. Check the Home Assistant logs for any errors
## Next Steps
- [Configure the integration](Configuration.md)
- [Explore available features](Features.md)
- [Set up automations](Advanced-Usage.md)
## Troubleshooting Installation
### HACS Issues
**Integration not found in HACS:**
- Ensure you're searching in the "Integrations" section
- Check if HACS is up to date
- Clear HACS cache and refresh
**Download fails:**
- Check your internet connection
- Verify HACS has proper GitHub access
- Try downloading manually and installing via Method 2
### Manual Installation Issues
**Integration not showing up:**
- Verify folder structure is correct
- Check file permissions
- Restart Home Assistant completely (not just config reload)
- Check Home Assistant logs for errors
**Permission errors:**
- Ensure Home Assistant has read/write access to custom_components
- Check file ownership matches Home Assistant user
### General Issues
**Integration fails to load:**
- Check Home Assistant logs: **Settings****System****Logs**
- Verify Python requirements are met
- Ensure Home Assistant version compatibility
**Cannot add integration:**
- Clear browser cache and cookies
- Try incognito/private browsing mode
- Check for JavaScript errors in browser console
## Getting Help
If you're still having issues:
1. Check the [Troubleshooting Guide](Troubleshooting.md)
2. Review [GitHub Issues](https://github.com/your-username/adguard-control-hub/issues)
3. Create a new issue with:
- Installation method used
- Home Assistant version
- Error messages from logs
- Steps you've already tried