initial, step 2 already
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user