From 46016d966d94f34e700bc6caf77e4e3f67e73da1 Mon Sep 17 00:00:00 2001 From: Kostas Drakontidis Date: Mon, 25 May 2026 05:55:43 +0300 Subject: [PATCH] ngsi-ld public api code --- .../src/api/livedata/livadata.routes.ts | 10 + .../src/api/livedata/livedata.controller.ts | 172 ++++++++++++++++++ .../src/api/livedata/livedata.repository.ts | 106 +++++++++++ 3 files changed, 288 insertions(+) create mode 100644 src/officesense_pi/src/api/livedata/livadata.routes.ts create mode 100644 src/officesense_pi/src/api/livedata/livedata.controller.ts create mode 100644 src/officesense_pi/src/api/livedata/livedata.repository.ts diff --git a/src/officesense_pi/src/api/livedata/livadata.routes.ts b/src/officesense_pi/src/api/livedata/livadata.routes.ts new file mode 100644 index 0000000..1ada5c2 --- /dev/null +++ b/src/officesense_pi/src/api/livedata/livadata.routes.ts @@ -0,0 +1,10 @@ +import express from "express"; +import { getEntities, getEntitiesOfType, getEntity } from "./livedata.controller.js"; + +const router = express.Router(); + +router.get("/entities", getEntities); +router.get("/entities", getEntitiesOfType); +router.get("/entities/:urn", getEntity); + +export default router; diff --git a/src/officesense_pi/src/api/livedata/livedata.controller.ts b/src/officesense_pi/src/api/livedata/livedata.controller.ts new file mode 100644 index 0000000..ae0a9bc --- /dev/null +++ b/src/officesense_pi/src/api/livedata/livedata.controller.ts @@ -0,0 +1,172 @@ +import type { Request, Response, NextFunction } from "express"; +import { getActiveUserData, getRedisRoomData, type RedisRoomData, type RedisUserData } from "./livedata.repository.js"; +import { prisma } from "../../lib/prisma.js"; + +interface Params { + urn: string; +} + +type Relationship = { + type: "Relationship"; + object: string; +}; + +type Property = { + type: "Property"; + value: T; +}; + +interface Entity { + id: string; + type: string; + name: { + type: "Property"; + value: string; + }; +} + +interface User extends Entity { + type: "User"; + locatedIn: Relationship; + rssi: Property; + authenticationStatus: Property; + observedAt: Property; +} + +interface Room extends Entity { + type: "Room"; + occupancy: Property; +} + +function convertToNGSIUser(user: RedisUserData): User { + return { + id: `urn:ngsi-ld:User:${user.userID}`, + type: "User", + name: { + type: "Property", + value: user.name + }, + locatedIn: { + type: "Relationship", + object: user.room + }, + rssi: { + type: "Property", + value: user.rssi, + }, + authenticationStatus: { + type: "Property", + value: user.verified ? "verified" : "unverified" + }, + observedAt: { + type: "Property", + value: (new Date(user.timestamp)).toISOString() + } + } +} + +async function convertToNGSIRoom(room: RedisRoomData): Promise { + const roomName = await prisma.room.findUnique({ + where: { id: room.roomID }, + select: { name: true } + }) + + if (!roomName?.name) return null; + + return { + id: `urn:ngsi-ld:Room:${room.roomID}`, + type: "Room", + name: { + type: "Property", + value: roomName.name + }, + occupancy: { + type: "Property", + value: room.occupancy + } + } +} + +async function getNGSIUsers(): Promise { + let users: User[] = []; + + const result: RedisUserData[] = await getActiveUserData(); + + for (const r of result) { + const user = convertToNGSIUser(r); + + users.push(user); + } + + return users; +} + +async function getNGSIRooms(): Promise { + let rooms: Room[] = []; + + const result: RedisRoomData[] = await getRedisRoomData(); + + for (const r of result) { + const room = await convertToNGSIRoom(r); + if (room == null) continue; + + rooms.push(room); + } + + return rooms; +} + +export async function getEntities(req: Request, res: Response, next: NextFunction) { + if (Object.keys(req.query).length > 0) return next(); + + let entities: Entity[] = []; + + entities.push(...await getNGSIUsers()); + entities.push(...await getNGSIRooms()); + + return res.status(200).send(entities); +} + +export async function getEntitiesOfType(req: Request, res: Response) { + const { type } = req.query; + + switch (type) { + case "user": return res.status(200).send(await getNGSIUsers()); + case "room": return res.status(200).send(await getNGSIRooms()); + default: + return res.sendStatus(400); + } +} + +export async function getEntity(req: Request, res: Response) { + const { urn } = req.params; + + if (!urn || !urn.startsWith("urn:ngsi-ld:")) return res.sendStatus(400); + + const entity = urn.slice(12); + + if (!entity) return res.sendStatus(400); + + if (entity.toLowerCase().startsWith("room:")) { + const roomID = entity.slice(5); + + if (!roomID) return res.sendStatus(400); + + const data = await getRedisRoomData(roomID); + + if (!data.length) return res.sendStatus(404); + + return res.status(200).send(await convertToNGSIRoom(data[0]!)); + } else if (entity.toLowerCase().startsWith("user:")) { + const userID = entity.slice(5); + + if (!userID) return res.sendStatus(400); + + const data = await getActiveUserData(userID); + + if (!data.length) return res.sendStatus(404); + + return res.status(200).send(convertToNGSIUser(data[0]!)); + } else + return res.sendStatus(400); +} diff --git a/src/officesense_pi/src/api/livedata/livedata.repository.ts b/src/officesense_pi/src/api/livedata/livedata.repository.ts new file mode 100644 index 0000000..fc3ebbd --- /dev/null +++ b/src/officesense_pi/src/api/livedata/livedata.repository.ts @@ -0,0 +1,106 @@ +import { prisma } from "../../lib/prisma.js"; +import { getRedis } from "../../redis/redis.js"; + +export interface RedisUserData { + userID: string; + name: string; + rssi: number; + room: string; + verified: boolean; + timestamp: number; +} + +export interface RedisRoomData { + roomID: string; + occupancy: number; +} + +export async function getActiveUserData(userID?: string): Promise { + const redis = getRedis(); + + if (!userID) { + let cursor = "0"; + let result: RedisUserData[] = []; + + do { + const res = await redis.scan(cursor, { + MATCH: "user:*", + COUNT: 100, + }); + + cursor = res.cursor; + + const values = await Promise.all( + res.keys.map((key) => redis.get(key)) + ); + + result.push(...values + .filter((v): v is string => v !== null) + .map((v) => JSON.parse(v)) + ); + + } while (cursor !== "0"); + + return result; + } + + const _userID = await redis.get(`_user:${userID}`); + + if (!_userID) return []; + + const res = await redis.get(`user:${_userID}`); + + if (!res) return []; + + const resObj: RedisUserData = JSON.parse(res); + + return [resObj]; +} + +export async function getRedisRoomData(roomID?: string): Promise { + const redis = getRedis(); + + if (!roomID) { + let cursor = "0"; + let result: RedisRoomData[] = []; + + do { + const res = await redis.scan(cursor, { + MATCH: "room:*", + COUNT: 100, + }); + + cursor = res.cursor; + + const values = await Promise.all( + res.keys.map(async (key) => ({ + roomID: key, + occupancy: await redis.get(key), + })) + ); + + result.push(...values + .filter((v) => v.occupancy !== null) + .map((v) => ({ + roomID: v.roomID.slice(5), + occupancy: Number(v.occupancy), + })) + ); + + } while (cursor !== "0"); + + return result; + } + + const res = await redis.get(`room:${roomID}`); + + if (!res) return []; + + return [{ roomID: roomID, occupancy: Number(res) }]; +} + +export async function getRoomIDs(): Promise<{ id: string; }[]> { + return await prisma.room.findMany({ + select: { id: true } + }); +} \ No newline at end of file