Add Development
526
Development.md
Normal file
526
Development.md
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
# 🛠️ 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start Home Assistant in development mode
|
||||||
|
hass --open-ui --verbose
|
||||||
|
|
||||||
|
# Or with specific config directory
|
||||||
|
hass -c ~/.homeassistant --open-ui --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install pre-commit
|
||||||
|
pip install pre-commit
|
||||||
|
|
||||||
|
# Set up git hooks
|
||||||
|
pre-commit install
|
||||||
|
|
||||||
|
# Run manually
|
||||||
|
pre-commit run --all-files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Formatting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
```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
|
||||||
|
```ini
|
||||||
|
[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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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`:
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```python
|
||||||
|
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
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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](Contributing) for submission guidelines and code standards!
|
Reference in New Issue
Block a user