edilkamin 1.10.0 → 1.10.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/src/cli.ts CHANGED
@@ -6,6 +6,7 @@ import { version } from "../package.json";
6
6
  import { NEW_API_URL, OLD_API_URL } from "./constants";
7
7
  import { configure, configureAmplify, getSession, signIn } from "./library";
8
8
  import { clearSession, createFileStorage } from "./token-storage";
9
+ import { AlarmCode, AlarmDescriptions } from "./types";
9
10
 
10
11
  const promptPassword = (): Promise<string> => {
11
12
  const rl = readline.createInterface({
@@ -336,6 +337,117 @@ const createProgram = (): Command => {
336
337
  mac: string,
337
338
  ) => api.getPelletAutonomyTime(jwtToken, mac),
338
339
  },
340
+ // Statistics getters
341
+ {
342
+ commandName: "getTotalCounters",
343
+ description:
344
+ "Get lifetime operating counters (power-ons, runtime by power level)",
345
+ getter: (
346
+ api: ReturnType<typeof configure>,
347
+ jwtToken: string,
348
+ mac: string,
349
+ ) => api.getTotalCounters(jwtToken, mac),
350
+ },
351
+ {
352
+ commandName: "getServiceCounters",
353
+ description: "Get service counters (runtime since last maintenance)",
354
+ getter: (
355
+ api: ReturnType<typeof configure>,
356
+ jwtToken: string,
357
+ mac: string,
358
+ ) => api.getServiceCounters(jwtToken, mac),
359
+ },
360
+ {
361
+ commandName: "getRegenerationData",
362
+ description: "Get regeneration and maintenance data",
363
+ getter: (
364
+ api: ReturnType<typeof configure>,
365
+ jwtToken: string,
366
+ mac: string,
367
+ ) => api.getRegenerationData(jwtToken, mac),
368
+ },
369
+ {
370
+ commandName: "getServiceTime",
371
+ description: "Get total service time in hours",
372
+ getter: (
373
+ api: ReturnType<typeof configure>,
374
+ jwtToken: string,
375
+ mac: string,
376
+ ) => api.getServiceTime(jwtToken, mac),
377
+ },
378
+ // Analytics getters
379
+ {
380
+ commandName: "getTotalOperatingHours",
381
+ description: "Get total operating hours across all power levels",
382
+ getter: (
383
+ api: ReturnType<typeof configure>,
384
+ jwtToken: string,
385
+ mac: string,
386
+ ) => api.getTotalOperatingHours(jwtToken, mac),
387
+ },
388
+ {
389
+ commandName: "getPowerDistribution",
390
+ description: "Get power level usage distribution as percentages",
391
+ getter: async (
392
+ api: ReturnType<typeof configure>,
393
+ jwtToken: string,
394
+ mac: string,
395
+ ) => {
396
+ const result = await api.getPowerDistribution(jwtToken, mac);
397
+ return {
398
+ p1: `${result.p1.toFixed(1)}%`,
399
+ p2: `${result.p2.toFixed(1)}%`,
400
+ p3: `${result.p3.toFixed(1)}%`,
401
+ p4: `${result.p4.toFixed(1)}%`,
402
+ p5: `${result.p5.toFixed(1)}%`,
403
+ };
404
+ },
405
+ },
406
+ {
407
+ commandName: "getServiceStatus",
408
+ description: "Get service status including whether maintenance is due",
409
+ getter: (
410
+ api: ReturnType<typeof configure>,
411
+ jwtToken: string,
412
+ mac: string,
413
+ ) => api.getServiceStatus(jwtToken, mac),
414
+ },
415
+ {
416
+ commandName: "getUsageAnalytics",
417
+ description: "Get comprehensive usage analytics in single response",
418
+ getter: async (
419
+ api: ReturnType<typeof configure>,
420
+ jwtToken: string,
421
+ mac: string,
422
+ ) => {
423
+ const analytics = await api.getUsageAnalytics(jwtToken, mac);
424
+ return {
425
+ lifetime: {
426
+ powerOnCount: analytics.totalPowerOns,
427
+ totalOperatingHours: analytics.totalOperatingHours,
428
+ blackoutCount: analytics.blackoutCount,
429
+ },
430
+ powerDistribution: {
431
+ p1: `${analytics.powerDistribution.p1.toFixed(1)}%`,
432
+ p2: `${analytics.powerDistribution.p2.toFixed(1)}%`,
433
+ p3: `${analytics.powerDistribution.p3.toFixed(1)}%`,
434
+ p4: `${analytics.powerDistribution.p4.toFixed(1)}%`,
435
+ p5: `${analytics.powerDistribution.p5.toFixed(1)}%`,
436
+ },
437
+ service: {
438
+ totalServiceHours: analytics.serviceStatus.totalServiceHours,
439
+ hoursSinceLastService: analytics.serviceStatus.hoursSinceService,
440
+ thresholdHours: analytics.serviceStatus.serviceThresholdHours,
441
+ isServiceDue: analytics.serviceStatus.isServiceDue,
442
+ lastMaintenanceDate:
443
+ analytics.lastMaintenanceDate?.toISOString() || "Never",
444
+ },
445
+ alarms: {
446
+ totalCount: analytics.alarmCount,
447
+ },
448
+ };
449
+ },
450
+ },
339
451
  ].forEach(({ commandName, description, getter }) => {
340
452
  addLegacyOption(
341
453
  addMacOption(
@@ -676,6 +788,50 @@ const createProgram = (): Command => {
676
788
  console.log(JSON.stringify(result, null, 2));
677
789
  });
678
790
 
791
+ // Alarm history command with human-readable descriptions
792
+ addLegacyOption(
793
+ addMacOption(
794
+ addAuthOptions(
795
+ program
796
+ .command("getAlarmHistory")
797
+ .description(
798
+ "Get alarm history log with human-readable descriptions",
799
+ ),
800
+ ),
801
+ ),
802
+ ).action(async (options) => {
803
+ const { username, password, mac, legacy = false } = options;
804
+ const normalizedMac = mac.replace(/:/g, "");
805
+ const storage = createFileStorage();
806
+ configureAmplify(storage);
807
+ let jwtToken: string;
808
+ try {
809
+ jwtToken = await getSession(false, legacy);
810
+ } catch {
811
+ if (!username) {
812
+ throw new Error(
813
+ "No session found. Please provide --username to sign in.",
814
+ );
815
+ }
816
+ const pwd = password || (await promptPassword());
817
+ jwtToken = await signIn(username, pwd, legacy);
818
+ }
819
+ const apiUrl = legacy ? OLD_API_URL : NEW_API_URL;
820
+ const api = configure(apiUrl);
821
+ const result = await api.getAlarmHistory(jwtToken, normalizedMac);
822
+ // Format alarms with human-readable descriptions
823
+ const formattedAlarms = result.alarms.map((alarm) => ({
824
+ ...alarm,
825
+ typeName: AlarmCode[alarm.type] || "UNKNOWN",
826
+ description:
827
+ AlarmDescriptions[alarm.type as AlarmCode] || "Unknown alarm",
828
+ date: new Date(alarm.timestamp * 1000).toISOString(),
829
+ }));
830
+ console.log(
831
+ JSON.stringify({ ...result, alarms: formattedAlarms }, null, 2),
832
+ );
833
+ });
834
+
679
835
  // Command: register
680
836
  addLegacyOption(
681
837
  addAuthOptions(
package/src/index.ts CHANGED
@@ -3,13 +3,15 @@ import { configure } from "./library";
3
3
  export { bleToWifiMac } from "./bluetooth-utils";
4
4
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
5
5
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
6
- export { configure, getSession, signIn } from "./library";
6
+ export { configure, deriveUsageAnalytics, getSession, signIn } from "./library";
7
7
  export {
8
8
  serialNumberDisplay,
9
9
  serialNumberFromHex,
10
10
  serialNumberToHex,
11
11
  } from "./serial-utils";
12
12
  export {
13
+ AlarmEntryType,
14
+ AlarmsLogType,
13
15
  BufferEncodedType,
14
16
  CommandsType,
15
17
  DeviceAssociationBody,
@@ -18,10 +20,18 @@ export {
18
20
  DeviceInfoType,
19
21
  DiscoveredDevice,
20
22
  EditDeviceAssociationBody,
23
+ PowerDistributionType,
24
+ RegenerationDataType,
25
+ ServiceCountersType,
26
+ ServiceStatusType,
27
+ StatusCountersType,
21
28
  StatusType,
22
29
  TemperaturesType,
30
+ TotalCountersType,
31
+ UsageAnalyticsType,
23
32
  UserParametersType,
24
33
  } from "./types";
34
+ export { AlarmCode, AlarmDescriptions } from "./types";
25
35
 
26
36
  export const {
27
37
  deviceInfo,
@@ -3,7 +3,11 @@ import * as amplifyAuth from "aws-amplify/auth";
3
3
  import pako from "pako";
4
4
  import sinon from "sinon";
5
5
 
6
- import { configure, createAuthService } from "../src/library";
6
+ import {
7
+ configure,
8
+ createAuthService,
9
+ deriveUsageAnalytics,
10
+ } from "../src/library";
7
11
  import { API_URL } from "./constants";
8
12
 
9
13
  /**
@@ -242,6 +246,17 @@ describe("library", () => {
242
246
  "getLanguage",
243
247
  "getPelletInReserve",
244
248
  "getPelletAutonomyTime",
249
+ // Statistics getters
250
+ "getTotalCounters",
251
+ "getServiceCounters",
252
+ "getAlarmHistory",
253
+ "getRegenerationData",
254
+ "getServiceTime",
255
+ // Analytics functions
256
+ "getTotalOperatingHours",
257
+ "getPowerDistribution",
258
+ "getServiceStatus",
259
+ "getUsageAnalytics",
245
260
  ];
246
261
  it("should create API methods with the correct baseURL", async () => {
247
262
  const baseURL = "https://example.com/api/";
@@ -1017,6 +1032,453 @@ describe("library", () => {
1017
1032
  });
1018
1033
  });
1019
1034
 
1035
+ describe("statistics getters", () => {
1036
+ const mockDeviceInfoWithStats = {
1037
+ status: {
1038
+ commands: { power: true },
1039
+ temperatures: { board: 25, enviroment: 20 },
1040
+ flags: { is_pellet_in_reserve: false },
1041
+ pellet: { autonomy_time: 900 },
1042
+ counters: { service_time: 1108 },
1043
+ },
1044
+ nvm: {
1045
+ user_parameters: {
1046
+ language: 1,
1047
+ is_auto: false,
1048
+ is_fahrenheit: false,
1049
+ is_sound_active: false,
1050
+ enviroment_1_temperature: 19,
1051
+ enviroment_2_temperature: 20,
1052
+ enviroment_3_temperature: 20,
1053
+ manual_power: 1,
1054
+ fan_1_ventilation: 3,
1055
+ fan_2_ventilation: 0,
1056
+ fan_3_ventilation: 0,
1057
+ is_standby_active: false,
1058
+ standby_waiting_time: 60,
1059
+ },
1060
+ total_counters: {
1061
+ power_ons: 278,
1062
+ p1_working_time: 833,
1063
+ p2_working_time: 15,
1064
+ p3_working_time: 19,
1065
+ p4_working_time: 8,
1066
+ p5_working_time: 17,
1067
+ },
1068
+ service_counters: {
1069
+ p1_working_time: 100,
1070
+ p2_working_time: 10,
1071
+ p3_working_time: 5,
1072
+ p4_working_time: 2,
1073
+ p5_working_time: 1,
1074
+ },
1075
+ alarms_log: {
1076
+ number: 2,
1077
+ index: 2,
1078
+ alarms: [
1079
+ { type: 3, timestamp: 1700000000 },
1080
+ { type: 21, timestamp: 1700001000 },
1081
+ ],
1082
+ },
1083
+ regeneration: {
1084
+ time: 0,
1085
+ last_intervention: 1577836800,
1086
+ daylight_time_flag: 0,
1087
+ blackout_counter: 43,
1088
+ airkare_working_hours_counter: 0,
1089
+ },
1090
+ },
1091
+ };
1092
+
1093
+ it("should get total counters", async () => {
1094
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1095
+ const api = configure(API_URL);
1096
+ const result = await api.getTotalCounters(
1097
+ expectedToken,
1098
+ "00:11:22:33:44:55",
1099
+ );
1100
+ assert.deepEqual(result, mockDeviceInfoWithStats.nvm.total_counters);
1101
+ });
1102
+
1103
+ it("should get service counters", async () => {
1104
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1105
+ const api = configure(API_URL);
1106
+ const result = await api.getServiceCounters(
1107
+ expectedToken,
1108
+ "00:11:22:33:44:55",
1109
+ );
1110
+ assert.deepEqual(result, mockDeviceInfoWithStats.nvm.service_counters);
1111
+ });
1112
+
1113
+ it("should get alarm history", async () => {
1114
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1115
+ const api = configure(API_URL);
1116
+ const result = await api.getAlarmHistory(
1117
+ expectedToken,
1118
+ "00:11:22:33:44:55",
1119
+ );
1120
+ assert.deepEqual(result, mockDeviceInfoWithStats.nvm.alarms_log);
1121
+ });
1122
+
1123
+ it("should get regeneration data", async () => {
1124
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1125
+ const api = configure(API_URL);
1126
+ const result = await api.getRegenerationData(
1127
+ expectedToken,
1128
+ "00:11:22:33:44:55",
1129
+ );
1130
+ assert.deepEqual(result, mockDeviceInfoWithStats.nvm.regeneration);
1131
+ });
1132
+
1133
+ it("should get service time", async () => {
1134
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1135
+ const api = configure(API_URL);
1136
+ const result = await api.getServiceTime(
1137
+ expectedToken,
1138
+ "00:11:22:33:44:55",
1139
+ );
1140
+ assert.equal(result, 1108);
1141
+ });
1142
+ });
1143
+
1144
+ describe("analytics functions", () => {
1145
+ const mockDeviceInfoWithStats = {
1146
+ status: {
1147
+ commands: { power: true },
1148
+ temperatures: { board: 25, enviroment: 20 },
1149
+ flags: { is_pellet_in_reserve: false },
1150
+ pellet: { autonomy_time: 900 },
1151
+ counters: { service_time: 1108 },
1152
+ },
1153
+ nvm: {
1154
+ user_parameters: {
1155
+ language: 1,
1156
+ is_auto: false,
1157
+ is_fahrenheit: false,
1158
+ is_sound_active: false,
1159
+ enviroment_1_temperature: 19,
1160
+ enviroment_2_temperature: 20,
1161
+ enviroment_3_temperature: 20,
1162
+ manual_power: 1,
1163
+ fan_1_ventilation: 3,
1164
+ fan_2_ventilation: 0,
1165
+ fan_3_ventilation: 0,
1166
+ is_standby_active: false,
1167
+ standby_waiting_time: 60,
1168
+ },
1169
+ total_counters: {
1170
+ power_ons: 278,
1171
+ p1_working_time: 833,
1172
+ p2_working_time: 15,
1173
+ p3_working_time: 19,
1174
+ p4_working_time: 8,
1175
+ p5_working_time: 17,
1176
+ },
1177
+ service_counters: {
1178
+ p1_working_time: 100,
1179
+ p2_working_time: 10,
1180
+ p3_working_time: 5,
1181
+ p4_working_time: 2,
1182
+ p5_working_time: 1,
1183
+ },
1184
+ alarms_log: {
1185
+ number: 2,
1186
+ index: 2,
1187
+ alarms: [
1188
+ { type: 3, timestamp: 1700000000 },
1189
+ { type: 21, timestamp: 1700001000 },
1190
+ ],
1191
+ },
1192
+ regeneration: {
1193
+ time: 0,
1194
+ last_intervention: 1577836800,
1195
+ daylight_time_flag: 0,
1196
+ blackout_counter: 43,
1197
+ airkare_working_hours_counter: 0,
1198
+ },
1199
+ },
1200
+ };
1201
+
1202
+ it("should calculate total operating hours", async () => {
1203
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1204
+ const api = configure(API_URL);
1205
+ const result = await api.getTotalOperatingHours(
1206
+ expectedToken,
1207
+ "00:11:22:33:44:55",
1208
+ );
1209
+ // 833 + 15 + 19 + 8 + 17 = 892
1210
+ assert.equal(result, 892);
1211
+ });
1212
+
1213
+ it("should calculate power distribution percentages", async () => {
1214
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1215
+ const api = configure(API_URL);
1216
+ const result = await api.getPowerDistribution(
1217
+ expectedToken,
1218
+ "00:11:22:33:44:55",
1219
+ );
1220
+ // Total: 892 hours
1221
+ assert.ok(result.p1 > 90); // 833/892 = 93.4%
1222
+ assert.ok(result.p2 < 5); // 15/892 = 1.7%
1223
+ // Sum should be ~100%
1224
+ const sum = result.p1 + result.p2 + result.p3 + result.p4 + result.p5;
1225
+ assert.ok(Math.abs(sum - 100) < 0.1);
1226
+ });
1227
+
1228
+ it("should handle zero operating hours in power distribution", async () => {
1229
+ const zeroHoursInfo = {
1230
+ ...mockDeviceInfoWithStats,
1231
+ nvm: {
1232
+ ...mockDeviceInfoWithStats.nvm,
1233
+ total_counters: {
1234
+ power_ons: 0,
1235
+ p1_working_time: 0,
1236
+ p2_working_time: 0,
1237
+ p3_working_time: 0,
1238
+ p4_working_time: 0,
1239
+ p5_working_time: 0,
1240
+ },
1241
+ },
1242
+ };
1243
+ fetchStub.resolves(mockResponse(zeroHoursInfo));
1244
+ const api = configure(API_URL);
1245
+ const result = await api.getPowerDistribution(
1246
+ expectedToken,
1247
+ "00:11:22:33:44:55",
1248
+ );
1249
+ assert.deepEqual(result, { p1: 0, p2: 0, p3: 0, p4: 0, p5: 0 });
1250
+ });
1251
+
1252
+ it("should calculate service status", async () => {
1253
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1254
+ const api = configure(API_URL);
1255
+ const result = await api.getServiceStatus(
1256
+ expectedToken,
1257
+ "00:11:22:33:44:55",
1258
+ );
1259
+ assert.equal(result.totalServiceHours, 1108);
1260
+ // 100 + 10 + 5 + 2 + 1 = 118 hours since service
1261
+ assert.equal(result.hoursSinceService, 118);
1262
+ assert.equal(result.isServiceDue, false); // 118 < 2000
1263
+ });
1264
+
1265
+ it("should indicate service is due when threshold exceeded", async () => {
1266
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1267
+ const api = configure(API_URL);
1268
+ // Use threshold of 100 hours
1269
+ const result = await api.getServiceStatus(
1270
+ expectedToken,
1271
+ "00:11:22:33:44:55",
1272
+ 100,
1273
+ );
1274
+ assert.equal(result.isServiceDue, true); // 118 >= 100
1275
+ });
1276
+
1277
+ it("should get comprehensive usage analytics", async () => {
1278
+ fetchStub.resolves(mockResponse(mockDeviceInfoWithStats));
1279
+ const api = configure(API_URL);
1280
+ const result = await api.getUsageAnalytics(
1281
+ expectedToken,
1282
+ "00:11:22:33:44:55",
1283
+ );
1284
+
1285
+ assert.equal(result.totalPowerOns, 278);
1286
+ assert.equal(result.totalOperatingHours, 892);
1287
+ assert.equal(result.blackoutCount, 43);
1288
+ assert.equal(result.alarmCount, 2);
1289
+ assert.ok(result.lastMaintenanceDate instanceof Date);
1290
+ assert.equal(result.serviceStatus.isServiceDue, false);
1291
+ });
1292
+
1293
+ it("should handle null lastMaintenanceDate when timestamp is 0", async () => {
1294
+ const noMaintenanceInfo = {
1295
+ ...mockDeviceInfoWithStats,
1296
+ nvm: {
1297
+ ...mockDeviceInfoWithStats.nvm,
1298
+ regeneration: {
1299
+ ...mockDeviceInfoWithStats.nvm.regeneration,
1300
+ last_intervention: 0,
1301
+ },
1302
+ },
1303
+ };
1304
+ fetchStub.resolves(mockResponse(noMaintenanceInfo));
1305
+ const api = configure(API_URL);
1306
+ const result = await api.getUsageAnalytics(
1307
+ expectedToken,
1308
+ "00:11:22:33:44:55",
1309
+ );
1310
+ assert.equal(result.lastMaintenanceDate, null);
1311
+ });
1312
+ });
1313
+
1314
+ describe("deriveUsageAnalytics", () => {
1315
+ const mockDeviceInfoForDerive = {
1316
+ status: {
1317
+ commands: {
1318
+ power: false,
1319
+ },
1320
+ temperatures: {
1321
+ enviroment: 20,
1322
+ set_air: 21,
1323
+ get_air: 20,
1324
+ set_water: 40,
1325
+ get_water: 35,
1326
+ },
1327
+ counters: {
1328
+ service_time: 1108,
1329
+ },
1330
+ flags: {
1331
+ is_pellet_in_reserve: false,
1332
+ },
1333
+ pellet: {
1334
+ autonomy_time: 180,
1335
+ },
1336
+ },
1337
+ nvm: {
1338
+ user_parameters: {
1339
+ language: 1,
1340
+ is_auto: false,
1341
+ is_fahrenheit: false,
1342
+ is_sound_active: false,
1343
+ enviroment_1_temperature: 19,
1344
+ enviroment_2_temperature: 20,
1345
+ enviroment_3_temperature: 20,
1346
+ manual_power: 1,
1347
+ fan_1_ventilation: 3,
1348
+ fan_2_ventilation: 0,
1349
+ fan_3_ventilation: 0,
1350
+ is_standby_active: false,
1351
+ standby_waiting_time: 60,
1352
+ },
1353
+ total_counters: {
1354
+ power_ons: 278,
1355
+ p1_working_time: 833,
1356
+ p2_working_time: 15,
1357
+ p3_working_time: 19,
1358
+ p4_working_time: 8,
1359
+ p5_working_time: 17,
1360
+ },
1361
+ service_counters: {
1362
+ p1_working_time: 100,
1363
+ p2_working_time: 10,
1364
+ p3_working_time: 5,
1365
+ p4_working_time: 2,
1366
+ p5_working_time: 1,
1367
+ },
1368
+ regeneration: {
1369
+ time: 0,
1370
+ last_intervention: 1577836800,
1371
+ daylight_time_flag: 0,
1372
+ blackout_counter: 43,
1373
+ airkare_working_hours_counter: 0,
1374
+ },
1375
+ alarms_log: {
1376
+ number: 6,
1377
+ index: 6,
1378
+ alarms: [],
1379
+ },
1380
+ },
1381
+ };
1382
+
1383
+ it("should derive analytics from device info without API call", () => {
1384
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1385
+ const analytics = deriveUsageAnalytics(mockDeviceInfoForDerive as any);
1386
+
1387
+ assert.equal(analytics.totalPowerOns, 278);
1388
+ assert.equal(analytics.totalOperatingHours, 892); // 833+15+19+8+17
1389
+ assert.equal(analytics.blackoutCount, 43);
1390
+ assert.equal(analytics.alarmCount, 6);
1391
+ });
1392
+
1393
+ it("should calculate power distribution correctly", () => {
1394
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1395
+ const analytics = deriveUsageAnalytics(mockDeviceInfoForDerive as any);
1396
+
1397
+ // P1: 833/892 ≈ 93.4%
1398
+ assert.ok(analytics.powerDistribution.p1 > 93);
1399
+ assert.ok(analytics.powerDistribution.p1 < 94);
1400
+
1401
+ // Sum should be 100%
1402
+ const sum = Object.values(analytics.powerDistribution).reduce(
1403
+ (a, b) => a + b,
1404
+ 0,
1405
+ );
1406
+ assert.ok(Math.abs(sum - 100) < 0.001);
1407
+ });
1408
+
1409
+ it("should handle zero operating hours", () => {
1410
+ const zeroHoursInfo = {
1411
+ ...mockDeviceInfoForDerive,
1412
+ nvm: {
1413
+ ...mockDeviceInfoForDerive.nvm,
1414
+ total_counters: {
1415
+ power_ons: 0,
1416
+ p1_working_time: 0,
1417
+ p2_working_time: 0,
1418
+ p3_working_time: 0,
1419
+ p4_working_time: 0,
1420
+ p5_working_time: 0,
1421
+ },
1422
+ },
1423
+ };
1424
+
1425
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1426
+ const analytics = deriveUsageAnalytics(zeroHoursInfo as any);
1427
+ assert.deepEqual(analytics.powerDistribution, {
1428
+ p1: 0,
1429
+ p2: 0,
1430
+ p3: 0,
1431
+ p4: 0,
1432
+ p5: 0,
1433
+ });
1434
+ });
1435
+
1436
+ it("should respect custom service threshold", () => {
1437
+ const analytics = deriveUsageAnalytics(
1438
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1439
+ mockDeviceInfoForDerive as any,
1440
+ 100,
1441
+ );
1442
+
1443
+ // 118 hours since service >= 100 threshold
1444
+ assert.equal(analytics.serviceStatus.isServiceDue, true);
1445
+ assert.equal(analytics.serviceStatus.serviceThresholdHours, 100);
1446
+ });
1447
+
1448
+ it("should use default threshold of 2000 hours", () => {
1449
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1450
+ const analytics = deriveUsageAnalytics(mockDeviceInfoForDerive as any);
1451
+
1452
+ assert.equal(analytics.serviceStatus.serviceThresholdHours, 2000);
1453
+ assert.equal(analytics.serviceStatus.isServiceDue, false); // 118 < 2000
1454
+ });
1455
+
1456
+ it("should convert last_intervention timestamp to Date", () => {
1457
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1458
+ const analytics = deriveUsageAnalytics(mockDeviceInfoForDerive as any);
1459
+
1460
+ assert.ok(analytics.lastMaintenanceDate instanceof Date);
1461
+ assert.equal(analytics.lastMaintenanceDate?.getTime(), 1577836800 * 1000);
1462
+ });
1463
+
1464
+ it("should return null for lastMaintenanceDate when timestamp is 0", () => {
1465
+ const noMaintenanceInfo = {
1466
+ ...mockDeviceInfoForDerive,
1467
+ nvm: {
1468
+ ...mockDeviceInfoForDerive.nvm,
1469
+ regeneration: {
1470
+ ...mockDeviceInfoForDerive.nvm.regeneration,
1471
+ last_intervention: 0,
1472
+ },
1473
+ },
1474
+ };
1475
+
1476
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1477
+ const analytics = deriveUsageAnalytics(noMaintenanceInfo as any);
1478
+ assert.equal(analytics.lastMaintenanceDate, null);
1479
+ });
1480
+ });
1481
+
1020
1482
  describe("Error Handling", () => {
1021
1483
  const errorTests = [
1022
1484
  { status: 400, statusText: "Bad Request" },