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

This commit is contained in:
glenn schrooyen
2025-09-12 17:44:23 +02:00
parent 60da5ea115
commit 8d1755fd52
13 changed files with 642 additions and 336 deletions

View File

@@ -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

View File

@@ -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)

View 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"
}
]
}

View File

@@ -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)

View File

@@ -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)

View File

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

View File

@@ -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)

View File

@@ -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},
] ]
} }

View File

@@ -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

View File

@@ -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}" ),
)
) )

View File

@@ -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},
] ]
} }

View File

@@ -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")),
} },
} }

View File

@@ -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,
) )