commit d4cdcc04c0fb5256a610a65f2e0eaf29a79cbbce Author: Rafal Zielinski Date: Thu Oct 2 16:00:15 2025 +0100 Initial commit Signed-off-by: Rafal Zielinski diff --git a/.gitea/.DS_Store b/.gitea/.DS_Store new file mode 100644 index 0000000..36b372c Binary files /dev/null and b/.gitea/.DS_Store differ diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..5c0f23a --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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 \ No newline at end of file diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml new file mode 100644 index 0000000..79aba02 --- /dev/null +++ b/.gitea/workflows/test.yml @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a1045f --- /dev/null +++ b/README.md @@ -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 diff --git a/custom_components/adguard_control_hub/__init__.py b/custom_components/adguard_control_hub/__init__.py new file mode 100644 index 0000000..beda5a1 --- /dev/null +++ b/custom_components/adguard_control_hub/__init__.py @@ -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) diff --git a/custom_components/adguard_control_hub/api.py b/custom_components/adguard_control_hub/api.py new file mode 100644 index 0000000..804c33e --- /dev/null +++ b/custom_components/adguard_control_hub/api.py @@ -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 diff --git a/custom_components/adguard_control_hub/binary_sensor.py b/custom_components/adguard_control_hub/binary_sensor.py new file mode 100644 index 0000000..e0c6e70 --- /dev/null +++ b/custom_components/adguard_control_hub/binary_sensor.py @@ -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 diff --git a/custom_components/adguard_control_hub/config_flow.py b/custom_components/adguard_control_hub/config_flow.py new file mode 100644 index 0000000..93413a7 --- /dev/null +++ b/custom_components/adguard_control_hub/config_flow.py @@ -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.""" diff --git a/custom_components/adguard_control_hub/const.py b/custom_components/adguard_control_hub/const.py new file mode 100644 index 0000000..b741fc8 --- /dev/null +++ b/custom_components/adguard_control_hub/const.py @@ -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 diff --git a/custom_components/adguard_control_hub/coordinator.py b/custom_components/adguard_control_hub/coordinator.py new file mode 100644 index 0000000..8053734 --- /dev/null +++ b/custom_components/adguard_control_hub/coordinator.py @@ -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 diff --git a/custom_components/adguard_control_hub/manifest.json b/custom_components/adguard_control_hub/manifest.json new file mode 100644 index 0000000..b7f831c --- /dev/null +++ b/custom_components/adguard_control_hub/manifest.json @@ -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" +} \ No newline at end of file diff --git a/custom_components/adguard_control_hub/sensor.py b/custom_components/adguard_control_hub/sensor.py new file mode 100644 index 0000000..9739239 --- /dev/null +++ b/custom_components/adguard_control_hub/sensor.py @@ -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 diff --git a/custom_components/adguard_control_hub/strings.json b/custom_components/adguard_control_hub/strings.json new file mode 100644 index 0000000..ad28dcb --- /dev/null +++ b/custom_components/adguard_control_hub/strings.json @@ -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" + } + } +} \ No newline at end of file diff --git a/custom_components/adguard_control_hub/switch.py b/custom_components/adguard_control_hub/switch.py new file mode 100644 index 0000000..5c2af4d --- /dev/null +++ b/custom_components/adguard_control_hub/switch.py @@ -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) diff --git a/custom_components/adguard_control_hub/translations/en.json b/custom_components/adguard_control_hub/translations/en.json new file mode 100644 index 0000000..64b1c73 --- /dev/null +++ b/custom_components/adguard_control_hub/translations/en.json @@ -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)" + } + } + } + } +} \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..626d46a --- /dev/null +++ b/hacs.json @@ -0,0 +1,9 @@ +{ + "name": "AdGuard Control Hub", + "render_readme": true, + "domains": [ + "switch", + "sensor", + "binary_sensor" + ] +} \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..58866f3 --- /dev/null +++ b/requirements_test.txt @@ -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 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a6f1d8e --- /dev/null +++ b/tests/conftest.py @@ -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 diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py new file mode 100644 index 0000000..8c3361b --- /dev/null +++ b/tests/test_config_flow.py @@ -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"} diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..5abf37e --- /dev/null +++ b/tests/test_init.py @@ -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 diff --git a/wiki/Advanced-Usage.md b/wiki/Advanced-Usage.md new file mode 100644 index 0000000..cba9b4b --- /dev/null +++ b/wiki/Advanced-Usage.md @@ -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) diff --git a/wiki/Configuration.md b/wiki/Configuration.md new file mode 100644 index 0000000..e055c57 --- /dev/null +++ b/wiki/Configuration.md @@ -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) diff --git a/wiki/Features.md b/wiki/Features.md new file mode 100644 index 0000000..c14f96f --- /dev/null +++ b/wiki/Features.md @@ -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) diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..b3b0cf3 --- /dev/null +++ b/wiki/Home.md @@ -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.* diff --git a/wiki/Installation.md b/wiki/Installation.md new file mode 100644 index 0000000..99b4690 --- /dev/null +++ b/wiki/Installation.md @@ -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