Compare commits

..

2 commits

Author SHA1 Message Date
2349e58924
api: automatically rediscover locks from CCUJack
All checks were successful
Build Container / Build Container (push) Successful in 1m35s
closes CCCHH/dooris#5
2026-05-19 15:12:55 +02:00
b1b624a7b3
api: simplify logging setup 2026-05-19 15:01:04 +02:00
4 changed files with 31 additions and 11 deletions

View file

@ -1,4 +1,6 @@
import os import os
import sys
import logging
from contextvars import ContextVar from contextvars import ContextVar
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser, Namespace
@ -72,6 +74,10 @@ def main():
) )
args = argp.parse_args() args = argp.parse_args()
# setup logging
logging.basicConfig(level=logging.DEBUG, format="[%(levelname)s] %(filename)s: %(message)s")
# setup app
app_config.set(args) app_config.set(args)
import uvicorn import uvicorn
from dooris_api.app import app from dooris_api.app import app
@ -80,6 +86,7 @@ def main():
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
app.mount("/", StaticFiles(directory=args.serve_static, html=True), name="static") app.mount("/", StaticFiles(directory=args.serve_static, html=True), name="static")
# start webserver
config = uvicorn.Config(app, port=8000, log_level="debug") config = uvicorn.Config(app, port=8000, log_level="debug")
server = uvicorn.Server(config) server = uvicorn.Server(config)
server.run() server.run()

View file

@ -1,7 +1,6 @@
from typing import Optional, List, AsyncIterable from typing import Optional, List, AsyncIterable
import logging import logging
import secrets import secrets
import sys
from datetime import datetime, UTC from datetime import datetime, UTC
from fastapi import FastAPI, Request, Response, status from fastapi import FastAPI, Request, Response, status
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
@ -22,12 +21,6 @@ logger = logging.getLogger(__name__)
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
app_cfg = app_config.get() app_cfg = app_config.get()
root_logger = logging.getLogger("")
root_logger.setLevel(logging.INFO)
root_logger.addHandler(logging.StreamHandler(sys.stderr))
app_logger = logging.getLogger("dooris_api")
app_logger.setLevel(logging.DEBUG)
app.extra["oidc_client"] = OpenidClient.from_issuer_url( app.extra["oidc_client"] = OpenidClient.from_issuer_url(
url=app_cfg.openid_issuer, url=app_cfg.openid_issuer,
authentication_redirect_uri=f"{app_cfg.base_url}/auth/login-callback", authentication_redirect_uri=f"{app_cfg.base_url}/auth/login-callback",
@ -42,8 +35,7 @@ async def lifespan(app: FastAPI):
auth=BasicAuth(app_cfg.ccujack_user, app_cfg.ccujack_password), auth=BasicAuth(app_cfg.ccujack_user, app_cfg.ccujack_password),
mqtt_conn=app_cfg.ccujack_mqtt, mqtt_conn=app_cfg.ccujack_mqtt,
) )
await app.extra["ccujack"].connect_mqtt() await app.extra["ccujack"].start()
await app.extra["ccujack"].find_locks()
yield yield

View file

@ -72,6 +72,7 @@ class CCUJackClient:
locks: LockData locks: LockData
param_values: Dict[str, Any] param_values: Dict[str, Any]
task_process_messages: asyncio.Task task_process_messages: asyncio.Task
task_find_locks: asyncio.Task
data_updated: asyncio.Event data_updated: asyncio.Event
def __init__(self, base_uri: str, auth: BasicAuth, mqtt_conn: str): def __init__(self, base_uri: str, auth: BasicAuth, mqtt_conn: str):
@ -85,10 +86,16 @@ class CCUJackClient:
self.locks = None self.locks = None
self.param_values = dict() self.param_values = dict()
self.task_process_messages = None self.task_process_messages = None
self.task_find_locks = None
self.data_updated = asyncio.Event() self.data_updated = asyncio.Event()
async def connect_mqtt(self): async def start(self):
await self.mqtt.connect() await self.mqtt.connect()
await self.find_locks()
self.task_find_locks = asyncio.get_running_loop().create_task(
self.cron(), name="ccujack-cron"
)
self.task_process_messages = asyncio.get_running_loop().create_task( self.task_process_messages = asyncio.get_running_loop().create_task(
self.process_mqt_messages(), name="process-mqtt-messages" self.process_mqt_messages(), name="process-mqtt-messages"
) )
@ -116,6 +123,7 @@ class CCUJackClient:
# save the result # save the result
new_locks = [i for i in device_infos if i[0].type == DEVICE_TYPE_LOCK] new_locks = [i for i in device_infos if i[0].type == DEVICE_TYPE_LOCK]
if new_locks != self.locks: if new_locks != self.locks:
logger.info("Found new locks, updating state")
self.locks = new_locks self.locks = new_locks
self.data_updated.set() self.data_updated.set()
self.data_updated.clear() self.data_updated.clear()
@ -148,6 +156,17 @@ class CCUJackClient:
finally: finally:
self.data_updated.clear() self.data_updated.clear()
async def cron(self):
while True:
try:
await asyncio.sleep(60 * 60) # 1 hour
logger.info("Running CCUJack cron")
await self.find_locks()
except Exception as e:
logger.exception(f"Error in CCUJack cron task: {e}")
async def query_param_value(self, address: str) -> CCUValue: async def query_param_value(self, address: str) -> CCUValue:
if address in self.param_values: if address in self.param_values:
return self.param_values[address] return self.param_values[address]

View file

@ -3,7 +3,7 @@
# https://github.com/eclipse-paho/paho.mqtt.python/blob/master/examples/loop_asyncio.py # https://github.com/eclipse-paho/paho.mqtt.python/blob/master/examples/loop_asyncio.py
# #
from typing import Any, List, Set, Iterable from typing import Any, Set, Iterable
import logging import logging
import asyncio import asyncio
import socket import socket
@ -132,6 +132,7 @@ class AsyncMqttClient:
self.fut_subscribe = asyncio.get_running_loop().create_future() self.fut_subscribe = asyncio.get_running_loop().create_future()
self.client.subscribe([(i, qos) for i in to_add]) self.client.subscribe([(i, qos) for i in to_add])
await self.fut_subscribe await self.fut_subscribe
self.active_subscriptions.update(to_add)
to_remove = self.active_subscriptions.difference(topics) to_remove = self.active_subscriptions.difference(topics)
if to_remove: if to_remove:
@ -139,6 +140,7 @@ class AsyncMqttClient:
self.fut_unsubscribe = asyncio.get_running_loop().create_future() self.fut_unsubscribe = asyncio.get_running_loop().create_future()
self.client.unsubscribe(list(to_remove)) self.client.unsubscribe(list(to_remove))
await self.fut_unsubscribe await self.fut_unsubscribe
self.active_subscriptions.difference_update(to_remove)
async def connect(self): async def connect(self):
server_host, server_port = self.connection_string.rsplit(":", maxsplit=1) server_host, server_port = self.connection_string.rsplit(":", maxsplit=1)