Refactor API Docs CI workflow to correct ESLint command syntax and enhance Python formatting in CI pipelines; update progress tracking documentation
Some checks failed
API Docs (Node.js Express) / test (16) (push) Failing after 5m29s
API Docs (Node.js Express) / test (18) (push) Failing after 5m25s
API Docs (Node.js Express) / test (20) (push) Failing after 1m4s
API Docs (Node.js Express) / build (push) Has been skipped
API Docs (Node.js Express) / security (push) Has been skipped
LabFusion CI/CD Pipeline / api-gateway (push) Failing after 4m52s
LabFusion CI/CD Pipeline / service-adapters (push) Failing after 5m1s
LabFusion CI/CD Pipeline / api-docs (push) Failing after 5m12s
LabFusion CI/CD Pipeline / frontend (push) Failing after 6m39s
LabFusion CI/CD Pipeline / integration-tests (push) Has been skipped
LabFusion CI/CD Pipeline / security-scan (push) Has been skipped
Docker Build and Push / build-and-push (push) Failing after 34s
Docker Build and Push / security-scan (push) Has been skipped
Integration Tests / integration-tests (push) Failing after 1m33s
Integration Tests / performance-tests (push) Has been skipped
Service Adapters (Python FastAPI) / test (3.1) (push) Failing after 35s
Service Adapters (Python FastAPI) / test (3.11) (push) Failing after 5m20s
Service Adapters (Python FastAPI) / test (3.12) (push) Failing after 5m27s
Service Adapters (Python FastAPI) / test (3.9) (push) Failing after 5m50s
Docker Build and Push / deploy-staging (push) Has been skipped
Service Adapters (Python FastAPI) / build (push) Has been skipped
Service Adapters (Python FastAPI) / security (push) Has been skipped
Docker Build and Push / deploy-production (push) Has been skipped
Some checks failed
API Docs (Node.js Express) / test (16) (push) Failing after 5m29s
API Docs (Node.js Express) / test (18) (push) Failing after 5m25s
API Docs (Node.js Express) / test (20) (push) Failing after 1m4s
API Docs (Node.js Express) / build (push) Has been skipped
API Docs (Node.js Express) / security (push) Has been skipped
LabFusion CI/CD Pipeline / api-gateway (push) Failing after 4m52s
LabFusion CI/CD Pipeline / service-adapters (push) Failing after 5m1s
LabFusion CI/CD Pipeline / api-docs (push) Failing after 5m12s
LabFusion CI/CD Pipeline / frontend (push) Failing after 6m39s
LabFusion CI/CD Pipeline / integration-tests (push) Has been skipped
LabFusion CI/CD Pipeline / security-scan (push) Has been skipped
Docker Build and Push / build-and-push (push) Failing after 34s
Docker Build and Push / security-scan (push) Has been skipped
Integration Tests / integration-tests (push) Failing after 1m33s
Integration Tests / performance-tests (push) Has been skipped
Service Adapters (Python FastAPI) / test (3.1) (push) Failing after 35s
Service Adapters (Python FastAPI) / test (3.11) (push) Failing after 5m20s
Service Adapters (Python FastAPI) / test (3.12) (push) Failing after 5m27s
Service Adapters (Python FastAPI) / test (3.9) (push) Failing after 5m50s
Docker Build and Push / deploy-staging (push) Has been skipped
Service Adapters (Python FastAPI) / build (push) Has been skipped
Service Adapters (Python FastAPI) / security (push) Has been skipped
Docker Build and Push / deploy-production (push) Has been skipped
This commit is contained in:
@@ -73,7 +73,7 @@ jobs:
|
|||||||
- name: Run linting
|
- name: Run linting
|
||||||
run: |
|
run: |
|
||||||
npx eslint . --ext .js
|
npx eslint . --ext .js
|
||||||
npx eslint . --ext .js --fix --dry-run
|
npx eslint . --ext .js --fix-dry-run
|
||||||
|
|
||||||
- name: Run type checking
|
- name: Run type checking
|
||||||
run: npm run type-check
|
run: npm run type-check
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ The modular structure allows for easy addition of new services:
|
|||||||
- [x] Implement offline mode and resilience (Frontend)
|
- [x] Implement offline mode and resilience (Frontend)
|
||||||
- [x] Set up CI/CD pipelines for automated testing and deployment
|
- [x] Set up CI/CD pipelines for automated testing and deployment
|
||||||
- [x] Fix Maven command not found error in CI/CD pipelines (Added Maven wrapper)
|
- [x] Fix Maven command not found error in CI/CD pipelines (Added Maven wrapper)
|
||||||
|
- [x] Fix Python formatting issues in CI/CD pipelines (Applied Black and isort formatting)
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
- [Project Specifications](specs.md)
|
- [Project Specifications](specs.md)
|
||||||
|
|||||||
230
services/service-adapters/bandit-report.json
Normal file
230
services/service-adapters/bandit-report.json
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
{
|
||||||
|
"errors": [],
|
||||||
|
"generated_at": "2025-09-12T15:43:08Z",
|
||||||
|
"metrics": {
|
||||||
|
".\\main.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 1,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 1,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 28,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\main_old.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 1,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 1,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 368,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\models\\__init__.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 0,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\models\\schemas.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 51,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\routes\\__init__.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 0,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\routes\\events.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 59,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\routes\\frigate.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 58,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\routes\\general.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 42,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\routes\\home_assistant.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 66,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\routes\\immich.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 57,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\services\\__init__.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 0,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\services\\config.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 25,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
".\\services\\redis_client.py": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 0,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 0,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 7,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
},
|
||||||
|
"_totals": {
|
||||||
|
"CONFIDENCE.HIGH": 0,
|
||||||
|
"CONFIDENCE.LOW": 0,
|
||||||
|
"CONFIDENCE.MEDIUM": 2,
|
||||||
|
"CONFIDENCE.UNDEFINED": 0,
|
||||||
|
"SEVERITY.HIGH": 0,
|
||||||
|
"SEVERITY.LOW": 0,
|
||||||
|
"SEVERITY.MEDIUM": 2,
|
||||||
|
"SEVERITY.UNDEFINED": 0,
|
||||||
|
"loc": 761,
|
||||||
|
"nosec": 0,
|
||||||
|
"skipped_tests": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"code": "37 \n38 uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n",
|
||||||
|
"col_offset": 26,
|
||||||
|
"end_col_offset": 35,
|
||||||
|
"filename": ".\\main.py",
|
||||||
|
"issue_confidence": "MEDIUM",
|
||||||
|
"issue_cwe": {
|
||||||
|
"id": 605,
|
||||||
|
"link": "https://cwe.mitre.org/data/definitions/605.html"
|
||||||
|
},
|
||||||
|
"issue_severity": "MEDIUM",
|
||||||
|
"issue_text": "Possible binding to all interfaces.",
|
||||||
|
"line_number": 38,
|
||||||
|
"line_range": [
|
||||||
|
38
|
||||||
|
],
|
||||||
|
"more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b104_hardcoded_bind_all_interfaces.html",
|
||||||
|
"test_id": "B104",
|
||||||
|
"test_name": "hardcoded_bind_all_interfaces"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "454 \n455 uvicorn.run(app, host=\"0.0.0.0\", port=8000)\n",
|
||||||
|
"col_offset": 26,
|
||||||
|
"end_col_offset": 35,
|
||||||
|
"filename": ".\\main_old.py",
|
||||||
|
"issue_confidence": "MEDIUM",
|
||||||
|
"issue_cwe": {
|
||||||
|
"id": 605,
|
||||||
|
"link": "https://cwe.mitre.org/data/definitions/605.html"
|
||||||
|
},
|
||||||
|
"issue_severity": "MEDIUM",
|
||||||
|
"issue_text": "Possible binding to all interfaces.",
|
||||||
|
"line_number": 455,
|
||||||
|
"line_range": [
|
||||||
|
455
|
||||||
|
],
|
||||||
|
"more_info": "https://bandit.readthedocs.io/en/1.8.6/plugins/b104_hardcoded_bind_all_interfaces.html",
|
||||||
|
"test_id": "B104",
|
||||||
|
"test_name": "hardcoded_bind_all_interfaces"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,27 +2,18 @@ from fastapi import FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
# Import route modules
|
# Import route modules
|
||||||
from routes import general, home_assistant, frigate, immich, events
|
from routes import events, frigate, general, home_assistant, immich
|
||||||
|
|
||||||
# Create FastAPI app
|
# Create FastAPI app
|
||||||
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",
|
||||||
license_info={
|
license_info={"name": "MIT License", "url": "https://opensource.org/licenses/MIT"},
|
||||||
"name": "MIT License",
|
|
||||||
"url": "https://opensource.org/licenses/MIT"
|
|
||||||
},
|
|
||||||
servers=[
|
servers=[
|
||||||
{
|
{"url": "http://localhost:8000", "description": "Development Server"},
|
||||||
"url": "http://localhost:8000",
|
{"url": "https://adapters.labfusion.dev", "description": "Production Server"},
|
||||||
"description": "Development Server"
|
],
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://adapters.labfusion.dev",
|
|
||||||
"description": "Production Server"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# CORS middleware
|
# CORS middleware
|
||||||
@@ -43,4 +34,5 @@ app.include_router(events.router)
|
|||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -1,36 +1,40 @@
|
|||||||
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
|
import json
|
||||||
from datetime import datetime
|
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import redis
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import BackgroundTasks, FastAPI, HTTPException, Path, Query
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
# Pydantic models for request/response schemas
|
# Pydantic models for request/response schemas
|
||||||
class ServiceStatus(BaseModel):
|
class ServiceStatus(BaseModel):
|
||||||
enabled: bool = Field(..., description="Whether the service is enabled")
|
enabled: bool = Field(..., description="Whether the service is enabled")
|
||||||
url: str = Field(..., description="Service URL")
|
url: str = Field(..., description="Service URL")
|
||||||
status: str = Field(..., description="Service status")
|
status: str = Field(..., description="Service status")
|
||||||
|
|
||||||
|
|
||||||
class HAAttributes(BaseModel):
|
class HAAttributes(BaseModel):
|
||||||
unit_of_measurement: Optional[str] = Field(None, description="Unit of measurement")
|
unit_of_measurement: Optional[str] = Field(None, description="Unit of measurement")
|
||||||
friendly_name: Optional[str] = Field(None, description="Friendly name")
|
friendly_name: Optional[str] = Field(None, description="Friendly name")
|
||||||
|
|
||||||
|
|
||||||
class HAEntity(BaseModel):
|
class HAEntity(BaseModel):
|
||||||
entity_id: str = Field(..., description="Entity ID")
|
entity_id: str = Field(..., description="Entity ID")
|
||||||
state: str = Field(..., description="Current state")
|
state: str = Field(..., description="Current state")
|
||||||
attributes: HAAttributes = Field(..., description="Entity attributes")
|
attributes: HAAttributes = Field(..., description="Entity attributes")
|
||||||
|
|
||||||
|
|
||||||
class HAEntitiesResponse(BaseModel):
|
class HAEntitiesResponse(BaseModel):
|
||||||
entities: List[HAEntity] = Field(..., description="List of Home Assistant entities")
|
entities: List[HAEntity] = Field(..., description="List of Home Assistant entities")
|
||||||
|
|
||||||
|
|
||||||
class FrigateEvent(BaseModel):
|
class FrigateEvent(BaseModel):
|
||||||
id: str = Field(..., description="Event ID")
|
id: str = Field(..., description="Event ID")
|
||||||
timestamp: str = Field(..., description="Event timestamp")
|
timestamp: str = Field(..., description="Event timestamp")
|
||||||
@@ -38,9 +42,11 @@ class FrigateEvent(BaseModel):
|
|||||||
label: str = Field(..., description="Detection label")
|
label: str = Field(..., description="Detection label")
|
||||||
confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
|
confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
|
||||||
|
|
||||||
|
|
||||||
class FrigateEventsResponse(BaseModel):
|
class FrigateEventsResponse(BaseModel):
|
||||||
events: List[FrigateEvent] = Field(..., description="List of Frigate events")
|
events: List[FrigateEvent] = Field(..., description="List of Frigate events")
|
||||||
|
|
||||||
|
|
||||||
class ImmichAsset(BaseModel):
|
class ImmichAsset(BaseModel):
|
||||||
id: str = Field(..., description="Asset ID")
|
id: str = Field(..., description="Asset ID")
|
||||||
filename: str = Field(..., description="Filename")
|
filename: str = Field(..., description="Filename")
|
||||||
@@ -48,35 +54,43 @@ class ImmichAsset(BaseModel):
|
|||||||
tags: List[str] = Field(..., description="Asset tags")
|
tags: List[str] = Field(..., description="Asset tags")
|
||||||
faces: List[str] = Field(..., description="Detected faces")
|
faces: List[str] = Field(..., description="Detected faces")
|
||||||
|
|
||||||
|
|
||||||
class ImmichAssetsResponse(BaseModel):
|
class ImmichAssetsResponse(BaseModel):
|
||||||
assets: List[ImmichAsset] = Field(..., description="List of Immich assets")
|
assets: List[ImmichAsset] = Field(..., description="List of Immich assets")
|
||||||
|
|
||||||
|
|
||||||
class EventData(BaseModel):
|
class EventData(BaseModel):
|
||||||
service: str = Field(..., description="Service name")
|
service: str = Field(..., description="Service name")
|
||||||
event_type: str = Field(..., description="Event type")
|
event_type: str = Field(..., description="Event type")
|
||||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="Event metadata")
|
metadata: Dict[str, Any] = Field(default_factory=dict, description="Event metadata")
|
||||||
|
|
||||||
|
|
||||||
class EventResponse(BaseModel):
|
class EventResponse(BaseModel):
|
||||||
status: str = Field(..., description="Publication status")
|
status: str = Field(..., description="Publication status")
|
||||||
event: Dict[str, Any] = Field(..., description="Published event")
|
event: Dict[str, Any] = Field(..., description="Published event")
|
||||||
|
|
||||||
|
|
||||||
class Event(BaseModel):
|
class Event(BaseModel):
|
||||||
timestamp: str = Field(..., description="Event timestamp")
|
timestamp: str = Field(..., description="Event timestamp")
|
||||||
service: str = Field(..., description="Service name")
|
service: str = Field(..., description="Service name")
|
||||||
event_type: str = Field(..., description="Event type")
|
event_type: str = Field(..., description="Event type")
|
||||||
metadata: str = Field(..., description="Event metadata as JSON string")
|
metadata: str = Field(..., description="Event metadata as JSON string")
|
||||||
|
|
||||||
|
|
||||||
class EventsResponse(BaseModel):
|
class EventsResponse(BaseModel):
|
||||||
events: List[Event] = Field(..., description="List of events")
|
events: List[Event] = Field(..., description="List of events")
|
||||||
|
|
||||||
|
|
||||||
class HealthResponse(BaseModel):
|
class HealthResponse(BaseModel):
|
||||||
status: str = Field(..., description="Service health status")
|
status: str = Field(..., description="Service health status")
|
||||||
timestamp: str = Field(..., description="Health check timestamp")
|
timestamp: str = Field(..., description="Health check timestamp")
|
||||||
|
|
||||||
|
|
||||||
class RootResponse(BaseModel):
|
class RootResponse(BaseModel):
|
||||||
message: str = Field(..., description="API message")
|
message: str = Field(..., description="API message")
|
||||||
version: str = Field(..., description="API version")
|
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",
|
||||||
@@ -84,22 +98,13 @@ app = FastAPI(
|
|||||||
contact={
|
contact={
|
||||||
"name": "LabFusion Team",
|
"name": "LabFusion Team",
|
||||||
"url": "https://github.com/labfusion/labfusion",
|
"url": "https://github.com/labfusion/labfusion",
|
||||||
"email": "team@labfusion.dev"
|
"email": "team@labfusion.dev",
|
||||||
},
|
|
||||||
license_info={
|
|
||||||
"name": "MIT License",
|
|
||||||
"url": "https://opensource.org/licenses/MIT"
|
|
||||||
},
|
},
|
||||||
|
license_info={"name": "MIT License", "url": "https://opensource.org/licenses/MIT"},
|
||||||
servers=[
|
servers=[
|
||||||
{
|
{"url": "http://localhost:8000", "description": "Development Server"},
|
||||||
"url": "http://localhost:8000",
|
{"url": "https://adapters.labfusion.dev", "description": "Production Server"},
|
||||||
"description": "Development Server"
|
],
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://adapters.labfusion.dev",
|
|
||||||
"description": "Production Server"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# CORS middleware
|
# CORS middleware
|
||||||
@@ -115,7 +120,7 @@ app.add_middleware(
|
|||||||
redis_client = redis.Redis(
|
redis_client = redis.Redis(
|
||||||
host=os.getenv("REDIS_HOST", "localhost"),
|
host=os.getenv("REDIS_HOST", "localhost"),
|
||||||
port=int(os.getenv("REDIS_PORT", 6379)),
|
port=int(os.getenv("REDIS_PORT", 6379)),
|
||||||
decode_responses=True
|
decode_responses=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Service configurations
|
# Service configurations
|
||||||
@@ -123,54 +128,57 @@ SERVICES = {
|
|||||||
"home_assistant": {
|
"home_assistant": {
|
||||||
"url": os.getenv("HOME_ASSISTANT_URL", "https://homeassistant.local:8123"),
|
"url": os.getenv("HOME_ASSISTANT_URL", "https://homeassistant.local:8123"),
|
||||||
"token": os.getenv("HOME_ASSISTANT_TOKEN", ""),
|
"token": os.getenv("HOME_ASSISTANT_TOKEN", ""),
|
||||||
"enabled": bool(os.getenv("HOME_ASSISTANT_TOKEN"))
|
"enabled": bool(os.getenv("HOME_ASSISTANT_TOKEN")),
|
||||||
},
|
},
|
||||||
"frigate": {
|
"frigate": {
|
||||||
"url": os.getenv("FRIGATE_URL", "http://frigate.local:5000"),
|
"url": os.getenv("FRIGATE_URL", "http://frigate.local:5000"),
|
||||||
"token": os.getenv("FRIGATE_TOKEN", ""),
|
"token": os.getenv("FRIGATE_TOKEN", ""),
|
||||||
"enabled": bool(os.getenv("FRIGATE_TOKEN"))
|
"enabled": bool(os.getenv("FRIGATE_TOKEN")),
|
||||||
},
|
},
|
||||||
"immich": {
|
"immich": {
|
||||||
"url": os.getenv("IMMICH_URL", "http://immich.local:2283"),
|
"url": os.getenv("IMMICH_URL", "http://immich.local:2283"),
|
||||||
"api_key": os.getenv("IMMICH_API_KEY", ""),
|
"api_key": os.getenv("IMMICH_API_KEY", ""),
|
||||||
"enabled": bool(os.getenv("IMMICH_API_KEY"))
|
"enabled": bool(os.getenv("IMMICH_API_KEY")),
|
||||||
},
|
},
|
||||||
"n8n": {
|
"n8n": {
|
||||||
"url": os.getenv("N8N_URL", "http://n8n.local:5678"),
|
"url": os.getenv("N8N_URL", "http://n8n.local:5678"),
|
||||||
"webhook_url": os.getenv("N8N_WEBHOOK_URL", ""),
|
"webhook_url": os.getenv("N8N_WEBHOOK_URL", ""),
|
||||||
"enabled": bool(os.getenv("N8N_WEBHOOK_URL"))
|
"enabled": bool(os.getenv("N8N_WEBHOOK_URL")),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/",
|
|
||||||
response_model=RootResponse,
|
@app.get(
|
||||||
summary="API Root",
|
"/",
|
||||||
description="Get basic API information",
|
response_model=RootResponse,
|
||||||
tags=["General"])
|
summary="API Root",
|
||||||
|
description="Get basic API information",
|
||||||
|
tags=["General"],
|
||||||
|
)
|
||||||
async def root():
|
async def root():
|
||||||
"""Get basic API information and version"""
|
"""Get basic API information and version"""
|
||||||
return RootResponse(
|
return RootResponse(message="LabFusion Service Adapters API", version="1.0.0")
|
||||||
message="LabFusion Service Adapters API",
|
|
||||||
version="1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/health",
|
|
||||||
response_model=HealthResponse,
|
@app.get(
|
||||||
summary="Health Check",
|
"/health",
|
||||||
description="Check service health status",
|
response_model=HealthResponse,
|
||||||
tags=["General"])
|
summary="Health Check",
|
||||||
|
description="Check service health status",
|
||||||
|
tags=["General"],
|
||||||
|
)
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Check the health status of the service adapters"""
|
"""Check the health status of the service adapters"""
|
||||||
return HealthResponse(
|
return HealthResponse(status="healthy", timestamp=datetime.now().isoformat())
|
||||||
status="healthy",
|
|
||||||
timestamp=datetime.now().isoformat()
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/services",
|
|
||||||
response_model=Dict[str, ServiceStatus],
|
@app.get(
|
||||||
summary="Get Service Status",
|
"/services",
|
||||||
description="Get status of all configured external services",
|
response_model=Dict[str, ServiceStatus],
|
||||||
tags=["Services"])
|
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 external services (Home Assistant, Frigate, Immich, n8n)"""
|
"""Get status of all configured external services (Home Assistant, Frigate, Immich, n8n)"""
|
||||||
service_status = {}
|
service_status = {}
|
||||||
@@ -178,25 +186,28 @@ async def get_services():
|
|||||||
service_status[service_name] = ServiceStatus(
|
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",
|
|
||||||
response_model=HAEntitiesResponse,
|
@app.get(
|
||||||
summary="Get Home Assistant Entities",
|
"/home-assistant/entities",
|
||||||
description="Retrieve all entities from Home Assistant",
|
response_model=HAEntitiesResponse,
|
||||||
responses={
|
summary="Get Home Assistant Entities",
|
||||||
200: {"description": "Successfully retrieved entities"},
|
description="Retrieve all entities from Home Assistant",
|
||||||
503: {"description": "Home Assistant integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved entities"},
|
||||||
tags=["Home Assistant"])
|
503: {"description": "Home Assistant integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Home Assistant"],
|
||||||
|
)
|
||||||
async def get_ha_entities():
|
async def get_ha_entities():
|
||||||
"""Get Home Assistant entities including sensors, switches, and other devices"""
|
"""Get Home Assistant entities including sensors, switches, and other devices"""
|
||||||
if not SERVICES["home_assistant"]["enabled"]:
|
if not SERVICES["home_assistant"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Home Assistant integration not configured. Please set HOME_ASSISTANT_TOKEN environment variable."
|
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
|
||||||
@@ -207,36 +218,37 @@ async def get_ha_entities():
|
|||||||
entity_id="sensor.cpu_usage",
|
entity_id="sensor.cpu_usage",
|
||||||
state="45.2",
|
state="45.2",
|
||||||
attributes=HAAttributes(
|
attributes=HAAttributes(
|
||||||
unit_of_measurement="%",
|
unit_of_measurement="%", friendly_name="CPU Usage"
|
||||||
friendly_name="CPU Usage"
|
),
|
||||||
)
|
|
||||||
),
|
),
|
||||||
HAEntity(
|
HAEntity(
|
||||||
entity_id="sensor.memory_usage",
|
entity_id="sensor.memory_usage",
|
||||||
state="2.1",
|
state="2.1",
|
||||||
attributes=HAAttributes(
|
attributes=HAAttributes(
|
||||||
unit_of_measurement="GB",
|
unit_of_measurement="GB", friendly_name="Memory Usage"
|
||||||
friendly_name="Memory Usage"
|
),
|
||||||
)
|
),
|
||||||
)
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/frigate/events",
|
|
||||||
response_model=FrigateEventsResponse,
|
@app.get(
|
||||||
summary="Get Frigate Events",
|
"/frigate/events",
|
||||||
description="Retrieve detection events from Frigate NVR",
|
response_model=FrigateEventsResponse,
|
||||||
responses={
|
summary="Get Frigate Events",
|
||||||
200: {"description": "Successfully retrieved events"},
|
description="Retrieve detection events from Frigate NVR",
|
||||||
503: {"description": "Frigate integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved events"},
|
||||||
tags=["Frigate"])
|
503: {"description": "Frigate integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Frigate"],
|
||||||
|
)
|
||||||
async def get_frigate_events():
|
async def get_frigate_events():
|
||||||
"""Get Frigate detection events including person, vehicle, and object detections"""
|
"""Get Frigate detection events including person, vehicle, and object detections"""
|
||||||
if not SERVICES["frigate"]["enabled"]:
|
if not SERVICES["frigate"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Frigate integration not configured. Please set FRIGATE_TOKEN environment variable."
|
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
|
||||||
@@ -248,26 +260,29 @@ async def get_frigate_events():
|
|||||||
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",
|
|
||||||
response_model=ImmichAssetsResponse,
|
@app.get(
|
||||||
summary="Get Immich Assets",
|
"/immich/assets",
|
||||||
description="Retrieve photo assets from Immich",
|
response_model=ImmichAssetsResponse,
|
||||||
responses={
|
summary="Get Immich Assets",
|
||||||
200: {"description": "Successfully retrieved assets"},
|
description="Retrieve photo assets from Immich",
|
||||||
503: {"description": "Immich integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved assets"},
|
||||||
tags=["Immich"])
|
503: {"description": "Immich integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Immich"],
|
||||||
|
)
|
||||||
async def get_immich_assets():
|
async def get_immich_assets():
|
||||||
"""Get Immich photo assets including metadata, tags, and face detection results"""
|
"""Get Immich photo assets including metadata, tags, and face detection results"""
|
||||||
if not SERVICES["immich"]["enabled"]:
|
if not SERVICES["immich"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Immich integration not configured. Please set IMMICH_API_KEY environment variable."
|
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
|
||||||
@@ -279,20 +294,23 @@ async def get_immich_assets():
|
|||||||
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",
|
|
||||||
response_model=EventResponse,
|
@app.post(
|
||||||
summary="Publish Event",
|
"/publish-event",
|
||||||
description="Publish an event to the Redis message bus",
|
response_model=EventResponse,
|
||||||
responses={
|
summary="Publish Event",
|
||||||
200: {"description": "Event published successfully"},
|
description="Publish an event to the Redis message bus",
|
||||||
500: {"description": "Failed to publish event"}
|
responses={
|
||||||
},
|
200: {"description": "Event published successfully"},
|
||||||
tags=["Events"])
|
500: {"description": "Failed to publish event"},
|
||||||
|
},
|
||||||
|
tags=["Events"],
|
||||||
|
)
|
||||||
async def publish_event(event_data: EventData, background_tasks: BackgroundTasks):
|
async def publish_event(event_data: EventData, background_tasks: BackgroundTasks):
|
||||||
"""Publish an event to the Redis message bus for consumption by other services"""
|
"""Publish an event to the Redis message bus for consumption by other services"""
|
||||||
try:
|
try:
|
||||||
@@ -300,29 +318,33 @@ async def publish_event(event_data: EventData, background_tasks: BackgroundTasks
|
|||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
"service": event_data.service,
|
"service": event_data.service,
|
||||||
"event_type": event_data.event_type,
|
"event_type": event_data.event_type,
|
||||||
"metadata": json.dumps(event_data.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 EventResponse(
|
return EventResponse(status="published", event=event)
|
||||||
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",
|
|
||||||
response_model=EventsResponse,
|
@app.get(
|
||||||
summary="Get Events",
|
"/events",
|
||||||
description="Retrieve recent events from the message bus",
|
response_model=EventsResponse,
|
||||||
responses={
|
summary="Get Events",
|
||||||
200: {"description": "Successfully retrieved events"},
|
description="Retrieve recent events from the message bus",
|
||||||
500: {"description": "Failed to retrieve events"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved events"},
|
||||||
tags=["Events"])
|
500: {"description": "Failed to retrieve events"},
|
||||||
async def get_events(limit: int = Query(100, ge=1, le=1000, description="Maximum number of events to retrieve")):
|
},
|
||||||
|
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"""
|
"""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)
|
||||||
@@ -338,22 +360,25 @@ async def get_events(limit: int = Query(100, ge=1, le=1000, description="Maximum
|
|||||||
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,
|
@app.get(
|
||||||
summary="Get Specific HA Entity",
|
"/home-assistant/entity/{entity_id}",
|
||||||
description="Get a specific Home Assistant entity by ID",
|
response_model=HAEntity,
|
||||||
responses={
|
summary="Get Specific HA Entity",
|
||||||
200: {"description": "Successfully retrieved entity"},
|
description="Get a specific Home Assistant entity by ID",
|
||||||
404: {"description": "Entity not found"},
|
responses={
|
||||||
503: {"description": "Home Assistant integration not configured"}
|
200: {"description": "Successfully retrieved entity"},
|
||||||
},
|
404: {"description": "Entity not found"},
|
||||||
tags=["Home Assistant"])
|
503: {"description": "Home Assistant integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Home Assistant"],
|
||||||
|
)
|
||||||
async def get_ha_entity(entity_id: str = Path(..., description="Entity ID")):
|
async def get_ha_entity(entity_id: str = Path(..., description="Entity ID")):
|
||||||
"""Get a specific Home Assistant entity by its ID"""
|
"""Get a specific Home Assistant entity by its ID"""
|
||||||
if not SERVICES["home_assistant"]["enabled"]:
|
if not SERVICES["home_assistant"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Home Assistant integration not configured. Please set HOME_ASSISTANT_TOKEN environment variable."
|
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
|
||||||
@@ -362,25 +387,27 @@ async def get_ha_entity(entity_id: str = Path(..., description="Entity ID")):
|
|||||||
entity_id=entity_id,
|
entity_id=entity_id,
|
||||||
state="unknown",
|
state="unknown",
|
||||||
attributes=HAAttributes(
|
attributes=HAAttributes(
|
||||||
unit_of_measurement="",
|
unit_of_measurement="", friendly_name=f"Entity {entity_id}"
|
||||||
friendly_name=f"Entity {entity_id}"
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/frigate/cameras",
|
|
||||||
summary="Get Frigate Cameras",
|
@app.get(
|
||||||
description="Get list of Frigate cameras",
|
"/frigate/cameras",
|
||||||
responses={
|
summary="Get Frigate Cameras",
|
||||||
200: {"description": "Successfully retrieved cameras"},
|
description="Get list of Frigate cameras",
|
||||||
503: {"description": "Frigate integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved cameras"},
|
||||||
tags=["Frigate"])
|
503: {"description": "Frigate integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Frigate"],
|
||||||
|
)
|
||||||
async def get_frigate_cameras():
|
async def get_frigate_cameras():
|
||||||
"""Get list of available Frigate cameras"""
|
"""Get list of available Frigate cameras"""
|
||||||
if not SERVICES["frigate"]["enabled"]:
|
if not SERVICES["frigate"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Frigate integration not configured. Please set FRIGATE_TOKEN environment variable."
|
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
|
||||||
@@ -389,24 +416,27 @@ async def get_frigate_cameras():
|
|||||||
"cameras": [
|
"cameras": [
|
||||||
{"name": "front_door", "enabled": True},
|
{"name": "front_door", "enabled": True},
|
||||||
{"name": "back_yard", "enabled": True},
|
{"name": "back_yard", "enabled": True},
|
||||||
{"name": "garage", "enabled": False}
|
{"name": "garage", "enabled": False},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/immich/albums",
|
|
||||||
summary="Get Immich Albums",
|
@app.get(
|
||||||
description="Get list of Immich albums",
|
"/immich/albums",
|
||||||
responses={
|
summary="Get Immich Albums",
|
||||||
200: {"description": "Successfully retrieved albums"},
|
description="Get list of Immich albums",
|
||||||
503: {"description": "Immich integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved albums"},
|
||||||
tags=["Immich"])
|
503: {"description": "Immich integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Immich"],
|
||||||
|
)
|
||||||
async def get_immich_albums():
|
async def get_immich_albums():
|
||||||
"""Get list of Immich albums"""
|
"""Get list of Immich albums"""
|
||||||
if not SERVICES["immich"]["enabled"]:
|
if not SERVICES["immich"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Immich integration not configured. Please set IMMICH_API_KEY environment variable."
|
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
|
||||||
@@ -414,10 +444,12 @@ async def get_immich_albums():
|
|||||||
return {
|
return {
|
||||||
"albums": [
|
"albums": [
|
||||||
{"id": "album_1", "name": "Family Photos", "asset_count": 150},
|
{"id": "album_1", "name": "Family Photos", "asset_count": 150},
|
||||||
{"id": "album_2", "name": "Vacation 2024", "asset_count": 75}
|
{"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)
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
|
|
||||||
class ServiceStatus(BaseModel):
|
class ServiceStatus(BaseModel):
|
||||||
enabled: bool = Field(..., description="Whether the service is enabled")
|
enabled: bool = Field(..., description="Whether the service is enabled")
|
||||||
url: str = Field(..., description="Service URL")
|
url: str = Field(..., description="Service URL")
|
||||||
status: str = Field(..., description="Service status")
|
status: str = Field(..., description="Service status")
|
||||||
|
|
||||||
|
|
||||||
class HAAttributes(BaseModel):
|
class HAAttributes(BaseModel):
|
||||||
unit_of_measurement: Optional[str] = Field(None, description="Unit of measurement")
|
unit_of_measurement: Optional[str] = Field(None, description="Unit of measurement")
|
||||||
friendly_name: Optional[str] = Field(None, description="Friendly name")
|
friendly_name: Optional[str] = Field(None, description="Friendly name")
|
||||||
|
|
||||||
|
|
||||||
class HAEntity(BaseModel):
|
class HAEntity(BaseModel):
|
||||||
entity_id: str = Field(..., description="Entity ID")
|
entity_id: str = Field(..., description="Entity ID")
|
||||||
state: str = Field(..., description="Current state")
|
state: str = Field(..., description="Current state")
|
||||||
attributes: HAAttributes = Field(..., description="Entity attributes")
|
attributes: HAAttributes = Field(..., description="Entity attributes")
|
||||||
|
|
||||||
|
|
||||||
class HAEntitiesResponse(BaseModel):
|
class HAEntitiesResponse(BaseModel):
|
||||||
entities: List[HAEntity] = Field(..., description="List of Home Assistant entities")
|
entities: List[HAEntity] = Field(..., description="List of Home Assistant entities")
|
||||||
|
|
||||||
|
|
||||||
class FrigateEvent(BaseModel):
|
class FrigateEvent(BaseModel):
|
||||||
id: str = Field(..., description="Event ID")
|
id: str = Field(..., description="Event ID")
|
||||||
timestamp: str = Field(..., description="Event timestamp")
|
timestamp: str = Field(..., description="Event timestamp")
|
||||||
@@ -25,9 +31,11 @@ class FrigateEvent(BaseModel):
|
|||||||
label: str = Field(..., description="Detection label")
|
label: str = Field(..., description="Detection label")
|
||||||
confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
|
confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
|
||||||
|
|
||||||
|
|
||||||
class FrigateEventsResponse(BaseModel):
|
class FrigateEventsResponse(BaseModel):
|
||||||
events: List[FrigateEvent] = Field(..., description="List of Frigate events")
|
events: List[FrigateEvent] = Field(..., description="List of Frigate events")
|
||||||
|
|
||||||
|
|
||||||
class ImmichAsset(BaseModel):
|
class ImmichAsset(BaseModel):
|
||||||
id: str = Field(..., description="Asset ID")
|
id: str = Field(..., description="Asset ID")
|
||||||
filename: str = Field(..., description="Filename")
|
filename: str = Field(..., description="Filename")
|
||||||
@@ -35,31 +43,38 @@ class ImmichAsset(BaseModel):
|
|||||||
tags: List[str] = Field(..., description="Asset tags")
|
tags: List[str] = Field(..., description="Asset tags")
|
||||||
faces: List[str] = Field(..., description="Detected faces")
|
faces: List[str] = Field(..., description="Detected faces")
|
||||||
|
|
||||||
|
|
||||||
class ImmichAssetsResponse(BaseModel):
|
class ImmichAssetsResponse(BaseModel):
|
||||||
assets: List[ImmichAsset] = Field(..., description="List of Immich assets")
|
assets: List[ImmichAsset] = Field(..., description="List of Immich assets")
|
||||||
|
|
||||||
|
|
||||||
class EventData(BaseModel):
|
class EventData(BaseModel):
|
||||||
service: str = Field(..., description="Service name")
|
service: str = Field(..., description="Service name")
|
||||||
event_type: str = Field(..., description="Event type")
|
event_type: str = Field(..., description="Event type")
|
||||||
metadata: Dict[str, Any] = Field(default_factory=dict, description="Event metadata")
|
metadata: Dict[str, Any] = Field(default_factory=dict, description="Event metadata")
|
||||||
|
|
||||||
|
|
||||||
class EventResponse(BaseModel):
|
class EventResponse(BaseModel):
|
||||||
status: str = Field(..., description="Publication status")
|
status: str = Field(..., description="Publication status")
|
||||||
event: Dict[str, Any] = Field(..., description="Published event")
|
event: Dict[str, Any] = Field(..., description="Published event")
|
||||||
|
|
||||||
|
|
||||||
class Event(BaseModel):
|
class Event(BaseModel):
|
||||||
timestamp: str = Field(..., description="Event timestamp")
|
timestamp: str = Field(..., description="Event timestamp")
|
||||||
service: str = Field(..., description="Service name")
|
service: str = Field(..., description="Service name")
|
||||||
event_type: str = Field(..., description="Event type")
|
event_type: str = Field(..., description="Event type")
|
||||||
metadata: str = Field(..., description="Event metadata as JSON string")
|
metadata: str = Field(..., description="Event metadata as JSON string")
|
||||||
|
|
||||||
|
|
||||||
class EventsResponse(BaseModel):
|
class EventsResponse(BaseModel):
|
||||||
events: List[Event] = Field(..., description="List of events")
|
events: List[Event] = Field(..., description="List of events")
|
||||||
|
|
||||||
|
|
||||||
class HealthResponse(BaseModel):
|
class HealthResponse(BaseModel):
|
||||||
status: str = Field(..., description="Service health status")
|
status: str = Field(..., description="Service health status")
|
||||||
timestamp: str = Field(..., description="Health check timestamp")
|
timestamp: str = Field(..., description="Health check timestamp")
|
||||||
|
|
||||||
|
|
||||||
class RootResponse(BaseModel):
|
class RootResponse(BaseModel):
|
||||||
message: str = Field(..., description="API message")
|
message: str = Field(..., description="API message")
|
||||||
version: str = Field(..., description="API version")
|
version: str = Field(..., description="API version")
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
from fastapi import APIRouter, HTTPException, Query, BackgroundTasks
|
|
||||||
from models.schemas import EventData, EventResponse, EventsResponse, Event
|
|
||||||
from services.redis_client import redis_client
|
|
||||||
from datetime import datetime
|
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, BackgroundTasks, HTTPException, Query
|
||||||
|
|
||||||
|
from models.schemas import Event, EventData, EventResponse, EventsResponse
|
||||||
|
from services.redis_client import redis_client
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/publish-event",
|
|
||||||
response_model=EventResponse,
|
@router.post(
|
||||||
summary="Publish Event",
|
"/publish-event",
|
||||||
description="Publish an event to the Redis message bus",
|
response_model=EventResponse,
|
||||||
responses={
|
summary="Publish Event",
|
||||||
200: {"description": "Event published successfully"},
|
description="Publish an event to the Redis message bus",
|
||||||
500: {"description": "Failed to publish event"}
|
responses={
|
||||||
},
|
200: {"description": "Event published successfully"},
|
||||||
tags=["Events"])
|
500: {"description": "Failed to publish event"},
|
||||||
|
},
|
||||||
|
tags=["Events"],
|
||||||
|
)
|
||||||
async def publish_event(event_data: EventData, background_tasks: BackgroundTasks):
|
async def publish_event(event_data: EventData, background_tasks: BackgroundTasks):
|
||||||
"""Publish an event to the Redis message bus for consumption by other services"""
|
"""Publish an event to the Redis message bus for consumption by other services"""
|
||||||
try:
|
try:
|
||||||
@@ -22,29 +27,33 @@ async def publish_event(event_data: EventData, background_tasks: BackgroundTasks
|
|||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
"service": event_data.service,
|
"service": event_data.service,
|
||||||
"event_type": event_data.event_type,
|
"event_type": event_data.event_type,
|
||||||
"metadata": json.dumps(event_data.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 EventResponse(
|
return EventResponse(status="published", event=event)
|
||||||
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))
|
||||||
|
|
||||||
@router.get("/events",
|
|
||||||
response_model=EventsResponse,
|
@router.get(
|
||||||
summary="Get Events",
|
"/events",
|
||||||
description="Retrieve recent events from the message bus",
|
response_model=EventsResponse,
|
||||||
responses={
|
summary="Get Events",
|
||||||
200: {"description": "Successfully retrieved events"},
|
description="Retrieve recent events from the message bus",
|
||||||
500: {"description": "Failed to retrieve events"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved events"},
|
||||||
tags=["Events"])
|
500: {"description": "Failed to retrieve events"},
|
||||||
async def get_events(limit: int = Query(100, ge=1, le=1000, description="Maximum number of events to retrieve")):
|
},
|
||||||
|
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"""
|
"""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)
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
from models.schemas import FrigateEventsResponse, FrigateEvent
|
|
||||||
from services.config import SERVICES
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from models.schemas import FrigateEvent, FrigateEventsResponse
|
||||||
|
from services.config import SERVICES
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/frigate/events",
|
|
||||||
response_model=FrigateEventsResponse,
|
@router.get(
|
||||||
summary="Get Frigate Events",
|
"/frigate/events",
|
||||||
description="Retrieve detection events from Frigate NVR",
|
response_model=FrigateEventsResponse,
|
||||||
responses={
|
summary="Get Frigate Events",
|
||||||
200: {"description": "Successfully retrieved events"},
|
description="Retrieve detection events from Frigate NVR",
|
||||||
503: {"description": "Frigate integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved events"},
|
||||||
tags=["Frigate"])
|
503: {"description": "Frigate integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Frigate"],
|
||||||
|
)
|
||||||
async def get_frigate_events():
|
async def get_frigate_events():
|
||||||
"""Get Frigate detection events including person, vehicle, and object detections"""
|
"""Get Frigate detection events including person, vehicle, and object detections"""
|
||||||
if not SERVICES["frigate"]["enabled"]:
|
if not SERVICES["frigate"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Frigate integration not configured. Please set FRIGATE_TOKEN environment variable."
|
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
|
||||||
@@ -31,25 +36,28 @@ async def get_frigate_events():
|
|||||||
timestamp=datetime.now().isoformat(),
|
timestamp=datetime.now().isoformat(),
|
||||||
camera="front_door",
|
camera="front_door",
|
||||||
label="person",
|
label="person",
|
||||||
confidence=0.95
|
confidence=0.95,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/frigate/cameras",
|
|
||||||
summary="Get Frigate Cameras",
|
@router.get(
|
||||||
description="Get list of Frigate cameras",
|
"/frigate/cameras",
|
||||||
responses={
|
summary="Get Frigate Cameras",
|
||||||
200: {"description": "Successfully retrieved cameras"},
|
description="Get list of Frigate cameras",
|
||||||
503: {"description": "Frigate integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved cameras"},
|
||||||
tags=["Frigate"])
|
503: {"description": "Frigate integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Frigate"],
|
||||||
|
)
|
||||||
async def get_frigate_cameras():
|
async def get_frigate_cameras():
|
||||||
"""Get list of available Frigate cameras"""
|
"""Get list of available Frigate cameras"""
|
||||||
if not SERVICES["frigate"]["enabled"]:
|
if not SERVICES["frigate"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Frigate integration not configured. Please set FRIGATE_TOKEN environment variable."
|
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
|
||||||
@@ -58,6 +66,6 @@ async def get_frigate_cameras():
|
|||||||
"cameras": [
|
"cameras": [
|
||||||
{"name": "front_door", "enabled": True},
|
{"name": "front_door", "enabled": True},
|
||||||
{"name": "back_yard", "enabled": True},
|
{"name": "back_yard", "enabled": True},
|
||||||
{"name": "garage", "enabled": False}
|
{"name": "garage", "enabled": False},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,44 @@
|
|||||||
from fastapi import APIRouter
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from models.schemas import RootResponse, HealthResponse, ServiceStatus
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from models.schemas import HealthResponse, RootResponse, ServiceStatus
|
||||||
from services.config import SERVICES
|
from services.config import SERVICES
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/",
|
|
||||||
response_model=RootResponse,
|
@router.get(
|
||||||
summary="API Root",
|
"/",
|
||||||
description="Get basic API information",
|
response_model=RootResponse,
|
||||||
tags=["General"])
|
summary="API Root",
|
||||||
|
description="Get basic API information",
|
||||||
|
tags=["General"],
|
||||||
|
)
|
||||||
async def root():
|
async def root():
|
||||||
"""Get basic API information and version"""
|
"""Get basic API information and version"""
|
||||||
return RootResponse(
|
return RootResponse(message="LabFusion Service Adapters API", version="1.0.0")
|
||||||
message="LabFusion Service Adapters API",
|
|
||||||
version="1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/health",
|
|
||||||
response_model=HealthResponse,
|
@router.get(
|
||||||
summary="Health Check",
|
"/health",
|
||||||
description="Check service health status",
|
response_model=HealthResponse,
|
||||||
tags=["General"])
|
summary="Health Check",
|
||||||
|
description="Check service health status",
|
||||||
|
tags=["General"],
|
||||||
|
)
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Check the health status of the service adapters"""
|
"""Check the health status of the service adapters"""
|
||||||
return HealthResponse(
|
return HealthResponse(status="healthy", timestamp=datetime.now().isoformat())
|
||||||
status="healthy",
|
|
||||||
timestamp=datetime.now().isoformat()
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/services",
|
|
||||||
response_model=dict,
|
@router.get(
|
||||||
summary="Get Service Status",
|
"/services",
|
||||||
description="Get status of all configured external services",
|
response_model=dict,
|
||||||
tags=["Services"])
|
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 external services (Home Assistant, Frigate, Immich, n8n)"""
|
"""Get status of all configured external services (Home Assistant, Frigate, Immich, n8n)"""
|
||||||
service_status = {}
|
service_status = {}
|
||||||
@@ -41,6 +46,6 @@ async def get_services():
|
|||||||
service_status[service_name] = ServiceStatus(
|
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
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
from fastapi import APIRouter, HTTPException, Path
|
from fastapi import APIRouter, HTTPException, Path
|
||||||
from models.schemas import HAEntitiesResponse, HAEntity, HAAttributes
|
|
||||||
|
from models.schemas import HAAttributes, HAEntitiesResponse, HAEntity
|
||||||
from services.config import SERVICES
|
from services.config import SERVICES
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/home-assistant/entities",
|
|
||||||
response_model=HAEntitiesResponse,
|
@router.get(
|
||||||
summary="Get Home Assistant Entities",
|
"/home-assistant/entities",
|
||||||
description="Retrieve all entities from Home Assistant",
|
response_model=HAEntitiesResponse,
|
||||||
responses={
|
summary="Get Home Assistant Entities",
|
||||||
200: {"description": "Successfully retrieved entities"},
|
description="Retrieve all entities from Home Assistant",
|
||||||
503: {"description": "Home Assistant integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved entities"},
|
||||||
tags=["Home Assistant"])
|
503: {"description": "Home Assistant integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Home Assistant"],
|
||||||
|
)
|
||||||
async def get_ha_entities():
|
async def get_ha_entities():
|
||||||
"""Get Home Assistant entities including sensors, switches, and other devices"""
|
"""Get Home Assistant entities including sensors, switches, and other devices"""
|
||||||
if not SERVICES["home_assistant"]["enabled"]:
|
if not SERVICES["home_assistant"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Home Assistant integration not configured. Please set HOME_ASSISTANT_TOKEN environment variable."
|
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
|
||||||
@@ -29,37 +33,38 @@ async def get_ha_entities():
|
|||||||
entity_id="sensor.cpu_usage",
|
entity_id="sensor.cpu_usage",
|
||||||
state="45.2",
|
state="45.2",
|
||||||
attributes=HAAttributes(
|
attributes=HAAttributes(
|
||||||
unit_of_measurement="%",
|
unit_of_measurement="%", friendly_name="CPU Usage"
|
||||||
friendly_name="CPU Usage"
|
),
|
||||||
)
|
|
||||||
),
|
),
|
||||||
HAEntity(
|
HAEntity(
|
||||||
entity_id="sensor.memory_usage",
|
entity_id="sensor.memory_usage",
|
||||||
state="2.1",
|
state="2.1",
|
||||||
attributes=HAAttributes(
|
attributes=HAAttributes(
|
||||||
unit_of_measurement="GB",
|
unit_of_measurement="GB", friendly_name="Memory Usage"
|
||||||
friendly_name="Memory Usage"
|
),
|
||||||
)
|
),
|
||||||
)
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/home-assistant/entity/{entity_id}",
|
|
||||||
response_model=HAEntity,
|
@router.get(
|
||||||
summary="Get Specific HA Entity",
|
"/home-assistant/entity/{entity_id}",
|
||||||
description="Get a specific Home Assistant entity by ID",
|
response_model=HAEntity,
|
||||||
responses={
|
summary="Get Specific HA Entity",
|
||||||
200: {"description": "Successfully retrieved entity"},
|
description="Get a specific Home Assistant entity by ID",
|
||||||
404: {"description": "Entity not found"},
|
responses={
|
||||||
503: {"description": "Home Assistant integration not configured"}
|
200: {"description": "Successfully retrieved entity"},
|
||||||
},
|
404: {"description": "Entity not found"},
|
||||||
tags=["Home Assistant"])
|
503: {"description": "Home Assistant integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Home Assistant"],
|
||||||
|
)
|
||||||
async def get_ha_entity(entity_id: str = Path(..., description="Entity ID")):
|
async def get_ha_entity(entity_id: str = Path(..., description="Entity ID")):
|
||||||
"""Get a specific Home Assistant entity by its ID"""
|
"""Get a specific Home Assistant entity by its ID"""
|
||||||
if not SERVICES["home_assistant"]["enabled"]:
|
if not SERVICES["home_assistant"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Home Assistant integration not configured. Please set HOME_ASSISTANT_TOKEN environment variable."
|
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
|
||||||
@@ -68,7 +73,6 @@ async def get_ha_entity(entity_id: str = Path(..., description="Entity ID")):
|
|||||||
entity_id=entity_id,
|
entity_id=entity_id,
|
||||||
state="unknown",
|
state="unknown",
|
||||||
attributes=HAAttributes(
|
attributes=HAAttributes(
|
||||||
unit_of_measurement="",
|
unit_of_measurement="", friendly_name=f"Entity {entity_id}"
|
||||||
friendly_name=f"Entity {entity_id}"
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
from models.schemas import ImmichAssetsResponse, ImmichAsset
|
|
||||||
from services.config import SERVICES
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from models.schemas import ImmichAsset, ImmichAssetsResponse
|
||||||
|
from services.config import SERVICES
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/immich/assets",
|
|
||||||
response_model=ImmichAssetsResponse,
|
@router.get(
|
||||||
summary="Get Immich Assets",
|
"/immich/assets",
|
||||||
description="Retrieve photo assets from Immich",
|
response_model=ImmichAssetsResponse,
|
||||||
responses={
|
summary="Get Immich Assets",
|
||||||
200: {"description": "Successfully retrieved assets"},
|
description="Retrieve photo assets from Immich",
|
||||||
503: {"description": "Immich integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved assets"},
|
||||||
tags=["Immich"])
|
503: {"description": "Immich integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Immich"],
|
||||||
|
)
|
||||||
async def get_immich_assets():
|
async def get_immich_assets():
|
||||||
"""Get Immich photo assets including metadata, tags, and face detection results"""
|
"""Get Immich photo assets including metadata, tags, and face detection results"""
|
||||||
if not SERVICES["immich"]["enabled"]:
|
if not SERVICES["immich"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Immich integration not configured. Please set IMMICH_API_KEY environment variable."
|
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
|
||||||
@@ -31,25 +36,28 @@ async def get_immich_assets():
|
|||||||
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"],
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/immich/albums",
|
|
||||||
summary="Get Immich Albums",
|
@router.get(
|
||||||
description="Get list of Immich albums",
|
"/immich/albums",
|
||||||
responses={
|
summary="Get Immich Albums",
|
||||||
200: {"description": "Successfully retrieved albums"},
|
description="Get list of Immich albums",
|
||||||
503: {"description": "Immich integration not configured"}
|
responses={
|
||||||
},
|
200: {"description": "Successfully retrieved albums"},
|
||||||
tags=["Immich"])
|
503: {"description": "Immich integration not configured"},
|
||||||
|
},
|
||||||
|
tags=["Immich"],
|
||||||
|
)
|
||||||
async def get_immich_albums():
|
async def get_immich_albums():
|
||||||
"""Get list of Immich albums"""
|
"""Get list of Immich albums"""
|
||||||
if not SERVICES["immich"]["enabled"]:
|
if not SERVICES["immich"]["enabled"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="Immich integration not configured. Please set IMMICH_API_KEY environment variable."
|
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
|
||||||
@@ -57,6 +65,6 @@ async def get_immich_albums():
|
|||||||
return {
|
return {
|
||||||
"albums": [
|
"albums": [
|
||||||
{"id": "album_1", "name": "Family Photos", "asset_count": 150},
|
{"id": "album_1", "name": "Family Photos", "asset_count": 150},
|
||||||
{"id": "album_2", "name": "Vacation 2024", "asset_count": 75}
|
{"id": "album_2", "name": "Vacation 2024", "asset_count": 75},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
@@ -9,21 +10,21 @@ SERVICES = {
|
|||||||
"home_assistant": {
|
"home_assistant": {
|
||||||
"url": os.getenv("HOME_ASSISTANT_URL", "https://homeassistant.local:8123"),
|
"url": os.getenv("HOME_ASSISTANT_URL", "https://homeassistant.local:8123"),
|
||||||
"token": os.getenv("HOME_ASSISTANT_TOKEN", ""),
|
"token": os.getenv("HOME_ASSISTANT_TOKEN", ""),
|
||||||
"enabled": bool(os.getenv("HOME_ASSISTANT_TOKEN"))
|
"enabled": bool(os.getenv("HOME_ASSISTANT_TOKEN")),
|
||||||
},
|
},
|
||||||
"frigate": {
|
"frigate": {
|
||||||
"url": os.getenv("FRIGATE_URL", "http://frigate.local:5000"),
|
"url": os.getenv("FRIGATE_URL", "http://frigate.local:5000"),
|
||||||
"token": os.getenv("FRIGATE_TOKEN", ""),
|
"token": os.getenv("FRIGATE_TOKEN", ""),
|
||||||
"enabled": bool(os.getenv("FRIGATE_TOKEN"))
|
"enabled": bool(os.getenv("FRIGATE_TOKEN")),
|
||||||
},
|
},
|
||||||
"immich": {
|
"immich": {
|
||||||
"url": os.getenv("IMMICH_URL", "http://immich.local:2283"),
|
"url": os.getenv("IMMICH_URL", "http://immich.local:2283"),
|
||||||
"api_key": os.getenv("IMMICH_API_KEY", ""),
|
"api_key": os.getenv("IMMICH_API_KEY", ""),
|
||||||
"enabled": bool(os.getenv("IMMICH_API_KEY"))
|
"enabled": bool(os.getenv("IMMICH_API_KEY")),
|
||||||
},
|
},
|
||||||
"n8n": {
|
"n8n": {
|
||||||
"url": os.getenv("N8N_URL", "http://n8n.local:5678"),
|
"url": os.getenv("N8N_URL", "http://n8n.local:5678"),
|
||||||
"webhook_url": os.getenv("N8N_WEBHOOK_URL", ""),
|
"webhook_url": os.getenv("N8N_WEBHOOK_URL", ""),
|
||||||
"enabled": bool(os.getenv("N8N_WEBHOOK_URL"))
|
"enabled": bool(os.getenv("N8N_WEBHOOK_URL")),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import redis
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import redis
|
||||||
|
|
||||||
# Redis connection
|
# Redis connection
|
||||||
redis_client = redis.Redis(
|
redis_client = redis.Redis(
|
||||||
host=os.getenv("REDIS_HOST", "localhost"),
|
host=os.getenv("REDIS_HOST", "localhost"),
|
||||||
port=int(os.getenv("REDIS_PORT", 6379)),
|
port=int(os.getenv("REDIS_PORT", 6379)),
|
||||||
decode_responses=True
|
decode_responses=True,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user