feat: endpoint impl.

This commit is contained in:
2024-12-30 23:29:09 +01:00
parent d2b299c83b
commit ad3e40ee3c
14 changed files with 552 additions and 33 deletions

View File

@ -0,0 +1,5 @@
import { EventEmitter } from "events";
class DomainEventEmitter extends EventEmitter {}
export const domainEventEmitter = new DomainEventEmitter();

View File

@ -0,0 +1,105 @@
import express, { Request, Response } from "express";
import { container } from "tsyringe";
import { domainEventEmitter } from "../config/eventEmitter";
import {
TtnMessageReceivedEvent,
TtnMessageReceivedEventName,
} from "../event/ttnMessageReceivedEvent";
import { validateData } from "../middleware/validationMiddleware";
import { TtnMessage } from "../models/ttnMessage";
import { LpTtnEndDeviceUplinksService } from "../services/lpTtnEndDeviceUplinksService";
import { TtnGatewayReceptionService } from "../services/ttnGatewayReceptionService";
import { WifiScanService } from "../services/wifiScanService";
import { ttnMessageValidator } from "../validation/ttn/ttnMessageValidation";
const lpTtnEndDeviceUplinksService = container.resolve(
LpTtnEndDeviceUplinksService
);
const ttnGatewayReceptionService = container.resolve(
TtnGatewayReceptionService
);
const wifiScanService = container.resolve(WifiScanService);
const router = express.Router();
router.post(
"/webhook",
validateData(ttnMessageValidator),
async (req: Request, res: Response) => {
try {
const message = req.body as TtnMessage;
const { lp_ttn_end_device_uplinks_id } =
await lpTtnEndDeviceUplinksService.createUplink({
device_id: message.end_device_ids.device_id,
application_ids:
message.end_device_ids.application_ids.application_id,
dev_eui: message.end_device_ids.dev_eui,
join_eui: message.end_device_ids.join_eui,
dev_addr: message.end_device_ids.dev_addr,
received_at_utc: new Date(message.received_at),
battery: message.uplink_message.decoded_payload?.messages[0].find(
(e) => e.type === "Battery"
)?.measurementValue,
latitude: 0 /*
message.uplink_message.decoded_payload.messages[0].find(
(e) => e.type === "Latitude"
)?.measurementValue
*/,
longitude: 0,
/*
message.uplink_message.decoded_payload.messages[0].find(
(e) => e.type === "Longitude"
)?.measurementValue
*/
});
const wifiScans =
message.uplink_message.decoded_payload?.messages[0]
.find((e) => e.type === "Wi-Fi Scan")
?.measurementValue?.map((w) => ({
lp_ttn_end_device_uplinks_id,
mac: w.mac,
rssi: w.rssi,
})) ?? [];
const ttnGatewayReceptions = message.uplink_message.rx_metadata.map(
(g) => ({
lp_ttn_end_device_uplinks_id,
gateway_id: g.gateway_ids.gateway_id,
eui: g.gateway_ids.eui,
rssi: g.rssi,
latitude: g.location.latitude,
longitude: g.location.longitude,
altitude: g.location.altitude,
})
);
const event: TtnMessageReceivedEvent = {
lp_ttn_end_device_uplinks_id,
wifis: wifiScans.map((w) => ({ mac: w.mac, rssi: w.rssi })),
ttnGateways: ttnGatewayReceptions.map((g) => ({
rssi: g.rssi,
altitude: g.altitude,
latitude: g.latitude,
longitude: g.longitude,
})),
};
domainEventEmitter.emit(TtnMessageReceivedEventName, event);
await Promise.all([
wifiScanService.createWifiScans(wifiScans),
ttnGatewayReceptionService.createGatewayReceptions(
ttnGatewayReceptions
),
]);
res.status(200);
} catch (error) {
console.log(error);
res.status(500).json({ error: "Error creating uplink" });
}
}
);
export default router;

View File

@ -0,0 +1,14 @@
export const TtnMessageReceivedEventName = "TtnMessageReceived";
export type TtnMessageReceivedEvent = {
lp_ttn_end_device_uplinks_id: string;
wifis: {
mac: string;
rssi: number;
}[];
ttnGateways: {
rssi: number;
latitude: number;
longitude: number;
altitude: number;
}[];
};

View File

@ -0,0 +1,13 @@
import { domainEventEmitter } from "../config/eventEmitter";
import {
TtnMessageReceivedEvent,
TtnMessageReceivedEventName,
} from "../event/ttnMessageReceivedEvent";
domainEventEmitter.on(
TtnMessageReceivedEventName,
async (event: TtnMessageReceivedEvent) => {
console.log(event);
// TODO Hendrik 🚀
}
);

View File

@ -1,10 +1,12 @@
import dotenv from "dotenv";
import express from "express";
import "reflect-metadata";
import "./eventHandler/ttnMessageReceivedEventHandler";
const cors = require("cors");
import locationRoutes from "./controller/locationController";
import lpTtnEndDeviceUplinksRoutes from "./controller/lpTtnEndDeviceUplinksController";
import ttnRoutes from "./controller/ttnController";
import ttnGatewayReceptionRoutes from "./controller/ttnGatewayReceptionController";
import wifiScanRoutes from "./controller/wifiScanController";
@ -20,6 +22,7 @@ app.use("/api/lp-ttn-end-device-uplinks", lpTtnEndDeviceUplinksRoutes);
app.use("/api/wifi-scans", wifiScanRoutes);
app.use("/api/ttn-gateway-receptions", ttnGatewayReceptionRoutes);
app.use("/api/locations", locationRoutes);
app.use("/api/ttn", ttnRoutes);
app.listen(PORT, () => {
console.log(`🚀 Server läuft auf http://localhost:${PORT}`);

View File

@ -0,0 +1,25 @@
import { NextFunction, Request, Response } from "express";
import { z, ZodError } from "zod";
import { StatusCodes } from "http-status-codes";
export function validateData(schema: z.ZodObject<any, any>) {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof ZodError) {
console.log(error.errors);
res.status(StatusCodes.BAD_REQUEST).json({
error: "Invalid request payload",
details: error.errors,
});
} else {
res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ error: "Internal Server Error" });
}
}
};
}

View File

@ -0,0 +1,101 @@
export interface TtnMessage {
end_device_ids: {
device_id: string;
application_ids: {
application_id: string;
};
dev_eui: string;
join_eui: string;
dev_addr: string;
};
correlation_ids: string[];
received_at: string;
uplink_message: {
session_key_id: string;
f_port?: number;
f_cnt: number;
frm_payload?: string;
decoded_payload?: {
err: number;
messages: Array<
[
{
measurementId: "4200";
measurementValue: any[];
motionId: number;
timestamp: number;
type: "Event Status";
},
{
measurementId: "5001";
measurementValue: {
mac: string;
rssi: number;
}[];
motionId: number;
timestamp: number;
type: "Wi-Fi Scan";
},
{
measurementId: "3000";
measurementValue: number;
motionId: number;
timestamp: number;
type: "Battery";
}
]
>;
payload: string;
valid: boolean;
};
rx_metadata: {
gateway_ids: {
gateway_id: string;
eui?: string;
};
time: string;
timestamp?: number;
rssi: number;
channel_rssi: number;
snr: number;
location: {
latitude: number;
longitude: number;
altitude: number;
source?: string;
};
uplink_token: string;
channel_index?: number;
received_at: string;
}[];
settings: {
data_rate: {
lora: {
bandwidth: number;
spreading_factor: number;
coding_rate: string;
};
};
frequency: string;
timestamp?: number;
time?: Date;
};
received_at: Date;
confirmed?: boolean;
consumed_airtime: string;
version_ids: {
brand_id: string;
model_id: string;
hardware_version: string;
firmware_version: string;
band_id: string;
};
network_ids: {
net_id: string;
ns_id: string;
tenant_id: string;
cluster_id: string;
cluster_address: string;
};
};
}

View File

@ -15,6 +15,10 @@ export class TtnGatewayReceptionRepository {
return await TtnGatewayReception.create(data);
}
public async createMany(data: Partial<TtnGatewayReception>[]) {
return await TtnGatewayReception.bulkCreate(data);
}
public async update(id: string, data: Partial<TtnGatewayReception>) {
const gatewayReception = await this.findById(id);
if (gatewayReception) {

View File

@ -15,6 +15,10 @@ export class WifiScanRepository {
return await WifiScan.create(data);
}
public async createMany(data: Partial<WifiScan>[]) {
return await WifiScan.bulkCreate(data);
}
public async update(id: string, data: Partial<WifiScan>) {
const wifiScan = await this.findById(id);
if (wifiScan) {

View File

@ -21,6 +21,10 @@ export class TtnGatewayReceptionService {
return this.repository.create(data);
}
public async createGatewayReceptions(data: Partial<TtnGatewayReception>[]) {
return this.repository.createMany(data);
}
public async updateGatewayReception(
id: string,
data: Partial<TtnGatewayReception>

View File

@ -20,6 +20,10 @@ export class WifiScanService {
return this.repository.create(data);
}
public async createWifiScans(data: Partial<WifiScan>[]) {
return this.repository.createMany(data);
}
public async updateWifiScan(id: string, data: Partial<WifiScan>) {
return this.repository.update(id, data);
}

View File

@ -0,0 +1,90 @@
import { z } from "zod";
export const ttnMessageValidator = z.object({
end_device_ids: z.object({
device_id: z.string(),
application_ids: z.object({
application_id: z.string(),
}),
dev_eui: z.string(),
join_eui: z.string(),
dev_addr: z.string(),
}),
correlation_ids: z.array(z.string()),
received_at: z.string(),
uplink_message: z.object({
session_key_id: z.string(),
f_port: z.number().optional(),
f_cnt: z.number(),
frm_payload: z.string().optional(),
decoded_payload: z
.object({
err: z.number(),
messages: z.array(
z.array(
z.object({
measurementId: z.string(),
measurementValue: z.union([z.array(z.any()), z.number()]),
motionId: z.number(),
timestamp: z.number(),
type: z.string(),
})
)
),
payload: z.string(),
valid: z.boolean(),
})
.optional(),
rx_metadata: z.array(
z.object({
gateway_ids: z.object({
gateway_id: z.string(),
eui: z.string().optional(),
}),
time: z.string(),
timestamp: z.number().optional(),
rssi: z.number(),
channel_rssi: z.number(),
snr: z.number(),
location: z.object({
latitude: z.number(),
longitude: z.number(),
altitude: z.number(),
source: z.string().optional(),
}),
uplink_token: z.string(),
channel_index: z.number().optional(),
received_at: z.string(),
})
),
settings: z.object({
data_rate: z.object({
lora: z.object({
bandwidth: z.number(),
spreading_factor: z.number(),
coding_rate: z.string(),
}),
}),
frequency: z.string(),
timestamp: z.number().optional(),
time: z.string().optional(),
}),
received_at: z.string(),
confirmed: z.boolean().optional(),
consumed_airtime: z.string(),
version_ids: z.object({
brand_id: z.string(),
model_id: z.string(),
hardware_version: z.string(),
firmware_version: z.string(),
band_id: z.string(),
}),
network_ids: z.object({
net_id: z.string(),
ns_id: z.string(),
tenant_id: z.string(),
cluster_id: z.string(),
cluster_address: z.string(),
}),
}),
});