edilkamin 1.10.1 → 1.11.0

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.
@@ -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
  /**
@@ -1307,6 +1311,174 @@ describe("library", () => {
1307
1311
  });
1308
1312
  });
1309
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
+
1310
1482
  describe("Error Handling", () => {
1311
1483
  const errorTests = [
1312
1484
  { status: 400, statusText: "Bad Request" },
package/src/library.ts CHANGED
@@ -718,6 +718,74 @@ const getServiceTime =
718
718
  */
719
719
  const DEFAULT_SERVICE_THRESHOLD = 2000;
720
720
 
721
+ /**
722
+ * Derives usage analytics from an existing DeviceInfo response.
723
+ * This is a pure function that performs client-side calculations without API calls.
724
+ *
725
+ * Use this when you already have a DeviceInfo object (e.g., from a previous deviceInfo() call)
726
+ * to avoid making an additional API request.
727
+ *
728
+ * @param {DeviceInfoType} deviceInfo - The device info response object.
729
+ * @param {number} [serviceThreshold=2000] - Service threshold in hours.
730
+ * @returns {UsageAnalyticsType} - Comprehensive usage analytics.
731
+ *
732
+ * @example
733
+ * const info = await api.deviceInfo(token, mac);
734
+ * const analytics = deriveUsageAnalytics(info);
735
+ */
736
+ export const deriveUsageAnalytics = (
737
+ deviceInfo: DeviceInfoType,
738
+ serviceThreshold: number = DEFAULT_SERVICE_THRESHOLD,
739
+ ): UsageAnalyticsType => {
740
+ const totalCounters = deviceInfo.nvm.total_counters;
741
+ const serviceCounters = deviceInfo.nvm.service_counters;
742
+ const regeneration = deviceInfo.nvm.regeneration;
743
+ const alarmsLog = deviceInfo.nvm.alarms_log;
744
+
745
+ const totalOperatingHours =
746
+ totalCounters.p1_working_time +
747
+ totalCounters.p2_working_time +
748
+ totalCounters.p3_working_time +
749
+ totalCounters.p4_working_time +
750
+ totalCounters.p5_working_time;
751
+
752
+ const hoursSinceService =
753
+ serviceCounters.p1_working_time +
754
+ serviceCounters.p2_working_time +
755
+ serviceCounters.p3_working_time +
756
+ serviceCounters.p4_working_time +
757
+ serviceCounters.p5_working_time;
758
+
759
+ const powerDistribution: PowerDistributionType =
760
+ totalOperatingHours === 0
761
+ ? { p1: 0, p2: 0, p3: 0, p4: 0, p5: 0 }
762
+ : {
763
+ p1: (totalCounters.p1_working_time / totalOperatingHours) * 100,
764
+ p2: (totalCounters.p2_working_time / totalOperatingHours) * 100,
765
+ p3: (totalCounters.p3_working_time / totalOperatingHours) * 100,
766
+ p4: (totalCounters.p4_working_time / totalOperatingHours) * 100,
767
+ p5: (totalCounters.p5_working_time / totalOperatingHours) * 100,
768
+ };
769
+
770
+ return {
771
+ totalPowerOns: totalCounters.power_ons,
772
+ totalOperatingHours,
773
+ powerDistribution,
774
+ serviceStatus: {
775
+ totalServiceHours: deviceInfo.status.counters.service_time,
776
+ hoursSinceService,
777
+ serviceThresholdHours: serviceThreshold,
778
+ isServiceDue: hoursSinceService >= serviceThreshold,
779
+ },
780
+ blackoutCount: regeneration.blackout_counter,
781
+ lastMaintenanceDate:
782
+ regeneration.last_intervention > 0
783
+ ? new Date(regeneration.last_intervention * 1000)
784
+ : null,
785
+ alarmCount: alarmsLog.number,
786
+ };
787
+ };
788
+
721
789
  const getTotalOperatingHours =
722
790
  (baseURL: string) =>
723
791
  /**
@@ -821,54 +889,7 @@ const getUsageAnalytics =
821
889
  serviceThreshold: number = DEFAULT_SERVICE_THRESHOLD,
822
890
  ): Promise<UsageAnalyticsType> => {
823
891
  const info = await deviceInfo(baseURL)(jwtToken, macAddress);
824
-
825
- const totalCounters = info.nvm.total_counters;
826
- const serviceCounters = info.nvm.service_counters;
827
- const regeneration = info.nvm.regeneration;
828
- const alarmsLog = info.nvm.alarms_log;
829
-
830
- const totalOperatingHours =
831
- totalCounters.p1_working_time +
832
- totalCounters.p2_working_time +
833
- totalCounters.p3_working_time +
834
- totalCounters.p4_working_time +
835
- totalCounters.p5_working_time;
836
-
837
- const hoursSinceService =
838
- serviceCounters.p1_working_time +
839
- serviceCounters.p2_working_time +
840
- serviceCounters.p3_working_time +
841
- serviceCounters.p4_working_time +
842
- serviceCounters.p5_working_time;
843
-
844
- const powerDistribution: PowerDistributionType =
845
- totalOperatingHours === 0
846
- ? { p1: 0, p2: 0, p3: 0, p4: 0, p5: 0 }
847
- : {
848
- p1: (totalCounters.p1_working_time / totalOperatingHours) * 100,
849
- p2: (totalCounters.p2_working_time / totalOperatingHours) * 100,
850
- p3: (totalCounters.p3_working_time / totalOperatingHours) * 100,
851
- p4: (totalCounters.p4_working_time / totalOperatingHours) * 100,
852
- p5: (totalCounters.p5_working_time / totalOperatingHours) * 100,
853
- };
854
-
855
- return {
856
- totalPowerOns: totalCounters.power_ons,
857
- totalOperatingHours,
858
- powerDistribution,
859
- serviceStatus: {
860
- totalServiceHours: info.status.counters.service_time,
861
- hoursSinceService,
862
- serviceThresholdHours: serviceThreshold,
863
- isServiceDue: hoursSinceService >= serviceThreshold,
864
- },
865
- blackoutCount: regeneration.blackout_counter,
866
- lastMaintenanceDate:
867
- regeneration.last_intervention > 0
868
- ? new Date(regeneration.last_intervention * 1000)
869
- : null,
870
- alarmCount: alarmsLog.number,
871
- };
892
+ return deriveUsageAnalytics(info, serviceThreshold);
872
893
  };
873
894
 
874
895
  const registerDevice =