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.
@@ -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.11.0",
4
4
  "description": "",
5
5
  "main": "dist/cjs/src/index.js",
6
6
  "module": "dist/esm/src/index.js",
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Edilkamin BLE Protocol Implementation
3
+ *
4
+ * Transport-agnostic protocol layer for communicating with Edilkamin stoves via BLE.
5
+ * Handles AES-128-CBC encryption, CRC16-Modbus checksums, and Modbus packet building/parsing.
6
+ *
7
+ * The consuming application is responsible for:
8
+ * - BLE device scanning and connection
9
+ * - Writing to BLE characteristics
10
+ * - Subscribing to BLE notifications
11
+ *
12
+ * Protocol details derived from: https://github.com/netmb/Edilkamin_BT
13
+ */
14
+ /** Edilkamin GATT service UUID */
15
+ export declare const SERVICE_UUID = "0000abf0-0000-1000-8000-00805f9b34fb";
16
+ /** Write characteristic UUID (WRITE NO RESPONSE) */
17
+ export declare const WRITE_CHARACTERISTIC_UUID = "0000abf1-0000-1000-8000-00805f9b34fb";
18
+ /** Notify characteristic UUID (NOTIFY) */
19
+ export declare const NOTIFY_CHARACTERISTIC_UUID = "0000abf2-0000-1000-8000-00805f9b34fb";
20
+ /**
21
+ * Parsed Modbus response from the device.
22
+ */
23
+ export interface ModbusResponse {
24
+ /** Slave address (always 0x01 for Edilkamin) */
25
+ slaveAddress: number;
26
+ /** Function code (0x03 for read, 0x06 for write) */
27
+ functionCode: number;
28
+ /** Byte count (for read responses) */
29
+ byteCount?: number;
30
+ /** Response data */
31
+ data: Uint8Array;
32
+ /** Whether the response indicates an error */
33
+ isError: boolean;
34
+ }
35
+ /**
36
+ * Calculate CRC16-Modbus checksum.
37
+ *
38
+ * @param data - Data to calculate CRC for
39
+ * @returns 2-byte CRC in little-endian order [crcLo, crcHi]
40
+ */
41
+ export declare const crc16Modbus: (data: Uint8Array) => Uint8Array;
42
+ /**
43
+ * Encrypt data using AES-128-CBC (raw, without PKCS7 padding).
44
+ *
45
+ * This manually applies PKCS7 padding to make input a multiple of 16 bytes,
46
+ * encrypts with Web Crypto, then strips the extra padding block from output.
47
+ *
48
+ * @param plaintext - Data to encrypt (must be 32 bytes)
49
+ * @returns Encrypted data (32 bytes)
50
+ */
51
+ export declare const aesEncrypt: (plaintext: Uint8Array) => Promise<Uint8Array>;
52
+ /**
53
+ * Decrypt data using AES-128-CBC (raw, handling PKCS7 padding).
54
+ *
55
+ * @param ciphertext - Data to decrypt (must be 32 bytes)
56
+ * @returns Decrypted data (32 bytes)
57
+ */
58
+ export declare const aesDecrypt: (ciphertext: Uint8Array) => Promise<Uint8Array>;
59
+ /**
60
+ * Build and encrypt a command packet to send to the device.
61
+ *
62
+ * Packet structure (32 bytes before encryption):
63
+ * - Bytes 0-3: Unix timestamp (big-endian)
64
+ * - Bytes 4-19: Fixed key
65
+ * - Bytes 20-25: Modbus command (6 bytes)
66
+ * - Bytes 26-27: CRC16-Modbus of command
67
+ * - Bytes 28-31: Padding [0x04, 0x04, 0x04, 0x04]
68
+ *
69
+ * @param modbusCommand - 6-byte Modbus RTU command
70
+ * @returns 32-byte encrypted packet ready to send via BLE
71
+ */
72
+ export declare const createPacket: (modbusCommand: Uint8Array) => Promise<Uint8Array>;
73
+ /**
74
+ * Decrypt and parse a response packet from the device.
75
+ *
76
+ * Response structure (32 bytes before decryption):
77
+ * - Bytes 0-3: Unix timestamp
78
+ * - Bytes 4-19: Fixed key
79
+ * - Bytes 20-26: Modbus response (7 bytes)
80
+ * - Bytes 27-28: CRC16-Modbus
81
+ * - Bytes 29-31: Padding [0x03, 0x03, 0x03]
82
+ *
83
+ * @param encrypted - 32-byte encrypted response from BLE notification
84
+ * @returns Parsed Modbus response
85
+ */
86
+ export declare const parseResponse: (encrypted: Uint8Array) => Promise<ModbusResponse>;
87
+ /**
88
+ * Pre-built Modbus read commands for querying device state.
89
+ * Each command is 6 bytes: [SlaveAddr, FuncCode, RegHi, RegLo, CountHi, CountLo]
90
+ */
91
+ export declare const readCommands: {
92
+ /** Power state (0=off, 1=on) */
93
+ power: Uint8Array<ArrayBuffer>;
94
+ /** Current ambient temperature (value / 10 = °C) */
95
+ temperature: Uint8Array<ArrayBuffer>;
96
+ /** Target temperature (value / 10 = °C) */
97
+ targetTemperature: Uint8Array<ArrayBuffer>;
98
+ /** Power level (1-5) */
99
+ powerLevel: Uint8Array<ArrayBuffer>;
100
+ /** Fan 1 speed (0=auto, 1-5=speed) */
101
+ fan1Speed: Uint8Array<ArrayBuffer>;
102
+ /** Fan 2 speed (0=auto, 1-5=speed) */
103
+ fan2Speed: Uint8Array<ArrayBuffer>;
104
+ /** Device state code */
105
+ state: Uint8Array<ArrayBuffer>;
106
+ /** Alarm status code */
107
+ alarm: Uint8Array<ArrayBuffer>;
108
+ /** Pellet warning status */
109
+ pelletAlarm: Uint8Array<ArrayBuffer>;
110
+ /** Auto mode (0=manual, 1=auto) */
111
+ autoMode: Uint8Array<ArrayBuffer>;
112
+ /** Standby mode status */
113
+ standby: Uint8Array<ArrayBuffer>;
114
+ };
115
+ /**
116
+ * Builder functions for Modbus write commands.
117
+ * Each function returns a 6-byte command: [SlaveAddr, FuncCode, RegHi, RegLo, ValHi, ValLo]
118
+ */
119
+ export declare const writeCommands: {
120
+ /**
121
+ * Turn power on or off.
122
+ * @param on - true to turn on, false to turn off
123
+ */
124
+ setPower: (on: boolean) => Uint8Array;
125
+ /**
126
+ * Set target temperature.
127
+ * @param tempCelsius - Temperature in Celsius (e.g., 21.5)
128
+ */
129
+ setTemperature: (tempCelsius: number) => Uint8Array;
130
+ /**
131
+ * Set power level.
132
+ * @param level - Power level (1-5)
133
+ */
134
+ setPowerLevel: (level: number) => Uint8Array;
135
+ /**
136
+ * Set fan 1 speed.
137
+ * @param speed - Fan speed (0=auto, 1-5=manual speed)
138
+ */
139
+ setFan1Speed: (speed: number) => Uint8Array;
140
+ /**
141
+ * Set fan 2 speed.
142
+ * @param speed - Fan speed (0=auto, 1-5=manual speed)
143
+ */
144
+ setFan2Speed: (speed: number) => Uint8Array;
145
+ /**
146
+ * Enable or disable auto mode.
147
+ * @param enabled - true to enable auto mode
148
+ */
149
+ setAutoMode: (enabled: boolean) => Uint8Array;
150
+ /**
151
+ * Enable or disable standby mode.
152
+ * @param enabled - true to enable standby mode
153
+ */
154
+ setStandby: (enabled: boolean) => Uint8Array;
155
+ };
156
+ /**
157
+ * Parser functions to extract meaningful values from Modbus responses.
158
+ */
159
+ export declare const parsers: {
160
+ /**
161
+ * Parse boolean response (power state, auto mode, etc.).
162
+ * @param response - Parsed Modbus response
163
+ * @returns true if value is 0x01, false if 0x00
164
+ */
165
+ boolean: (response: ModbusResponse) => boolean;
166
+ /**
167
+ * Parse temperature response.
168
+ * @param response - Parsed Modbus response
169
+ * @returns Temperature in Celsius
170
+ */
171
+ temperature: (response: ModbusResponse) => number;
172
+ /**
173
+ * Parse numeric value (power level, fan speed, state code, etc.).
174
+ * @param response - Parsed Modbus response
175
+ * @returns Numeric value
176
+ */
177
+ number: (response: ModbusResponse) => number;
178
+ };