Add API Documentation Service and enhance existing services with OpenAPI support

This commit is contained in:
glenn schrooyen
2025-09-11 22:24:56 +02:00
parent 21e4972ab1
commit 63b4bb487d
14 changed files with 800 additions and 69 deletions

View File

@@ -85,6 +85,27 @@ services:
- ./frontend:/app - ./frontend:/app
- /app/node_modules - /app/node_modules
# API Documentation Service (Development)
api-docs:
build:
context: ./services/api-docs
dockerfile: Dockerfile.dev
ports:
- "8083:8083"
environment:
- API_GATEWAY_URL=http://api-gateway:8080
- SERVICE_ADAPTERS_URL=http://service-adapters:8000
- METRICS_COLLECTOR_URL=http://metrics-collector:8081
- NOTIFICATION_SERVICE_URL=http://notification-service:8082
depends_on:
- api-gateway
- service-adapters
networks:
- labfusion-network
volumes:
- ./services/api-docs:/app
- /app/node_modules
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:

View File

@@ -76,6 +76,24 @@ services:
networks: networks:
- labfusion-network - labfusion-network
# API Documentation Service
api-docs:
build:
context: ./services/api-docs
dockerfile: Dockerfile
ports:
- "8083:8083"
environment:
- API_GATEWAY_URL=http://api-gateway:8080
- SERVICE_ADAPTERS_URL=http://service-adapters:8000
- METRICS_COLLECTOR_URL=http://metrics-collector:8081
- NOTIFICATION_SERVICE_URL=http://notification-service:8082
depends_on:
- api-gateway
- service-adapters
networks:
- labfusion-network
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:

View File

@@ -52,6 +52,20 @@ LabFusion is a unified dashboard and integration hub for homelab services, built
- Progress tracking document - Progress tracking document
- Updated project structure documentation - Updated project structure documentation
- [x] **API Documentation Service** (2024-11-09)
- Unified Swagger/OpenAPI documentation service
- Aggregates API specs from all services
- Service health monitoring
- Dynamic spec generation with service prefixing
- Express.js service with Swagger UI integration
- [x] **Enhanced Service Adapters** (2024-11-09)
- Comprehensive OpenAPI documentation with Pydantic models
- Detailed request/response schemas for all endpoints
- Service-specific tags and descriptions
- Enhanced error handling with proper HTTP status codes
- Additional endpoints for better service integration
## Current Status 🚧 ## Current Status 🚧
### Services Directory Structure ### Services Directory Structure
@@ -60,13 +74,15 @@ services/
├── api-gateway/ # Java Spring Boot (Port 8080) ✅ ├── api-gateway/ # Java Spring Boot (Port 8080) ✅
├── service-adapters/ # Python FastAPI (Port 8000) ✅ ├── service-adapters/ # Python FastAPI (Port 8000) ✅
├── metrics-collector/ # Go service (Port 8081) 🚧 ├── metrics-collector/ # Go service (Port 8081) 🚧
── notification-service/ # Node.js service (Port 8082) 🚧 ── notification-service/ # Node.js service (Port 8082) 🚧
└── api-docs/ # API Documentation (Port 8083) ✅
``` ```
### Infrastructure ### Infrastructure
- **Database**: PostgreSQL (Port 5432) ✅ - **Database**: PostgreSQL (Port 5432) ✅
- **Message Bus**: Redis (Port 6379) ✅ - **Message Bus**: Redis (Port 6379) ✅
- **Frontend**: React (Port 3000) ✅ - **Frontend**: React (Port 3000) ✅
- **API Documentation**: Unified Swagger UI (Port 8083) ✅
- **Containerization**: Docker Compose ✅ - **Containerization**: Docker Compose ✅
## Next Steps 🎯 ## Next Steps 🎯

View File

@@ -29,12 +29,18 @@ labfusion/
│ │ ├── Dockerfile # Production container (planned) │ │ ├── Dockerfile # Production container (planned)
│ │ ├── Dockerfile.dev # Development container (planned) │ │ ├── Dockerfile.dev # Development container (planned)
│ │ └── README.md # Service documentation │ │ └── README.md # Service documentation
── notification-service/ # Node.js Notification Service (Port 8082) 🚧 ── notification-service/ # Node.js Notification Service (Port 8082) 🚧
│ ├── src/ # TypeScript source (planned) │ ├── src/ # TypeScript source (planned)
│ ├── package.json # Node.js dependencies (planned) │ ├── package.json # Node.js dependencies (planned)
│ ├── Dockerfile # Production container (planned) │ ├── Dockerfile # Production container (planned)
│ ├── Dockerfile.dev # Development container (planned) │ ├── Dockerfile.dev # Development container (planned)
│ └── README.md # Service documentation │ └── README.md # Service documentation
│ └── api-docs/ # API Documentation Service (Port 8083) ✅
│ ├── server.js # Express server for unified docs
│ ├── package.json # Node.js dependencies
│ ├── Dockerfile # Production container
│ ├── Dockerfile.dev # Development container
│ └── README.md # Service documentation
├── frontend/ # React Frontend (Port 3000) ├── frontend/ # React Frontend (Port 3000)
│ ├── src/ │ ├── src/
│ │ ├── components/ # React components │ │ ├── components/ # React components

View File

@@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install --only=production
# Copy source code
COPY . .
# Expose port
EXPOSE 8083
# Start the application
CMD ["npm", "start"]

View File

@@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Expose port
EXPOSE 8083
# Start the application in development mode
CMD ["npm", "run", "dev"]

View File

@@ -0,0 +1,30 @@
# API Documentation Service
A unified API documentation service that aggregates OpenAPI specifications from all LabFusion services.
## Purpose
- Provide a single entry point for all API documentation
- Aggregate OpenAPI specs from all active services
- Display unified Swagger UI for the entire LabFusion ecosystem
- Monitor service health and availability
## Technology Stack
- **Language**: Node.js
- **Port**: 8083
- **Dependencies**: Express, Swagger UI, Axios
## Features
- **Unified Documentation**: Single Swagger UI for all services
- **Service Health Monitoring**: Real-time status of all services
- **Dynamic Spec Generation**: Automatically fetches and merges OpenAPI specs
- **Service Prefixing**: Each service's endpoints are prefixed for clarity
- **Fallback Handling**: Graceful handling of unavailable services
## API Endpoints
- `GET /` - Swagger UI interface
- `GET /openapi.json` - Unified OpenAPI specification
- `GET /services` - Service health status
- `GET /health` - Documentation service health
## Development Status
**Complete** - Ready for use

View File

@@ -0,0 +1,30 @@
{
"name": "labfusion-api-docs",
"version": "1.0.0",
"description": "Unified API documentation service for LabFusion",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"swagger-ui-express": "^5.0.0",
"swagger-jsdoc": "^6.2.8",
"axios": "^1.6.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1"
},
"devDependencies": {
"nodemon": "^3.0.2"
},
"keywords": [
"api",
"documentation",
"swagger",
"openapi",
"labfusion"
],
"author": "LabFusion Team",
"license": "MIT"
}

228
services/api-docs/server.js Normal file
View File

@@ -0,0 +1,228 @@
const express = require('express');
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
const axios = require('axios');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 8083;
// Middleware
app.use(cors());
app.use(express.json());
// Service configurations
const SERVICES = {
'api-gateway': {
name: 'API Gateway',
url: process.env.API_GATEWAY_URL || 'http://localhost:8080',
openapiPath: '/v3/api-docs',
description: 'Core API gateway for authentication, dashboards, and data management'
},
'service-adapters': {
name: 'Service Adapters',
url: process.env.SERVICE_ADAPTERS_URL || 'http://localhost:8000',
openapiPath: '/openapi.json',
description: 'Integration adapters for Home Assistant, Frigate, Immich, and other services'
},
'metrics-collector': {
name: 'Metrics Collector',
url: process.env.METRICS_COLLECTOR_URL || 'http://localhost:8081',
openapiPath: '/openapi.json',
description: 'System metrics collection and monitoring service',
status: 'planned'
},
'notification-service': {
name: 'Notification Service',
url: process.env.NOTIFICATION_SERVICE_URL || 'http://localhost:8082',
openapiPath: '/openapi.json',
description: 'Notification and alert management service',
status: 'planned'
}
};
// Fetch OpenAPI spec from a service
async function fetchServiceSpec(serviceKey, service) {
try {
if (service.status === 'planned') {
return {
openapi: '3.0.0',
info: {
title: service.name,
description: service.description,
version: '1.0.0'
},
paths: {},
components: {},
tags: [{
name: service.name.toLowerCase().replace(/\s+/g, '-'),
description: service.description
}]
};
}
const response = await axios.get(`${service.url}${service.openapiPath}`, {
timeout: 5000
});
return response.data;
} catch (error) {
console.warn(`Failed to fetch spec from ${service.name}:`, error.message);
return {
openapi: '3.0.0',
info: {
title: service.name,
description: service.description,
version: '1.0.0'
},
paths: {},
components: {},
tags: [{
name: service.name.toLowerCase().replace(/\s+/g, '-'),
description: service.description
}],
x-service-status: 'unavailable'
};
}
}
// Generate unified OpenAPI spec
async function generateUnifiedSpec() {
const unifiedSpec = {
openapi: '3.0.0',
info: {
title: 'LabFusion API',
description: 'Unified API documentation for all LabFusion services',
version: '1.0.0',
contact: {
name: 'LabFusion Team',
url: 'https://github.com/labfusion/labfusion'
}
},
servers: [
{
url: 'http://localhost:8080',
description: 'API Gateway (Production)'
},
{
url: 'http://localhost:8000',
description: 'Service Adapters (Production)'
},
{
url: 'http://localhost:8081',
description: 'Metrics Collector (Production)'
},
{
url: 'http://localhost:8082',
description: 'Notification Service (Production)'
}
],
paths: {},
components: {
schemas: {},
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
},
tags: []
};
// Fetch specs from all services
for (const [serviceKey, service] of Object.entries(SERVICES)) {
const spec = await fetchServiceSpec(serviceKey, service);
// Merge paths with service prefix
if (spec.paths) {
for (const [path, methods] of Object.entries(spec.paths)) {
const prefixedPath = `/${serviceKey}${path}`;
unifiedSpec.paths[prefixedPath] = methods;
}
}
// Merge components
if (spec.components) {
if (spec.components.schemas) {
Object.assign(unifiedSpec.components.schemas, spec.components.schemas);
}
}
// Add service tag
unifiedSpec.tags.push({
name: service.name,
description: service.description,
'x-service-url': service.url,
'x-service-status': service.status || 'active'
});
}
return unifiedSpec;
}
// Routes
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
app.get('/services', async (req, res) => {
const serviceStatus = {};
for (const [serviceKey, service] of Object.entries(SERVICES)) {
try {
const response = await axios.get(`${service.url}/health`, { timeout: 2000 });
serviceStatus[serviceKey] = {
name: service.name,
url: service.url,
status: 'healthy',
responseTime: response.headers['x-response-time'] || 'unknown'
};
} catch (error) {
serviceStatus[serviceKey] = {
name: service.name,
url: service.url,
status: service.status || 'unhealthy',
error: error.message
};
}
}
res.json(serviceStatus);
});
// Dynamic OpenAPI spec endpoint
app.get('/openapi.json', async (req, res) => {
try {
const spec = await generateUnifiedSpec();
res.json(spec);
} catch (error) {
res.status(500).json({ error: 'Failed to generate OpenAPI spec', details: error.message });
}
});
// Swagger UI
app.use('/', swaggerUi.serve);
app.get('/', swaggerUi.setup(null, {
swaggerOptions: {
url: '/openapi.json',
deepLinking: true,
displayRequestDuration: true,
filter: true,
showExtensions: true,
showCommonExtensions: true
},
customCss: `
.swagger-ui .topbar { display: none; }
.swagger-ui .info { margin: 20px 0; }
.swagger-ui .info .title { color: #1890ff; }
`,
customSiteTitle: 'LabFusion API Documentation'
}));
// Start server
app.listen(PORT, () => {
console.log(`LabFusion API Docs server running on port ${PORT}`);
console.log(`Access the documentation at: http://localhost:${PORT}`);
});

View File

@@ -0,0 +1,49 @@
package com.labfusion.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.Components;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("LabFusion API Gateway")
.description("Core API gateway for LabFusion homelab dashboard. Provides authentication, dashboard management, and data storage.")
.version("1.0.0")
.contact(new Contact()
.name("LabFusion Team")
.url("https://github.com/labfusion/labfusion")
.email("team@labfusion.dev"))
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")))
.servers(List.of(
new Server()
.url("http://localhost:8080")
.description("Development Server"),
new Server()
.url("https://api.labfusion.dev")
.description("Production Server")
))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT token authentication")));
}
}

View File

@@ -3,6 +3,14 @@ package com.labfusion.controller;
import com.labfusion.model.Dashboard; import com.labfusion.model.Dashboard;
import com.labfusion.model.User; import com.labfusion.model.User;
import com.labfusion.service.DashboardService; import com.labfusion.service.DashboardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -14,19 +22,37 @@ import java.util.Optional;
@RestController @RestController
@RequestMapping("/api/dashboards") @RequestMapping("/api/dashboards")
@CrossOrigin(origins = "*") @CrossOrigin(origins = "*")
@Tag(name = "Dashboard Management", description = "APIs for managing user dashboards and widgets")
@SecurityRequirement(name = "bearerAuth")
public class DashboardController { public class DashboardController {
@Autowired @Autowired
private DashboardService dashboardService; private DashboardService dashboardService;
@GetMapping @GetMapping
@Operation(summary = "Get user dashboards", description = "Retrieve all dashboards for the authenticated user")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved dashboards",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = Dashboard.class))),
@ApiResponse(responseCode = "401", description = "Unauthorized")
})
public ResponseEntity<List<Dashboard>> getDashboards(@AuthenticationPrincipal User user) { public ResponseEntity<List<Dashboard>> getDashboards(@AuthenticationPrincipal User user) {
List<Dashboard> dashboards = dashboardService.getDashboardsByUser(user); List<Dashboard> dashboards = dashboardService.getDashboardsByUser(user);
return ResponseEntity.ok(dashboards); return ResponseEntity.ok(dashboards);
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public ResponseEntity<Dashboard> getDashboard(@PathVariable Long id, @AuthenticationPrincipal User user) { @Operation(summary = "Get dashboard by ID", description = "Retrieve a specific dashboard by its ID")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved dashboard",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = Dashboard.class))),
@ApiResponse(responseCode = "403", description = "Forbidden - User doesn't own this dashboard"),
@ApiResponse(responseCode = "404", description = "Dashboard not found"),
@ApiResponse(responseCode = "401", description = "Unauthorized")
})
public ResponseEntity<Dashboard> getDashboard(
@Parameter(description = "Dashboard ID") @PathVariable Long id,
@AuthenticationPrincipal User user) {
Optional<Dashboard> dashboard = dashboardService.getDashboardById(id); Optional<Dashboard> dashboard = dashboardService.getDashboardById(id);
if (dashboard.isPresent()) { if (dashboard.isPresent()) {
// Check if user owns the dashboard // Check if user owns the dashboard

View File

@@ -4,6 +4,13 @@ import com.labfusion.model.DeviceState;
import com.labfusion.model.Event; import com.labfusion.model.Event;
import com.labfusion.repository.DeviceStateRepository; import com.labfusion.repository.DeviceStateRepository;
import com.labfusion.repository.EventRepository; import com.labfusion.repository.EventRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -14,6 +21,7 @@ import java.util.List;
@RestController @RestController
@RequestMapping("/api/system") @RequestMapping("/api/system")
@CrossOrigin(origins = "*") @CrossOrigin(origins = "*")
@Tag(name = "System Data", description = "APIs for system events, device states, and metrics")
public class SystemController { public class SystemController {
@Autowired @Autowired

View File

@@ -44,3 +44,14 @@ management:
endpoint: endpoint:
health: health:
show-details: always show-details: always
# OpenAPI/Swagger Configuration
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
tagsSorter: alpha
display-request-duration: true
show-actuator: true

View File

@@ -1,5 +1,8 @@
from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi import FastAPI, HTTPException, BackgroundTasks, Query, Path
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
import asyncio import asyncio
import redis import redis
import json import json
@@ -10,10 +13,93 @@ from dotenv import load_dotenv
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
# Pydantic models for request/response schemas
class ServiceStatus(BaseModel):
enabled: bool = Field(..., description="Whether the service is enabled")
url: str = Field(..., description="Service URL")
status: str = Field(..., description="Service status")
class HAAttributes(BaseModel):
unit_of_measurement: Optional[str] = Field(None, description="Unit of measurement")
friendly_name: Optional[str] = Field(None, description="Friendly name")
class HAEntity(BaseModel):
entity_id: str = Field(..., description="Entity ID")
state: str = Field(..., description="Current state")
attributes: HAAttributes = Field(..., description="Entity attributes")
class HAEntitiesResponse(BaseModel):
entities: List[HAEntity] = Field(..., description="List of Home Assistant entities")
class FrigateEvent(BaseModel):
id: str = Field(..., description="Event ID")
timestamp: str = Field(..., description="Event timestamp")
camera: str = Field(..., description="Camera name")
label: str = Field(..., description="Detection label")
confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
class FrigateEventsResponse(BaseModel):
events: List[FrigateEvent] = Field(..., description="List of Frigate events")
class ImmichAsset(BaseModel):
id: str = Field(..., description="Asset ID")
filename: str = Field(..., description="Filename")
created_at: str = Field(..., description="Creation timestamp")
tags: List[str] = Field(..., description="Asset tags")
faces: List[str] = Field(..., description="Detected faces")
class ImmichAssetsResponse(BaseModel):
assets: List[ImmichAsset] = Field(..., description="List of Immich assets")
class EventData(BaseModel):
service: str = Field(..., description="Service name")
event_type: str = Field(..., description="Event type")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Event metadata")
class EventResponse(BaseModel):
status: str = Field(..., description="Publication status")
event: Dict[str, Any] = Field(..., description="Published event")
class Event(BaseModel):
timestamp: str = Field(..., description="Event timestamp")
service: str = Field(..., description="Service name")
event_type: str = Field(..., description="Event type")
metadata: str = Field(..., description="Event metadata as JSON string")
class EventsResponse(BaseModel):
events: List[Event] = Field(..., description="List of events")
class HealthResponse(BaseModel):
status: str = Field(..., description="Service health status")
timestamp: str = Field(..., description="Health check timestamp")
class RootResponse(BaseModel):
message: str = Field(..., description="API message")
version: str = Field(..., description="API version")
app = FastAPI( app = FastAPI(
title="LabFusion Service Adapters", title="LabFusion Service Adapters",
description="Service integration adapters for Home Assistant, Frigate, Immich, and other homelab services", description="Service integration adapters for Home Assistant, Frigate, Immich, and other homelab services",
version="1.0.0" version="1.0.0",
contact={
"name": "LabFusion Team",
"url": "https://github.com/labfusion/labfusion",
"email": "team@labfusion.dev"
},
license_info={
"name": "MIT License",
"url": "https://opensource.org/licenses/MIT"
},
servers=[
{
"url": "http://localhost:8000",
"description": "Development Server"
},
{
"url": "https://adapters.labfusion.dev",
"description": "Production Server"
}
]
) )
# CORS middleware # CORS middleware
@@ -56,116 +142,282 @@ SERVICES = {
} }
} }
@app.get("/") @app.get("/",
response_model=RootResponse,
summary="API Root",
description="Get basic API information",
tags=["General"])
async def root(): async def root():
return {"message": "LabFusion Service Adapters API", "version": "1.0.0"} """Get basic API information and version"""
return RootResponse(
message="LabFusion Service Adapters API",
version="1.0.0"
)
@app.get("/health") @app.get("/health",
response_model=HealthResponse,
summary="Health Check",
description="Check service health status",
tags=["General"])
async def health_check(): async def health_check():
return {"status": "healthy", "timestamp": datetime.now().isoformat()} """Check the health status of the service adapters"""
return HealthResponse(
status="healthy",
timestamp=datetime.now().isoformat()
)
@app.get("/services") @app.get("/services",
response_model=Dict[str, ServiceStatus],
summary="Get Service Status",
description="Get status of all configured external services",
tags=["Services"])
async def get_services(): async def get_services():
"""Get status of all configured services""" """Get status of all configured external services (Home Assistant, Frigate, Immich, n8n)"""
service_status = {} service_status = {}
for service_name, config in SERVICES.items(): for service_name, config in SERVICES.items():
service_status[service_name] = { service_status[service_name] = ServiceStatus(
"enabled": config["enabled"], enabled=config["enabled"],
"url": config["url"], url=config["url"],
"status": "unknown" # Would check actual service status status="unknown" # Would check actual service status
} )
return service_status return service_status
@app.get("/home-assistant/entities") @app.get("/home-assistant/entities",
response_model=HAEntitiesResponse,
summary="Get Home Assistant Entities",
description="Retrieve all entities from Home Assistant",
responses={
200: {"description": "Successfully retrieved entities"},
503: {"description": "Home Assistant integration not configured"}
},
tags=["Home Assistant"])
async def get_ha_entities(): async def get_ha_entities():
"""Get Home Assistant entities""" """Get Home Assistant entities including sensors, switches, and other devices"""
if not SERVICES["home_assistant"]["enabled"]: if not SERVICES["home_assistant"]["enabled"]:
raise HTTPException(status_code=503, detail="Home Assistant integration not configured") raise HTTPException(
status_code=503,
detail="Home Assistant integration not configured. Please set HOME_ASSISTANT_TOKEN environment variable."
)
# This would make actual API calls to Home Assistant # This would make actual API calls to Home Assistant
# For now, return mock data # For now, return mock data
return { return HAEntitiesResponse(
"entities": [ entities=[
{ HAEntity(
"entity_id": "sensor.cpu_usage", entity_id="sensor.cpu_usage",
"state": "45.2", state="45.2",
"attributes": {"unit_of_measurement": "%", "friendly_name": "CPU Usage"} attributes=HAAttributes(
}, unit_of_measurement="%",
{ friendly_name="CPU Usage"
"entity_id": "sensor.memory_usage", )
"state": "2.1", ),
"attributes": {"unit_of_measurement": "GB", "friendly_name": "Memory Usage"} HAEntity(
} entity_id="sensor.memory_usage",
state="2.1",
attributes=HAAttributes(
unit_of_measurement="GB",
friendly_name="Memory Usage"
)
)
] ]
} )
@app.get("/frigate/events") @app.get("/frigate/events",
response_model=FrigateEventsResponse,
summary="Get Frigate Events",
description="Retrieve detection events from Frigate NVR",
responses={
200: {"description": "Successfully retrieved events"},
503: {"description": "Frigate integration not configured"}
},
tags=["Frigate"])
async def get_frigate_events(): async def get_frigate_events():
"""Get Frigate detection events""" """Get Frigate detection events including person, vehicle, and object detections"""
if not SERVICES["frigate"]["enabled"]: if not SERVICES["frigate"]["enabled"]:
raise HTTPException(status_code=503, detail="Frigate integration not configured") raise HTTPException(
status_code=503,
detail="Frigate integration not configured. Please set FRIGATE_TOKEN environment variable."
)
# This would make actual API calls to Frigate # This would make actual API calls to Frigate
# For now, return mock data # For now, return mock data
return { return FrigateEventsResponse(
"events": [ events=[
{ FrigateEvent(
"id": "event_123", id="event_123",
"timestamp": datetime.now().isoformat(), timestamp=datetime.now().isoformat(),
"camera": "front_door", camera="front_door",
"label": "person", label="person",
"confidence": 0.95 confidence=0.95
} )
] ]
} )
@app.get("/immich/assets") @app.get("/immich/assets",
response_model=ImmichAssetsResponse,
summary="Get Immich Assets",
description="Retrieve photo assets from Immich",
responses={
200: {"description": "Successfully retrieved assets"},
503: {"description": "Immich integration not configured"}
},
tags=["Immich"])
async def get_immich_assets(): async def get_immich_assets():
"""Get Immich photo assets""" """Get Immich photo assets including metadata, tags, and face detection results"""
if not SERVICES["immich"]["enabled"]: if not SERVICES["immich"]["enabled"]:
raise HTTPException(status_code=503, detail="Immich integration not configured") raise HTTPException(
status_code=503,
detail="Immich integration not configured. Please set IMMICH_API_KEY environment variable."
)
# This would make actual API calls to Immich # This would make actual API calls to Immich
# For now, return mock data # For now, return mock data
return { return ImmichAssetsResponse(
"assets": [ assets=[
{ ImmichAsset(
"id": "asset_123", id="asset_123",
"filename": "photo_001.jpg", filename="photo_001.jpg",
"created_at": datetime.now().isoformat(), created_at=datetime.now().isoformat(),
"tags": ["person", "outdoor"], tags=["person", "outdoor"],
"faces": ["Alice", "Bob"] faces=["Alice", "Bob"]
} )
] ]
} )
@app.post("/publish-event") @app.post("/publish-event",
async def publish_event(event_data: dict, background_tasks: BackgroundTasks): response_model=EventResponse,
"""Publish an event to the message bus""" summary="Publish Event",
description="Publish an event to the Redis message bus",
responses={
200: {"description": "Event published successfully"},
500: {"description": "Failed to publish event"}
},
tags=["Events"])
async def publish_event(event_data: EventData, background_tasks: BackgroundTasks):
"""Publish an event to the Redis message bus for consumption by other services"""
try: try:
event = { event = {
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"service": event_data.get("service", "unknown"), "service": event_data.service,
"event_type": event_data.get("event_type", "unknown"), "event_type": event_data.event_type,
"metadata": json.dumps(event_data.get("metadata", {})) "metadata": json.dumps(event_data.metadata)
} }
# Publish to Redis # Publish to Redis
redis_client.lpush("events", json.dumps(event)) redis_client.lpush("events", json.dumps(event))
return {"status": "published", "event": event} return EventResponse(
status="published",
event=event
)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.get("/events") @app.get("/events",
async def get_events(limit: int = 100): response_model=EventsResponse,
"""Get recent events from the message bus""" summary="Get Events",
description="Retrieve recent events from the message bus",
responses={
200: {"description": "Successfully retrieved events"},
500: {"description": "Failed to retrieve events"}
},
tags=["Events"])
async def get_events(limit: int = Query(100, ge=1, le=1000, description="Maximum number of events to retrieve")):
"""Get recent events from the Redis message bus"""
try: try:
events = redis_client.lrange("events", 0, limit - 1) events = redis_client.lrange("events", 0, limit - 1)
return {"events": [json.loads(event) for event in events]} parsed_events = []
for event in events:
try:
event_data = json.loads(event)
parsed_events.append(Event(**event_data))
except json.JSONDecodeError:
continue
return EventsResponse(events=parsed_events)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.get("/home-assistant/entity/{entity_id}",
response_model=HAEntity,
summary="Get Specific HA Entity",
description="Get a specific Home Assistant entity by ID",
responses={
200: {"description": "Successfully retrieved entity"},
404: {"description": "Entity not found"},
503: {"description": "Home Assistant integration not configured"}
},
tags=["Home Assistant"])
async def get_ha_entity(entity_id: str = Path(..., description="Entity ID")):
"""Get a specific Home Assistant entity by its ID"""
if not SERVICES["home_assistant"]["enabled"]:
raise HTTPException(
status_code=503,
detail="Home Assistant integration not configured. Please set HOME_ASSISTANT_TOKEN environment variable."
)
# This would make actual API calls to Home Assistant
# For now, return mock data
return HAEntity(
entity_id=entity_id,
state="unknown",
attributes=HAAttributes(
unit_of_measurement="",
friendly_name=f"Entity {entity_id}"
)
)
@app.get("/frigate/cameras",
summary="Get Frigate Cameras",
description="Get list of Frigate cameras",
responses={
200: {"description": "Successfully retrieved cameras"},
503: {"description": "Frigate integration not configured"}
},
tags=["Frigate"])
async def get_frigate_cameras():
"""Get list of available Frigate cameras"""
if not SERVICES["frigate"]["enabled"]:
raise HTTPException(
status_code=503,
detail="Frigate integration not configured. Please set FRIGATE_TOKEN environment variable."
)
# This would make actual API calls to Frigate
# For now, return mock data
return {
"cameras": [
{"name": "front_door", "enabled": True},
{"name": "back_yard", "enabled": True},
{"name": "garage", "enabled": False}
]
}
@app.get("/immich/albums",
summary="Get Immich Albums",
description="Get list of Immich albums",
responses={
200: {"description": "Successfully retrieved albums"},
503: {"description": "Immich integration not configured"}
},
tags=["Immich"])
async def get_immich_albums():
"""Get list of Immich albums"""
if not SERVICES["immich"]["enabled"]:
raise HTTPException(
status_code=503,
detail="Immich integration not configured. Please set IMMICH_API_KEY environment variable."
)
# This would make actual API calls to Immich
# For now, return mock data
return {
"albums": [
{"id": "album_1", "name": "Family Photos", "asset_count": 150},
{"id": "album_2", "name": "Vacation 2024", "asset_count": 75}
]
}
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)