Add API Documentation Service and enhance existing services with OpenAPI support
This commit is contained in:
18
services/api-docs/Dockerfile
Normal file
18
services/api-docs/Dockerfile
Normal 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"]
|
||||
18
services/api-docs/Dockerfile.dev
Normal file
18
services/api-docs/Dockerfile.dev
Normal 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"]
|
||||
30
services/api-docs/README.md
Normal file
30
services/api-docs/README.md
Normal 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
|
||||
30
services/api-docs/package.json
Normal file
30
services/api-docs/package.json
Normal 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
228
services/api-docs/server.js
Normal 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}`);
|
||||
});
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,14 @@ package com.labfusion.controller;
|
||||
import com.labfusion.model.Dashboard;
|
||||
import com.labfusion.model.User;
|
||||
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.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
@@ -14,19 +22,37 @@ import java.util.Optional;
|
||||
@RestController
|
||||
@RequestMapping("/api/dashboards")
|
||||
@CrossOrigin(origins = "*")
|
||||
@Tag(name = "Dashboard Management", description = "APIs for managing user dashboards and widgets")
|
||||
@SecurityRequirement(name = "bearerAuth")
|
||||
public class DashboardController {
|
||||
|
||||
@Autowired
|
||||
private DashboardService dashboardService;
|
||||
|
||||
@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) {
|
||||
List<Dashboard> dashboards = dashboardService.getDashboardsByUser(user);
|
||||
return ResponseEntity.ok(dashboards);
|
||||
}
|
||||
|
||||
@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);
|
||||
if (dashboard.isPresent()) {
|
||||
// Check if user owns the dashboard
|
||||
|
||||
@@ -4,6 +4,13 @@ import com.labfusion.model.DeviceState;
|
||||
import com.labfusion.model.Event;
|
||||
import com.labfusion.repository.DeviceStateRepository;
|
||||
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.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -14,6 +21,7 @@ import java.util.List;
|
||||
@RestController
|
||||
@RequestMapping("/api/system")
|
||||
@CrossOrigin(origins = "*")
|
||||
@Tag(name = "System Data", description = "APIs for system events, device states, and metrics")
|
||||
public class SystemController {
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -44,3 +44,14 @@ management:
|
||||
endpoint:
|
||||
health:
|
||||
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
|
||||
@@ -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.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any
|
||||
import asyncio
|
||||
import redis
|
||||
import json
|
||||
@@ -10,10 +13,93 @@ from dotenv import load_dotenv
|
||||
# Load environment variables
|
||||
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(
|
||||
title="LabFusion Service Adapters",
|
||||
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
|
||||
@@ -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():
|
||||
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():
|
||||
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():
|
||||
"""Get status of all configured services"""
|
||||
"""Get status of all configured external services (Home Assistant, Frigate, Immich, n8n)"""
|
||||
service_status = {}
|
||||
for service_name, config in SERVICES.items():
|
||||
service_status[service_name] = {
|
||||
"enabled": config["enabled"],
|
||||
"url": config["url"],
|
||||
"status": "unknown" # Would check actual service status
|
||||
}
|
||||
service_status[service_name] = ServiceStatus(
|
||||
enabled=config["enabled"],
|
||||
url=config["url"],
|
||||
status="unknown" # Would check actual 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():
|
||||
"""Get Home Assistant entities"""
|
||||
"""Get Home Assistant entities including sensors, switches, and other devices"""
|
||||
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
|
||||
# For now, return mock data
|
||||
return {
|
||||
"entities": [
|
||||
{
|
||||
"entity_id": "sensor.cpu_usage",
|
||||
"state": "45.2",
|
||||
"attributes": {"unit_of_measurement": "%", "friendly_name": "CPU Usage"}
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.memory_usage",
|
||||
"state": "2.1",
|
||||
"attributes": {"unit_of_measurement": "GB", "friendly_name": "Memory Usage"}
|
||||
}
|
||||
return HAEntitiesResponse(
|
||||
entities=[
|
||||
HAEntity(
|
||||
entity_id="sensor.cpu_usage",
|
||||
state="45.2",
|
||||
attributes=HAAttributes(
|
||||
unit_of_measurement="%",
|
||||
friendly_name="CPU 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():
|
||||
"""Get Frigate detection events"""
|
||||
"""Get Frigate detection events including person, vehicle, and object detections"""
|
||||
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
|
||||
# For now, return mock data
|
||||
return {
|
||||
"events": [
|
||||
{
|
||||
"id": "event_123",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"camera": "front_door",
|
||||
"label": "person",
|
||||
"confidence": 0.95
|
||||
}
|
||||
return FrigateEventsResponse(
|
||||
events=[
|
||||
FrigateEvent(
|
||||
id="event_123",
|
||||
timestamp=datetime.now().isoformat(),
|
||||
camera="front_door",
|
||||
label="person",
|
||||
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():
|
||||
"""Get Immich photo assets"""
|
||||
"""Get Immich photo assets including metadata, tags, and face detection results"""
|
||||
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
|
||||
# For now, return mock data
|
||||
return {
|
||||
"assets": [
|
||||
{
|
||||
"id": "asset_123",
|
||||
"filename": "photo_001.jpg",
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"tags": ["person", "outdoor"],
|
||||
"faces": ["Alice", "Bob"]
|
||||
}
|
||||
return ImmichAssetsResponse(
|
||||
assets=[
|
||||
ImmichAsset(
|
||||
id="asset_123",
|
||||
filename="photo_001.jpg",
|
||||
created_at=datetime.now().isoformat(),
|
||||
tags=["person", "outdoor"],
|
||||
faces=["Alice", "Bob"]
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@app.post("/publish-event")
|
||||
async def publish_event(event_data: dict, background_tasks: BackgroundTasks):
|
||||
"""Publish an event to the message bus"""
|
||||
@app.post("/publish-event",
|
||||
response_model=EventResponse,
|
||||
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:
|
||||
event = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"service": event_data.get("service", "unknown"),
|
||||
"event_type": event_data.get("event_type", "unknown"),
|
||||
"metadata": json.dumps(event_data.get("metadata", {}))
|
||||
"service": event_data.service,
|
||||
"event_type": event_data.event_type,
|
||||
"metadata": json.dumps(event_data.metadata)
|
||||
}
|
||||
|
||||
# Publish to Redis
|
||||
redis_client.lpush("events", json.dumps(event))
|
||||
|
||||
return {"status": "published", "event": event}
|
||||
return EventResponse(
|
||||
status="published",
|
||||
event=event
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/events")
|
||||
async def get_events(limit: int = 100):
|
||||
"""Get recent events from the message bus"""
|
||||
@app.get("/events",
|
||||
response_model=EventsResponse,
|
||||
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:
|
||||
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:
|
||||
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__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
Reference in New Issue
Block a user