@internetderdinge/api 1.224.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +77 -0
- package/CHANGELOG.md +11 -0
- package/README.md +52 -0
- package/package.json +112 -0
- package/src/accounts/accounts.controller.ts +166 -0
- package/src/accounts/accounts.route.ts +107 -0
- package/src/accounts/accounts.schemas.ts +16 -0
- package/src/accounts/accounts.service.ts +85 -0
- package/src/accounts/accounts.validation.ts +118 -0
- package/src/accounts/auth0.service.ts +226 -0
- package/src/config/config.ts +49 -0
- package/src/config/logger.ts +33 -0
- package/src/config/morgan.ts +22 -0
- package/src/config/passport.cjs +30 -0
- package/src/config/roles.ts +13 -0
- package/src/config/tokens.cjs +10 -0
- package/src/devices/devices.controller.ts +276 -0
- package/src/devices/devices.model.ts +126 -0
- package/src/devices/devices.route.ts +198 -0
- package/src/devices/devices.schemas.ts +94 -0
- package/src/devices/devices.service.ts +320 -0
- package/src/devices/devices.validation.ts +221 -0
- package/src/devicesNotifications/devicesNotifications.controller.ts +72 -0
- package/src/devicesNotifications/devicesNotifications.model.ts +67 -0
- package/src/devicesNotifications/devicesNotifications.route.ts +150 -0
- package/src/devicesNotifications/devicesNotifications.schemas.ts +11 -0
- package/src/devicesNotifications/devicesNotifications.service.ts +222 -0
- package/src/devicesNotifications/devicesNotifications.validation.ts +56 -0
- package/src/email/email.service.ts +609 -0
- package/src/files/upload.service.ts +145 -0
- package/src/i18n/i18n.ts +51 -0
- package/src/i18n/saveMissingLocalJsonBackend.ts +92 -0
- package/src/index.ts +7 -0
- package/src/iotdevice/iotdevice.controller.ts +136 -0
- package/src/iotdevice/iotdevice.model.ts +32 -0
- package/src/iotdevice/iotdevice.route.ts +181 -0
- package/src/iotdevice/iotdevice.schemas.ts +79 -0
- package/src/iotdevice/iotdevice.service.ts +732 -0
- package/src/iotdevice/iotdevice.validation.ts +61 -0
- package/src/middlewares/auth.ts +110 -0
- package/src/middlewares/checkJwt.cjs +19 -0
- package/src/middlewares/error.js.legacy +44 -0
- package/src/middlewares/error.ts +41 -0
- package/src/middlewares/mongooseValidations/ensureSameOrganization.ts +15 -0
- package/src/middlewares/rateLimiter.ts +10 -0
- package/src/middlewares/validate.ts +25 -0
- package/src/middlewares/validateAction.ts +41 -0
- package/src/middlewares/validateAdmin.ts +21 -0
- package/src/middlewares/validateAi.ts +24 -0
- package/src/middlewares/validateCurrentAuthUser.ts +23 -0
- package/src/middlewares/validateCurrentUser.ts +35 -0
- package/src/middlewares/validateDevice.ts +191 -0
- package/src/middlewares/validateDeviceUserOrganization.ts +54 -0
- package/src/middlewares/validateOrganization.ts +109 -0
- package/src/middlewares/validateQuerySearchUserAndOrganization.ts +75 -0
- package/src/middlewares/validateTokens.ts +36 -0
- package/src/middlewares/validateUser.ts +75 -0
- package/src/middlewares/validateZod.ts +54 -0
- package/src/models/plugins/index.ts +7 -0
- package/src/models/plugins/paginate.plugin.ts +145 -0
- package/src/models/plugins/paginateNew.plugin.ts +206 -0
- package/src/models/plugins/simplePopulate.ts +12 -0
- package/src/models/plugins/toJSON.plugin.ts +51 -0
- package/src/organizations/organizations.controller.ts +101 -0
- package/src/organizations/organizations.model.ts +62 -0
- package/src/organizations/organizations.route.ts +119 -0
- package/src/organizations/organizations.schemas.ts +8 -0
- package/src/organizations/organizations.service.ts +85 -0
- package/src/organizations/organizations.validation.ts +76 -0
- package/src/pdf/pdf.controller.ts +18 -0
- package/src/pdf/pdf.route.ts +28 -0
- package/src/pdf/pdf.schemas.ts +7 -0
- package/src/pdf/pdf.service.ts +89 -0
- package/src/pdf/pdf.validation.ts +30 -0
- package/src/tokens/tokens.controller.ts +81 -0
- package/src/tokens/tokens.model.ts +24 -0
- package/src/tokens/tokens.route.ts +66 -0
- package/src/tokens/tokens.schemas.ts +15 -0
- package/src/tokens/tokens.service.ts +46 -0
- package/src/tokens/tokens.validation.ts +13 -0
- package/src/types/routeSpec.ts +1 -0
- package/src/users/users.controller.ts +234 -0
- package/src/users/users.model.ts +89 -0
- package/src/users/users.route.ts +171 -0
- package/src/users/users.schemas.ts +79 -0
- package/src/users/users.service.ts +393 -0
- package/src/users/users.validation.ts +166 -0
- package/src/utils/ApiError.ts +18 -0
- package/src/utils/buildRouterAndDocs.ts +85 -0
- package/src/utils/catchAsync.ts +9 -0
- package/src/utils/comparePapers.service.ts +48 -0
- package/src/utils/filterOptions.ts +37 -0
- package/src/utils/medicationName.ts +12 -0
- package/src/utils/pick.ts +16 -0
- package/src/utils/registerOpenApi.ts +32 -0
- package/src/utils/urlUtils.ts +14 -0
- package/src/utils/userName.ts +27 -0
- package/src/utils/zValidations.ts +89 -0
- package/src/validations/auth.validation.cjs +60 -0
- package/src/validations/custom.validation.ts +26 -0
- package/src/validations/index.cjs +2 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import httpStatus from "http-status";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import AWS from "aws-sdk";
|
|
4
|
+
import { deviceKindHasFeature } from "@wirewire/helpers";
|
|
5
|
+
import ApiError from "../utils/ApiError";
|
|
6
|
+
import { getAuth0Token } from "../accounts/auth0.service";
|
|
7
|
+
import {
|
|
8
|
+
uploadFile,
|
|
9
|
+
uploadImage,
|
|
10
|
+
getSignedFileUrl,
|
|
11
|
+
} from "../files/upload.service";
|
|
12
|
+
import { compareImages } from "../utils/comparePapers.service";
|
|
13
|
+
import IotDevice from "./iotdevice.model";
|
|
14
|
+
import fileType from "file-type";
|
|
15
|
+
|
|
16
|
+
import type { AxiosRequestConfig } from "axios";
|
|
17
|
+
import type { Device } from "../devices.model";
|
|
18
|
+
import type { AxiosResponse } from "axios";
|
|
19
|
+
|
|
20
|
+
import iotsdk from "aws-iot-device-sdk-v2";
|
|
21
|
+
const iot = iotsdk.iot;
|
|
22
|
+
const mqtt = iotsdk.mqtt;
|
|
23
|
+
|
|
24
|
+
export const SIMILARITY_THRESHOLD = Number(
|
|
25
|
+
process.env.EPAPER_SIMILARITY_THRESHOLD ?? 99.995,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
type UploadSingleImageParams = {
|
|
29
|
+
deviceName: string;
|
|
30
|
+
buffer: Buffer;
|
|
31
|
+
bufferOriginal?: Buffer;
|
|
32
|
+
bufferEditable?: Buffer;
|
|
33
|
+
id: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const buildUploadResponse = (
|
|
37
|
+
data: any,
|
|
38
|
+
similarityPercentage: number | null,
|
|
39
|
+
skippedUpload: boolean,
|
|
40
|
+
) => {
|
|
41
|
+
return { /*...data,*/ key: data?.Key, similarityPercentage, skippedUpload };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const downloadPreviousOriginalImage = async (
|
|
45
|
+
id: string,
|
|
46
|
+
): Promise<Buffer | null> => {
|
|
47
|
+
if (!id) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const signedUrl = await getSignedFileUrl({
|
|
53
|
+
fileName: `ePaperImages/${id}original.png`,
|
|
54
|
+
});
|
|
55
|
+
const response = await axios.get<ArrayBuffer>(signedUrl, {
|
|
56
|
+
responseType: "arraybuffer",
|
|
57
|
+
});
|
|
58
|
+
return Buffer.from(response.data);
|
|
59
|
+
} catch (error: any) {
|
|
60
|
+
console.warn(
|
|
61
|
+
`Unable to download previous image for ${id}:`,
|
|
62
|
+
error?.message || error,
|
|
63
|
+
);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const evaluateSimilarityBeforeUpload = async (
|
|
69
|
+
id: string,
|
|
70
|
+
bufferOriginal?: Buffer,
|
|
71
|
+
): Promise<{ similarityPercentage: number | null; skipUpload: boolean }> => {
|
|
72
|
+
if (!bufferOriginal) {
|
|
73
|
+
return { similarityPercentage: null, skipUpload: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const previousBuffer = await downloadPreviousOriginalImage(id);
|
|
77
|
+
if (!previousBuffer) {
|
|
78
|
+
return { similarityPercentage: null, skipUpload: false };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const similarityPercentage = await compareImages(
|
|
83
|
+
previousBuffer,
|
|
84
|
+
bufferOriginal,
|
|
85
|
+
);
|
|
86
|
+
return {
|
|
87
|
+
similarityPercentage,
|
|
88
|
+
skipUpload: similarityPercentage >= SIMILARITY_THRESHOLD,
|
|
89
|
+
};
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
console.warn(
|
|
92
|
+
`Similarity comparison failed for ${id}:`,
|
|
93
|
+
error?.message || error,
|
|
94
|
+
);
|
|
95
|
+
return { similarityPercentage: null, skipUpload: false };
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get events for a device
|
|
101
|
+
* @param {Object} params
|
|
102
|
+
* @returns {Promise<any>}
|
|
103
|
+
*/
|
|
104
|
+
export const getEvents = async (params: {
|
|
105
|
+
DeviceId: string;
|
|
106
|
+
DateStart: string;
|
|
107
|
+
DateEnd: string;
|
|
108
|
+
TypeFilter?: string;
|
|
109
|
+
createdAt?: string;
|
|
110
|
+
}): Promise<any> => {
|
|
111
|
+
const accessToken = await getAuth0Token();
|
|
112
|
+
try {
|
|
113
|
+
const response: AxiosResponse = await axios.get(
|
|
114
|
+
`${process.env.IOT_API_URL}getevent`,
|
|
115
|
+
{
|
|
116
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
117
|
+
params: {
|
|
118
|
+
DeviceId: params.DeviceId,
|
|
119
|
+
DateStart: !params.createdAt
|
|
120
|
+
? params.DateStart
|
|
121
|
+
: params.DateStart < params.createdAt
|
|
122
|
+
? params.DateStart
|
|
123
|
+
: params.createdAt,
|
|
124
|
+
DateEnd: params.DateEnd,
|
|
125
|
+
TypeFilter: params.TypeFilter,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
return response.data;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Activate a device
|
|
137
|
+
* @param {string} deviceId
|
|
138
|
+
* @param {string} organization
|
|
139
|
+
* @param {boolean} enable
|
|
140
|
+
* @param {boolean} resetDevice
|
|
141
|
+
* @returns {Promise<any>}
|
|
142
|
+
*/
|
|
143
|
+
export const activateDevice = async (
|
|
144
|
+
deviceId: string,
|
|
145
|
+
organization: string,
|
|
146
|
+
enable = true,
|
|
147
|
+
resetDevice = false,
|
|
148
|
+
): Promise<any> => {
|
|
149
|
+
const accessToken = await getAuth0Token();
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const data = {
|
|
153
|
+
deviceName: deviceId,
|
|
154
|
+
organizationName: organization,
|
|
155
|
+
reset: resetDevice,
|
|
156
|
+
enable,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const config: AxiosRequestConfig = {
|
|
160
|
+
method: "post",
|
|
161
|
+
url: `${deviceId.slice(0, 3) === "epd" ? process.env.IOT_API_URL_EPAPER : process.env.IOT_API_URL}activatedevice`,
|
|
162
|
+
headers: {
|
|
163
|
+
Authorization: `Bearer ${accessToken}`,
|
|
164
|
+
},
|
|
165
|
+
data,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const response: AxiosResponse = await axios(config);
|
|
169
|
+
return response.data;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(error);
|
|
172
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get activated");
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Ping device
|
|
178
|
+
* @param {string} deviceId
|
|
179
|
+
* @returns {Promise<Device>}
|
|
180
|
+
*/
|
|
181
|
+
export const pingDevice = async (deviceId, body) => {
|
|
182
|
+
const accessToken = await getAuth0Token();
|
|
183
|
+
try {
|
|
184
|
+
const config = {
|
|
185
|
+
method: "get",
|
|
186
|
+
url: `${process.env.IOT_API_URL}ping`,
|
|
187
|
+
params: {
|
|
188
|
+
DeviceId: deviceId,
|
|
189
|
+
...body,
|
|
190
|
+
},
|
|
191
|
+
headers: {
|
|
192
|
+
Authorization: `Bearer ${accessToken}`,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const response = await axios(config);
|
|
197
|
+
return response.data;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
//console.log(error);
|
|
200
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get pinged");
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reboot device
|
|
206
|
+
* @param {String} deviveId
|
|
207
|
+
* @returns {Promise<Device>}
|
|
208
|
+
*/
|
|
209
|
+
export const rebootDevice = async (deviceId) => {
|
|
210
|
+
const accessToken = await getAuth0Token();
|
|
211
|
+
try {
|
|
212
|
+
const config = {
|
|
213
|
+
method: "get",
|
|
214
|
+
url: `${process.env.IOT_API_URL}rebootdevice?DeviceId=${deviceId}`,
|
|
215
|
+
headers: {
|
|
216
|
+
Authorization: `Bearer ${accessToken}`,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const response = await axios(config);
|
|
221
|
+
return response.data;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t reboot");
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Update device meta
|
|
229
|
+
* @param {String} deviveId
|
|
230
|
+
* @returns {Promise<Device>}
|
|
231
|
+
*/
|
|
232
|
+
export const updateDevice = async (deviceId, meta) => {
|
|
233
|
+
const accessToken = await getAuth0Token();
|
|
234
|
+
try {
|
|
235
|
+
const config = {
|
|
236
|
+
method: "post",
|
|
237
|
+
url: `${process.env.IOT_API_URL}updatedevice`,
|
|
238
|
+
headers: {
|
|
239
|
+
Authorization: `Bearer ${accessToken}`,
|
|
240
|
+
},
|
|
241
|
+
data: {
|
|
242
|
+
deviceName: deviceId,
|
|
243
|
+
...meta,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const response = await axios(config);
|
|
248
|
+
return response.data;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.log(error);
|
|
251
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t update meta");
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Case status of device
|
|
257
|
+
* @param {String} deviveId
|
|
258
|
+
* @returns {Promise<Device>}
|
|
259
|
+
*/
|
|
260
|
+
const caseStatus = async (deviceId) => {
|
|
261
|
+
const accessToken = await getAuth0Token();
|
|
262
|
+
try {
|
|
263
|
+
const config = {
|
|
264
|
+
method: "get",
|
|
265
|
+
url: `${process.env.IOT_API_URL}getcase?DeviceId=${deviceId}`,
|
|
266
|
+
headers: {
|
|
267
|
+
Authorization: `Bearer ${accessToken}`,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const response = await axios(config);
|
|
272
|
+
return response.data;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
//console.log(error);
|
|
275
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get status");
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Case status of device
|
|
281
|
+
* @param {String} deviveId
|
|
282
|
+
* @returns {Promise<Device>}
|
|
283
|
+
*/
|
|
284
|
+
export const alarmsDevice = async (deviceId) => {
|
|
285
|
+
const accessToken = await getAuth0Token();
|
|
286
|
+
try {
|
|
287
|
+
const config = {
|
|
288
|
+
method: "get",
|
|
289
|
+
url: `${process.env.IOT_API_URL}getcase?DeviceId=${deviceId}`,
|
|
290
|
+
headers: {
|
|
291
|
+
Authorization: `Bearer ${accessToken}`,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const response = await axios(config);
|
|
296
|
+
return response.data;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
// console.log(error);
|
|
299
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get status");
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get device information
|
|
305
|
+
* @param {Object} userBody
|
|
306
|
+
* @returns {Promise<Device>}
|
|
307
|
+
*/
|
|
308
|
+
export const getDevice = async (deviceName) => {
|
|
309
|
+
const accessToken = await getAuth0Token();
|
|
310
|
+
try {
|
|
311
|
+
const response = await axios.post(
|
|
312
|
+
`${process.env.IOT_API_URL}getdevicelist`,
|
|
313
|
+
{ deviceName },
|
|
314
|
+
{
|
|
315
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
return response.data;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error(error.message);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get device information
|
|
328
|
+
* shows battery level, signal strength, and other information
|
|
329
|
+
* @param {Object} userBody
|
|
330
|
+
* @returns {Promise<Device>}
|
|
331
|
+
*/
|
|
332
|
+
export const getDeviceStatusList = async (deviceName) => {
|
|
333
|
+
const accessToken = await getAuth0Token();
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const response = await axios.post(
|
|
337
|
+
`${process.env.IOT_API_URL}getdevicestatuslist`,
|
|
338
|
+
{
|
|
339
|
+
returnAll: true,
|
|
340
|
+
DeviceId: [],
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
// console.log('response', response.data);
|
|
347
|
+
return response.data.message;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error(error);
|
|
350
|
+
console.error(`getDeviceStatusList device error: ${deviceName}`);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get device information
|
|
357
|
+
* @param {Object} userBody
|
|
358
|
+
* @returns {Promise<Device>}
|
|
359
|
+
*/
|
|
360
|
+
export const getDeviceStatus = async (deviceName, kind) => {
|
|
361
|
+
const accessToken = await getAuth0Token();
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const response = await axios.get(
|
|
365
|
+
`${deviceKindHasFeature("epaper", kind) ? process.env.IOT_API_URL_EPAPER : process.env.IOT_API_URL}getdevicestatus`,
|
|
366
|
+
{
|
|
367
|
+
params: { DeviceId: deviceName },
|
|
368
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// console.log('getDeviceStatus response', response.data);
|
|
373
|
+
// TODO: Align return values
|
|
374
|
+
return deviceKindHasFeature("epaper", kind)
|
|
375
|
+
? response.data
|
|
376
|
+
: response.data.message;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error(
|
|
379
|
+
`getDeviceStatus device error: ${deviceName}`,
|
|
380
|
+
error.message,
|
|
381
|
+
error.statusMessage,
|
|
382
|
+
);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Shadow alarm update
|
|
389
|
+
* @param {Object} userBody
|
|
390
|
+
* @returns {Promise<Device>}
|
|
391
|
+
*/
|
|
392
|
+
export const shadowAlarmGet = async (deviceName, shadowName) => {
|
|
393
|
+
const iotdata = new AWS.IotData({
|
|
394
|
+
endpoint: "a2vm6rc8xrtk10-ats.iot.eu-central-1.amazonaws.com",
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (!deviceName) return { error: "deviceName is required" };
|
|
398
|
+
const params = {
|
|
399
|
+
thingName: deviceName,
|
|
400
|
+
shadowName,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const response = await iotdata.getThingShadow(params).promise();
|
|
405
|
+
|
|
406
|
+
return JSON.parse(response.payload);
|
|
407
|
+
} catch (e) {
|
|
408
|
+
// console.log(deviceName, e);
|
|
409
|
+
return "error";
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* LED update with Timeout
|
|
415
|
+
* @param {Object} userBody
|
|
416
|
+
* @returns {Promise<Device>}
|
|
417
|
+
*/
|
|
418
|
+
export const ledLightHint = async (deviceName, body) => {
|
|
419
|
+
const accessToken = await getAuth0Token();
|
|
420
|
+
try {
|
|
421
|
+
const response = await axios.post(
|
|
422
|
+
`${process.env.IOT_API_URL}setled?DeviceId=${deviceName}`,
|
|
423
|
+
body,
|
|
424
|
+
{
|
|
425
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
return response.data;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(error);
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Shadow alarm update
|
|
437
|
+
* @param {Object} userBody
|
|
438
|
+
* @returns {Promise<Device>}
|
|
439
|
+
*/
|
|
440
|
+
const shadowAlarmUpdate = async (deviceName, alarms, shadowName) => {
|
|
441
|
+
const data = alarms;
|
|
442
|
+
|
|
443
|
+
const iotdata = new AWS.IotData({
|
|
444
|
+
endpoint: "a2vm6rc8xrtk10-ats.iot.eu-central-1.amazonaws.com",
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (!deviceName) {
|
|
448
|
+
return { error: "no deviceId" };
|
|
449
|
+
}
|
|
450
|
+
const params = {
|
|
451
|
+
payload: JSON.stringify(data),
|
|
452
|
+
thingName: deviceName,
|
|
453
|
+
shadowName,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const response = await iotdata.updateThingShadow(params).promise();
|
|
458
|
+
return JSON.parse(response.payload);
|
|
459
|
+
} catch (e) {
|
|
460
|
+
return "error";
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
export const uploadSingleImage = async ({
|
|
465
|
+
deviceName,
|
|
466
|
+
buffer,
|
|
467
|
+
bufferOriginal,
|
|
468
|
+
bufferEditable,
|
|
469
|
+
id,
|
|
470
|
+
paperId,
|
|
471
|
+
}: UploadSingleImageParams) => {
|
|
472
|
+
try {
|
|
473
|
+
const { skipUpload, similarityPercentage } =
|
|
474
|
+
await evaluateSimilarityBeforeUpload(id, bufferOriginal);
|
|
475
|
+
var response: any = {};
|
|
476
|
+
|
|
477
|
+
if (skipUpload) {
|
|
478
|
+
return buildUploadResponse(
|
|
479
|
+
{ message: "Image skipped due to similarity threshold" },
|
|
480
|
+
similarityPercentage,
|
|
481
|
+
true,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!bufferOriginal) {
|
|
486
|
+
throw new Error("bufferOriginal is required to upload an image");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// console.log(`Uploading image for ${id} on ${deviceName}; similarity ${similarityPercentage?.toFixed(5)}%`);
|
|
490
|
+
if (deviceName) {
|
|
491
|
+
const accessToken = await getAuth0Token();
|
|
492
|
+
|
|
493
|
+
response = await axios.post(
|
|
494
|
+
`${process.env.IOT_API_URL_EPAPER}uploads`,
|
|
495
|
+
{ deviceName },
|
|
496
|
+
{
|
|
497
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
498
|
+
},
|
|
499
|
+
);
|
|
500
|
+
if (!response.data.uploadURL) {
|
|
501
|
+
console.log("No upload URL received", response.data);
|
|
502
|
+
} else {
|
|
503
|
+
await axios.put(response.data.uploadURL, buffer, {
|
|
504
|
+
// onUploadProgress: (progressEvent) => console.log('file progress', progressEvent.loaded),
|
|
505
|
+
headers: { "Content-Type": "text/octet-stream" },
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const type = await fileType(buffer);
|
|
511
|
+
const fileName = `ePaperImages/${id}`;
|
|
512
|
+
|
|
513
|
+
await uploadImage({ blob: buffer, key: fileName + ".png", type });
|
|
514
|
+
|
|
515
|
+
await uploadImage({
|
|
516
|
+
blob: bufferOriginal,
|
|
517
|
+
key: fileName + "original.png",
|
|
518
|
+
type,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (bufferEditable) {
|
|
522
|
+
await uploadImage({
|
|
523
|
+
blob: bufferEditable,
|
|
524
|
+
key: fileName + "editable.json",
|
|
525
|
+
type,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return buildUploadResponse(response.data, similarityPercentage, false);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.error(error);
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get device by ID
|
|
538
|
+
* @param {string} id
|
|
539
|
+
* @returns {Promise<Device | null>}
|
|
540
|
+
*/
|
|
541
|
+
export const getById = async (id: string): Promise<Device | null> => {
|
|
542
|
+
return IotDevice.findById(id);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Update device by ID
|
|
547
|
+
* @param {string} userId
|
|
548
|
+
* @param {Partial<Device>} updateBody
|
|
549
|
+
* @returns {Promise<Device>}
|
|
550
|
+
*/
|
|
551
|
+
export const updateById = async (
|
|
552
|
+
userId: string,
|
|
553
|
+
updateBody: Partial<Device>,
|
|
554
|
+
): Promise<Device> => {
|
|
555
|
+
const iotDevice = await getById(userId);
|
|
556
|
+
if (!iotDevice) {
|
|
557
|
+
throw new ApiError(httpStatus.NOT_FOUND, "IoT Device not found");
|
|
558
|
+
}
|
|
559
|
+
Object.assign(iotDevice, updateBody);
|
|
560
|
+
await iotDevice.save();
|
|
561
|
+
return iotDevice;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Delete device by ID
|
|
566
|
+
* @param {string} userId
|
|
567
|
+
* @returns {Promise<Device>}
|
|
568
|
+
*/
|
|
569
|
+
export const deleteById = async (userId: string): Promise<Device> => {
|
|
570
|
+
const iotDevice = await getById(userId);
|
|
571
|
+
if (!iotDevice) {
|
|
572
|
+
throw new ApiError(httpStatus.NOT_FOUND, "IoT Device not found");
|
|
573
|
+
}
|
|
574
|
+
await iotDevice.deleteOne();
|
|
575
|
+
return iotDevice;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
export const getApiStatus = async (kind: string): Promise<any> => {
|
|
579
|
+
const accessToken = await getAuth0Token();
|
|
580
|
+
try {
|
|
581
|
+
const response: AxiosResponse = await axios.get(
|
|
582
|
+
`${process.env.IOT_API_URL}status/${kind}`,
|
|
583
|
+
{
|
|
584
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
585
|
+
},
|
|
586
|
+
);
|
|
587
|
+
return response.data;
|
|
588
|
+
} catch (error) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
export const resetDevice = async (deviceId: string): Promise<any> => {
|
|
594
|
+
const accessToken = await getAuth0Token();
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
const response: AxiosResponse = await axios.get(
|
|
598
|
+
`${process.env.IOT_API_URL}resetdevice`,
|
|
599
|
+
{
|
|
600
|
+
params: {
|
|
601
|
+
DeviceId: deviceId,
|
|
602
|
+
},
|
|
603
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
return response.data;
|
|
607
|
+
} catch (error) {
|
|
608
|
+
//console.error(error);
|
|
609
|
+
throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get reset");
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
export async function subscribeToLiveEvents(deviceName: string) {
|
|
614
|
+
const clientId = `web-client-${deviceName}-${Date.now()}`;
|
|
615
|
+
|
|
616
|
+
const client = new mqtt.MqttClient();
|
|
617
|
+
const config =
|
|
618
|
+
iot.AwsIotMqttConnectionConfigBuilder.new_builder_for_websocket()
|
|
619
|
+
.with_clean_session(true)
|
|
620
|
+
.with_client_id(clientId)
|
|
621
|
+
.with_endpoint("a2vm6rc8xrtk10-ats.iot.eu-central-1.amazonaws.com")
|
|
622
|
+
.with_credentials(
|
|
623
|
+
process.env.AWS_REGION,
|
|
624
|
+
process.env.AWS_ACCESS_KEY_ID,
|
|
625
|
+
process.env.AWS_SECRET_ACCESS_KEY,
|
|
626
|
+
// currentCredentials.sessionToken,
|
|
627
|
+
)
|
|
628
|
+
.with_keep_alive_seconds(60)
|
|
629
|
+
.build();
|
|
630
|
+
|
|
631
|
+
const mqttClient = client.new_connection(config);
|
|
632
|
+
|
|
633
|
+
console.log(`Subscribed to live events for device: ${deviceName}`);
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
// 1) actually start the connection
|
|
637
|
+
await mqttClient.connect();
|
|
638
|
+
console.log(`✓ MQTT connected for device: ${deviceName}`);
|
|
639
|
+
|
|
640
|
+
// 2) subscribe to the correct topic
|
|
641
|
+
|
|
642
|
+
const topic = `${deviceName}/send`;
|
|
643
|
+
// subscribe to topic; rely on global 'message' handler to forward payloads
|
|
644
|
+
await mqttClient.subscribe(topic, mqtt.QoS.AtLeastOnce);
|
|
645
|
+
console.log(
|
|
646
|
+
`✓ Subscribed to live events for device: ${deviceName} on topic: ${topic}`,
|
|
647
|
+
);
|
|
648
|
+
} catch (err: any) {
|
|
649
|
+
console.error("⚠️ MQTT Unexpected error:", err.message || err);
|
|
650
|
+
throw err;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
mqttClient.on("connect", () => {
|
|
654
|
+
mqttClient.subscribe(`nrf-351358815278525/send`);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
mqttClient.on("message", (topic, payload) => {
|
|
658
|
+
const msg = payload.toString();
|
|
659
|
+
console.log(`🛰 [${topic}]`, msg);
|
|
660
|
+
// emit to your socket server, save to db, whatever…
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
mqttClient.on("error", (err) => {
|
|
664
|
+
console.error("MQTT Error", err);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
return mqttClient;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// NEW:
|
|
671
|
+
export const liveEventsWs = async (
|
|
672
|
+
ws: WebSocket,
|
|
673
|
+
req: Request & { params: { deviceId: string } },
|
|
674
|
+
) => {
|
|
675
|
+
console.log("WebSocket connection established for live events");
|
|
676
|
+
// lookup the real Thing name
|
|
677
|
+
/* const device = await devicesService.getById(req.params.deviceId);
|
|
678
|
+
if (!device) {
|
|
679
|
+
ws.send(JSON.stringify({ error: 'device not found' }));
|
|
680
|
+
return ws.close();
|
|
681
|
+
}*/
|
|
682
|
+
|
|
683
|
+
// subscribe via MQTT
|
|
684
|
+
const client = await subscribeToLiveEvents(req.params.deviceId);
|
|
685
|
+
|
|
686
|
+
client.on("message", (_topic, payload) => {
|
|
687
|
+
// normalize various binary payload types to string
|
|
688
|
+
let msg: string;
|
|
689
|
+
if (payload instanceof ArrayBuffer) {
|
|
690
|
+
msg = Buffer.from(payload).toString("utf8");
|
|
691
|
+
} else if (ArrayBuffer.isView(payload)) {
|
|
692
|
+
// Uint8Array, DataView, etc.
|
|
693
|
+
msg = Buffer.from(payload as Uint8Array).toString("utf8");
|
|
694
|
+
} else {
|
|
695
|
+
msg = payload.toString();
|
|
696
|
+
}
|
|
697
|
+
console.log("🛰 Forwarding MQTT → WS:", msg);
|
|
698
|
+
ws.send(msg);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// wait 4 1 second before sending a message to the client
|
|
702
|
+
console.log("Waiting 1 second before sending initial message to client...");
|
|
703
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
704
|
+
// send initial connection established message and then every 10 seconds
|
|
705
|
+
ws.send(JSON.stringify({ message: "WebSocket connection established" }));
|
|
706
|
+
const intervalId = setInterval(() => {
|
|
707
|
+
ws.send(JSON.stringify({ message: "send message" }));
|
|
708
|
+
}, 10000);
|
|
709
|
+
|
|
710
|
+
ws.on("close", () => {
|
|
711
|
+
clearInterval(intervalId);
|
|
712
|
+
client.disconnect();
|
|
713
|
+
});
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
export default {
|
|
717
|
+
activateDevice,
|
|
718
|
+
getEvents,
|
|
719
|
+
getById,
|
|
720
|
+
updateById,
|
|
721
|
+
deleteById,
|
|
722
|
+
getApiStatus,
|
|
723
|
+
getDevice,
|
|
724
|
+
uploadSingleImage,
|
|
725
|
+
liveEventsWs,
|
|
726
|
+
shadowAlarmGet,
|
|
727
|
+
shadowAlarmUpdate,
|
|
728
|
+
ledLightHint,
|
|
729
|
+
getDeviceStatus,
|
|
730
|
+
getDeviceStatusList,
|
|
731
|
+
subscribeToLiveEvents,
|
|
732
|
+
};
|