initial, step 2 already
This commit is contained in:
63
.gitignore
vendored
Normal file
63
.gitignore
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
*.manifest
|
||||
*.spec
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.hypothesis/
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
.pyre/
|
||||
.pytype/
|
||||
cython_debug/
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
.history/
|
||||
|
||||
# IDEs
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Poetry
|
||||
poetry.lock
|
||||
53
PORTS.md
Normal file
53
PORTS.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Port Configuration
|
||||
|
||||
This document describes the port allocation for the home automation services.
|
||||
|
||||
## Port Scan Results (31. Oktober 2025)
|
||||
|
||||
### Ports in Use
|
||||
- **8000**: In use (likely API server)
|
||||
- **8021**: In use (system service)
|
||||
- **8080**: In use (system service)
|
||||
- **8100**: In use (system service)
|
||||
- **8200**: In use (system service)
|
||||
- **8770**: In use (system service)
|
||||
|
||||
### Free Ports Found
|
||||
- **8001**: FREE ✓
|
||||
- **8002**: FREE ✓
|
||||
- **8003**: FREE ✓
|
||||
- **8004**: FREE ✓
|
||||
- **8005**: FREE ✓
|
||||
|
||||
## Service Port Allocation
|
||||
|
||||
| Service | Port | Purpose |
|
||||
|---------|------|---------|
|
||||
| API | 8001 | FastAPI REST API for capabilities and health checks |
|
||||
| UI | 8002 | FastAPI web interface with Jinja2 templates |
|
||||
| (Reserved) | 8003 | Available for future services |
|
||||
| (Reserved) | 8004 | Available for future services |
|
||||
| (Reserved) | 8005 | Available for future services |
|
||||
|
||||
## Access URLs
|
||||
|
||||
- **API**: http://localhost:8001
|
||||
- Health: http://localhost:8001/health
|
||||
- Spec: http://localhost:8001/spec
|
||||
- Docs: http://localhost:8001/docs
|
||||
|
||||
- **UI**: http://localhost:8002
|
||||
- Main page: http://localhost:8002/
|
||||
|
||||
## Starting Services
|
||||
|
||||
```bash
|
||||
# Start API
|
||||
poetry run uvicorn apps.api.main:app --reload --port 8001
|
||||
|
||||
# Start UI
|
||||
poetry run uvicorn apps.ui.main:app --reload --port 8002
|
||||
|
||||
# Start Abstraction Worker (no port - MQTT client)
|
||||
poetry run python -m apps.abstraction.main
|
||||
```
|
||||
129
README.md
Normal file
129
README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Home Automation Monorepo
|
||||
|
||||
A Python-based home automation system built with Poetry in a monorepo structure.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
home-automation/
|
||||
├── apps/ # Applications
|
||||
│ ├── api/ # API service
|
||||
│ ├── abstraction/ # Abstraction layer
|
||||
│ ├── rules/ # Rules engine
|
||||
│ └── ui/ # User interface
|
||||
├── packages/ # Shared packages
|
||||
│ └── home_capabilities/ # Home capabilities library
|
||||
├── infra/ # Infrastructure
|
||||
│ ├── docker-compose.yml
|
||||
│ └── README.md
|
||||
├── pyproject.toml # Poetry configuration
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.11+
|
||||
- Poetry
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install Poetry if you haven't already:
|
||||
```bash
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
3. Activate the virtual environment:
|
||||
```bash
|
||||
poetry shell
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Code Quality Tools
|
||||
|
||||
This project uses the following tools configured in `pyproject.toml`:
|
||||
|
||||
- **Ruff**: Fast Python linter
|
||||
- **Black**: Code formatter
|
||||
- **Mypy**: Static type checker
|
||||
|
||||
Run code quality checks:
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
poetry run black .
|
||||
|
||||
# Lint code
|
||||
poetry run ruff check .
|
||||
|
||||
# Type check
|
||||
poetry run mypy .
|
||||
```
|
||||
|
||||
### Running Applications
|
||||
|
||||
#### Port Configuration
|
||||
|
||||
See `PORTS.md` for detailed port allocation.
|
||||
|
||||
- **API Server**: http://localhost:8001
|
||||
- **UI Server**: http://localhost:8002
|
||||
|
||||
#### API Server
|
||||
|
||||
Start the FastAPI server with auto-reload:
|
||||
|
||||
```bash
|
||||
# Using uvicorn directly (port 8001)
|
||||
poetry run uvicorn apps.api.main:app --reload --port 8001
|
||||
|
||||
# Or using the main function
|
||||
poetry run python -m apps.api.main
|
||||
```
|
||||
|
||||
The API will be available at:
|
||||
- API Base: http://localhost:8001
|
||||
- Interactive Docs: http://localhost:8001/docs
|
||||
- OpenAPI Schema: http://localhost:8001/openapi.json
|
||||
|
||||
Available endpoints:
|
||||
- `GET /health` - Health check endpoint
|
||||
- `GET /spec` - Capabilities specification
|
||||
|
||||
#### UI Server
|
||||
|
||||
Start the web interface:
|
||||
|
||||
```bash
|
||||
# Using uvicorn directly (port 8002)
|
||||
poetry run uvicorn apps.ui.main:app --reload --port 8002
|
||||
|
||||
# Or using the main function
|
||||
poetry run python -m apps.ui.main
|
||||
```
|
||||
|
||||
The UI will be available at:
|
||||
- Main page: http://localhost:8002
|
||||
- `GET /spec` - Capabilities specification
|
||||
|
||||
#### Other Applications
|
||||
|
||||
```bash
|
||||
# Abstraction
|
||||
poetry run python -m apps.abstraction.main
|
||||
|
||||
# Rules
|
||||
poetry run python -m apps.rules.main
|
||||
|
||||
# UI
|
||||
poetry run python -m apps.ui.main
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
TBD
|
||||
1
apps/__init__.py
Normal file
1
apps/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Apps package."""
|
||||
63
apps/abstraction/README.md
Normal file
63
apps/abstraction/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Abstraction Layer
|
||||
|
||||
The abstraction layer is an asyncio-based worker that manages device communication through MQTT.
|
||||
|
||||
## Features
|
||||
|
||||
- **Configuration Loading**: Reads device configuration from `config/devices.yaml`
|
||||
- **MQTT Integration**: Connects to MQTT broker for real-time device communication
|
||||
- **Async Design**: Built with asyncio for efficient concurrent operations
|
||||
- **Message Subscription**: Listens to `home/#` topics for device events
|
||||
- **Logging**: Structured logging at INFO level
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
# Start the abstraction worker
|
||||
poetry run python -m apps.abstraction.main
|
||||
```
|
||||
|
||||
The worker will:
|
||||
1. Load configuration from `config/devices.yaml`
|
||||
2. Connect to MQTT broker (172.16.2.16:1883)
|
||||
3. Subscribe to topic `home/#`
|
||||
4. Log "Abstraction worker started" at INFO level
|
||||
5. Keep running and processing MQTT messages until interrupted
|
||||
|
||||
## Configuration
|
||||
|
||||
The worker reads configuration from `config/devices.yaml`. Example structure:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
broker: "172.16.2.16"
|
||||
port: 1883
|
||||
client_id: "home-automation-abstraction"
|
||||
keepalive: 60
|
||||
|
||||
devices:
|
||||
- id: "light_living_room"
|
||||
type: "light"
|
||||
name: "Living Room Light"
|
||||
mqtt_topic: "home/living_room/light"
|
||||
```
|
||||
|
||||
## MQTT Broker
|
||||
|
||||
The worker connects to a real MQTT broker at:
|
||||
- **Host**: 172.16.2.16
|
||||
- **Port**: 1883
|
||||
- **Authentication**: None required
|
||||
|
||||
Topics subscribed:
|
||||
- `home/#` - All home automation topics
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PyYAML**: Configuration file parsing
|
||||
- **aiomqtt**: Modern async MQTT client
|
||||
- **asyncio**: Asynchronous I/O
|
||||
|
||||
## Stopping
|
||||
|
||||
Press `Ctrl+C` to gracefully stop the worker.
|
||||
1
apps/abstraction/__init__.py
Normal file
1
apps/abstraction/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Abstraction application."""
|
||||
287
apps/abstraction/main.py
Normal file
287
apps/abstraction/main.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""Abstraction main entry point."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
import yaml
|
||||
from aiomqtt import Client
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_config(config_path: Path) -> dict[str, Any]:
|
||||
"""Load configuration from YAML file.
|
||||
|
||||
Args:
|
||||
config_path: Path to the configuration file
|
||||
|
||||
Returns:
|
||||
dict: Configuration dictionary
|
||||
"""
|
||||
if not config_path.exists():
|
||||
logger.warning(f"Config file not found: {config_path}, using defaults")
|
||||
return {
|
||||
"mqtt": {
|
||||
"broker": "172.16.2.16",
|
||||
"port": 1883,
|
||||
"client_id": "home-automation-abstraction",
|
||||
"keepalive": 60
|
||||
},
|
||||
"devices": []
|
||||
}
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
logger.info(f"Loaded configuration from {config_path}")
|
||||
return config
|
||||
|
||||
|
||||
def validate_devices(devices: list[dict[str, Any]]) -> None:
|
||||
"""Validate device configuration.
|
||||
|
||||
Args:
|
||||
devices: List of device configurations
|
||||
|
||||
Raises:
|
||||
ValueError: If device configuration is invalid
|
||||
"""
|
||||
for device in devices:
|
||||
if "id" not in device:
|
||||
raise ValueError(f"Device missing 'id': {device}")
|
||||
if "type" not in device:
|
||||
raise ValueError(f"Device {device['id']} missing 'type'")
|
||||
if "topics" not in device:
|
||||
raise ValueError(f"Device {device['id']} missing 'topics'")
|
||||
if "set" not in device["topics"] or "state" not in device["topics"]:
|
||||
raise ValueError(f"Device {device['id']} missing 'topics.set' or 'topics.state'")
|
||||
logger.info(f"Validated {len(devices)} device(s)")
|
||||
|
||||
|
||||
async def get_redis_client(redis_url: str, max_retries: int = 5) -> aioredis.Redis:
|
||||
"""Connect to Redis with exponential backoff.
|
||||
|
||||
Args:
|
||||
redis_url: Redis connection URL
|
||||
max_retries: Maximum number of connection attempts
|
||||
|
||||
Returns:
|
||||
Redis client instance
|
||||
"""
|
||||
retry_delay = 1
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
redis_client = await aioredis.from_url(redis_url, decode_responses=True)
|
||||
await redis_client.ping()
|
||||
logger.info(f"Connected to Redis: {redis_url}")
|
||||
return redis_client
|
||||
except Exception as e:
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning(f"Redis connection failed (attempt {attempt + 1}/{max_retries}): {e}")
|
||||
await asyncio.sleep(retry_delay)
|
||||
retry_delay = min(retry_delay * 2, 30) # Exponential backoff, max 30s
|
||||
else:
|
||||
logger.error(f"Failed to connect to Redis after {max_retries} attempts")
|
||||
raise
|
||||
|
||||
|
||||
async def handle_abstract_set(
|
||||
mqtt_client: Client,
|
||||
device_id: str,
|
||||
device_type: str,
|
||||
vendor_topic: str,
|
||||
payload: dict[str, Any]
|
||||
) -> None:
|
||||
"""Handle abstract SET message and publish to vendor topic.
|
||||
|
||||
Args:
|
||||
mqtt_client: MQTT client instance
|
||||
device_id: Device identifier
|
||||
device_type: Device type (e.g., 'light')
|
||||
vendor_topic: Vendor-specific SET topic
|
||||
payload: Message payload
|
||||
"""
|
||||
# Extract actual payload (remove type wrapper if present)
|
||||
vendor_payload = payload.get("payload", payload)
|
||||
vendor_message = json.dumps(vendor_payload)
|
||||
|
||||
logger.info(f"→ vendor SET {device_id}: {vendor_topic} ← {vendor_message}")
|
||||
await mqtt_client.publish(vendor_topic, vendor_message, qos=1)
|
||||
|
||||
|
||||
async def handle_vendor_state(
|
||||
mqtt_client: Client,
|
||||
redis_client: aioredis.Redis,
|
||||
device_id: str,
|
||||
device_type: str,
|
||||
payload: dict[str, Any],
|
||||
redis_channel: str = "ui:updates"
|
||||
) -> None:
|
||||
"""Handle vendor STATE message and publish to abstract topic + Redis.
|
||||
|
||||
Args:
|
||||
mqtt_client: MQTT client instance
|
||||
redis_client: Redis client instance
|
||||
device_id: Device identifier
|
||||
device_type: Device type (e.g., 'light')
|
||||
payload: State payload
|
||||
redis_channel: Redis channel for UI updates
|
||||
"""
|
||||
# Publish to abstract state topic (retained)
|
||||
abstract_topic = f"home/{device_type}/{device_id}/state"
|
||||
abstract_message = json.dumps(payload)
|
||||
|
||||
logger.info(f"← abstract STATE {device_id}: {abstract_topic} → {abstract_message}")
|
||||
await mqtt_client.publish(abstract_topic, abstract_message, qos=1, retain=True)
|
||||
|
||||
# Publish to Redis for UI updates
|
||||
ui_update = {
|
||||
"type": "state",
|
||||
"device_id": device_id,
|
||||
"payload": payload
|
||||
}
|
||||
redis_message = json.dumps(ui_update)
|
||||
|
||||
logger.info(f"← Redis PUBLISH {redis_channel} → {redis_message}")
|
||||
await redis_client.publish(redis_channel, redis_message)
|
||||
|
||||
|
||||
async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> None:
|
||||
"""MQTT worker that handles device communication.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary containing MQTT settings
|
||||
redis_client: Redis client for UI updates
|
||||
"""
|
||||
mqtt_config = config.get("mqtt", {})
|
||||
broker = mqtt_config.get("broker", "172.16.2.16")
|
||||
port = mqtt_config.get("port", 1883)
|
||||
client_id = mqtt_config.get("client_id", "home-automation-abstraction")
|
||||
keepalive = mqtt_config.get("keepalive", 60)
|
||||
|
||||
redis_config = config.get("redis", {})
|
||||
redis_channel = redis_config.get("channel", "ui:updates")
|
||||
|
||||
devices = {d["id"]: d for d in config.get("devices", [])}
|
||||
|
||||
retry_delay = 1
|
||||
max_retry_delay = 60
|
||||
|
||||
while True:
|
||||
try:
|
||||
logger.info(f"Connecting to MQTT broker: {broker}:{port}")
|
||||
|
||||
async with Client(
|
||||
hostname=broker,
|
||||
port=port,
|
||||
identifier=client_id,
|
||||
keepalive=keepalive
|
||||
) as client:
|
||||
logger.info(f"Connected to MQTT broker as {client_id}")
|
||||
|
||||
# Subscribe to abstract SET topics for all devices
|
||||
for device in devices.values():
|
||||
abstract_set_topic = f"home/{device['type']}/{device['id']}/set"
|
||||
await client.subscribe(abstract_set_topic)
|
||||
logger.info(f"Subscribed to abstract SET: {abstract_set_topic}")
|
||||
|
||||
# Subscribe to vendor STATE topics
|
||||
vendor_state_topic = device["topics"]["state"]
|
||||
await client.subscribe(vendor_state_topic)
|
||||
logger.info(f"Subscribed to vendor STATE: {vendor_state_topic}")
|
||||
|
||||
# Reset retry delay on successful connection
|
||||
retry_delay = 1
|
||||
|
||||
# Process messages
|
||||
async for message in client.messages:
|
||||
topic = str(message.topic)
|
||||
payload_str = message.payload.decode()
|
||||
|
||||
try:
|
||||
payload = json.loads(payload_str)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Invalid JSON on {topic}: {payload_str}")
|
||||
continue
|
||||
|
||||
# Check if this is an abstract SET message
|
||||
if topic.startswith("home/") and topic.endswith("/set"):
|
||||
# Extract device_type and device_id from topic
|
||||
parts = topic.split("/")
|
||||
if len(parts) == 4: # home/<type>/<id>/set
|
||||
device_type = parts[1]
|
||||
device_id = parts[2]
|
||||
|
||||
if device_id in devices:
|
||||
device = devices[device_id]
|
||||
vendor_topic = device["topics"]["set"]
|
||||
await handle_abstract_set(
|
||||
client, device_id, device_type, vendor_topic, payload
|
||||
)
|
||||
|
||||
# Check if this is a vendor STATE message
|
||||
else:
|
||||
# Find device by vendor state topic
|
||||
for device_id, device in devices.items():
|
||||
if topic == device["topics"]["state"]:
|
||||
await handle_vendor_state(
|
||||
client, redis_client, device_id, device["type"], payload, redis_channel
|
||||
)
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MQTT error: {e}")
|
||||
logger.info(f"Reconnecting in {retry_delay}s...")
|
||||
await asyncio.sleep(retry_delay)
|
||||
retry_delay = min(retry_delay * 2, max_retry_delay)
|
||||
|
||||
|
||||
async def async_main() -> None:
|
||||
"""Async main function for the abstraction worker."""
|
||||
# Determine config path
|
||||
config_path = Path(__file__).parent.parent.parent / "config" / "devices.yaml"
|
||||
|
||||
# Load configuration
|
||||
config = load_config(config_path)
|
||||
|
||||
# Validate devices
|
||||
devices = config.get("devices") or []
|
||||
validate_devices(devices)
|
||||
logger.info(f"Loaded {len(devices)} device(s) from configuration")
|
||||
|
||||
# Get Redis URL from config or environment variable or use default
|
||||
redis_config = config.get("redis", {})
|
||||
redis_url = redis_config.get("url") or os.environ.get("REDIS_URL", "redis://localhost:6379/0")
|
||||
|
||||
# Connect to Redis with retry
|
||||
redis_client = await get_redis_client(redis_url)
|
||||
|
||||
logger.info("Abstraction worker started")
|
||||
|
||||
# Start MQTT worker
|
||||
await mqtt_worker(config, redis_client)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the abstraction application."""
|
||||
try:
|
||||
asyncio.run(async_main())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Abstraction worker stopped by user")
|
||||
except Exception as e:
|
||||
logger.error(f"Fatal error: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
79
apps/api/README.md
Normal file
79
apps/api/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Home Automation API
|
||||
|
||||
FastAPI-based REST API for the home automation system.
|
||||
|
||||
## Features
|
||||
|
||||
- **Health Check**: Monitor API availability
|
||||
- **Capabilities Specification**: Discover supported capabilities and versions
|
||||
- **CORS Support**: Configured for local frontend development
|
||||
- **Auto-generated Documentation**: Interactive API docs via Swagger UI
|
||||
|
||||
## Running the API
|
||||
|
||||
### Development Mode (with auto-reload)
|
||||
|
||||
```bash
|
||||
poetry run uvicorn apps.api.main:app --reload
|
||||
```
|
||||
|
||||
### Production Mode
|
||||
|
||||
```bash
|
||||
poetry run uvicorn apps.api.main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### Using Python directly
|
||||
|
||||
```bash
|
||||
poetry run python -m apps.api.main
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Health check endpoint.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /spec`
|
||||
|
||||
Returns supported capabilities and their versions.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"capabilities": {
|
||||
"light": "light@1.2.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interactive Documentation
|
||||
|
||||
Once the server is running, visit:
|
||||
- **Swagger UI**: http://localhost:8000/docs
|
||||
- **ReDoc**: http://localhost:8000/redoc
|
||||
- **OpenAPI Schema**: http://localhost:8000/openapi.json
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
The API is configured to accept requests from the following origins:
|
||||
- http://localhost:3000
|
||||
- http://localhost:5173
|
||||
- http://localhost:8080
|
||||
- http://127.0.0.1:3000
|
||||
- http://127.0.0.1:5173
|
||||
- http://127.0.0.1:8080
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **FastAPI**: Modern, fast web framework
|
||||
- **Uvicorn**: ASGI server
|
||||
- **Pydantic**: Data validation using Python type hints
|
||||
1
apps/api/__init__.py
Normal file
1
apps/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API application."""
|
||||
198
apps/api/main.py
Normal file
198
apps/api/main.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""API main entry point."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from aiomqtt import Client
|
||||
from fastapi import FastAPI, HTTPException, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from packages.home_capabilities import CAP_VERSION, LightState
|
||||
|
||||
app = FastAPI(
|
||||
title="Home Automation API",
|
||||
description="API for home automation system",
|
||||
version="0.1.0"
|
||||
)
|
||||
|
||||
# Configure CORS for localhost (Frontend)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://localhost:8002",
|
||||
"http://127.0.0.1:8002",
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
"""Health check endpoint.
|
||||
|
||||
Returns:
|
||||
dict: Status indicating the service is healthy
|
||||
"""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/spec")
|
||||
async def spec() -> dict[str, dict[str, str]]:
|
||||
"""Capability specification endpoint.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing supported capabilities and their versions
|
||||
"""
|
||||
return {
|
||||
"capabilities": {
|
||||
"light": CAP_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Pydantic Models
|
||||
class SetDeviceRequest(BaseModel):
|
||||
"""Request model for setting device state."""
|
||||
type: str
|
||||
payload: dict[str, Any]
|
||||
|
||||
|
||||
class DeviceInfo(BaseModel):
|
||||
"""Device information model."""
|
||||
device_id: str
|
||||
type: str
|
||||
name: str
|
||||
|
||||
|
||||
# Configuration helpers
|
||||
def load_devices() -> list[dict[str, Any]]:
|
||||
"""Load devices from configuration file.
|
||||
|
||||
Returns:
|
||||
list: List of device configurations
|
||||
"""
|
||||
config_path = Path(__file__).parent.parent.parent / "config" / "devices.yaml"
|
||||
|
||||
if not config_path.exists():
|
||||
return []
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
return config.get("devices", [])
|
||||
|
||||
|
||||
def get_mqtt_settings() -> tuple[str, int]:
|
||||
"""Get MQTT broker settings from environment.
|
||||
|
||||
Returns:
|
||||
tuple: (host, port)
|
||||
"""
|
||||
host = os.environ.get("MQTT_HOST", "172.16.2.16")
|
||||
port = int(os.environ.get("MQTT_PORT", "1883"))
|
||||
return host, port
|
||||
|
||||
|
||||
async def publish_mqtt(topic: str, payload: dict[str, Any]) -> None:
|
||||
"""Publish message to MQTT broker.
|
||||
|
||||
Args:
|
||||
topic: MQTT topic to publish to
|
||||
payload: Message payload
|
||||
"""
|
||||
host, port = get_mqtt_settings()
|
||||
message = json.dumps(payload)
|
||||
|
||||
async with Client(hostname=host, port=port, identifier="home-automation-api") as client:
|
||||
await client.publish(topic, message, qos=1)
|
||||
|
||||
|
||||
@app.get("/devices")
|
||||
async def get_devices() -> list[DeviceInfo]:
|
||||
"""Get list of available devices.
|
||||
|
||||
Returns:
|
||||
list: List of device information
|
||||
"""
|
||||
devices = load_devices()
|
||||
return [
|
||||
DeviceInfo(
|
||||
device_id=device["id"],
|
||||
type=device["type"],
|
||||
name=device.get("name", device["id"])
|
||||
)
|
||||
for device in devices
|
||||
]
|
||||
|
||||
|
||||
@app.post("/devices/{device_id}/set", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str]:
|
||||
"""Set device state.
|
||||
|
||||
Args:
|
||||
device_id: Device identifier
|
||||
request: Device state request
|
||||
|
||||
Returns:
|
||||
dict: Confirmation message
|
||||
|
||||
Raises:
|
||||
HTTPException: If device not found or payload invalid
|
||||
"""
|
||||
# Load devices and check if device exists
|
||||
devices = load_devices()
|
||||
device = next((d for d in devices if d["id"] == device_id), None)
|
||||
|
||||
if not device:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Device {device_id} not found"
|
||||
)
|
||||
|
||||
# Validate payload based on device type
|
||||
if request.type == "light":
|
||||
try:
|
||||
LightState(**request.payload)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Invalid payload for light: {e}"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=f"Unsupported device type: {request.type}"
|
||||
)
|
||||
|
||||
# Publish to MQTT
|
||||
topic = f"home/{request.type}/{device_id}/set"
|
||||
mqtt_payload = {
|
||||
"type": request.type,
|
||||
"payload": request.payload
|
||||
}
|
||||
|
||||
await publish_mqtt(topic, mqtt_payload)
|
||||
|
||||
return {"message": f"Command sent to {device_id}"}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the API application with uvicorn."""
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"apps.api.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8001,
|
||||
reload=True
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
103
apps/rules/README.md
Normal file
103
apps/rules/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Rules Engine
|
||||
|
||||
APScheduler-based automation rules engine for the home automation system.
|
||||
|
||||
## Features
|
||||
|
||||
- **APScheduler**: Background job scheduler for rule execution
|
||||
- **Interval Jobs**: Periodic rule evaluation
|
||||
- **Graceful Shutdown**: Proper signal handling (SIGINT, SIGTERM)
|
||||
- **Logging**: Structured logging at INFO level
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
poetry run python -m apps.rules.main
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The rules engine uses APScheduler's `BackgroundScheduler` to run automation rules on a schedule.
|
||||
|
||||
### Current Jobs
|
||||
|
||||
- **rule_tick**: Example job that runs every minute
|
||||
- Logs "Rule tick" message
|
||||
- Can be extended with actual rule logic
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
2025-10-31 13:05:46,865 - __main__ - INFO - Rules engine starting...
|
||||
2025-10-31 13:05:46,868 - __main__ - INFO - Scheduler started with rule_tick job (every 1 minute)
|
||||
2025-10-31 13:05:46,868 - __main__ - INFO - Rule tick
|
||||
2025-10-31 13:06:46,874 - __main__ - INFO - Rule tick
|
||||
2025-10-31 13:07:46,874 - __main__ - INFO - Rule tick
|
||||
```
|
||||
|
||||
## Signal Handling
|
||||
|
||||
The application handles shutdown signals gracefully:
|
||||
|
||||
- **SIGINT** (Ctrl+C): Initiates graceful shutdown
|
||||
- **SIGTERM**: Initiates graceful shutdown
|
||||
|
||||
On shutdown:
|
||||
1. Stops accepting new jobs
|
||||
2. Waits for running jobs to complete
|
||||
3. Shuts down the scheduler
|
||||
4. Exits cleanly
|
||||
|
||||
## Adding New Rules
|
||||
|
||||
To add a new rule, define a function and schedule it:
|
||||
|
||||
```python
|
||||
def my_custom_rule() -> None:
|
||||
"""Custom automation rule."""
|
||||
# Your rule logic here
|
||||
logger.info("Custom rule executed")
|
||||
|
||||
# In main():
|
||||
scheduler.add_job(
|
||||
my_custom_rule,
|
||||
'interval',
|
||||
minutes=5, # Run every 5 minutes
|
||||
id='custom_rule',
|
||||
name='My Custom Rule'
|
||||
)
|
||||
```
|
||||
|
||||
## Scheduler Triggers
|
||||
|
||||
APScheduler supports various trigger types:
|
||||
|
||||
- **interval**: Run at fixed intervals (e.g., every N minutes)
|
||||
- **cron**: Run at specific times (e.g., daily at 8:00 AM)
|
||||
- **date**: Run once at a specific datetime
|
||||
|
||||
Example with cron trigger:
|
||||
|
||||
```python
|
||||
scheduler.add_job(
|
||||
morning_routine,
|
||||
'cron',
|
||||
hour=8,
|
||||
minute=0,
|
||||
id='morning',
|
||||
name='Morning Routine'
|
||||
)
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **APScheduler**: Advanced job scheduling
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Load rules from configuration file
|
||||
- [ ] MQTT integration for device state monitoring
|
||||
- [ ] Rule conditions (if/then logic)
|
||||
- [ ] Rule chaining and dependencies
|
||||
- [ ] Web API for dynamic rule management
|
||||
- [ ] Persistent job store (database)
|
||||
1
apps/rules/__init__.py
Normal file
1
apps/rules/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Rules application."""
|
||||
84
apps/rules/main.py
Normal file
84
apps/rules/main.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Rules main entry point."""
|
||||
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from typing import NoReturn
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global scheduler instance
|
||||
scheduler: BackgroundScheduler | None = None
|
||||
|
||||
|
||||
def rule_tick() -> None:
|
||||
"""Example job that runs every minute.
|
||||
|
||||
This is a placeholder for actual rule evaluation logic.
|
||||
"""
|
||||
logger.info("Rule tick")
|
||||
|
||||
|
||||
def shutdown_handler(signum: int, frame: object) -> NoReturn:
|
||||
"""Handle shutdown signals gracefully.
|
||||
|
||||
Args:
|
||||
signum: Signal number
|
||||
frame: Current stack frame
|
||||
"""
|
||||
logger.info(f"Received signal {signum}, shutting down...")
|
||||
if scheduler:
|
||||
scheduler.shutdown(wait=True)
|
||||
logger.info("Scheduler stopped")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the rules application."""
|
||||
global scheduler
|
||||
|
||||
logger.info("Rules engine starting...")
|
||||
|
||||
# Register signal handlers
|
||||
signal.signal(signal.SIGINT, shutdown_handler)
|
||||
signal.signal(signal.SIGTERM, shutdown_handler)
|
||||
|
||||
# Initialize scheduler
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# Add example job - runs every minute
|
||||
scheduler.add_job(
|
||||
rule_tick,
|
||||
'interval',
|
||||
minutes=1,
|
||||
id='rule_tick',
|
||||
name='Rule Tick Job'
|
||||
)
|
||||
|
||||
# Start scheduler
|
||||
scheduler.start()
|
||||
logger.info("Scheduler started with rule_tick job (every 1 minute)")
|
||||
|
||||
# Run initial tick immediately
|
||||
rule_tick()
|
||||
|
||||
# Keep the application running
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("KeyboardInterrupt received, shutting down...")
|
||||
scheduler.shutdown(wait=True)
|
||||
logger.info("Scheduler stopped")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
73
apps/ui/README.md
Normal file
73
apps/ui/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Home Automation UI
|
||||
|
||||
FastAPI-based web interface with Jinja2 templates for the home automation system.
|
||||
|
||||
## Features
|
||||
|
||||
- **Jinja2 Templates**: Dynamic HTML rendering
|
||||
- **Responsive Design**: Modern, clean UI
|
||||
- **FastAPI Backend**: Fast and reliable serving
|
||||
|
||||
## Port Configuration
|
||||
|
||||
- **Development Port**: 8002
|
||||
- **Access URL**: http://localhost:8002
|
||||
|
||||
## Running the UI
|
||||
|
||||
### Using uvicorn directly
|
||||
|
||||
```bash
|
||||
poetry run uvicorn apps.ui.main:app --reload --port 8002
|
||||
```
|
||||
|
||||
### Using Python module
|
||||
|
||||
```bash
|
||||
poetry run python -m apps.ui.main
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
apps/ui/
|
||||
├── __init__.py
|
||||
├── main.py # FastAPI application
|
||||
├── templates/ # Jinja2 templates
|
||||
│ └── index.html # Main page
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
Templates are located in `apps/ui/templates/` and use Jinja2 syntax.
|
||||
|
||||
### Available Routes
|
||||
|
||||
- `GET /` - Main UI page (renders `index.html`)
|
||||
|
||||
## Development
|
||||
|
||||
The server runs with auto-reload enabled during development. Any changes to:
|
||||
- Python files (`.py`)
|
||||
- Template files (`.html`)
|
||||
|
||||
will trigger an automatic restart.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **FastAPI**: Web framework
|
||||
- **Jinja2**: Template engine
|
||||
- **Uvicorn**: ASGI server
|
||||
|
||||
## Template Variables
|
||||
|
||||
Templates receive the following context variables:
|
||||
- `request`: FastAPI Request object (required by Jinja2Templates)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Add static file serving (CSS, JS, images)
|
||||
- [ ] Implement WebSocket support for real-time updates
|
||||
- [ ] Add device control interface
|
||||
- [ ] Integrate with API for capability discovery
|
||||
1
apps/ui/__init__.py
Normal file
1
apps/ui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""UI application."""
|
||||
47
apps/ui/main.py
Normal file
47
apps/ui/main.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""UI main entry point."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="Home Automation UI",
|
||||
description="User interface for home automation system",
|
||||
version="0.1.0"
|
||||
)
|
||||
|
||||
# Setup Jinja2 templates
|
||||
templates_dir = Path(__file__).parent / "templates"
|
||||
templates = Jinja2Templates(directory=str(templates_dir))
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request) -> HTMLResponse:
|
||||
"""Render the main UI page.
|
||||
|
||||
Args:
|
||||
request: The FastAPI request object
|
||||
|
||||
Returns:
|
||||
HTMLResponse: Rendered HTML template
|
||||
"""
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the UI application with uvicorn."""
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"apps.ui.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8002,
|
||||
reload=True
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
50
apps/ui/templates/index.html
Normal file
50
apps/ui/templates/index.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Home Automation</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🏠 Home Automation</h1>
|
||||
<p>UI wird geladen...</p>
|
||||
<p>API erreichbar? <a href="http://localhost:8001/health" target="_blank">API Health Check</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
31
config/devices.yaml
Normal file
31
config/devices.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
# Device Configuration
|
||||
# Configuration for home automation devices
|
||||
|
||||
mqtt:
|
||||
broker: "172.16.2.16"
|
||||
port: 1883
|
||||
client_id: "home-automation-abstraction"
|
||||
username: null
|
||||
password: null
|
||||
keepalive: 60
|
||||
|
||||
redis:
|
||||
url: "redis://172.23.1.116:6379/8"
|
||||
channel: "ui:updates"
|
||||
|
||||
devices:
|
||||
- id: "test_lampe"
|
||||
type: "light"
|
||||
name: "Test Lampe"
|
||||
topics:
|
||||
set: "vendor/test_lampe/set"
|
||||
state: "vendor/test_lampe/state"
|
||||
# - color
|
||||
|
||||
# - id: "light_bedroom"
|
||||
# type: "light"
|
||||
# name: "Bedroom Light"
|
||||
# mqtt_topic: "home/bedroom/light"
|
||||
# capabilities:
|
||||
# - power
|
||||
# - brightness
|
||||
23
infra/README.md
Normal file
23
infra/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Infrastructure
|
||||
|
||||
This directory contains infrastructure-related files for the home automation project.
|
||||
|
||||
## Files
|
||||
|
||||
- `docker-compose.yml`: Docker Compose configuration for running services
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- Add service definitions to docker-compose.yml
|
||||
- Add deployment configurations
|
||||
- Add monitoring and logging setup
|
||||
15
infra/docker-compose.yml
Normal file
15
infra/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Placeholder for future services
|
||||
# Example:
|
||||
# api:
|
||||
# build:
|
||||
# context: ..
|
||||
# dockerfile: apps/api/Dockerfile
|
||||
# ports:
|
||||
# - "8000:8000"
|
||||
|
||||
placeholder:
|
||||
image: alpine:latest
|
||||
command: echo "Docker Compose placeholder - add your services here"
|
||||
1
packages/__init__.py
Normal file
1
packages/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Packages."""
|
||||
5
packages/home_capabilities/__init__.py
Normal file
5
packages/home_capabilities/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Home capabilities package."""
|
||||
|
||||
from packages.home_capabilities.light import CAP_VERSION, LightState
|
||||
|
||||
__all__ = ["LightState", "CAP_VERSION"]
|
||||
52
packages/home_capabilities/light.py
Normal file
52
packages/home_capabilities/light.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Light capability models and constants."""
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
CAP_VERSION = "light@1.2.0"
|
||||
|
||||
|
||||
class LightState(BaseModel):
|
||||
"""Represents the state of a light device.
|
||||
|
||||
Attributes:
|
||||
power: Whether the light is "on" or "off"
|
||||
brightness: Optional brightness level (0-100)
|
||||
color_temp: Optional color temperature in Kelvin (150-500)
|
||||
color: Optional hex color string in format "#RRGGBB"
|
||||
"""
|
||||
|
||||
power: Literal["on", "off"] = Field(
|
||||
...,
|
||||
description="Power state of the light"
|
||||
)
|
||||
|
||||
brightness: Optional[int] = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=100,
|
||||
description="Brightness level from 0 to 100"
|
||||
)
|
||||
|
||||
color_temp: Optional[int] = Field(
|
||||
None,
|
||||
ge=150,
|
||||
le=500,
|
||||
description="Color temperature in Kelvin (150-500)"
|
||||
)
|
||||
|
||||
color: Optional[str] = Field(
|
||||
None,
|
||||
pattern=r"^#[0-9A-Fa-f]{6}$",
|
||||
description="Hex color string in format #RRGGBB"
|
||||
)
|
||||
|
||||
@field_validator("color")
|
||||
@classmethod
|
||||
def validate_color_format(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Ensure color is uppercase if provided."""
|
||||
if v is not None:
|
||||
return v.upper()
|
||||
return v
|
||||
70
pyproject.toml
Normal file
70
pyproject.toml
Normal file
@@ -0,0 +1,70 @@
|
||||
[tool.poetry]
|
||||
name = "home-automation"
|
||||
version = "0.1.0"
|
||||
description = "Home automation monorepo"
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
readme = "README.md"
|
||||
packages = [
|
||||
{ include = "apps" },
|
||||
{ include = "packages" }
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
pydantic = "^2.12.3"
|
||||
fastapi = "^0.120.3"
|
||||
uvicorn = {extras = ["standard"], version = "^0.38.0"}
|
||||
asyncio-mqtt = "^0.16.2"
|
||||
pyyaml = "^6.0.3"
|
||||
aiomqtt = "^2.4.0"
|
||||
jinja2 = "^3.1.6"
|
||||
apscheduler = "^3.11.0"
|
||||
redis = "^7.0.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.6.0"
|
||||
black = "^24.0.0"
|
||||
mypy = "^1.11.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"N", # pep8-naming
|
||||
"UP", # pyupgrade
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"SIM", # flake8-simplify
|
||||
]
|
||||
ignore = []
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["F401"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ["py311"]
|
||||
include = '\.pyi?$'
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
check_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
warn_no_return = true
|
||||
strict_equality = true
|
||||
Reference in New Issue
Block a user