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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/bluetooth-protocol.d.ts +178 -0
- package/dist/cjs/src/bluetooth-protocol.js +423 -0
- package/dist/cjs/src/bluetooth-protocol.test.d.ts +1 -0
- package/dist/cjs/src/bluetooth-protocol.test.js +389 -0
- package/dist/cjs/src/bluetooth.d.ts +2 -0
- package/dist/cjs/src/bluetooth.js +17 -1
- package/dist/cjs/src/index.d.ts +1 -1
- package/dist/cjs/src/index.js +2 -1
- package/dist/cjs/src/library.d.ts +16 -0
- package/dist/cjs/src/library.js +59 -40
- package/dist/cjs/src/library.test.js +133 -0
- package/dist/esm/package.json +1 -1
- package/dist/esm/src/bluetooth-protocol.d.ts +178 -0
- package/dist/esm/src/bluetooth-protocol.js +415 -0
- package/dist/esm/src/bluetooth-protocol.test.d.ts +1 -0
- package/dist/esm/src/bluetooth-protocol.test.js +387 -0
- package/dist/esm/src/bluetooth.d.ts +2 -0
- package/dist/esm/src/bluetooth.js +8 -0
- package/dist/esm/src/index.d.ts +1 -1
- package/dist/esm/src/index.js +1 -1
- package/dist/esm/src/library.d.ts +16 -0
- package/dist/esm/src/library.js +57 -39
- package/dist/esm/src/library.test.js +134 -1
- package/package.json +1 -1
- package/src/bluetooth-protocol.test.ts +497 -0
- package/src/bluetooth-protocol.ts +524 -0
- package/src/bluetooth.ts +21 -0
- package/src/index.ts +1 -1
- package/src/library.test.ts +173 -1
- package/src/library.ts +69 -48
|
@@ -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" },
|
package/dist/esm/package.json
CHANGED
|
@@ -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
|
+
};
|