edilkamin 1.10.1 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.10.1",
3
+ "version": "1.10.2",
4
4
  "description": "",
5
5
  "main": "dist/cjs/src/index.js",
6
6
  "module": "dist/esm/src/index.js",
@@ -1,7 +1,7 @@
1
1
  export { bleToWifiMac } from "./bluetooth-utils";
2
2
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
3
3
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
4
- export { configure, getSession, signIn } from "./library";
4
+ export { configure, deriveUsageAnalytics, getSession, signIn } from "./library";
5
5
  export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
6
6
  export { AlarmEntryType, AlarmsLogType, BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, DiscoveredDevice, EditDeviceAssociationBody, PowerDistributionType, RegenerationDataType, ServiceCountersType, ServiceStatusType, StatusCountersType, StatusType, TemperaturesType, TotalCountersType, UsageAnalyticsType, UserParametersType, } from "./types";
7
7
  export { AlarmCode, AlarmDescriptions } from "./types";
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.setTargetTemperature = exports.getTargetTemperature = exports.getEnvironmentTemperature = exports.getPower = exports.setPowerOn = exports.setPowerOff = exports.setPower = exports.editDevice = exports.registerDevice = exports.deviceInfo = exports.AlarmDescriptions = exports.AlarmCode = exports.serialNumberToHex = exports.serialNumberFromHex = exports.serialNumberDisplay = exports.signIn = exports.getSession = exports.configure = exports.OLD_API_URL = exports.NEW_API_URL = exports.API_URL = exports.processResponse = exports.isBuffer = exports.decompressBuffer = exports.bleToWifiMac = void 0;
4
+ exports.setTargetTemperature = exports.getTargetTemperature = exports.getEnvironmentTemperature = exports.getPower = exports.setPowerOn = exports.setPowerOff = exports.setPower = exports.editDevice = exports.registerDevice = exports.deviceInfo = exports.AlarmDescriptions = exports.AlarmCode = exports.serialNumberToHex = exports.serialNumberFromHex = exports.serialNumberDisplay = exports.signIn = exports.getSession = exports.deriveUsageAnalytics = exports.configure = exports.OLD_API_URL = exports.NEW_API_URL = exports.API_URL = exports.processResponse = exports.isBuffer = exports.decompressBuffer = exports.bleToWifiMac = void 0;
5
5
  const library_1 = require("./library");
6
6
  var bluetooth_utils_1 = require("./bluetooth-utils");
7
7
  Object.defineProperty(exports, "bleToWifiMac", { enumerable: true, get: function () { return bluetooth_utils_1.bleToWifiMac; } });
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "NEW_API_URL", { enumerable: true, get: function
15
15
  Object.defineProperty(exports, "OLD_API_URL", { enumerable: true, get: function () { return constants_1.OLD_API_URL; } });
16
16
  var library_2 = require("./library");
17
17
  Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return library_2.configure; } });
18
+ Object.defineProperty(exports, "deriveUsageAnalytics", { enumerable: true, get: function () { return library_2.deriveUsageAnalytics; } });
18
19
  Object.defineProperty(exports, "getSession", { enumerable: true, get: function () { return library_2.getSession; } });
19
20
  Object.defineProperty(exports, "signIn", { enumerable: true, get: function () { return library_2.signIn; } });
20
21
  var serial_utils_1 = require("./serial-utils");
@@ -29,6 +29,22 @@ declare const createAuthService: (auth: typeof amplifyAuth) => {
29
29
  getSession: (forceRefresh?: boolean, legacy?: boolean) => Promise<string>;
30
30
  };
31
31
  declare const signIn: (username: string, password: string, legacy?: boolean) => Promise<string>, getSession: (forceRefresh?: boolean, legacy?: boolean) => Promise<string>;
32
+ /**
33
+ * Derives usage analytics from an existing DeviceInfo response.
34
+ * This is a pure function that performs client-side calculations without API calls.
35
+ *
36
+ * Use this when you already have a DeviceInfo object (e.g., from a previous deviceInfo() call)
37
+ * to avoid making an additional API request.
38
+ *
39
+ * @param {DeviceInfoType} deviceInfo - The device info response object.
40
+ * @param {number} [serviceThreshold=2000] - Service threshold in hours.
41
+ * @returns {UsageAnalyticsType} - Comprehensive usage analytics.
42
+ *
43
+ * @example
44
+ * const info = await api.deviceInfo(token, mac);
45
+ * const analytics = deriveUsageAnalytics(info);
46
+ */
47
+ export declare const deriveUsageAnalytics: (deviceInfo: DeviceInfoType, serviceThreshold?: number) => UsageAnalyticsType;
32
48
  /**
33
49
  * Configures the library for API interactions.
34
50
  * Initializes API methods with a specified base URL.
@@ -42,7 +42,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  });
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.signIn = exports.headers = exports.getSession = exports.createAuthService = exports.configureAmplify = exports.configure = void 0;
45
+ exports.signIn = exports.headers = exports.getSession = exports.createAuthService = exports.configureAmplify = exports.configure = exports.deriveUsageAnalytics = void 0;
46
46
  const assert_1 = require("assert");
47
47
  const aws_amplify_1 = require("aws-amplify");
48
48
  const amplifyAuth = __importStar(require("aws-amplify/auth"));
@@ -584,6 +584,63 @@ const getServiceTime = (baseURL) =>
584
584
  * Most devices use 2000 hours.
585
585
  */
586
586
  const DEFAULT_SERVICE_THRESHOLD = 2000;
587
+ /**
588
+ * Derives usage analytics from an existing DeviceInfo response.
589
+ * This is a pure function that performs client-side calculations without API calls.
590
+ *
591
+ * Use this when you already have a DeviceInfo object (e.g., from a previous deviceInfo() call)
592
+ * to avoid making an additional API request.
593
+ *
594
+ * @param {DeviceInfoType} deviceInfo - The device info response object.
595
+ * @param {number} [serviceThreshold=2000] - Service threshold in hours.
596
+ * @returns {UsageAnalyticsType} - Comprehensive usage analytics.
597
+ *
598
+ * @example
599
+ * const info = await api.deviceInfo(token, mac);
600
+ * const analytics = deriveUsageAnalytics(info);
601
+ */
602
+ const deriveUsageAnalytics = (deviceInfo, serviceThreshold = DEFAULT_SERVICE_THRESHOLD) => {
603
+ const totalCounters = deviceInfo.nvm.total_counters;
604
+ const serviceCounters = deviceInfo.nvm.service_counters;
605
+ const regeneration = deviceInfo.nvm.regeneration;
606
+ const alarmsLog = deviceInfo.nvm.alarms_log;
607
+ const totalOperatingHours = totalCounters.p1_working_time +
608
+ totalCounters.p2_working_time +
609
+ totalCounters.p3_working_time +
610
+ totalCounters.p4_working_time +
611
+ totalCounters.p5_working_time;
612
+ const hoursSinceService = serviceCounters.p1_working_time +
613
+ serviceCounters.p2_working_time +
614
+ serviceCounters.p3_working_time +
615
+ serviceCounters.p4_working_time +
616
+ serviceCounters.p5_working_time;
617
+ const powerDistribution = totalOperatingHours === 0
618
+ ? { p1: 0, p2: 0, p3: 0, p4: 0, p5: 0 }
619
+ : {
620
+ p1: (totalCounters.p1_working_time / totalOperatingHours) * 100,
621
+ p2: (totalCounters.p2_working_time / totalOperatingHours) * 100,
622
+ p3: (totalCounters.p3_working_time / totalOperatingHours) * 100,
623
+ p4: (totalCounters.p4_working_time / totalOperatingHours) * 100,
624
+ p5: (totalCounters.p5_working_time / totalOperatingHours) * 100,
625
+ };
626
+ return {
627
+ totalPowerOns: totalCounters.power_ons,
628
+ totalOperatingHours,
629
+ powerDistribution,
630
+ serviceStatus: {
631
+ totalServiceHours: deviceInfo.status.counters.service_time,
632
+ hoursSinceService,
633
+ serviceThresholdHours: serviceThreshold,
634
+ isServiceDue: hoursSinceService >= serviceThreshold,
635
+ },
636
+ blackoutCount: regeneration.blackout_counter,
637
+ lastMaintenanceDate: regeneration.last_intervention > 0
638
+ ? new Date(regeneration.last_intervention * 1000)
639
+ : null,
640
+ alarmCount: alarmsLog.number,
641
+ };
642
+ };
643
+ exports.deriveUsageAnalytics = deriveUsageAnalytics;
587
644
  const getTotalOperatingHours = (baseURL) =>
588
645
  /**
589
646
  * Calculates total operating hours across all power levels.
@@ -662,45 +719,7 @@ const getUsageAnalytics = (baseURL) =>
662
719
  */
663
720
  (jwtToken_1, macAddress_1, ...args_1) => __awaiter(void 0, [jwtToken_1, macAddress_1, ...args_1], void 0, function* (jwtToken, macAddress, serviceThreshold = DEFAULT_SERVICE_THRESHOLD) {
664
721
  const info = yield deviceInfo(baseURL)(jwtToken, macAddress);
665
- const totalCounters = info.nvm.total_counters;
666
- const serviceCounters = info.nvm.service_counters;
667
- const regeneration = info.nvm.regeneration;
668
- const alarmsLog = info.nvm.alarms_log;
669
- const totalOperatingHours = totalCounters.p1_working_time +
670
- totalCounters.p2_working_time +
671
- totalCounters.p3_working_time +
672
- totalCounters.p4_working_time +
673
- totalCounters.p5_working_time;
674
- const hoursSinceService = serviceCounters.p1_working_time +
675
- serviceCounters.p2_working_time +
676
- serviceCounters.p3_working_time +
677
- serviceCounters.p4_working_time +
678
- serviceCounters.p5_working_time;
679
- const powerDistribution = totalOperatingHours === 0
680
- ? { p1: 0, p2: 0, p3: 0, p4: 0, p5: 0 }
681
- : {
682
- p1: (totalCounters.p1_working_time / totalOperatingHours) * 100,
683
- p2: (totalCounters.p2_working_time / totalOperatingHours) * 100,
684
- p3: (totalCounters.p3_working_time / totalOperatingHours) * 100,
685
- p4: (totalCounters.p4_working_time / totalOperatingHours) * 100,
686
- p5: (totalCounters.p5_working_time / totalOperatingHours) * 100,
687
- };
688
- return {
689
- totalPowerOns: totalCounters.power_ons,
690
- totalOperatingHours,
691
- powerDistribution,
692
- serviceStatus: {
693
- totalServiceHours: info.status.counters.service_time,
694
- hoursSinceService,
695
- serviceThresholdHours: serviceThreshold,
696
- isServiceDue: hoursSinceService >= serviceThreshold,
697
- },
698
- blackoutCount: regeneration.blackout_counter,
699
- lastMaintenanceDate: regeneration.last_intervention > 0
700
- ? new Date(regeneration.last_intervention * 1000)
701
- : null,
702
- alarmCount: alarmsLog.number,
703
- };
722
+ return (0, exports.deriveUsageAnalytics)(info, serviceThreshold);
704
723
  });
705
724
  const registerDevice = (baseURL) =>
706
725
  /**
@@ -1014,6 +1014,139 @@ describe("library", () => {
1014
1014
  assert_1.strict.equal(result.lastMaintenanceDate, null);
1015
1015
  }));
1016
1016
  });
1017
+ describe("deriveUsageAnalytics", () => {
1018
+ const mockDeviceInfoForDerive = {
1019
+ status: {
1020
+ commands: {
1021
+ power: false,
1022
+ },
1023
+ temperatures: {
1024
+ enviroment: 20,
1025
+ set_air: 21,
1026
+ get_air: 20,
1027
+ set_water: 40,
1028
+ get_water: 35,
1029
+ },
1030
+ counters: {
1031
+ service_time: 1108,
1032
+ },
1033
+ flags: {
1034
+ is_pellet_in_reserve: false,
1035
+ },
1036
+ pellet: {
1037
+ autonomy_time: 180,
1038
+ },
1039
+ },
1040
+ nvm: {
1041
+ user_parameters: {
1042
+ language: 1,
1043
+ is_auto: false,
1044
+ is_fahrenheit: false,
1045
+ is_sound_active: false,
1046
+ enviroment_1_temperature: 19,
1047
+ enviroment_2_temperature: 20,
1048
+ enviroment_3_temperature: 20,
1049
+ manual_power: 1,
1050
+ fan_1_ventilation: 3,
1051
+ fan_2_ventilation: 0,
1052
+ fan_3_ventilation: 0,
1053
+ is_standby_active: false,
1054
+ standby_waiting_time: 60,
1055
+ },
1056
+ total_counters: {
1057
+ power_ons: 278,
1058
+ p1_working_time: 833,
1059
+ p2_working_time: 15,
1060
+ p3_working_time: 19,
1061
+ p4_working_time: 8,
1062
+ p5_working_time: 17,
1063
+ },
1064
+ service_counters: {
1065
+ p1_working_time: 100,
1066
+ p2_working_time: 10,
1067
+ p3_working_time: 5,
1068
+ p4_working_time: 2,
1069
+ p5_working_time: 1,
1070
+ },
1071
+ regeneration: {
1072
+ time: 0,
1073
+ last_intervention: 1577836800,
1074
+ daylight_time_flag: 0,
1075
+ blackout_counter: 43,
1076
+ airkare_working_hours_counter: 0,
1077
+ },
1078
+ alarms_log: {
1079
+ number: 6,
1080
+ index: 6,
1081
+ alarms: [],
1082
+ },
1083
+ },
1084
+ };
1085
+ it("should derive analytics from device info without API call", () => {
1086
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1087
+ const analytics = (0, library_1.deriveUsageAnalytics)(mockDeviceInfoForDerive);
1088
+ assert_1.strict.equal(analytics.totalPowerOns, 278);
1089
+ assert_1.strict.equal(analytics.totalOperatingHours, 892); // 833+15+19+8+17
1090
+ assert_1.strict.equal(analytics.blackoutCount, 43);
1091
+ assert_1.strict.equal(analytics.alarmCount, 6);
1092
+ });
1093
+ it("should calculate power distribution correctly", () => {
1094
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1095
+ const analytics = (0, library_1.deriveUsageAnalytics)(mockDeviceInfoForDerive);
1096
+ // P1: 833/892 ≈ 93.4%
1097
+ assert_1.strict.ok(analytics.powerDistribution.p1 > 93);
1098
+ assert_1.strict.ok(analytics.powerDistribution.p1 < 94);
1099
+ // Sum should be 100%
1100
+ const sum = Object.values(analytics.powerDistribution).reduce((a, b) => a + b, 0);
1101
+ assert_1.strict.ok(Math.abs(sum - 100) < 0.001);
1102
+ });
1103
+ it("should handle zero operating hours", () => {
1104
+ const zeroHoursInfo = Object.assign(Object.assign({}, mockDeviceInfoForDerive), { nvm: Object.assign(Object.assign({}, mockDeviceInfoForDerive.nvm), { total_counters: {
1105
+ power_ons: 0,
1106
+ p1_working_time: 0,
1107
+ p2_working_time: 0,
1108
+ p3_working_time: 0,
1109
+ p4_working_time: 0,
1110
+ p5_working_time: 0,
1111
+ } }) });
1112
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1113
+ const analytics = (0, library_1.deriveUsageAnalytics)(zeroHoursInfo);
1114
+ assert_1.strict.deepEqual(analytics.powerDistribution, {
1115
+ p1: 0,
1116
+ p2: 0,
1117
+ p3: 0,
1118
+ p4: 0,
1119
+ p5: 0,
1120
+ });
1121
+ });
1122
+ it("should respect custom service threshold", () => {
1123
+ const analytics = (0, library_1.deriveUsageAnalytics)(
1124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1125
+ mockDeviceInfoForDerive, 100);
1126
+ // 118 hours since service >= 100 threshold
1127
+ assert_1.strict.equal(analytics.serviceStatus.isServiceDue, true);
1128
+ assert_1.strict.equal(analytics.serviceStatus.serviceThresholdHours, 100);
1129
+ });
1130
+ it("should use default threshold of 2000 hours", () => {
1131
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1132
+ const analytics = (0, library_1.deriveUsageAnalytics)(mockDeviceInfoForDerive);
1133
+ assert_1.strict.equal(analytics.serviceStatus.serviceThresholdHours, 2000);
1134
+ assert_1.strict.equal(analytics.serviceStatus.isServiceDue, false); // 118 < 2000
1135
+ });
1136
+ it("should convert last_intervention timestamp to Date", () => {
1137
+ var _a;
1138
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1139
+ const analytics = (0, library_1.deriveUsageAnalytics)(mockDeviceInfoForDerive);
1140
+ assert_1.strict.ok(analytics.lastMaintenanceDate instanceof Date);
1141
+ assert_1.strict.equal((_a = analytics.lastMaintenanceDate) === null || _a === void 0 ? void 0 : _a.getTime(), 1577836800 * 1000);
1142
+ });
1143
+ it("should return null for lastMaintenanceDate when timestamp is 0", () => {
1144
+ const noMaintenanceInfo = Object.assign(Object.assign({}, mockDeviceInfoForDerive), { nvm: Object.assign(Object.assign({}, mockDeviceInfoForDerive.nvm), { regeneration: Object.assign(Object.assign({}, mockDeviceInfoForDerive.nvm.regeneration), { last_intervention: 0 }) }) });
1145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1146
+ const analytics = (0, library_1.deriveUsageAnalytics)(noMaintenanceInfo);
1147
+ assert_1.strict.equal(analytics.lastMaintenanceDate, null);
1148
+ });
1149
+ });
1017
1150
  describe("Error Handling", () => {
1018
1151
  const errorTests = [
1019
1152
  { status: 400, statusText: "Bad Request" },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.10.1",
3
+ "version": "1.10.2",
4
4
  "description": "",
5
5
  "main": "dist/cjs/src/index.js",
6
6
  "module": "dist/esm/src/index.js",
@@ -1,7 +1,7 @@
1
1
  export { bleToWifiMac } from "./bluetooth-utils";
2
2
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
3
3
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
4
- export { configure, getSession, signIn } from "./library";
4
+ export { configure, deriveUsageAnalytics, getSession, signIn } from "./library";
5
5
  export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
6
6
  export { AlarmEntryType, AlarmsLogType, BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, DiscoveredDevice, EditDeviceAssociationBody, PowerDistributionType, RegenerationDataType, ServiceCountersType, ServiceStatusType, StatusCountersType, StatusType, TemperaturesType, TotalCountersType, UsageAnalyticsType, UserParametersType, } from "./types";
7
7
  export { AlarmCode, AlarmDescriptions } from "./types";
@@ -2,7 +2,7 @@ import { configure } from "./library";
2
2
  export { bleToWifiMac } from "./bluetooth-utils";
3
3
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
4
4
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
5
- export { configure, getSession, signIn } from "./library";
5
+ export { configure, deriveUsageAnalytics, getSession, signIn } from "./library";
6
6
  export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
7
7
  export { AlarmCode, AlarmDescriptions } from "./types";
8
8
  export const { deviceInfo, registerDevice, editDevice, setPower, setPowerOff, setPowerOn, getPower, getEnvironmentTemperature, getTargetTemperature, setTargetTemperature, } = configure();
@@ -29,6 +29,22 @@ declare const createAuthService: (auth: typeof amplifyAuth) => {
29
29
  getSession: (forceRefresh?: boolean, legacy?: boolean) => Promise<string>;
30
30
  };
31
31
  declare const signIn: (username: string, password: string, legacy?: boolean) => Promise<string>, getSession: (forceRefresh?: boolean, legacy?: boolean) => Promise<string>;
32
+ /**
33
+ * Derives usage analytics from an existing DeviceInfo response.
34
+ * This is a pure function that performs client-side calculations without API calls.
35
+ *
36
+ * Use this when you already have a DeviceInfo object (e.g., from a previous deviceInfo() call)
37
+ * to avoid making an additional API request.
38
+ *
39
+ * @param {DeviceInfoType} deviceInfo - The device info response object.
40
+ * @param {number} [serviceThreshold=2000] - Service threshold in hours.
41
+ * @returns {UsageAnalyticsType} - Comprehensive usage analytics.
42
+ *
43
+ * @example
44
+ * const info = await api.deviceInfo(token, mac);
45
+ * const analytics = deriveUsageAnalytics(info);
46
+ */
47
+ export declare const deriveUsageAnalytics: (deviceInfo: DeviceInfoType, serviceThreshold?: number) => UsageAnalyticsType;
32
48
  /**
33
49
  * Configures the library for API interactions.
34
50
  * Initializes API methods with a specified base URL.
@@ -543,6 +543,62 @@ const getServiceTime = (baseURL) =>
543
543
  * Most devices use 2000 hours.
544
544
  */
545
545
  const DEFAULT_SERVICE_THRESHOLD = 2000;
546
+ /**
547
+ * Derives usage analytics from an existing DeviceInfo response.
548
+ * This is a pure function that performs client-side calculations without API calls.
549
+ *
550
+ * Use this when you already have a DeviceInfo object (e.g., from a previous deviceInfo() call)
551
+ * to avoid making an additional API request.
552
+ *
553
+ * @param {DeviceInfoType} deviceInfo - The device info response object.
554
+ * @param {number} [serviceThreshold=2000] - Service threshold in hours.
555
+ * @returns {UsageAnalyticsType} - Comprehensive usage analytics.
556
+ *
557
+ * @example
558
+ * const info = await api.deviceInfo(token, mac);
559
+ * const analytics = deriveUsageAnalytics(info);
560
+ */
561
+ export const deriveUsageAnalytics = (deviceInfo, serviceThreshold = DEFAULT_SERVICE_THRESHOLD) => {
562
+ const totalCounters = deviceInfo.nvm.total_counters;
563
+ const serviceCounters = deviceInfo.nvm.service_counters;
564
+ const regeneration = deviceInfo.nvm.regeneration;
565
+ const alarmsLog = deviceInfo.nvm.alarms_log;
566
+ const totalOperatingHours = totalCounters.p1_working_time +
567
+ totalCounters.p2_working_time +
568
+ totalCounters.p3_working_time +
569
+ totalCounters.p4_working_time +
570
+ totalCounters.p5_working_time;
571
+ const hoursSinceService = serviceCounters.p1_working_time +
572
+ serviceCounters.p2_working_time +
573
+ serviceCounters.p3_working_time +
574
+ serviceCounters.p4_working_time +
575
+ serviceCounters.p5_working_time;
576
+ const powerDistribution = totalOperatingHours === 0
577
+ ? { p1: 0, p2: 0, p3: 0, p4: 0, p5: 0 }
578
+ : {
579
+ p1: (totalCounters.p1_working_time / totalOperatingHours) * 100,
580
+ p2: (totalCounters.p2_working_time / totalOperatingHours) * 100,
581
+ p3: (totalCounters.p3_working_time / totalOperatingHours) * 100,
582
+ p4: (totalCounters.p4_working_time / totalOperatingHours) * 100,
583
+ p5: (totalCounters.p5_working_time / totalOperatingHours) * 100,
584
+ };
585
+ return {
586
+ totalPowerOns: totalCounters.power_ons,
587
+ totalOperatingHours,
588
+ powerDistribution,
589
+ serviceStatus: {
590
+ totalServiceHours: deviceInfo.status.counters.service_time,
591
+ hoursSinceService,
592
+ serviceThresholdHours: serviceThreshold,
593
+ isServiceDue: hoursSinceService >= serviceThreshold,
594
+ },
595
+ blackoutCount: regeneration.blackout_counter,
596
+ lastMaintenanceDate: regeneration.last_intervention > 0
597
+ ? new Date(regeneration.last_intervention * 1000)
598
+ : null,
599
+ alarmCount: alarmsLog.number,
600
+ };
601
+ };
546
602
  const getTotalOperatingHours = (baseURL) =>
547
603
  /**
548
604
  * Calculates total operating hours across all power levels.
@@ -621,45 +677,7 @@ const getUsageAnalytics = (baseURL) =>
621
677
  */
622
678
  (jwtToken_1, macAddress_1, ...args_1) => __awaiter(void 0, [jwtToken_1, macAddress_1, ...args_1], void 0, function* (jwtToken, macAddress, serviceThreshold = DEFAULT_SERVICE_THRESHOLD) {
623
679
  const info = yield deviceInfo(baseURL)(jwtToken, macAddress);
624
- const totalCounters = info.nvm.total_counters;
625
- const serviceCounters = info.nvm.service_counters;
626
- const regeneration = info.nvm.regeneration;
627
- const alarmsLog = info.nvm.alarms_log;
628
- const totalOperatingHours = totalCounters.p1_working_time +
629
- totalCounters.p2_working_time +
630
- totalCounters.p3_working_time +
631
- totalCounters.p4_working_time +
632
- totalCounters.p5_working_time;
633
- const hoursSinceService = serviceCounters.p1_working_time +
634
- serviceCounters.p2_working_time +
635
- serviceCounters.p3_working_time +
636
- serviceCounters.p4_working_time +
637
- serviceCounters.p5_working_time;
638
- const powerDistribution = totalOperatingHours === 0
639
- ? { p1: 0, p2: 0, p3: 0, p4: 0, p5: 0 }
640
- : {
641
- p1: (totalCounters.p1_working_time / totalOperatingHours) * 100,
642
- p2: (totalCounters.p2_working_time / totalOperatingHours) * 100,
643
- p3: (totalCounters.p3_working_time / totalOperatingHours) * 100,
644
- p4: (totalCounters.p4_working_time / totalOperatingHours) * 100,
645
- p5: (totalCounters.p5_working_time / totalOperatingHours) * 100,
646
- };
647
- return {
648
- totalPowerOns: totalCounters.power_ons,
649
- totalOperatingHours,
650
- powerDistribution,
651
- serviceStatus: {
652
- totalServiceHours: info.status.counters.service_time,
653
- hoursSinceService,
654
- serviceThresholdHours: serviceThreshold,
655
- isServiceDue: hoursSinceService >= serviceThreshold,
656
- },
657
- blackoutCount: regeneration.blackout_counter,
658
- lastMaintenanceDate: regeneration.last_intervention > 0
659
- ? new Date(regeneration.last_intervention * 1000)
660
- : null,
661
- alarmCount: alarmsLog.number,
662
- };
680
+ return deriveUsageAnalytics(info, serviceThreshold);
663
681
  });
664
682
  const registerDevice = (baseURL) =>
665
683
  /**
@@ -10,7 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { strict as assert } from "assert";
11
11
  import pako from "pako";
12
12
  import sinon from "sinon";
13
- import { configure, createAuthService } from "../src/library";
13
+ import { configure, createAuthService, deriveUsageAnalytics, } from "../src/library";
14
14
  import { API_URL } from "./constants";
15
15
  /**
16
16
  * Helper to create a gzip-compressed Buffer object for testing.
@@ -1009,6 +1009,139 @@ describe("library", () => {
1009
1009
  assert.equal(result.lastMaintenanceDate, null);
1010
1010
  }));
1011
1011
  });
1012
+ describe("deriveUsageAnalytics", () => {
1013
+ const mockDeviceInfoForDerive = {
1014
+ status: {
1015
+ commands: {
1016
+ power: false,
1017
+ },
1018
+ temperatures: {
1019
+ enviroment: 20,
1020
+ set_air: 21,
1021
+ get_air: 20,
1022
+ set_water: 40,
1023
+ get_water: 35,
1024
+ },
1025
+ counters: {
1026
+ service_time: 1108,
1027
+ },
1028
+ flags: {
1029
+ is_pellet_in_reserve: false,
1030
+ },
1031
+ pellet: {
1032
+ autonomy_time: 180,
1033
+ },
1034
+ },
1035
+ nvm: {
1036
+ user_parameters: {
1037
+ language: 1,
1038
+ is_auto: false,
1039
+ is_fahrenheit: false,
1040
+ is_sound_active: false,
1041
+ enviroment_1_temperature: 19,
1042
+ enviroment_2_temperature: 20,
1043
+ enviroment_3_temperature: 20,
1044
+ manual_power: 1,
1045
+ fan_1_ventilation: 3,
1046
+ fan_2_ventilation: 0,
1047
+ fan_3_ventilation: 0,
1048
+ is_standby_active: false,
1049
+ standby_waiting_time: 60,
1050
+ },
1051
+ total_counters: {
1052
+ power_ons: 278,
1053
+ p1_working_time: 833,
1054
+ p2_working_time: 15,
1055
+ p3_working_time: 19,
1056
+ p4_working_time: 8,
1057
+ p5_working_time: 17,
1058
+ },
1059
+ service_counters: {
1060
+ p1_working_time: 100,
1061
+ p2_working_time: 10,
1062
+ p3_working_time: 5,
1063
+ p4_working_time: 2,
1064
+ p5_working_time: 1,
1065
+ },
1066
+ regeneration: {
1067
+ time: 0,
1068
+ last_intervention: 1577836800,
1069
+ daylight_time_flag: 0,
1070
+ blackout_counter: 43,
1071
+ airkare_working_hours_counter: 0,
1072
+ },
1073
+ alarms_log: {
1074
+ number: 6,
1075
+ index: 6,
1076
+ alarms: [],
1077
+ },
1078
+ },
1079
+ };
1080
+ it("should derive analytics from device info without API call", () => {
1081
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1082
+ const analytics = deriveUsageAnalytics(mockDeviceInfoForDerive);
1083
+ assert.equal(analytics.totalPowerOns, 278);
1084
+ assert.equal(analytics.totalOperatingHours, 892); // 833+15+19+8+17
1085
+ assert.equal(analytics.blackoutCount, 43);
1086
+ assert.equal(analytics.alarmCount, 6);
1087
+ });
1088
+ it("should calculate power distribution correctly", () => {
1089
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1090
+ const analytics = deriveUsageAnalytics(mockDeviceInfoForDerive);
1091
+ // P1: 833/892 ≈ 93.4%
1092
+ assert.ok(analytics.powerDistribution.p1 > 93);
1093
+ assert.ok(analytics.powerDistribution.p1 < 94);
1094
+ // Sum should be 100%
1095
+ const sum = Object.values(analytics.powerDistribution).reduce((a, b) => a + b, 0);
1096
+ assert.ok(Math.abs(sum - 100) < 0.001);
1097
+ });
1098
+ it("should handle zero operating hours", () => {
1099
+ const zeroHoursInfo = Object.assign(Object.assign({}, mockDeviceInfoForDerive), { nvm: Object.assign(Object.assign({}, mockDeviceInfoForDerive.nvm), { total_counters: {
1100
+ power_ons: 0,
1101
+ p1_working_time: 0,
1102
+ p2_working_time: 0,
1103
+ p3_working_time: 0,
1104
+ p4_working_time: 0,
1105
+ p5_working_time: 0,
1106
+ } }) });
1107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1108
+ const analytics = deriveUsageAnalytics(zeroHoursInfo);
1109
+ assert.deepEqual(analytics.powerDistribution, {
1110
+ p1: 0,
1111
+ p2: 0,
1112
+ p3: 0,
1113
+ p4: 0,
1114
+ p5: 0,
1115
+ });
1116
+ });
1117
+ it("should respect custom service threshold", () => {
1118
+ const analytics = deriveUsageAnalytics(
1119
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1120
+ mockDeviceInfoForDerive, 100);
1121
+ // 118 hours since service >= 100 threshold
1122
+ assert.equal(analytics.serviceStatus.isServiceDue, true);
1123
+ assert.equal(analytics.serviceStatus.serviceThresholdHours, 100);
1124
+ });
1125
+ it("should use default threshold of 2000 hours", () => {
1126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1127
+ const analytics = deriveUsageAnalytics(mockDeviceInfoForDerive);
1128
+ assert.equal(analytics.serviceStatus.serviceThresholdHours, 2000);
1129
+ assert.equal(analytics.serviceStatus.isServiceDue, false); // 118 < 2000
1130
+ });
1131
+ it("should convert last_intervention timestamp to Date", () => {
1132
+ var _a;
1133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1134
+ const analytics = deriveUsageAnalytics(mockDeviceInfoForDerive);
1135
+ assert.ok(analytics.lastMaintenanceDate instanceof Date);
1136
+ assert.equal((_a = analytics.lastMaintenanceDate) === null || _a === void 0 ? void 0 : _a.getTime(), 1577836800 * 1000);
1137
+ });
1138
+ it("should return null for lastMaintenanceDate when timestamp is 0", () => {
1139
+ const noMaintenanceInfo = Object.assign(Object.assign({}, mockDeviceInfoForDerive), { nvm: Object.assign(Object.assign({}, mockDeviceInfoForDerive.nvm), { regeneration: Object.assign(Object.assign({}, mockDeviceInfoForDerive.nvm.regeneration), { last_intervention: 0 }) }) });
1140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1141
+ const analytics = deriveUsageAnalytics(noMaintenanceInfo);
1142
+ assert.equal(analytics.lastMaintenanceDate, null);
1143
+ });
1144
+ });
1012
1145
  describe("Error Handling", () => {
1013
1146
  const errorTests = [
1014
1147
  { status: 400, statusText: "Bad Request" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.10.1",
3
+ "version": "1.10.2",
4
4
  "description": "",
5
5
  "main": "dist/cjs/src/index.js",
6
6
  "module": "dist/esm/src/index.js",
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ 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,
@@ -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 =