Add API Documentation Service and enhance existing services with OpenAPI support
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 🎯
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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.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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user