refactor: Refactoring most of the project
Some checks failed
🧪 Integration Testing / 🔧 Test Integration (2025.9.4, 3.13) (push) Failing after 24s
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:
@@ -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
|
@@ -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
41
.gitignore
vendored
@@ -1,31 +1,18 @@
|
||||
# Python
|
||||
.venv
|
||||
venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.pyc
|
||||
|
||||
# Home Assistant
|
||||
*.log
|
||||
*.db
|
||||
*.db-journal
|
||||
.HA_VERSION
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.directory
|
||||
|
||||
# Testing
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
.mypy_cache/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
*.log
|
||||
.env
|
34
README.md
34
README.md
@@ -1,13 +1,7 @@
|
||||
# 🛡️ AdGuard Control Hub
|
||||
|
||||
[](https://github.com/custom-components/hacs)
|
||||
[](https://git.sq4ind.eu/sq4ind/adguard-control-hub/releases)
|
||||
[](LICENSE)
|
||||
|
||||
**The ultimate Home Assistant integration for AdGuard Home**
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🎯 Smart Client Management
|
||||
@@ -27,16 +21,12 @@
|
||||
- Automation-friendly services for advanced workflows
|
||||
- Real-time DNS and blocking statistics
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### 🔧 Method 1: HACS (Recommended)
|
||||
|
||||
1. Open Home Assistant and go to **HACS > Integrations**
|
||||
2. Click menu (⋮) → **Custom repositories**
|
||||
3. Add repository URL:
|
||||
`https://git.sq4ind.eu/sq4ind/adguard-control-hub`
|
||||
3. Add repository URL: `https://git.sq4ind.eu/sq4ind/adguard-control-hub`
|
||||
4. Set category to **Integration**, click **Add**
|
||||
5. Search for **AdGuard Control Hub**
|
||||
6. Click **Install**, then restart Home Assistant
|
||||
@@ -44,24 +34,18 @@
|
||||
8. Search and select **AdGuard Control Hub**, enter your AdGuard Home details
|
||||
|
||||
### 🛠️ Method 2: Manual Installation
|
||||
|
||||
1. Download the latest release zip from your Gitea repository
|
||||
2. Extract `custom_components/adguard_hub` into your Home Assistant config directory
|
||||
3. Restart Home Assistant
|
||||
4. Add integration via UI as per above
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
- **Host**: IP or hostname of your AdGuard Home
|
||||
- **Port**: Default 3000 unless customized
|
||||
- **Username & Password**: Admin credentials for AdGuard Home
|
||||
- **SSL**: Enable if AdGuard Home runs HTTPS
|
||||
- **Verify SSL**: Disable for self-signed certificates
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Use Cases & Examples
|
||||
|
||||
### Parental Controls - Kids Bedtime Automation
|
||||
@@ -106,8 +90,6 @@ data:
|
||||
clients: ["all"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 Dashboard Examples
|
||||
|
||||
**Main Control Panel:**
|
||||
@@ -143,24 +125,12 @@ tap_action:
|
||||
clients: ["all"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Support & Contribution
|
||||
|
||||
- Full documentation and setup examples in the repository wiki.
|
||||
- Report issues or request features via the repository's issue tracker.
|
||||
- Contributions welcome—please read the contribution guidelines.
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
This project is licensed under the MIT License. See the LICENSE file for details.
|
||||
|
||||
Made with ❤️ and professional care, so you control your AdGuard Home network integration!
|
||||
|
||||
---
|
||||
|
||||
[](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)
|
1
custom_components/__init__.py
Normal file
1
custom_components/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Custom components for Home Assistant."""
|
@@ -109,7 +109,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for AdGuard Control Hub."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
|
@@ -182,6 +182,7 @@ class AdGuardControlHubServices:
|
||||
]
|
||||
|
||||
for service in services:
|
||||
if self.hass.services.has_service(DOMAIN, service):
|
||||
self.hass.services.remove(DOMAIN, service)
|
||||
|
||||
def _get_api_for_entry(self, entry_id: str) -> AdGuardHomeAPI:
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"content_in_root": false,
|
||||
"filename": "adguard_hub",
|
||||
"country": ["US", "GB", "CA", "AU", "DE", "FR", "NL", "SE", "NO", "DK"],
|
||||
"homeassistant": "2023.1.0",
|
||||
"homeassistant": "2025.1.0",
|
||||
"render_readme": true,
|
||||
"iot_class": "Local Polling"
|
||||
}
|
70
info.md
70
info.md
@@ -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**
|
||||
- **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! 🚀**
|
||||
Restart Home Assistant and add via Integrations UI.
|
@@ -1,6 +1,6 @@
|
||||
[tool.black]
|
||||
line-length = 127
|
||||
target-version = ['py311']
|
||||
target-version = ['py313']
|
||||
include = '\.pyi?$'
|
||||
|
||||
[tool.isort]
|
||||
@@ -13,7 +13,7 @@ use_parentheses = true
|
||||
ensure_newline_before_comments = true
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
python_version = "3.13"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
|
@@ -1,13 +1,13 @@
|
||||
# Development dependencies
|
||||
black==23.12.0
|
||||
flake8==6.1.0
|
||||
black==24.3.0
|
||||
flake8==7.0.0
|
||||
isort==5.13.2
|
||||
mypy==1.8.0
|
||||
bandit==1.7.5
|
||||
safety==2.3.5
|
||||
pytest==7.4.3
|
||||
pytest-homeassistant-custom-component==0.13.104
|
||||
pytest-cov==4.1.0
|
||||
mypy==1.9.0
|
||||
bandit==1.7.7
|
||||
safety==3.1.0
|
||||
pytest==8.1.1
|
||||
pytest-homeassistant-custom-component==0.13.281
|
||||
pytest-cov==5.0.0
|
||||
|
||||
# Home Assistant testing
|
||||
homeassistant==2023.12.0
|
||||
homeassistant==2025.9.4
|
@@ -1 +1 @@
|
||||
"""Tests for AdGuard Control Hub."""
|
||||
"""Tests for AdGuard Control Hub integration."""
|
@@ -1,3 +1,4 @@
|
||||
"""Test configuration and fixtures."""
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -5,3 +6,11 @@ import pytest
|
||||
def auto_enable_custom_integrations(enable_custom_integrations):
|
||||
"""Enable custom integrations for all tests."""
|
||||
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
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"""Test API functionality."""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from custom_components.adguard_hub.api import AdGuardHomeAPI
|
||||
|
||||
|
||||
@@ -13,10 +13,19 @@ def mock_session():
|
||||
response.json = AsyncMock(return_value={"status": "ok"})
|
||||
response.status = 200
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_connection(mock_session):
|
||||
"""Test API connection."""
|
||||
api = AdGuardHomeAPI(
|
||||
@@ -31,6 +40,7 @@ async def test_api_connection(mock_session):
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_get_status(mock_session):
|
||||
"""Test getting status."""
|
||||
api = AdGuardHomeAPI(
|
||||
@@ -41,3 +51,33 @@ async def test_api_get_status(mock_session):
|
||||
|
||||
status = await api.get_status()
|
||||
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()
|
||||
|
@@ -1,19 +1,38 @@
|
||||
"""Test config flow for AdGuard Control Hub."""
|
||||
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
|
||||
|
||||
async def test_config_flow_success(hass):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_flow_success(hass: HomeAssistant):
|
||||
"""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(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
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."""
|
||||
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(
|
||||
DOMAIN,
|
||||
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"
|
||||
|
||||
|
||||
@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"
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
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 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
|
||||
def mock_config_entry():
|
||||
"""Mock config entry."""
|
||||
"""Mock config entry compatible with HA 2025.9.4."""
|
||||
return ConfigEntry(
|
||||
version=1,
|
||||
minor_version=1,
|
||||
domain=DOMAIN,
|
||||
title="Test AdGuard",
|
||||
data={
|
||||
@@ -23,8 +24,12 @@ def mock_config_entry():
|
||||
CONF_USERNAME: "admin",
|
||||
CONF_PASSWORD: "password",
|
||||
},
|
||||
source="user",
|
||||
options={},
|
||||
source=SOURCE_USER,
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_services_registration(hass: HomeAssistant):
|
||||
def test_services_registration(hass: HomeAssistant):
|
||||
"""Test that services are properly registered."""
|
||||
from custom_components.adguard_hub.services import AdGuardControlHubServices
|
||||
|
||||
# Create services without running inside an existing event loop
|
||||
services = AdGuardControlHubServices(hass)
|
||||
services.register_services()
|
||||
|
||||
|
Reference in New Issue
Block a user