1
Development
sq4ind edited this page 2025-09-28 12:47:05 +00:00

🛠️ Development Guide

Set up a development environment for AdGuard Control Hub and learn how to contribute to the project.

🚀 Development Environment Setup

Prerequisites

  • Python 3.11+ (Home Assistant requirement)
  • Git for version control
  • Home Assistant Core (development installation)
  • AdGuard Home instance for testing
  • VS Code (recommended) with Python extension

1. Clone the Repository

# Clone your fork or the main repository
git clone https://your-gitea-domain.com/your-username/adguard-control-hub.git
cd adguard-control-hub

# Set up remote for upstream (if fork)
git remote add upstream https://your-gitea-domain.com/original-user/adguard-control-hub.git

2. Set Up Python Environment

# Create virtual environment
python3 -m venv venv
source venv/bin/activate  # Linux/Mac
# or
venv\Scripts\activate  # Windows

# Upgrade pip
python -m pip install --upgrade pip

# Install development dependencies
pip install -r requirements-dev.txt

3. Install Home Assistant Core

# Install Home Assistant for development
pip install homeassistant

# Or use development version
git clone https://github.com/home-assistant/core.git
cd core
pip install -e .

4. Set Up Test AdGuard Home

# Download and run AdGuard Home for testing
wget https://github.com/AdguardTeam/AdGuardHome/releases/latest/download/AdGuardHome_linux_amd64.tar.gz
tar -xzf AdGuardHome_linux_amd64.tar.gz
cd AdGuardHome

# Initial setup
./AdGuardHome -s install
./AdGuardHome -s start

# Access web interface at http://localhost:3000
# Set up admin user: admin/development

🔧 Development Workflow

Project Structure

adguard-control-hub/
├── custom_components/adguard_hub/  # Main integration code
│   ├── __init__.py                 # Integration setup
│   ├── api.py                      # API wrapper
│   ├── config_flow.py              # Configuration flow
│   ├── const.py                    # Constants
│   ├── manifest.json               # Integration metadata
│   ├── services.py                 # Custom services
│   ├── strings.json                # UI strings
│   └── switch.py                   # Switch platform
├── tests/                          # Test suite
│   ├── __init__.py
│   ├── conftest.py                 # Test configuration
│   ├── test_api.py                 # API tests
│   ├── test_config_flow.py         # Config flow tests
│   └── test_switch.py              # Switch tests
├── .gitea/workflows/               # CI/CD pipelines
├── docs/                           # Documentation
├── requirements-dev.txt            # Development dependencies
├── pyproject.toml                  # Tool configuration
├── .flake8                         # Linting configuration
└── README.md                       # Project documentation

Development Installation

# Link integration to Home Assistant config
mkdir -p ~/.homeassistant/custom_components
ln -s $(pwd)/custom_components/adguard_hub ~/.homeassistant/custom_components/

# Or copy files during development
cp -r custom_components/adguard_hub ~/.homeassistant/custom_components/

Running Home Assistant

# Start Home Assistant in development mode
hass --open-ui --verbose

# Or with specific config directory
hass -c ~/.homeassistant --open-ui --verbose

🧪 Testing

Running Tests

# Run all tests
pytest

# Run specific test file
pytest tests/test_api.py

# Run with coverage
pytest --cov=custom_components.adguard_hub --cov-report=html

# Run integration tests only
pytest tests/test_integration.py -v

Test Configuration

# tests/conftest.py
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component

@pytest.fixture
async def hass(event_loop):
    """Create Home Assistant instance for testing."""
    hass = HomeAssistant()
    await async_setup_component(hass, "homeassistant", {})
    yield hass
    await hass.async_stop()

@pytest.fixture
def mock_adguard_api():
    """Mock AdGuard Home API for testing."""
    with patch("custom_components.adguard_hub.api.AdGuardHomeAPI") as mock:
        mock.return_value.test_connection.return_value = True
        mock.return_value.get_status.return_value = {"protection_enabled": True}
        yield mock.return_value

Writing Tests

# Example test
async def test_protection_switch_toggle(hass, mock_adguard_api):
    """Test protection switch can be toggled."""
    # Set up integration
    config_entry = MockConfigEntry(
        domain=DOMAIN,
        data={"host": "localhost", "port": 3000, "username": "admin", "password": "test"}
    )
    config_entry.add_to_hass(hass)

    # Load integration
    await hass.config_entries.async_setup(config_entry.entry_id)
    await hass.async_block_till_done()

    # Test switch toggle
    await hass.services.async_call(
        "switch", "turn_off", {"entity_id": "switch.adguard_protection"}
    )
    await hass.async_block_till_done()

    # Verify API was called
    mock_adguard_api.set_protection.assert_called_with(False)

🔍 Code Quality Tools

Pre-commit Setup

# Install pre-commit
pip install pre-commit

# Set up git hooks
pre-commit install

# Run manually
pre-commit run --all-files

Code Formatting

# Format code with Black
black custom_components/ tests/

# Sort imports with isort
isort custom_components/ tests/

# Check formatting
black --check custom_components/ tests/
isort --check-only custom_components/ tests/

Linting

# Lint with flake8
flake8 custom_components/ tests/

# Type checking with mypy
mypy custom_components/

# Security scanning with bandit
bandit -r custom_components/

Configuration Files

pyproject.toml

[tool.black]
line-length = 127
target-version = ['py311']
include = '\.pyi?$'

[tool.isort]
profile = "black"
line_length = 127
multi_line_output = 3
include_trailing_comma = true

[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --tb=short"

.flake8

[flake8]
max-line-length = 127
exclude = .git,__pycache__,.venv,venv,.pytest_cache
ignore = E203,W503,E501

🔄 Continuous Integration

Gitea Actions Workflow

The project uses Gitea Actions for CI/CD. Key workflows:

  1. quality-check.yml - Code quality and security
  2. integration-test.yml - Integration testing
  3. release.yml - Automated releases

Local CI Testing

# Run quality checks locally
black --check .
isort --check-only .
flake8 .
mypy custom_components/
bandit -r custom_components/
safety check

# Run tests
pytest --cov=custom_components.adguard_hub

🐛 Debugging

VS Code Configuration

Create .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Home Assistant",
            "type": "python",
            "request": "launch",
            "program": "/path/to/venv/bin/hass",
            "args": [
                "-c",
                "/home/user/.homeassistant",
                "--debug",
                "--verbose"
            ],
            "console": "integratedTerminal",
            "justMyCode": false
        }
    ]
}

Debug Logging

# In your code
import logging
_LOGGER = logging.getLogger(__name__)

# Debug statements
_LOGGER.debug("Debugging info: %s", variable)
_LOGGER.info("General info: %s", status)
_LOGGER.warning("Warning: %s", issue)
_LOGGER.error("Error occurred: %s", error)

Home Assistant Debug Mode

# configuration.yaml
logger:
  default: info
  logs:
    custom_components.adguard_hub: debug
    custom_components.adguard_hub.api: debug

# Enable Home Assistant debug mode
homeassistant:
  debug: true

📝 Documentation

Code Documentation

class AdGuardHomeAPI:
    """API wrapper for AdGuard Home.

    Provides methods to interact with AdGuard Home's REST API,
    including client management and service blocking.

    Args:
        host: AdGuard Home hostname or IP address
        port: AdGuard Home web interface port
        username: Admin username for authentication
        password: Admin password for authentication
        ssl: Whether to use HTTPS
        session: aiohttp session for HTTP requests

    Example:
        >>> api = AdGuardHomeAPI("192.168.1.100", 3000, "admin", "password")
        >>> await api.test_connection()
        True
    """

    async def get_clients(self) -> dict:
        """Get all configured clients.

        Returns:
            Dictionary containing client list and settings.

        Raises:
            aiohttp.ClientError: If API request fails

        Example:
            >>> clients = await api.get_clients()
            >>> print(clients["clients"][0]["name"])
            "Kids iPad"
        """

Wiki Updates

When adding new features:

  1. Update relevant wiki pages
  2. Add examples to documentation
  3. Update troubleshooting guide if needed
  4. Add API documentation for new endpoints

🔧 Adding New Features

Feature Development Process

  1. Plan: Create issue describing the feature
  2. Branch: Create feature branch from main
  3. Develop: Implement feature with tests
  4. Test: Run full test suite
  5. Document: Update documentation
  6. Review: Submit pull request
  7. Merge: Merge after approval

Adding New Service

# 1. Define service schema in const.py
NEW_SERVICE_SCHEMA = vol.Schema({
    vol.Required("client_name"): cv.string,
    vol.Required("new_parameter"): cv.string,
})

# 2. Implement service in services.py
async def new_service_handler(call):
    """Handle new service call."""
    client_name = call.data["client_name"]
    new_parameter = call.data["new_parameter"]

    try:
        # Implement service logic
        await api.new_api_method(client_name, new_parameter)
    except Exception as err:
        _LOGGER.error("New service failed: %s", err)
        raise HomeAssistantError(f"Service failed: {err}")

# 3. Register service
hass.services.async_register(
    DOMAIN,
    "new_service",
    new_service_handler,
    schema=NEW_SERVICE_SCHEMA,
)

# 4. Add to strings.json
{
    "services": {
        "new_service": {
            "name": "New Service",
            "description": "Description of new service",
            "fields": {
                "client_name": {
                    "name": "Client Name",
                    "description": "Name of the client"
                }
            }
        }
    }
}

# 5. Write tests
async def test_new_service(hass, mock_api):
    """Test new service functionality."""
    # Test implementation

Adding New Platform

# 1. Create new platform file (e.g., sensor.py)
async def async_setup_entry(hass, config_entry, async_add_entities):
    """Set up sensors."""
    coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]

    entities = []
    # Create sensor entities
    entities.append(NewSensor(coordinator))

    async_add_entities(entities)

# 2. Add to PLATFORMS in const.py
PLATFORMS = ["switch", "binary_sensor", "sensor", "new_platform"]

# 3. Update manifest.json if needed
{
    "requirements": ["aiohttp>=3.8.0", "new_dependency>=1.0.0"]
}

🚀 Release Process

Version Management

# Update version in manifest.json
{
    "version": "1.1.0"
}

# Update CHANGELOG.md
## [1.1.0] - 2025-01-XX
### Added
- New feature description
### Fixed  
- Bug fix description

# Commit changes
git add .
git commit -m "Bump version to 1.1.0"
git push

# Create release tag
git tag -a v1.1.0 -m "Release version 1.1.0"
git push origin v1.1.0

Release Checklist

  • All tests pass
  • Documentation updated
  • Version bumped in manifest.json
  • CHANGELOG.md updated
  • Git tag created
  • Release notes written
  • Tested with multiple Home Assistant versions

Ready to contribute? Check out the Contributing Guide for submission guidelines and code standards!