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/dist/cjs/package.json +1 -1
- package/dist/cjs/src/cli.js +105 -0
- package/dist/cjs/src/index.d.ts +3 -2
- package/dist/cjs/src/index.js +5 -1
- package/dist/cjs/src/library.d.ts +26 -1
- package/dist/cjs/src/library.js +219 -1
- package/dist/cjs/src/library.test.js +356 -0
- package/dist/cjs/src/types.d.ts +127 -1
- package/dist/cjs/src/types.js +64 -0
- package/dist/esm/package.json +1 -1
- package/dist/esm/src/cli.js +105 -0
- package/dist/esm/src/index.d.ts +3 -2
- package/dist/esm/src/index.js +2 -1
- package/dist/esm/src/library.d.ts +26 -1
- package/dist/esm/src/library.js +217 -0
- package/dist/esm/src/library.test.js +357 -1
- package/dist/esm/src/types.d.ts +127 -1
- package/dist/esm/src/types.js +63 -1
- package/package.json +1 -1
- package/src/cli.ts +156 -0
- package/src/index.ts +11 -1
- package/src/library.test.ts +463 -1
- package/src/library.ts +279 -0
- package/src/types.ts +180 -0
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,
|
package/src/library.test.ts
CHANGED
|
@@ -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 {
|
|
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" },
|