199 lines
4.7 KiB
Python
199 lines
4.7 KiB
Python
"""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()
|