api: implement ccu querying

This commit is contained in:
lilly 2026-05-09 21:16:43 +02:00
commit c1a78e4dc9
Signed by: lilly
SSH key fingerprint: SHA256:y9T5GFw2A20WVklhetIxG1+kcg/Ce0shnQmbu1LQ37g
6 changed files with 628 additions and 22 deletions

View file

@ -1,16 +1,20 @@
from typing import Optional, List
from typing import Optional, List, Any
import logging
import secrets
import sys
import os
from datetime import datetime, UTC
from fastapi import FastAPI, Request, Response, HTTPException, status
from fastapi import FastAPI, Request, Response, status
from fastapi.responses import RedirectResponse
from contextlib import asynccontextmanager
from simple_openid_connect.client import OpenidClient
from simple_openid_connect.data import TokenSuccessResponse
from cachetools import TTLCache
from aiohttp import BasicAuth
import asyncio
from dooris_api import deps, models, exceptions
from dooris_api.ccujack import CCUJackClient
logger = logging.getLogger(__name__)
@ -32,6 +36,8 @@ async def lifespan(app: FastAPI):
)
app.extra["cache"] = TTLCache(maxsize=64, ttl=30 * 60)
app.extra["ccujack"] = CCUJackClient("https://hmdooris-ccu.ccchh.net:2122", auth=BasicAuth("dooris", os.environ["HMDOORIS_PW"]))
yield
@ -106,19 +112,64 @@ async def login_callback(req: Request, resp: Response, oidc_client: deps.OpenidC
logger.debug("successfully authenticated user")
return str(req.url_for("get-user-info"))
else:
logger.debu("could not authenticate user because of OIDC error; redirecting to error page with error messages intact")
logger.debug("could not authenticate user because of OIDC error; redirecting to error page with error messages intact")
return f"/auth/login-error?{req.query_params}"
@app.get("/api/doors/", tags=["doors"], responses={status.HTTP_401_UNAUTHORIZED: {"model": models.HttpProblemDetail}})
async def list_doors(cache: deps.Cache, _user: deps.CurrentUser) -> List[models.Door]:
return [
models.Door(name="door1", description="A static door for testing", status="unknown"),
models.Door(name="door2", description="another static door for testing", status="unknown"),
]
@app.get("/api/locks/", tags=["locks"], responses={status.HTTP_401_UNAUTHORIZED: {"model": models.HttpProblemDetail}})
async def list_locks(ccujack: deps.CCUJackClient) -> List[models.Lock]:
locks = await ccujack.find_locks()
# TODO: Properly associate parameters with their values
# values = await asyncio.gather(*[
# ccujack.query_param_value(f"{i_lock.address}/{i_channel.index}/{i_param.id}/~pv")
# for i_lock, lock_channels in locks
# for i_channel, channel_params in lock_channels
# for i_param in channel_params
# ])
# assemble result objects
result = []
for i_lock, lock_channels in locks:
status_data = dict()
for i_channel, channel_params in lock_channels:
for i_param in channel_params:
value = await ccujack.query_param_value(f"{i_lock.address}/{i_channel.index}/{i_param.id}")
match i_param.id:
case "LOCK_STATE":
match value.v:
case 0:
status_data["lock_state"] = "unknown"
case 1:
status_data["lock_state"] = "locked"
case 2:
status_data["lock_state"] = "unlocked"
case "ACTIVITY_STATE":
match value.v:
case 1:
status_data["activity_state"] = "unlocking"
case 2:
status_data["activity_state"] = "locking"
case 3:
status_data["activity_state"] = "stable"
case "LOCK_TARGET_LEVEL":
match value.v:
case 0:
status_data["lock_target_level"] = "locked"
case 1:
status_data["lock_target_level"] = "unlocked"
case 2:
status_data["lock_target_level"] = "open"
case "LOW_BAT":
status_data["is_low_battery"] = value.v
case "ERROR_JAMMED":
status_data["is_error_jammed"] = value.v
case "UNREACH":
status_data["is_unreachable"] = value.v
result.append(models.Lock(name=i_lock.title, status=models.LockStatus(**status_data)))
return result
@app.get("/api/doors/{name}", tags=["doors"], responses={status.HTTP_404_NOT_FOUND: {"model": models.HttpProblemDetail}, status.HTTP_401_UNAUTHORIZED: {"model": models.HttpProblemDetail}})
async def get_door(name: str, req: Request, cache: deps.Cache, _user: deps.CurrentUser) -> Optional[models.Door]:
raise exceptions.HttpProblemException(models.HttpProblemDetail.new_door_not_found(name, req.url))