"""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()