60
custom_components/adguard_control_hub/__init__.py
Normal file
60
custom_components/adguard_control_hub/__init__.py
Normal 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)
|
||||
154
custom_components/adguard_control_hub/api.py
Normal file
154
custom_components/adguard_control_hub/api.py
Normal 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
|
||||
80
custom_components/adguard_control_hub/binary_sensor.py
Normal file
80
custom_components/adguard_control_hub/binary_sensor.py
Normal 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
|
||||
100
custom_components/adguard_control_hub/config_flow.py
Normal file
100
custom_components/adguard_control_hub/config_flow.py
Normal 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."""
|
||||
108
custom_components/adguard_control_hub/const.py
Normal file
108
custom_components/adguard_control_hub/const.py
Normal 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
|
||||
100
custom_components/adguard_control_hub/coordinator.py
Normal file
100
custom_components/adguard_control_hub/coordinator.py
Normal 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
|
||||
16
custom_components/adguard_control_hub/manifest.json
Normal file
16
custom_components/adguard_control_hub/manifest.json
Normal 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"
|
||||
}
|
||||
98
custom_components/adguard_control_hub/sensor.py
Normal file
98
custom_components/adguard_control_hub/sensor.py
Normal 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
|
||||
26
custom_components/adguard_control_hub/strings.json
Normal file
26
custom_components/adguard_control_hub/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
custom_components/adguard_control_hub/switch.py
Normal file
91
custom_components/adguard_control_hub/switch.py
Normal 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)
|
||||
36
custom_components/adguard_control_hub/translations/en.json
Normal file
36
custom_components/adguard_control_hub/translations/en.json
Normal 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user