feat: endpoint impl.
This commit is contained in:
5
server/src/config/eventEmitter.ts
Normal file
5
server/src/config/eventEmitter.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
class DomainEventEmitter extends EventEmitter {}
|
||||
|
||||
export const domainEventEmitter = new DomainEventEmitter();
|
105
server/src/controller/ttnController.ts
Normal file
105
server/src/controller/ttnController.ts
Normal 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;
|
14
server/src/event/ttnMessageReceivedEvent.ts
Normal file
14
server/src/event/ttnMessageReceivedEvent.ts
Normal 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;
|
||||
}[];
|
||||
};
|
13
server/src/eventHandler/ttnMessageReceivedEventHandler.ts
Normal file
13
server/src/eventHandler/ttnMessageReceivedEventHandler.ts
Normal 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 🚀
|
||||
}
|
||||
);
|
@ -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}`);
|
||||
|
25
server/src/middleware/validationMiddleware.ts
Normal file
25
server/src/middleware/validationMiddleware.ts
Normal 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" });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
101
server/src/models/ttnMessage.ts
Normal file
101
server/src/models/ttnMessage.ts
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
90
server/src/validation/ttn/ttnMessageValidation.ts
Normal file
90
server/src/validation/ttn/ttnMessageValidation.ts
Normal 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(),
|
||||
}),
|
||||
}),
|
||||
});
|
Reference in New Issue
Block a user