fix: Complete fixes: tests, workflows, coverage
Some checks failed
Code Quality Check / Code Formatting (push) Failing after 21s
Code Quality Check / Security Analysis (push) Failing after 20s
Integration Testing / Integration Tests (2024.12.0, 3.13) (push) Failing after 1m32s
Integration Testing / Integration Tests (2025.9.4, 3.13) (push) Failing after 20s

Signed-off-by: Rafal Zielinski <sq4ind@gmail.com>
This commit is contained in:
2025-09-28 17:58:31 +01:00
parent 7074a1ca11
commit ed94d40e96
17 changed files with 996 additions and 1671 deletions

View File

@@ -1,18 +1,21 @@
"""Sensor platform for AdGuard Control Hub integration."""
"""AdGuard Control Hub sensor platform."""
import logging
from typing import Any, Optional
from typing import Any, Dict, List, Optional
from homeassistant.components.sensor import SensorEntity, SensorStateClass, SensorDeviceClass
from homeassistant.components.sensor import (
SensorEntity,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.const import PERCENTAGE, UnitOfTime
from . import AdGuardControlHubCoordinator
from .api import AdGuardHomeAPI
from .const import DOMAIN, MANUFACTURER, ICON_STATISTICS, ICON_BLOCKED, ICON_QUERIES, ICON_PERCENTAGE, ICON_CLIENTS
from .const import DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
@@ -26,199 +29,191 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
api = hass.data[DOMAIN][config_entry.entry_id]["api"]
entities = [
entities: List[SensorEntity] = []
# Add main sensors
entities.extend([
AdGuardQueriesCounterSensor(coordinator, api),
AdGuardBlockedCounterSensor(coordinator, api),
AdGuardBlockingPercentageSensor(coordinator, api),
AdGuardClientCountSensor(coordinator, api),
AdGuardClientsCountSensor(coordinator, api),
AdGuardProcessingTimeSensor(coordinator, api),
AdGuardFilteringRulesSensor(coordinator, api),
]
AdGuardUpstreamServersSensor(coordinator, api),
AdGuardVersionSensor(coordinator, api),
])
async_add_entities(entities, update_before_add=True)
async_add_entities(entities)
class AdGuardBaseSensor(CoordinatorEntity, SensorEntity):
"""Base class for AdGuard sensors."""
"""Base AdGuard sensor."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.api = api
self._attr_device_info = {
"identifiers": {(DOMAIN, f"{api.host}:{api.port}")},
"name": f"AdGuard Control Hub ({api.host})",
"manufacturer": MANUFACTURER,
"model": "AdGuard Home",
"configuration_url": f"{'https' if api.ssl else 'http'}://{api.host}:{api.port}",
}
@property
def available(self) -> bool:
"""Return if sensor is available."""
return self.coordinator.last_update_success and bool(self.coordinator.statistics)
def device_info(self) -> DeviceInfo:
"""Return device info."""
return DeviceInfo(
identifiers={(DOMAIN, "adguard_home")},
name="AdGuard Home",
manufacturer=MANUFACTURER,
model="AdGuard Home",
configuration_url=self.api.base_url,
)
class AdGuardQueriesCounterSensor(AdGuardBaseSensor):
"""Sensor to track DNS queries count."""
"""AdGuard DNS queries counter sensor."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_dns_queries"
self._attr_name = "AdGuard DNS Queries"
self._attr_icon = ICON_QUERIES
self._attr_unique_id = f"{DOMAIN}_dns_queries"
self._attr_device_class = SensorDeviceClass.ENUM
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
self._attr_native_unit_of_measurement = "queries"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
self._attr_icon = "mdi:dns"
@property
def native_value(self) -> Optional[int]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
return stats.get("num_dns_queries", 0)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
stats = self.coordinator.statistics
return {
"queries_today": stats.get("num_dns_queries_today", 0),
"replaced_safebrowsing": stats.get("num_replaced_safebrowsing", 0),
"replaced_parental": stats.get("num_replaced_parental", 0),
"replaced_safesearch": stats.get("num_replaced_safesearch", 0),
}
return self.coordinator.statistics.get("num_dns_queries", 0)
class AdGuardBlockedCounterSensor(AdGuardBaseSensor):
"""Sensor to track blocked queries count."""
"""AdGuard blocked queries counter sensor."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_blocked_queries"
self._attr_name = "AdGuard Blocked Queries"
self._attr_icon = ICON_BLOCKED
self._attr_unique_id = f"{DOMAIN}_blocked_queries"
self._attr_device_class = SensorDeviceClass.ENUM
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
self._attr_native_unit_of_measurement = "queries"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
self._attr_icon = "mdi:shield-check"
@property
def native_value(self) -> Optional[int]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
return stats.get("num_blocked_filtering", 0)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
stats = self.coordinator.statistics
return {
"blocked_today": stats.get("num_blocked_filtering_today", 0),
"malware_phishing": stats.get("num_replaced_safebrowsing", 0),
"adult_websites": stats.get("num_replaced_parental", 0),
}
return self.coordinator.statistics.get("num_blocked_filtering", 0)
class AdGuardBlockingPercentageSensor(AdGuardBaseSensor):
"""Sensor to track blocking percentage."""
"""AdGuard blocking percentage sensor."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_blocking_percentage"
self._attr_name = "AdGuard Blocking Percentage"
self._attr_icon = ICON_PERCENTAGE
self._attr_unique_id = f"{DOMAIN}_blocking_percentage"
self._attr_device_class = SensorDeviceClass.ENUM
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = PERCENTAGE
self._attr_entity_category = EntityCategory.DIAGNOSTIC
self._attr_icon = "mdi:percent"
@property
def native_value(self) -> Optional[float]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
total_queries = stats.get("num_dns_queries", 0)
blocked_queries = stats.get("num_blocked_filtering", 0)
total_queries = self.coordinator.statistics.get("num_dns_queries", 0)
blocked_queries = self.coordinator.statistics.get("num_blocked_filtering", 0)
if total_queries == 0:
return 0.0
percentage = (blocked_queries / total_queries) * 100
return round(percentage, 2)
if total_queries > 0:
return round((blocked_queries / total_queries) * 100, 2)
return 0.0
class AdGuardClientCountSensor(AdGuardBaseSensor):
"""Sensor to track active clients count."""
class AdGuardClientsCountSensor(AdGuardBaseSensor):
"""AdGuard clients count sensor."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_clients_count"
self._attr_name = "AdGuard Clients Count"
self._attr_icon = ICON_CLIENTS
self._attr_unique_id = f"{DOMAIN}_clients_count"
self._attr_device_class = SensorDeviceClass.ENUM
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = "clients"
self._attr_icon = "mdi:account-multiple"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def native_value(self) -> Optional[int]:
def native_value(self) -> int:
"""Return the state of the sensor."""
return len(self.coordinator.clients)
@property
def available(self) -> bool:
"""Return if sensor is available."""
return self.coordinator.last_update_success
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
clients = self.coordinator.clients
protected_clients = sum(1 for c in clients.values() if c.get("filtering_enabled", True))
return {
"protected_clients": protected_clients,
"unprotected_clients": len(clients) - protected_clients,
"client_names": list(clients.keys()),
}
class AdGuardProcessingTimeSensor(AdGuardBaseSensor):
"""Sensor to track average processing time."""
"""AdGuard average processing time sensor."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_avg_processing_time"
self._attr_name = "AdGuard Average Processing Time"
self._attr_icon = "mdi:speedometer"
self._attr_unique_id = f"{DOMAIN}_avg_processing_time"
self._attr_device_class = SensorDeviceClass.DURATION
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = UnitOfTime.MILLISECONDS
self._attr_icon = "mdi:speedometer"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
self._attr_device_class = SensorDeviceClass.DURATION
@property
def native_value(self) -> Optional[float]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
avg_time = stats.get("avg_processing_time", 0)
return round(avg_time, 2) if avg_time else 0
return self.coordinator.statistics.get("avg_processing_time", 0.0)
class AdGuardFilteringRulesSensor(AdGuardBaseSensor):
"""Sensor to track number of filtering rules."""
"""AdGuard filtering rules count sensor."""
def __init__(self, coordinator: AdGuardControlHubCoordinator, api: AdGuardHomeAPI) -> None:
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_unique_id = f"{api.host}_{api.port}_filtering_rules"
self._attr_name = "AdGuard Filtering Rules"
self._attr_icon = "mdi:filter"
self._attr_unique_id = f"{DOMAIN}_filtering_rules"
self._attr_device_class = SensorDeviceClass.ENUM
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_native_unit_of_measurement = "rules"
self._attr_icon = "mdi:filter"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def native_value(self) -> Optional[int]:
"""Return the state of the sensor."""
stats = self.coordinator.statistics
return stats.get("filtering_rules_count", 0)
return self.coordinator.protection_status.get("num_filtering_rules", 0)
class AdGuardUpstreamServersSensor(AdGuardBaseSensor):
"""AdGuard upstream servers sensor."""
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_name = "AdGuard Upstream Servers"
self._attr_unique_id = f"{DOMAIN}_upstream_servers"
self._attr_icon = "mdi:server-network"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def native_value(self) -> str:
"""Return the state of the sensor."""
servers = self.coordinator.protection_status.get("dns_addresses", [])
return ", ".join(servers) if servers else "Unknown"
class AdGuardVersionSensor(AdGuardBaseSensor):
"""AdGuard version sensor."""
def __init__(self, coordinator, api: AdGuardHomeAPI) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, api)
self._attr_name = "AdGuard Version"
self._attr_unique_id = f"{DOMAIN}_version"
self._attr_icon = "mdi:information"
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@property
def native_value(self) -> str:
"""Return the state of the sensor."""
return self.coordinator.protection_status.get("version", "Unknown")