implement spaceapid integration into API
All checks were successful
Build Container / Build Container (push) Successful in 4m38s
All checks were successful
Build Container / Build Container (push) Successful in 4m38s
This commit is contained in:
parent
394b423b8e
commit
ecd6c2c48f
6 changed files with 129 additions and 2 deletions
|
|
@ -79,7 +79,11 @@ def main():
|
||||||
required=False,
|
required=False,
|
||||||
nargs=1,
|
nargs=1,
|
||||||
action="append",
|
action="append",
|
||||||
default=[i for i in os.environ.get("DOORIS_STATIC_API_TOKENS", "").split(",") if bool(i)],
|
default=[
|
||||||
|
i
|
||||||
|
for i in os.environ.get("DOORIS_STATIC_API_TOKENS", "").split(",")
|
||||||
|
if bool(i)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
argp.add_argument(
|
argp.add_argument(
|
||||||
"--kc-ssh-attr-group",
|
"--kc-ssh-attr-group",
|
||||||
|
|
@ -93,6 +97,33 @@ def main():
|
||||||
type=Path,
|
type=Path,
|
||||||
help="A file to which ssh authorized keys fetched from keycloak will be written",
|
help="A file to which ssh authorized keys fetched from keycloak will be written",
|
||||||
)
|
)
|
||||||
|
argp.add_argument(
|
||||||
|
"--spaceapid-base-uri",
|
||||||
|
required=False,
|
||||||
|
default=os.environ.get(
|
||||||
|
"DOORIS_SPACEAPID_BASE_URI", "https://spaceapi.hamburg.ccc.de/"
|
||||||
|
),
|
||||||
|
help="The URL under which a running spaceapid is reachable",
|
||||||
|
)
|
||||||
|
argp.add_argument(
|
||||||
|
"--spaceapid-user",
|
||||||
|
required=False,
|
||||||
|
default=os.environ.get("DOORIS_SPACEAPID_USER", "dooris"),
|
||||||
|
help="The username used to authenticate against spaceapid",
|
||||||
|
)
|
||||||
|
argp.add_argument(
|
||||||
|
"--spaceapid-password",
|
||||||
|
required="DOORIS_SPACEAPID_PASSWORD" not in os.environ,
|
||||||
|
default=os.environ.get("DOORIS_SPACEAPID_PASSWORD", None),
|
||||||
|
help="The password used to authenticate against spaceapid",
|
||||||
|
)
|
||||||
|
argp.add_argument(
|
||||||
|
"--main-lock",
|
||||||
|
required=False,
|
||||||
|
default=os.environ.get("DOORIS_MAIN_DOOR", None),
|
||||||
|
help="Name or ID of the door which will be considered the main door for spaceapi. If none is set, integration to spaceapi is disabled",
|
||||||
|
)
|
||||||
|
|
||||||
args = argp.parse_args()
|
args = argp.parse_args()
|
||||||
|
|
||||||
# setup logging
|
# setup logging
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from aiohttp import BasicAuth
|
||||||
from dooris_api import deps, models, exceptions, app_config
|
from dooris_api import deps, models, exceptions, app_config
|
||||||
from dooris_api.ccujack import CCUJackClient
|
from dooris_api.ccujack import CCUJackClient
|
||||||
from dooris_api.keycloak import KeycloakClient
|
from dooris_api.keycloak import KeycloakClient
|
||||||
|
from dooris_api.spaceapi import SpaceApiClient
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -31,10 +32,17 @@ async def lifespan(app: FastAPI):
|
||||||
scope=app_cfg.openid_scope,
|
scope=app_cfg.openid_scope,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.extra["spaceapi"] = SpaceApiClient(
|
||||||
|
base_uri=app_cfg.spaceapid_base_uri,
|
||||||
|
auth=BasicAuth(app_cfg.spaceapid_user, app_cfg.spaceapid_password),
|
||||||
|
)
|
||||||
|
|
||||||
app.extra["ccujack"] = CCUJackClient(
|
app.extra["ccujack"] = CCUJackClient(
|
||||||
base_uri=app_cfg.ccujack_url,
|
base_uri=app_cfg.ccujack_url,
|
||||||
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,
|
||||||
|
spaceapi_client=app.extra["spaceapi"],
|
||||||
|
main_lock=app_cfg.main_lock,
|
||||||
)
|
)
|
||||||
await app.extra["ccujack"].start()
|
await app.extra["ccujack"].start()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import asyncio
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from dooris_api.mqtt_client import AsyncMqttClient
|
from dooris_api.mqtt_client import AsyncMqttClient
|
||||||
|
from dooris_api.spaceapi import SpaceApiClient
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -74,8 +75,10 @@ class CCUJackClient:
|
||||||
task_process_messages: asyncio.Task
|
task_process_messages: asyncio.Task
|
||||||
task_find_locks: asyncio.Task
|
task_find_locks: asyncio.Task
|
||||||
data_updated: asyncio.Event
|
data_updated: asyncio.Event
|
||||||
|
spaceapi_client: SpaceApiClient
|
||||||
|
main_lock: Optional[str]
|
||||||
|
|
||||||
def __init__(self, base_uri: str, auth: BasicAuth, mqtt_conn: str):
|
def __init__(self, base_uri: str, auth: BasicAuth, mqtt_conn: str, spaceapi_client: SpaceApiClient, main_lock: Optional[str]):
|
||||||
self.http = ClientSession(
|
self.http = ClientSession(
|
||||||
base_url=base_uri,
|
base_url=base_uri,
|
||||||
auth=auth,
|
auth=auth,
|
||||||
|
|
@ -88,6 +91,8 @@ class CCUJackClient:
|
||||||
self.task_process_messages = None
|
self.task_process_messages = None
|
||||||
self.task_find_locks = None
|
self.task_find_locks = None
|
||||||
self.data_updated = asyncio.Event()
|
self.data_updated = asyncio.Event()
|
||||||
|
self.spaceapi_client = spaceapi_client
|
||||||
|
self.main_lock = main_lock
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
self.task_process_messages = asyncio.get_running_loop().create_task(
|
self.task_process_messages = asyncio.get_running_loop().create_task(
|
||||||
|
|
@ -136,6 +141,7 @@ class CCUJackClient:
|
||||||
f"device/status/{i_lock.address}/{i_channel.index}/{i_param.id}"
|
f"device/status/{i_lock.address}/{i_channel.index}/{i_param.id}"
|
||||||
)
|
)
|
||||||
await self.mqtt.update_subscriptions(mqtt_topics)
|
await self.mqtt.update_subscriptions(mqtt_topics)
|
||||||
|
await self._update_spaceapi()
|
||||||
|
|
||||||
async def process_mqt_messages(self):
|
async def process_mqt_messages(self):
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -150,6 +156,9 @@ class CCUJackClient:
|
||||||
self.param_values[param_name] = param_value
|
self.param_values[param_name] = param_value
|
||||||
self.data_updated.set()
|
self.data_updated.set()
|
||||||
|
|
||||||
|
if param_name.endswith("/LOCK_STATE"):
|
||||||
|
await self._update_spaceapi()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"could not process incoming mqtt message: {e}")
|
logger.exception(f"could not process incoming mqtt message: {e}")
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -187,6 +196,46 @@ class CCUJackClient:
|
||||||
logger.debug("Writing parameter value '%s' to '%s'", value, address)
|
logger.debug("Writing parameter value '%s' to '%s'", value, address)
|
||||||
await self.http.put(f"/device/{address}/~pv", json={"v": value})
|
await self.http.put(f"/device/{address}/~pv", json={"v": value})
|
||||||
|
|
||||||
|
async def _update_spaceapi(self):
|
||||||
|
"""
|
||||||
|
Write the current lock state into spaceapid
|
||||||
|
"""
|
||||||
|
if not self.main_lock:
|
||||||
|
logger.debug("No main lock is configured, skipping spaceapid update")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Pushing current state of lock {self.main_lock} to spaceapid")
|
||||||
|
for (i_lock, lock_channels) in self.locks:
|
||||||
|
if i_lock.identifier.lower() == self.main_lock.lower() or i_lock.title.lower() == self.main_lock.lower():
|
||||||
|
is_currently_open = False
|
||||||
|
|
||||||
|
# find the correct parameter and extract it into a usable variable
|
||||||
|
for (i_channel, params) in lock_channels:
|
||||||
|
for i_param in params:
|
||||||
|
if i_param.id == "LOCK_STATE":
|
||||||
|
|
||||||
|
# do a proper lookup and not just directly access the cache because the method
|
||||||
|
# could have been triggered by something other than MQTT in which case the value
|
||||||
|
# would not necessarily be available yet
|
||||||
|
value = await self.query_param_value(
|
||||||
|
f"{i_lock.address}/{i_channel.index}/{i_param.id}"
|
||||||
|
)
|
||||||
|
match value.v:
|
||||||
|
case 0: # unknown
|
||||||
|
is_currently_open = False
|
||||||
|
case 1: # locked
|
||||||
|
is_currently_open = False
|
||||||
|
case 2: # unlocked
|
||||||
|
is_currently_open = True
|
||||||
|
|
||||||
|
# actually post the update
|
||||||
|
await self.spaceapi_client.set_status(is_currently_open)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error(f"Could not push current lock state of lock {self.main_lock} to spaceapid because no such lock is known")
|
||||||
|
|
||||||
async def _inspect_ccu_device(
|
async def _inspect_ccu_device(
|
||||||
self, device_ref: CCURef
|
self, device_ref: CCURef
|
||||||
) -> Tuple[CCUDeviceInfo, List[Tuple[CCUChannelInfo, List[CCUParamInfo]]]]:
|
) -> Tuple[CCUDeviceInfo, List[Tuple[CCUChannelInfo, List[CCUParamInfo]]]]:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from simple_openid_connect.client import OpenidClient
|
||||||
from dooris_api import app_config
|
from dooris_api import app_config
|
||||||
from dooris_api import models, exceptions
|
from dooris_api import models, exceptions
|
||||||
from dooris_api.ccujack import CCUJackClient
|
from dooris_api.ccujack import CCUJackClient
|
||||||
|
from dooris_api.spaceapi import SpaceApiClient
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -213,3 +214,12 @@ def get_ccujack(req: Request) -> CCUJackClient:
|
||||||
|
|
||||||
|
|
||||||
CCUJackClient = Annotated[CCUJackClient, Depends(get_ccujack)]
|
CCUJackClient = Annotated[CCUJackClient, Depends(get_ccujack)]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_spaceapi_client(req: Request) -> SpaceApiClient:
|
||||||
|
return req.app.extra["spaceapi"]
|
||||||
|
|
||||||
|
|
||||||
|
SpaceApiClient = Annotated[SpaceApiClient, Depends(get_spaceapi_client)]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ class LockStatus(BaseModel):
|
||||||
is_unreachable: bool
|
is_unreachable: bool
|
||||||
is_low_battery: bool
|
is_low_battery: bool
|
||||||
is_error_jammed: bool
|
is_error_jammed: bool
|
||||||
|
is_main_lock: bool
|
||||||
lock_target_level: Literal["locked", "unlocked", "open"]
|
lock_target_level: Literal["locked", "unlocked", "open"]
|
||||||
lock_state: Literal["unknown", "locked", "unlocked"]
|
lock_state: Literal["unknown", "locked", "unlocked"]
|
||||||
activity_state: Literal["unknown", "locking", "unlocking", "stable"]
|
activity_state: Literal["unknown", "locking", "unlocking", "stable"]
|
||||||
|
|
|
||||||
28
api/src/dooris_api/spaceapi.py
Normal file
28
api/src/dooris_api/spaceapi.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import logging
|
||||||
|
from aiohttp import ClientSession, BasicAuth
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SpaceApiClient:
|
||||||
|
"""
|
||||||
|
Client for interacting with spaceapid
|
||||||
|
|
||||||
|
ref: https://git.hamburg.ccc.de/CCCHH/spaceapid
|
||||||
|
"""
|
||||||
|
base_uri: str
|
||||||
|
http: ClientSession
|
||||||
|
|
||||||
|
def __init__(self, base_uri: str, auth: BasicAuth):
|
||||||
|
self.base_uri = base_uri
|
||||||
|
self.http = ClientSession(
|
||||||
|
base_url=base_uri,
|
||||||
|
auth=auth,
|
||||||
|
raise_for_status=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_status(self, is_open: bool):
|
||||||
|
logger.info(f"Setting club status to open={is_open} via spaceapid")
|
||||||
|
await self.http.put("/state/open", data=str(is_open).encode("ASCII"))
|
||||||
|
|
||||||
Loading…
Reference in a new issue