edilkamin 1.10.2 → 1.12.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.
Files changed (52) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/bluetooth-protocol.d.ts +178 -0
  3. package/dist/cjs/src/bluetooth-protocol.js +423 -0
  4. package/dist/cjs/src/bluetooth-protocol.test.d.ts +1 -0
  5. package/dist/cjs/src/bluetooth-protocol.test.js +389 -0
  6. package/dist/cjs/src/bluetooth-utils.js +2 -6
  7. package/dist/cjs/src/bluetooth.d.ts +2 -0
  8. package/dist/cjs/src/bluetooth.js +17 -1
  9. package/dist/cjs/src/cli.js +9 -8
  10. package/dist/cjs/src/index.d.ts +4 -3
  11. package/dist/cjs/src/index.js +14 -1
  12. package/dist/cjs/src/library.d.ts +30 -0
  13. package/dist/cjs/src/library.js +97 -3
  14. package/dist/cjs/src/library.test.js +225 -4
  15. package/dist/cjs/src/mac-utils.d.ts +15 -0
  16. package/dist/cjs/src/mac-utils.js +24 -0
  17. package/dist/cjs/src/mac-utils.test.d.ts +1 -0
  18. package/dist/cjs/src/mac-utils.test.js +41 -0
  19. package/dist/cjs/src/types.d.ts +94 -2
  20. package/dist/cjs/src/types.js +95 -1
  21. package/dist/esm/package.json +1 -1
  22. package/dist/esm/src/bluetooth-protocol.d.ts +178 -0
  23. package/dist/esm/src/bluetooth-protocol.js +415 -0
  24. package/dist/esm/src/bluetooth-protocol.test.d.ts +1 -0
  25. package/dist/esm/src/bluetooth-protocol.test.js +387 -0
  26. package/dist/esm/src/bluetooth-utils.js +2 -6
  27. package/dist/esm/src/bluetooth.d.ts +2 -0
  28. package/dist/esm/src/bluetooth.js +8 -0
  29. package/dist/esm/src/cli.js +9 -8
  30. package/dist/esm/src/index.d.ts +4 -3
  31. package/dist/esm/src/index.js +3 -2
  32. package/dist/esm/src/library.d.ts +30 -0
  33. package/dist/esm/src/library.js +94 -2
  34. package/dist/esm/src/library.test.js +226 -5
  35. package/dist/esm/src/mac-utils.d.ts +15 -0
  36. package/dist/esm/src/mac-utils.js +21 -0
  37. package/dist/esm/src/mac-utils.test.d.ts +1 -0
  38. package/dist/esm/src/mac-utils.test.js +39 -0
  39. package/dist/esm/src/types.d.ts +94 -2
  40. package/dist/esm/src/types.js +89 -1
  41. package/package.json +1 -1
  42. package/src/bluetooth-protocol.test.ts +497 -0
  43. package/src/bluetooth-protocol.ts +524 -0
  44. package/src/bluetooth-utils.ts +3 -7
  45. package/src/bluetooth.ts +21 -0
  46. package/src/cli.ts +9 -8
  47. package/src/index.ts +24 -2
  48. package/src/library.test.ts +325 -4
  49. package/src/library.ts +109 -2
  50. package/src/mac-utils.test.ts +60 -0
  51. package/src/mac-utils.ts +22 -0
  52. package/src/types.ts +144 -1
@@ -0,0 +1,415 @@
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
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
15
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
16
+ return new (P || (P = Promise))(function (resolve, reject) {
17
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
18
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
19
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
20
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
21
+ });
22
+ };
23
+ // =============================================================================
24
+ // BLE Characteristic UUIDs (for consuming apps)
25
+ // =============================================================================
26
+ /** Edilkamin GATT service UUID */
27
+ export const SERVICE_UUID = "0000abf0-0000-1000-8000-00805f9b34fb";
28
+ /** Write characteristic UUID (WRITE NO RESPONSE) */
29
+ export const WRITE_CHARACTERISTIC_UUID = "0000abf1-0000-1000-8000-00805f9b34fb";
30
+ /** Notify characteristic UUID (NOTIFY) */
31
+ export const NOTIFY_CHARACTERISTIC_UUID = "0000abf2-0000-1000-8000-00805f9b34fb";
32
+ // =============================================================================
33
+ // Constants (private)
34
+ // =============================================================================
35
+ /** AES-128-CBC key (16 bytes) */
36
+ const AES_KEY = new Uint8Array([
37
+ 0x80, 0x29, 0x47, 0x46, 0xdb, 0x35, 0x4d, 0xb7, 0x4c, 0x37, 0x01, 0xcf, 0x30,
38
+ 0xef, 0xdd, 0x65,
39
+ ]);
40
+ /** AES-128-CBC initialization vector (16 bytes) */
41
+ const AES_IV = new Uint8Array([
42
+ 0xda, 0x1a, 0x55, 0x73, 0x49, 0xf2, 0x5c, 0x64, 0x1b, 0x1a, 0x21, 0xd2, 0x6f,
43
+ 0x5b, 0x21, 0x8a,
44
+ ]);
45
+ /** Fixed key embedded in every packet (16 bytes) */
46
+ const FIXED_KEY = new Uint8Array([
47
+ 0x31, 0xdd, 0x34, 0x51, 0x26, 0x39, 0x20, 0x23, 0x9f, 0x4b, 0x68, 0x20, 0xe7,
48
+ 0x25, 0xfc, 0x75,
49
+ ]);
50
+ /** CRC16-Modbus high byte lookup table */
51
+ const CRC_HI_TABLE = new Uint8Array([
52
+ 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00,
53
+ 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1,
54
+ 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81,
55
+ 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
56
+ 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01,
57
+ 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0,
58
+ 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80,
59
+ 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41,
60
+ 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00,
61
+ 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0,
62
+ 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80,
63
+ 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41,
64
+ 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01,
65
+ 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1,
66
+ 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81,
67
+ 0x40, 0x01, 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
68
+ 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1, 0x81, 0x40, 0x01,
69
+ 0xc0, 0x80, 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x00, 0xc1,
70
+ 0x81, 0x40, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40, 0x01, 0xc0, 0x80,
71
+ 0x41, 0x01, 0xc0, 0x80, 0x41, 0x00, 0xc1, 0x81, 0x40,
72
+ ]);
73
+ /** CRC16-Modbus low byte lookup table */
74
+ const CRC_LO_TABLE = new Uint8Array([
75
+ 0x00, 0xc0, 0xc1, 0x01, 0xc3, 0x03, 0x02, 0xc2, 0xc6, 0x06, 0x07, 0xc7, 0x05,
76
+ 0xc5, 0xc4, 0x04, 0xcc, 0x0c, 0x0d, 0xcd, 0x0f, 0xcf, 0xce, 0x0e, 0x0a, 0xca,
77
+ 0xcb, 0x0b, 0xc9, 0x09, 0x08, 0xc8, 0xd8, 0x18, 0x19, 0xd9, 0x1b, 0xdb, 0xda,
78
+ 0x1a, 0x1e, 0xde, 0xdf, 0x1f, 0xdd, 0x1d, 0x1c, 0xdc, 0x14, 0xd4, 0xd5, 0x15,
79
+ 0xd7, 0x17, 0x16, 0xd6, 0xd2, 0x12, 0x13, 0xd3, 0x11, 0xd1, 0xd0, 0x10, 0xf0,
80
+ 0x30, 0x31, 0xf1, 0x33, 0xf3, 0xf2, 0x32, 0x36, 0xf6, 0xf7, 0x37, 0xf5, 0x35,
81
+ 0x34, 0xf4, 0x3c, 0xfc, 0xfd, 0x3d, 0xff, 0x3f, 0x3e, 0xfe, 0xfa, 0x3a, 0x3b,
82
+ 0xfb, 0x39, 0xf9, 0xf8, 0x38, 0x28, 0xe8, 0xe9, 0x29, 0xeb, 0x2b, 0x2a, 0xea,
83
+ 0xee, 0x2e, 0x2f, 0xef, 0x2d, 0xed, 0xec, 0x2c, 0xe4, 0x24, 0x25, 0xe5, 0x27,
84
+ 0xe7, 0xe6, 0x26, 0x22, 0xe2, 0xe3, 0x23, 0xe1, 0x21, 0x20, 0xe0, 0xa0, 0x60,
85
+ 0x61, 0xa1, 0x63, 0xa3, 0xa2, 0x62, 0x66, 0xa6, 0xa7, 0x67, 0xa5, 0x65, 0x64,
86
+ 0xa4, 0x6c, 0xac, 0xad, 0x6d, 0xaf, 0x6f, 0x6e, 0xae, 0xaa, 0x6a, 0x6b, 0xab,
87
+ 0x69, 0xa9, 0xa8, 0x68, 0x78, 0xb8, 0xb9, 0x79, 0xbb, 0x7b, 0x7a, 0xba, 0xbe,
88
+ 0x7e, 0x7f, 0xbf, 0x7d, 0xbd, 0xbc, 0x7c, 0xb4, 0x74, 0x75, 0xb5, 0x77, 0xb7,
89
+ 0xb6, 0x76, 0x72, 0xb2, 0xb3, 0x73, 0xb1, 0x71, 0x70, 0xb0, 0x50, 0x90, 0x91,
90
+ 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54,
91
+ 0x9c, 0x5c, 0x5d, 0x9d, 0x5f, 0x9f, 0x9e, 0x5e, 0x5a, 0x9a, 0x9b, 0x5b, 0x99,
92
+ 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4b, 0x8b, 0x8a, 0x4a, 0x4e, 0x8e,
93
+ 0x8f, 0x4f, 0x8d, 0x4d, 0x4c, 0x8c, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46,
94
+ 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40,
95
+ ]);
96
+ // =============================================================================
97
+ // CRC16-Modbus
98
+ // =============================================================================
99
+ /**
100
+ * Calculate CRC16-Modbus checksum.
101
+ *
102
+ * @param data - Data to calculate CRC for
103
+ * @returns 2-byte CRC in little-endian order [crcLo, crcHi]
104
+ */
105
+ export const crc16Modbus = (data) => {
106
+ let crcHi = 0xff;
107
+ let crcLo = 0xff;
108
+ for (let i = 0; i < data.length; i++) {
109
+ const index = crcLo ^ data[i];
110
+ crcLo = crcHi ^ CRC_HI_TABLE[index];
111
+ crcHi = CRC_LO_TABLE[index];
112
+ }
113
+ // Return [crcLo, crcHi] - note the order!
114
+ return new Uint8Array([crcLo, crcHi]);
115
+ };
116
+ // =============================================================================
117
+ // AES-128-CBC Encryption
118
+ // =============================================================================
119
+ /**
120
+ * Import AES key for encryption/decryption.
121
+ */
122
+ const importAesKey = (usage) => __awaiter(void 0, void 0, void 0, function* () {
123
+ return crypto.subtle.importKey("raw", AES_KEY.buffer, { name: "AES-CBC" }, false, [usage]);
124
+ });
125
+ /**
126
+ * Encrypt data using AES-128-CBC (raw, without PKCS7 padding).
127
+ *
128
+ * This manually applies PKCS7 padding to make input a multiple of 16 bytes,
129
+ * encrypts with Web Crypto, then strips the extra padding block from output.
130
+ *
131
+ * @param plaintext - Data to encrypt (must be 32 bytes)
132
+ * @returns Encrypted data (32 bytes)
133
+ */
134
+ export const aesEncrypt = (plaintext) => __awaiter(void 0, void 0, void 0, function* () {
135
+ const key = yield importAesKey("encrypt");
136
+ // Clone IV since AES-CBC modifies it during operation
137
+ const iv = new Uint8Array(AES_IV);
138
+ // Add PKCS7 padding (16 bytes of 0x10 for 32-byte input)
139
+ const padded = new Uint8Array(48);
140
+ padded.set(plaintext, 0);
141
+ padded.fill(0x10, 32);
142
+ const encrypted = yield crypto.subtle.encrypt({ name: "AES-CBC", iv: iv.buffer }, key, padded.buffer);
143
+ // Return first 32 bytes (skip the padding block)
144
+ return new Uint8Array(encrypted).slice(0, 32);
145
+ });
146
+ /**
147
+ * Decrypt data using AES-128-CBC (raw, handling PKCS7 padding).
148
+ *
149
+ * @param ciphertext - Data to decrypt (must be 32 bytes)
150
+ * @returns Decrypted data (32 bytes)
151
+ */
152
+ export const aesDecrypt = (ciphertext) => __awaiter(void 0, void 0, void 0, function* () {
153
+ const key = yield importAesKey("decrypt");
154
+ // Clone IV since AES-CBC modifies it during operation
155
+ const iv = new Uint8Array(AES_IV);
156
+ // To decrypt without padding validation, we need to:
157
+ // 1. Decrypt to get raw blocks
158
+ // 2. Handle padding ourselves
159
+ //
160
+ // The trick is to append a valid padding block that we encrypt separately,
161
+ // then decrypt the whole thing.
162
+ // First, encrypt a padding block using the last ciphertext block as IV
163
+ // This creates the correct padding block for decryption
164
+ const lastBlock = ciphertext.slice(16, 32);
165
+ const paddingPlain = new Uint8Array(16).fill(0x10); // Valid PKCS7 for 0 extra bytes
166
+ const paddingKey = yield importAesKey("encrypt");
167
+ const encryptedPadding = yield crypto.subtle.encrypt({ name: "AES-CBC", iv: lastBlock.buffer }, paddingKey, paddingPlain.buffer);
168
+ // Build full ciphertext with valid padding block
169
+ const fullCiphertext = new Uint8Array(48);
170
+ fullCiphertext.set(ciphertext, 0);
171
+ fullCiphertext.set(new Uint8Array(encryptedPadding).slice(0, 16), 32);
172
+ const decrypted = yield crypto.subtle.decrypt({ name: "AES-CBC", iv: iv.buffer }, key, fullCiphertext.buffer);
173
+ // Return first 32 bytes (the actual data)
174
+ return new Uint8Array(decrypted).slice(0, 32);
175
+ });
176
+ // =============================================================================
177
+ // Packet Building
178
+ // =============================================================================
179
+ /**
180
+ * Get current Unix timestamp as 4 big-endian bytes.
181
+ */
182
+ const getTimestamp = () => {
183
+ const now = Math.floor(Date.now() / 1000);
184
+ return new Uint8Array([
185
+ (now >> 24) & 0xff,
186
+ (now >> 16) & 0xff,
187
+ (now >> 8) & 0xff,
188
+ now & 0xff,
189
+ ]);
190
+ };
191
+ /**
192
+ * Build and encrypt a command packet to send to the device.
193
+ *
194
+ * Packet structure (32 bytes before encryption):
195
+ * - Bytes 0-3: Unix timestamp (big-endian)
196
+ * - Bytes 4-19: Fixed key
197
+ * - Bytes 20-25: Modbus command (6 bytes)
198
+ * - Bytes 26-27: CRC16-Modbus of command
199
+ * - Bytes 28-31: Padding [0x04, 0x04, 0x04, 0x04]
200
+ *
201
+ * @param modbusCommand - 6-byte Modbus RTU command
202
+ * @returns 32-byte encrypted packet ready to send via BLE
203
+ */
204
+ export const createPacket = (modbusCommand) => __awaiter(void 0, void 0, void 0, function* () {
205
+ if (modbusCommand.length !== 6) {
206
+ throw new Error("Modbus command must be exactly 6 bytes");
207
+ }
208
+ // Build 32-byte plaintext packet
209
+ const packet = new Uint8Array(32);
210
+ // Timestamp (4 bytes, big-endian)
211
+ packet.set(getTimestamp(), 0);
212
+ // Fixed key (16 bytes)
213
+ packet.set(FIXED_KEY, 4);
214
+ // Modbus payload (6 bytes)
215
+ packet.set(modbusCommand, 20);
216
+ // CRC16-Modbus (2 bytes)
217
+ const crc = crc16Modbus(modbusCommand);
218
+ packet.set(crc, 26);
219
+ // Padding (4 bytes)
220
+ packet.set([0x04, 0x04, 0x04, 0x04], 28);
221
+ // Encrypt with AES-128-CBC
222
+ return aesEncrypt(packet);
223
+ });
224
+ // =============================================================================
225
+ // Response Parsing
226
+ // =============================================================================
227
+ /**
228
+ * Decrypt and parse a response packet from the device.
229
+ *
230
+ * Response structure (32 bytes before decryption):
231
+ * - Bytes 0-3: Unix timestamp
232
+ * - Bytes 4-19: Fixed key
233
+ * - Bytes 20-26: Modbus response (7 bytes)
234
+ * - Bytes 27-28: CRC16-Modbus
235
+ * - Bytes 29-31: Padding [0x03, 0x03, 0x03]
236
+ *
237
+ * @param encrypted - 32-byte encrypted response from BLE notification
238
+ * @returns Parsed Modbus response
239
+ */
240
+ export const parseResponse = (encrypted) => __awaiter(void 0, void 0, void 0, function* () {
241
+ if (encrypted.length !== 32) {
242
+ throw new Error("Response must be exactly 32 bytes");
243
+ }
244
+ // Decrypt
245
+ const decrypted = yield aesDecrypt(encrypted);
246
+ // Extract Modbus response (bytes 20-26, 7 bytes)
247
+ const modbusResponse = decrypted.slice(20, 27);
248
+ const slaveAddress = modbusResponse[0];
249
+ const functionCode = modbusResponse[1];
250
+ // Check for Modbus error (function code has high bit set)
251
+ const isError = (functionCode & 0x80) !== 0;
252
+ if (isError) {
253
+ return {
254
+ slaveAddress,
255
+ functionCode: functionCode & 0x7f,
256
+ data: new Uint8Array([modbusResponse[2]]), // Error code
257
+ isError: true,
258
+ };
259
+ }
260
+ // Read response: [slaveAddr, funcCode, byteCount, dataHi, dataLo, crcLo, crcHi]
261
+ if (functionCode === 0x03) {
262
+ const byteCount = modbusResponse[2];
263
+ return {
264
+ slaveAddress,
265
+ functionCode,
266
+ byteCount,
267
+ data: modbusResponse.slice(3, 3 + byteCount),
268
+ isError: false,
269
+ };
270
+ }
271
+ // Write response: [slaveAddr, funcCode, regHi, regLo, valHi, valLo, crcLo, crcHi]
272
+ // (echo of command - return register and value bytes)
273
+ return {
274
+ slaveAddress,
275
+ functionCode,
276
+ data: modbusResponse.slice(2, 6),
277
+ isError: false,
278
+ };
279
+ });
280
+ // =============================================================================
281
+ // Modbus Read Commands (Function Code 0x03)
282
+ // =============================================================================
283
+ /**
284
+ * Pre-built Modbus read commands for querying device state.
285
+ * Each command is 6 bytes: [SlaveAddr, FuncCode, RegHi, RegLo, CountHi, CountLo]
286
+ */
287
+ export const readCommands = {
288
+ /** Power state (0=off, 1=on) */
289
+ power: new Uint8Array([0x01, 0x03, 0x05, 0x29, 0x00, 0x01]),
290
+ /** Current ambient temperature (value / 10 = °C) */
291
+ temperature: new Uint8Array([0x01, 0x03, 0x05, 0x25, 0x00, 0x01]),
292
+ /** Target temperature (value / 10 = °C) */
293
+ targetTemperature: new Uint8Array([0x01, 0x03, 0x05, 0x37, 0x00, 0x01]),
294
+ /** Power level (1-5) */
295
+ powerLevel: new Uint8Array([0x01, 0x03, 0x04, 0x40, 0x00, 0x01]),
296
+ /** Fan 1 speed (0=auto, 1-5=speed) */
297
+ fan1Speed: new Uint8Array([0x01, 0x03, 0x05, 0x4b, 0x00, 0x01]),
298
+ /** Fan 2 speed (0=auto, 1-5=speed) */
299
+ fan2Speed: new Uint8Array([0x01, 0x03, 0x05, 0x4d, 0x00, 0x01]),
300
+ /** Device state code */
301
+ state: new Uint8Array([0x01, 0x03, 0x05, 0x3b, 0x00, 0x01]),
302
+ /** Alarm status code */
303
+ alarm: new Uint8Array([0x01, 0x03, 0x04, 0xc7, 0x00, 0x01]),
304
+ /** Pellet warning status */
305
+ pelletAlarm: new Uint8Array([0x01, 0x03, 0x04, 0xd5, 0x00, 0x01]),
306
+ /** Auto mode (0=manual, 1=auto) */
307
+ autoMode: new Uint8Array([0x01, 0x03, 0x04, 0x43, 0x00, 0x01]),
308
+ /** Standby mode status */
309
+ standby: new Uint8Array([0x01, 0x03, 0x04, 0x44, 0x00, 0x01]),
310
+ };
311
+ // =============================================================================
312
+ // Modbus Write Commands (Function Code 0x06)
313
+ // =============================================================================
314
+ /**
315
+ * Builder functions for Modbus write commands.
316
+ * Each function returns a 6-byte command: [SlaveAddr, FuncCode, RegHi, RegLo, ValHi, ValLo]
317
+ */
318
+ export const writeCommands = {
319
+ /**
320
+ * Turn power on or off.
321
+ * @param on - true to turn on, false to turn off
322
+ */
323
+ setPower: (on) => new Uint8Array([0x01, 0x06, 0x03, 0x1c, 0x00, on ? 0x01 : 0x00]),
324
+ /**
325
+ * Set target temperature.
326
+ * @param tempCelsius - Temperature in Celsius (e.g., 21.5)
327
+ */
328
+ setTemperature: (tempCelsius) => {
329
+ const value = Math.round(tempCelsius * 10);
330
+ return new Uint8Array([
331
+ 0x01,
332
+ 0x06,
333
+ 0x05,
334
+ 0x25,
335
+ (value >> 8) & 0xff,
336
+ value & 0xff,
337
+ ]);
338
+ },
339
+ /**
340
+ * Set power level.
341
+ * @param level - Power level (1-5)
342
+ */
343
+ setPowerLevel: (level) => {
344
+ if (level < 1 || level > 5)
345
+ throw new Error("Power level must be 1-5");
346
+ return new Uint8Array([0x01, 0x06, 0x04, 0x40, 0x00, level]);
347
+ },
348
+ /**
349
+ * Set fan 1 speed.
350
+ * @param speed - Fan speed (0=auto, 1-5=manual speed)
351
+ */
352
+ setFan1Speed: (speed) => {
353
+ if (speed < 0 || speed > 5)
354
+ throw new Error("Fan speed must be 0-5");
355
+ return new Uint8Array([0x01, 0x06, 0x05, 0x4b, 0x00, speed]);
356
+ },
357
+ /**
358
+ * Set fan 2 speed.
359
+ * @param speed - Fan speed (0=auto, 1-5=manual speed)
360
+ */
361
+ setFan2Speed: (speed) => {
362
+ if (speed < 0 || speed > 5)
363
+ throw new Error("Fan speed must be 0-5");
364
+ return new Uint8Array([0x01, 0x06, 0x05, 0x4d, 0x00, speed]);
365
+ },
366
+ /**
367
+ * Enable or disable auto mode.
368
+ * @param enabled - true to enable auto mode
369
+ */
370
+ setAutoMode: (enabled) => new Uint8Array([0x01, 0x06, 0x04, 0x43, 0x00, enabled ? 0x01 : 0x00]),
371
+ /**
372
+ * Enable or disable standby mode.
373
+ * @param enabled - true to enable standby mode
374
+ */
375
+ setStandby: (enabled) => new Uint8Array([0x01, 0x06, 0x04, 0x44, 0x00, enabled ? 0x01 : 0x00]),
376
+ };
377
+ // =============================================================================
378
+ // Response Parsers
379
+ // =============================================================================
380
+ /**
381
+ * Parser functions to extract meaningful values from Modbus responses.
382
+ */
383
+ export const parsers = {
384
+ /**
385
+ * Parse boolean response (power state, auto mode, etc.).
386
+ * @param response - Parsed Modbus response
387
+ * @returns true if value is 0x01, false if 0x00
388
+ */
389
+ boolean: (response) => {
390
+ if (response.isError)
391
+ throw new Error(`Modbus error: ${response.data[0]}`);
392
+ return response.data[1] === 0x01;
393
+ },
394
+ /**
395
+ * Parse temperature response.
396
+ * @param response - Parsed Modbus response
397
+ * @returns Temperature in Celsius
398
+ */
399
+ temperature: (response) => {
400
+ if (response.isError)
401
+ throw new Error(`Modbus error: ${response.data[0]}`);
402
+ const value = (response.data[0] << 8) | response.data[1];
403
+ return value / 10;
404
+ },
405
+ /**
406
+ * Parse numeric value (power level, fan speed, state code, etc.).
407
+ * @param response - Parsed Modbus response
408
+ * @returns Numeric value
409
+ */
410
+ number: (response) => {
411
+ if (response.isError)
412
+ throw new Error(`Modbus error: ${response.data[0]}`);
413
+ return (response.data[0] << 8) | response.data[1];
414
+ },
415
+ };
@@ -0,0 +1 @@
1
+ export {};