refactor: Refactoring most of the project
Some checks failed
🧪 Integration Testing / 🔧 Test Integration (2025.9.4, 3.13) (push) Failing after 24s

Signed-off-by: Rafal Zielinski <sq4ind@gmail.com>
This commit is contained in:
2025-09-28 15:34:46 +01:00
parent 13905df0ee
commit 4553eb454a
18 changed files with 180 additions and 259 deletions

View File

@@ -1,55 +0,0 @@
name: 🛡️ Code Quality & Security Check
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
code-quality:
name: 🔍 Code Quality Analysis
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 flake8 black isort mypy bandit safety
pip install homeassistant==2025.9.4
pip install -r requirements-dev.txt || echo "No dev requirements found"
- name: 🎨 Check Code Formatting (Black)
run: |
black --check --diff custom_components/
- name: 📊 Import Sorting (isort)
run: |
isort --check-only --diff custom_components/
- name: 🔍 Linting (Flake8)
run: |
flake8 custom_components/ --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 custom_components/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: 🔒 Security Scan (Bandit)
run: |
bandit -r custom_components/ -f json -o bandit-report.json || true
bandit -r custom_components/ --severity-level medium
- name: 🛡️ Dependency Security Check (Safety)
run: |
safety check --json --output safety-report.json || true
safety check
- name: 🏷️ Type Checking (MyPy)
run: |
mypy custom_components/ --ignore-missing-imports --no-strict-optional

View File

@@ -1,31 +0,0 @@
name: 🚀 Release
on:
push:
tags:
- 'v*'
jobs:
release:
name: 📦 Create Release
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout Code
uses: actions/checkout@v4
- name: 🏷️ Get Version
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: 📦 Create Archive
run: |
cd custom_components
zip -r ../adguard-control-hub-${{ steps.version.outputs.VERSION }}.zip adguard_hub/
- name: 🚀 Create Release
run: |
echo "Creating release for ${{ steps.version.outputs.TAG }}"

41
.gitignore vendored
View File

@@ -1,31 +1,18 @@
# Python .venv
venv/
__pycache__/ __pycache__/
*.py[cod]
*$py.class
*.pyc *.pyc
*.pyo
# Home Assistant *.pyd
*.log .Python
*.db
*.db-journal
.HA_VERSION
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
.directory
# Testing
.pytest_cache/ .pytest_cache/
.coverage .coverage
htmlcov/ .mypy_cache/
*.egg-info/
# Virtual environments dist/
venv/ build/
env/ .DS_Store
.vscode/
.idea/
*.log
.env

View File

@@ -1,13 +1,7 @@
# 🛡️ AdGuard Control Hub # 🛡️ AdGuard Control Hub
[![HACS Custom Integration](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs)
[![Gitea Release](https://img.shields.io/gitea/v/release/your-username/adguard-control-hub?gitea_url=https://git.sq4ind.eu)](https://git.sq4ind.eu/sq4ind/adguard-control-hub/releases)
[![License MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
**The ultimate Home Assistant integration for AdGuard Home** **The ultimate Home Assistant integration for AdGuard Home**
---
## ✨ Features ## ✨ Features
### 🎯 Smart Client Management ### 🎯 Smart Client Management
@@ -27,16 +21,12 @@
- Automation-friendly services for advanced workflows - Automation-friendly services for advanced workflows
- Real-time DNS and blocking statistics - Real-time DNS and blocking statistics
---
## 📦 Installation ## 📦 Installation
### 🔧 Method 1: HACS (Recommended) ### 🔧 Method 1: HACS (Recommended)
1. Open Home Assistant and go to **HACS > Integrations** 1. Open Home Assistant and go to **HACS > Integrations**
2. Click menu (⋮) → **Custom repositories** 2. Click menu (⋮) → **Custom repositories**
3. Add repository URL: 3. Add repository URL: `https://git.sq4ind.eu/sq4ind/adguard-control-hub`
`https://git.sq4ind.eu/sq4ind/adguard-control-hub`
4. Set category to **Integration**, click **Add** 4. Set category to **Integration**, click **Add**
5. Search for **AdGuard Control Hub** 5. Search for **AdGuard Control Hub**
6. Click **Install**, then restart Home Assistant 6. Click **Install**, then restart Home Assistant
@@ -44,24 +34,18 @@
8. Search and select **AdGuard Control Hub**, enter your AdGuard Home details 8. Search and select **AdGuard Control Hub**, enter your AdGuard Home details
### 🛠️ Method 2: Manual Installation ### 🛠️ Method 2: Manual Installation
1. Download the latest release zip from your Gitea repository 1. Download the latest release zip from your Gitea repository
2. Extract `custom_components/adguard_hub` into your Home Assistant config directory 2. Extract `custom_components/adguard_hub` into your Home Assistant config directory
3. Restart Home Assistant 3. Restart Home Assistant
4. Add integration via UI as per above 4. Add integration via UI as per above
---
## ⚙️ Configuration ## ⚙️ Configuration
- **Host**: IP or hostname of your AdGuard Home - **Host**: IP or hostname of your AdGuard Home
- **Port**: Default 3000 unless customized - **Port**: Default 3000 unless customized
- **Username & Password**: Admin credentials for AdGuard Home - **Username & Password**: Admin credentials for AdGuard Home
- **SSL**: Enable if AdGuard Home runs HTTPS - **SSL**: Enable if AdGuard Home runs HTTPS
- **Verify SSL**: Disable for self-signed certificates - **Verify SSL**: Disable for self-signed certificates
---
## 🎬 Use Cases & Examples ## 🎬 Use Cases & Examples
### Parental Controls - Kids Bedtime Automation ### Parental Controls - Kids Bedtime Automation
@@ -106,8 +90,6 @@ data:
clients: ["all"] clients: ["all"]
``` ```
---
## 📱 Dashboard Examples ## 📱 Dashboard Examples
**Main Control Panel:** **Main Control Panel:**
@@ -143,24 +125,12 @@ tap_action:
clients: ["all"] clients: ["all"]
``` ```
---
## 🤝 Support & Contribution ## 🤝 Support & Contribution
- Full documentation and setup examples in the repository wiki. - Full documentation and setup examples in the repository wiki.
- Report issues or request features via the repository's issue tracker. - Report issues or request features via the repository's issue tracker.
- Contributions welcome—please read the contribution guidelines. - Contributions welcome—please read the contribution guidelines.
---
## 📄 License ## 📄 License
This project is licensed under the MIT License. See the LICENSE file for details.
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
---
Made with ❤️ and professional care, so you control your AdGuard Home network integration! Made with ❤️ and professional care, so you control your AdGuard Home network integration!
---
[![HACS Custom Integration](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) | [Releases](https://git.sq4ind.eu/sq4ind/adguard-control-hub/releases) | [Issues](https://git.sq4ind.eu/sq4ind/adguard-control-hub/issues) | [License](LICENSE)

View File

@@ -0,0 +1 @@
"""Custom components for Home Assistant."""

View File

@@ -109,7 +109,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for AdGuard Control Hub.""" """Handle a config flow for AdGuard Control Hub."""
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL MINOR_VERSION = 1
async def async_step_user( async def async_step_user(
self, user_input: Optional[Dict[str, Any]] = None self, user_input: Optional[Dict[str, Any]] = None

View File

@@ -182,6 +182,7 @@ class AdGuardControlHubServices:
] ]
for service in services: for service in services:
if self.hass.services.has_service(DOMAIN, service):
self.hass.services.remove(DOMAIN, service) self.hass.services.remove(DOMAIN, service)
def _get_api_for_entry(self, entry_id: str) -> AdGuardHomeAPI: def _get_api_for_entry(self, entry_id: str) -> AdGuardHomeAPI:

View File

@@ -3,7 +3,7 @@
"content_in_root": false, "content_in_root": false,
"filename": "adguard_hub", "filename": "adguard_hub",
"country": ["US", "GB", "CA", "AU", "DE", "FR", "NL", "SE", "NO", "DK"], "country": ["US", "GB", "CA", "AU", "DE", "FR", "NL", "SE", "NO", "DK"],
"homeassistant": "2023.1.0", "homeassistant": "2025.1.0",
"render_readme": true, "render_readme": true,
"iot_class": "Local Polling" "iot_class": "Local Polling"
} }

70
info.md
View File

@@ -1,63 +1,15 @@
# 🛡️ AdGuard Control Hub # AdGuard Control Hub
**Transform your AdGuard Home into a smart network management powerhouse!** The complete Home Assistant integration for AdGuard Home network management.
## 🎯 What Makes This Special? ## Features
- Smart client management and discovery
- Granular service blocking controls
- Emergency unblock capabilities
- Real-time statistics and monitoring
- Rich automation services
Unlike the basic AdGuard integration, **AdGuard Control Hub** gives you complete control over every aspect of your network filtering: ## Installation
Install via HACS or manually extract to `custom_components/adguard_hub/`
### 🏠 **Smart Home Integration** Restart Home Assistant and add via Integrations UI.
- **Individual Device Control**: Every AdGuard client becomes a Home Assistant switch
- **Service-Level Blocking**: Block YouTube on kids' tablets while allowing it on work computers
- **Automated Parenting**: Bedtime routines that automatically restrict internet access
- **Work Productivity**: Focus modes that eliminate distracting websites during work hours
### 🎛️ **Rich Dashboard Controls**
- **One-Click Emergency Override**: Temporary unblock for urgent situations
- **Family Management Panel**: Control all kids' devices from a single card
- **Guest Network Controls**: Different rules for visitor devices
- **Real-Time Statistics**: See exactly what's being blocked and when
### 🤖 **Automation Paradise**
- **Time-Based Rules**: Different restrictions for school nights vs weekends
- **Presence Detection**: Automatically adjust rules when people arrive/leave
- **Bulk Operations**: Update multiple devices with pattern matching
- **Custom Schedules**: Per-device blocking schedules with precise time control
## 🛠️ **What You Get**
### **Entities Created**
- `switch.adguard_protection` - Global protection toggle
- `switch.adguard_client_*` - Individual device protection
- `switch.adguard_client_*_service_*` - Per-device service blocking
- `sensor.adguard_*` - DNS statistics and monitoring
- `binary_sensor.adguard_*` - Status indicators
### **Services Available**
- `adguard_hub.add_client` - Add new devices
- `adguard_hub.block_services` - Block specific services
- `adguard_hub.bulk_update_clients` - Manage multiple devices
- `adguard_hub.emergency_unblock` - Temporary access override
## 🎬 **Perfect For**
- **Parents** wanting automated screen time controls
- **Remote Workers** needing focus mode automation
- **Tech Enthusiasts** wanting complete network visibility
- **Families** needing different rules for different people
- **Anyone** who wants their network to be truly "smart"
## 📋 **Requirements**
- Home Assistant 2023.1+
- AdGuard Home with API access enabled
- Admin credentials for your AdGuard Home instance
## 🚀 **Quick Start**
1. Install via HACS or manually
2. Add integration: Settings → Devices & Services → Add Integration
3. Enter your AdGuard Home IP, port, username, and password
4. Watch as all your devices appear as controllable entities!
**Ready to take control of your network? Let's get started! 🚀**

View File

@@ -1,6 +1,6 @@
[tool.black] [tool.black]
line-length = 127 line-length = 127
target-version = ['py311'] target-version = ['py313']
include = '\.pyi?$' include = '\.pyi?$'
[tool.isort] [tool.isort]
@@ -13,7 +13,7 @@ use_parentheses = true
ensure_newline_before_comments = true ensure_newline_before_comments = true
[tool.mypy] [tool.mypy]
python_version = "3.11" python_version = "3.13"
warn_return_any = true warn_return_any = true
warn_unused_configs = true warn_unused_configs = true
disallow_untyped_defs = true disallow_untyped_defs = true

View File

@@ -1,13 +1,13 @@
# Development dependencies # Development dependencies
black==23.12.0 black==24.3.0
flake8==6.1.0 flake8==7.0.0
isort==5.13.2 isort==5.13.2
mypy==1.8.0 mypy==1.9.0
bandit==1.7.5 bandit==1.7.7
safety==2.3.5 safety==3.1.0
pytest==7.4.3 pytest==8.1.1
pytest-homeassistant-custom-component==0.13.104 pytest-homeassistant-custom-component==0.13.281
pytest-cov==4.1.0 pytest-cov==5.0.0
# Home Assistant testing # Home Assistant testing
homeassistant==2023.12.0 homeassistant==2025.9.4

View File

@@ -1 +1 @@
"""Tests for AdGuard Control Hub.""" """Tests for AdGuard Control Hub integration."""

View File

@@ -1,3 +1,4 @@
"""Test configuration and fixtures."""
import pytest import pytest
@@ -5,3 +6,11 @@ import pytest
def auto_enable_custom_integrations(enable_custom_integrations): def auto_enable_custom_integrations(enable_custom_integrations):
"""Enable custom integrations for all tests.""" """Enable custom integrations for all tests."""
yield yield
@pytest.fixture(autouse=True)
def mock_platform_setup():
"""Mock platform setup to avoid actual platform loading."""
from unittest.mock import patch
with patch("homeassistant.config_entries.ConfigEntry.async_forward_entry_setups"):
yield

View File

@@ -1,6 +1,6 @@
"""Test API functionality.""" """Test API functionality."""
import pytest import pytest
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock, patch
from custom_components.adguard_hub.api import AdGuardHomeAPI from custom_components.adguard_hub.api import AdGuardHomeAPI
@@ -13,10 +13,19 @@ def mock_session():
response.json = AsyncMock(return_value={"status": "ok"}) response.json = AsyncMock(return_value={"status": "ok"})
response.status = 200 response.status = 200
response.content_length = 100 response.content_length = 100
session.request = AsyncMock(return_value=response)
# Create async context manager for session.request
async def mock_request(*args, **kwargs):
return response
session.request = MagicMock()
session.request.return_value.__aenter__ = AsyncMock(return_value=response)
session.request.return_value.__aexit__ = AsyncMock(return_value=None)
return session return session
@pytest.mark.asyncio
async def test_api_connection(mock_session): async def test_api_connection(mock_session):
"""Test API connection.""" """Test API connection."""
api = AdGuardHomeAPI( api = AdGuardHomeAPI(
@@ -31,6 +40,7 @@ async def test_api_connection(mock_session):
assert result is True assert result is True
@pytest.mark.asyncio
async def test_api_get_status(mock_session): async def test_api_get_status(mock_session):
"""Test getting status.""" """Test getting status."""
api = AdGuardHomeAPI( api = AdGuardHomeAPI(
@@ -41,3 +51,33 @@ async def test_api_get_status(mock_session):
status = await api.get_status() status = await api.get_status()
assert status == {"status": "ok"} assert status == {"status": "ok"}
@pytest.mark.asyncio
async def test_api_context_manager():
"""Test API as async context manager."""
async with AdGuardHomeAPI(host="test-host", port=3000) as api:
assert api is not None
assert api.host == "test-host"
assert api.port == 3000
@pytest.mark.asyncio
async def test_api_error_handling():
"""Test API error handling."""
from custom_components.adguard_hub.api import AdGuardConnectionError
# Test with a session that raises an exception
session = MagicMock()
session.request = MagicMock()
session.request.return_value.__aenter__ = AsyncMock(side_effect=Exception("Connection error"))
session.request.return_value.__aexit__ = AsyncMock(return_value=None)
api = AdGuardHomeAPI(
host="test-host",
port=3000,
session=session
)
with pytest.raises(Exception): # Should raise AdGuardHomeError
await api.get_status()

View File

@@ -1,19 +1,38 @@
"""Test config flow for AdGuard Control Hub.""" """Test config flow for AdGuard Control Hub."""
import pytest import pytest
from homeassistant import config_entries, setup from unittest.mock import AsyncMock, patch
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from custom_components.adguard_hub.const import DOMAIN from custom_components.adguard_hub.const import DOMAIN
async def test_config_flow_success(hass):
@pytest.mark.asyncio
async def test_config_flow_success(hass: HomeAssistant):
"""Test successful config flow.""" """Test successful config flow."""
with patch("custom_components.adguard_hub.config_flow.validate_input") as mock_validate:
mock_validate.return_value = {
"title": "AdGuard Control Hub (192.168.1.100)",
"host": "192.168.1.100",
}
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == "form" assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
async def test_config_flow_cannot_connect(hass):
@pytest.mark.asyncio
async def test_config_flow_cannot_connect(hass: HomeAssistant):
"""Test config flow with connection error.""" """Test config flow with connection error."""
from custom_components.adguard_hub.config_flow import CannotConnect
with patch("custom_components.adguard_hub.config_flow.validate_input") as mock_validate:
mock_validate.side_effect = CannotConnect("Connection failed")
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
@@ -25,5 +44,28 @@ async def test_config_flow_cannot_connect(hass):
}, },
) )
assert result["type"] == "form" assert result["type"] == FlowResultType.FORM
assert result["errors"]["base"] == "cannot_connect" assert result["errors"]["base"] == "cannot_connect"
@pytest.mark.asyncio
async def test_config_flow_invalid_auth(hass: HomeAssistant):
"""Test config flow with authentication error."""
from custom_components.adguard_hub.config_flow import InvalidAuth
with patch("custom_components.adguard_hub.config_flow.validate_input") as mock_validate:
mock_validate.side_effect = InvalidAuth("Auth failed")
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={
"host": "192.168.1.100",
"port": 3000,
"username": "wrong",
"password": "wrong",
},
)
assert result["type"] == FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"

View File

@@ -2,7 +2,7 @@
import pytest import pytest
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from custom_components.adguard_hub import async_setup_entry, async_unload_entry from custom_components.adguard_hub import async_setup_entry, async_unload_entry
@@ -12,9 +12,10 @@ from custom_components.adguard_hub.const import DOMAIN
@pytest.fixture @pytest.fixture
def mock_config_entry(): def mock_config_entry():
"""Mock config entry.""" """Mock config entry compatible with HA 2025.9.4."""
return ConfigEntry( return ConfigEntry(
version=1, version=1,
minor_version=1,
domain=DOMAIN, domain=DOMAIN,
title="Test AdGuard", title="Test AdGuard",
data={ data={
@@ -23,8 +24,12 @@ def mock_config_entry():
CONF_USERNAME: "admin", CONF_USERNAME: "admin",
CONF_PASSWORD: "password", CONF_PASSWORD: "password",
}, },
source="user", options={},
source=SOURCE_USER,
entry_id="test_entry_id", entry_id="test_entry_id",
unique_id="192.168.1.100:3000",
discovery_keys=set(),
subentries_data={},
) )
@@ -145,11 +150,11 @@ async def test_api_error_handling(mock_api):
await mock_api.get_clients() await mock_api.get_clients()
@pytest.mark.asyncio def test_services_registration(hass: HomeAssistant):
async def test_services_registration(hass: HomeAssistant):
"""Test that services are properly registered.""" """Test that services are properly registered."""
from custom_components.adguard_hub.services import AdGuardControlHubServices from custom_components.adguard_hub.services import AdGuardControlHubServices
# Create services without running inside an existing event loop
services = AdGuardControlHubServices(hass) services = AdGuardControlHubServices(hass)
services.register_services() services.register_services()