From 60c4770280520ed3075ca8e07324876c53530d59 Mon Sep 17 00:00:00 2001
From: lilly
Date: Thu, 14 May 2026 15:47:59 +0200
Subject: [PATCH] api: implement operating locks
---
api/src/dooris_api/app.py | 36 +++++++++++++++++++++++++++++++----
api/src/dooris_api/ccujack.py | 7 ++++---
api/src/dooris_api/models.py | 14 +++++++++-----
3 files changed, 45 insertions(+), 12 deletions(-)
diff --git a/api/src/dooris_api/app.py b/api/src/dooris_api/app.py
index 3c4e794..96af9e4 100644
--- a/api/src/dooris_api/app.py
+++ b/api/src/dooris_api/app.py
@@ -240,9 +240,37 @@ async def list_locks(
@app.patch(
- "/api/locks/{name}",
+ "/api/locks/{lock_id}",
tags=["locks"],
- responses={status.HTTP_401_UNAUTHORIZED: {"model": models.HttpProblemDetail}},
+ responses={
+ status.HTTP_401_UNAUTHORIZED: {"model": models.HttpProblemDetail},
+ status.HTTP_404_NOT_FOUND: {"model": models.HttpProblemDetail},
+ },
)
-async def operate_lock(name: str):
- pass
+async def operate_lock(req: Request, lock_id: str, requested_op: models.LockOperation, ccujack: deps.CCUJackClient, _current_user: deps.CurrentUser) -> None:
+ # TODO: Validate that the user is authorized
+ # find appropriate lock from ccujack
+ for i_lock, lock_channels in ccujack.locks:
+ if i_lock.identifier == lock_id:
+ for i_channel, channel_params in lock_channels:
+ if i_channel.type == "DOOR_LOCK_STATE_TRANSMITTER":
+ for i_param in channel_params:
+ if i_param.id == "LOCK_TARGET_LEVEL":
+ addr = f"{i_lock.address}/{i_channel.index}/{i_param.id}"
+
+ # match readable request parameter to ccujack value
+ match requested_op.desired_state:
+ case "closed":
+ ccujack_value = 0
+ case "open":
+ ccujack_value = 1
+
+ # write to ccujack
+ await ccujack.set_param_value(addr, ccujack_value)
+ return
+
+
+ else:
+ raise exceptions.HttpProblemException(
+ models.HttpProblemDetail.new_lock_not_found(lock_id, req.url)
+ )
diff --git a/api/src/dooris_api/ccujack.py b/api/src/dooris_api/ccujack.py
index 7b6091c..9925aee 100644
--- a/api/src/dooris_api/ccujack.py
+++ b/api/src/dooris_api/ccujack.py
@@ -83,13 +83,14 @@ class CCUJackClient:
]
- async def query_param_value(self, address):
+ async def query_param_value(self, address: str):
logger.debug("Querying parameter value from '%s'", address)
async with self.http.get(f"/device/{address}/~pv") as resp:
return CCUValue.model_validate(await resp.json())
- # async def toggle_lock(self, lock_id, new_state):
- # pass
+ async def set_param_value(self, address: str, value: Any):
+ logger.debug("Writing parameter value '%s' to '%s'", value, address)
+ await self.http.put(f"/device/{address}/~pv", json={"v": value})
async def _inspect_ccu_device(self, device_ref: CCURef) -> Tuple[CCUDeviceInfo, List[Tuple[CCUChannelInfo, List[CCUParamInfo]]]]:
logger.debug("Inspecting device '%s' (%s)", device_ref.href, device_ref.title)
diff --git a/api/src/dooris_api/models.py b/api/src/dooris_api/models.py
index 9a18bcf..5328d4a 100644
--- a/api/src/dooris_api/models.py
+++ b/api/src/dooris_api/models.py
@@ -13,7 +13,7 @@ class HttpProblemType(Enum):
"""
UNAUTHORIZED = "type:noc@hamburg.ccc.de,2026:UNAUTHORIZED"
- DOOR_NOT_FOUND = "type:noc@hamburg.ccc.de,2026:DOOR_NOT_FOUND"
+ LOCK_NOT_FOUND = "type:noc@hamburg.ccc.de,2026:LOCK_NOT_FOUND"
class HttpProblemDetail(BaseModel):
@@ -38,12 +38,12 @@ class HttpProblemDetail(BaseModel):
)
@classmethod
- def new_door_not_found(cls, requested_door: str, request_uri: str | URL) -> Self:
+ def new_lock_not_found(cls, requested_lock: str, request_uri: str | URL) -> Self:
return cls(
- type=HttpProblemType.DOOR_NOT_FOUND,
+ type=HttpProblemType.LOCK_NOT_FOUND,
status=status.HTTP_404_NOT_FOUND,
- title="Door not found",
- detail=f"You tried to interact with door {requested_door!r} that is not known to dooris",
+ title="Lock not found",
+ detail=f"You tried to interact with lock {requested_lock!r} that is not known to dooris",
instance=str(request_uri),
)
@@ -73,3 +73,7 @@ class Lock(BaseModel):
name: str
id: str
status: LockStatus
+
+
+class LockOperation(BaseModel):
+ desired_state: Literal["open", "closed"]