edilkamin 1.7.4 → 1.9.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 (40) hide show
  1. package/.github/dependabot.yml +5 -1
  2. package/README.md +92 -4
  3. package/dist/cjs/package.json +16 -5
  4. package/dist/cjs/src/bluetooth-utils.d.ts +13 -0
  5. package/dist/cjs/src/bluetooth-utils.js +28 -0
  6. package/dist/cjs/src/bluetooth-utils.test.d.ts +1 -0
  7. package/dist/cjs/src/bluetooth-utils.test.js +35 -0
  8. package/dist/cjs/src/bluetooth.d.ts +40 -0
  9. package/dist/cjs/src/bluetooth.js +107 -0
  10. package/dist/cjs/src/cli.js +130 -0
  11. package/dist/cjs/src/index.d.ts +2 -1
  12. package/dist/cjs/src/index.js +3 -1
  13. package/dist/cjs/src/library.d.ts +26 -0
  14. package/dist/cjs/src/library.js +361 -0
  15. package/dist/cjs/src/library.test.js +266 -0
  16. package/dist/cjs/src/types.d.ts +30 -1
  17. package/dist/esm/package.json +16 -5
  18. package/dist/esm/src/bluetooth-utils.d.ts +13 -0
  19. package/dist/esm/src/bluetooth-utils.js +25 -0
  20. package/dist/esm/src/bluetooth-utils.test.d.ts +1 -0
  21. package/dist/esm/src/bluetooth-utils.test.js +33 -0
  22. package/dist/esm/src/bluetooth.d.ts +40 -0
  23. package/dist/esm/src/bluetooth.js +100 -0
  24. package/dist/esm/src/cli.js +130 -0
  25. package/dist/esm/src/index.d.ts +2 -1
  26. package/dist/esm/src/index.js +1 -0
  27. package/dist/esm/src/library.d.ts +26 -0
  28. package/dist/esm/src/library.js +361 -0
  29. package/dist/esm/src/library.test.js +266 -0
  30. package/dist/esm/src/types.d.ts +30 -1
  31. package/package.json +16 -5
  32. package/src/bluetooth-utils.test.ts +46 -0
  33. package/src/bluetooth-utils.ts +29 -0
  34. package/src/bluetooth.ts +115 -0
  35. package/src/cli.ts +249 -0
  36. package/src/index.ts +2 -0
  37. package/src/library.test.ts +372 -0
  38. package/src/library.ts +426 -0
  39. package/src/types.ts +35 -0
  40. package/tsconfig.json +1 -0
@@ -182,9 +182,35 @@ describe("library", () => {
182
182
  "setPowerOff",
183
183
  "setPowerOn",
184
184
  "getPower",
185
+ "setPowerLevel",
186
+ "getPowerLevel",
187
+ "setFan1Speed",
188
+ "setFan2Speed",
189
+ "setFan3Speed",
190
+ "getFan1Speed",
191
+ "getFan2Speed",
192
+ "getFan3Speed",
193
+ "setAirkare",
194
+ "setRelax",
195
+ "setStandby",
196
+ "getStandby",
197
+ "setStandbyTime",
198
+ "getStandbyTime",
199
+ "setAuto",
200
+ "getAuto",
185
201
  "getEnvironmentTemperature",
186
202
  "getTargetTemperature",
187
203
  "setTargetTemperature",
204
+ "setEnvironment2Temperature",
205
+ "getEnvironment2Temperature",
206
+ "setEnvironment3Temperature",
207
+ "getEnvironment3Temperature",
208
+ "setMeasureUnit",
209
+ "getMeasureUnit",
210
+ "setLanguage",
211
+ "getLanguage",
212
+ "getPelletInReserve",
213
+ "getPelletAutonomyTime",
188
214
  ];
189
215
  it("should create API methods with the correct baseURL", () => __awaiter(void 0, void 0, void 0, function* () {
190
216
  const baseURL = "https://example.com/api/";
@@ -215,10 +241,27 @@ describe("library", () => {
215
241
  temperatures: {
216
242
  enviroment: 19,
217
243
  },
244
+ flags: {
245
+ is_pellet_in_reserve: false,
246
+ },
247
+ pellet: {
248
+ autonomy_time: 180,
249
+ },
218
250
  },
219
251
  nvm: {
220
252
  user_parameters: {
221
253
  enviroment_1_temperature: 22,
254
+ enviroment_2_temperature: 18,
255
+ enviroment_3_temperature: 20,
256
+ manual_power: 3,
257
+ fan_1_ventilation: 2,
258
+ fan_2_ventilation: 3,
259
+ fan_3_ventilation: 4,
260
+ is_standby_active: true,
261
+ standby_waiting_time: 30,
262
+ is_auto: true,
263
+ is_fahrenheit: false,
264
+ language: 2,
222
265
  },
223
266
  },
224
267
  };
@@ -274,6 +317,26 @@ describe("library", () => {
274
317
  call: (api, token, mac) => api.getPower(token, mac),
275
318
  expectedResult: true,
276
319
  },
320
+ {
321
+ method: "getPowerLevel",
322
+ call: (api, token, mac) => api.getPowerLevel(token, mac),
323
+ expectedResult: 3,
324
+ },
325
+ {
326
+ method: "getFan1Speed",
327
+ call: (api, token, mac) => api.getFan1Speed(token, mac),
328
+ expectedResult: 2,
329
+ },
330
+ {
331
+ method: "getFan2Speed",
332
+ call: (api, token, mac) => api.getFan2Speed(token, mac),
333
+ expectedResult: 3,
334
+ },
335
+ {
336
+ method: "getFan3Speed",
337
+ call: (api, token, mac) => api.getFan3Speed(token, mac),
338
+ expectedResult: 4,
339
+ },
277
340
  {
278
341
  method: "getEnvironmentTemperature",
279
342
  call: (api, token, mac) => api.getEnvironmentTemperature(token, mac),
@@ -284,6 +347,51 @@ describe("library", () => {
284
347
  call: (api, token, mac) => api.getTargetTemperature(token, mac),
285
348
  expectedResult: 22,
286
349
  },
350
+ {
351
+ method: "getStandby",
352
+ call: (api, token, mac) => api.getStandby(token, mac),
353
+ expectedResult: true,
354
+ },
355
+ {
356
+ method: "getStandbyTime",
357
+ call: (api, token, mac) => api.getStandbyTime(token, mac),
358
+ expectedResult: 30,
359
+ },
360
+ {
361
+ method: "getAuto",
362
+ call: (api, token, mac) => api.getAuto(token, mac),
363
+ expectedResult: true,
364
+ },
365
+ {
366
+ method: "getEnvironment2Temperature",
367
+ call: (api, token, mac) => api.getEnvironment2Temperature(token, mac),
368
+ expectedResult: 18,
369
+ },
370
+ {
371
+ method: "getEnvironment3Temperature",
372
+ call: (api, token, mac) => api.getEnvironment3Temperature(token, mac),
373
+ expectedResult: 20,
374
+ },
375
+ {
376
+ method: "getMeasureUnit",
377
+ call: (api, token, mac) => api.getMeasureUnit(token, mac),
378
+ expectedResult: false,
379
+ },
380
+ {
381
+ method: "getLanguage",
382
+ call: (api, token, mac) => api.getLanguage(token, mac),
383
+ expectedResult: 2,
384
+ },
385
+ {
386
+ method: "getPelletInReserve",
387
+ call: (api, token, mac) => api.getPelletInReserve(token, mac),
388
+ expectedResult: false,
389
+ },
390
+ {
391
+ method: "getPelletAutonomyTime",
392
+ call: (api, token, mac) => api.getPelletAutonomyTime(token, mac),
393
+ expectedResult: 180,
394
+ },
287
395
  ];
288
396
  getterTests.forEach(({ method, call, expectedResult }) => {
289
397
  it(`should call fetch and return the correct value for ${method}`, () => __awaiter(void 0, void 0, void 0, function* () {
@@ -301,6 +409,38 @@ describe("library", () => {
301
409
  });
302
410
  // Setter tests
303
411
  const setterTests = [
412
+ {
413
+ method: "setPowerLevel",
414
+ call: (api, token, mac, value) => api.setPowerLevel(token, mac, value),
415
+ payload: {
416
+ name: "power_level",
417
+ value: 4,
418
+ },
419
+ },
420
+ {
421
+ method: "setFan1Speed",
422
+ call: (api, token, mac, value) => api.setFan1Speed(token, mac, value),
423
+ payload: {
424
+ name: "fan_1_speed",
425
+ value: 3,
426
+ },
427
+ },
428
+ {
429
+ method: "setFan2Speed",
430
+ call: (api, token, mac, value) => api.setFan2Speed(token, mac, value),
431
+ payload: {
432
+ name: "fan_2_speed",
433
+ value: 4,
434
+ },
435
+ },
436
+ {
437
+ method: "setFan3Speed",
438
+ call: (api, token, mac, value) => api.setFan3Speed(token, mac, value),
439
+ payload: {
440
+ name: "fan_3_speed",
441
+ value: 5,
442
+ },
443
+ },
304
444
  {
305
445
  method: "setTargetTemperature",
306
446
  call: (api, token, mac, value) => api.setTargetTemperature(token, mac, value),
@@ -309,6 +449,38 @@ describe("library", () => {
309
449
  value: 20,
310
450
  },
311
451
  },
452
+ {
453
+ method: "setStandbyTime",
454
+ call: (api, token, mac, value) => api.setStandbyTime(token, mac, value),
455
+ payload: {
456
+ name: "standby_time",
457
+ value: 45,
458
+ },
459
+ },
460
+ {
461
+ method: "setEnvironment2Temperature",
462
+ call: (api, token, mac, value) => api.setEnvironment2Temperature(token, mac, value),
463
+ payload: {
464
+ name: "enviroment_2_temperature",
465
+ value: 21,
466
+ },
467
+ },
468
+ {
469
+ method: "setEnvironment3Temperature",
470
+ call: (api, token, mac, value) => api.setEnvironment3Temperature(token, mac, value),
471
+ payload: {
472
+ name: "enviroment_3_temperature",
473
+ value: 23,
474
+ },
475
+ },
476
+ {
477
+ method: "setLanguage",
478
+ call: (api, token, mac, value) => api.setLanguage(token, mac, value),
479
+ payload: {
480
+ name: "language",
481
+ value: 2,
482
+ },
483
+ },
312
484
  ];
313
485
  setterTests.forEach(({ method, call, payload }) => {
314
486
  it(`should call fetch and send the correct payload for ${method}`, () => __awaiter(void 0, void 0, void 0, function* () {
@@ -327,6 +499,69 @@ describe("library", () => {
327
499
  });
328
500
  }));
329
501
  });
502
+ // Boolean setter tests (for mode controls)
503
+ const booleanSetterTests = [
504
+ {
505
+ method: "setAirkare",
506
+ call: (api, token, mac, enabled) => api.setAirkare(token, mac, enabled),
507
+ truePayload: { name: "airkare_function", value: 1 },
508
+ falsePayload: { name: "airkare_function", value: 0 },
509
+ },
510
+ {
511
+ method: "setRelax",
512
+ call: (api, token, mac, enabled) => api.setRelax(token, mac, enabled),
513
+ truePayload: { name: "relax_mode", value: true },
514
+ falsePayload: { name: "relax_mode", value: false },
515
+ },
516
+ {
517
+ method: "setStandby",
518
+ call: (api, token, mac, enabled) => api.setStandby(token, mac, enabled),
519
+ truePayload: { name: "standby_mode", value: true },
520
+ falsePayload: { name: "standby_mode", value: false },
521
+ },
522
+ {
523
+ method: "setAuto",
524
+ call: (api, token, mac, enabled) => api.setAuto(token, mac, enabled),
525
+ truePayload: { name: "auto_mode", value: true },
526
+ falsePayload: { name: "auto_mode", value: false },
527
+ },
528
+ {
529
+ method: "setMeasureUnit",
530
+ call: (api, token, mac, enabled) => api.setMeasureUnit(token, mac, enabled),
531
+ truePayload: { name: "measure_unit", value: true },
532
+ falsePayload: { name: "measure_unit", value: false },
533
+ },
534
+ ];
535
+ booleanSetterTests.forEach(({ method, call, truePayload, falsePayload }) => {
536
+ it(`should call fetch with correct payload for ${method}(true)`, () => __awaiter(void 0, void 0, void 0, function* () {
537
+ fetchStub.resolves(mockResponse({ success: true }));
538
+ const api = configure("https://example.com/api/");
539
+ yield call(api, expectedToken, "mockMacAddress", true);
540
+ assert.ok(fetchStub.calledOnce);
541
+ assert.deepEqual(fetchStub.firstCall.args[1], {
542
+ method: "PUT",
543
+ headers: {
544
+ "Content-Type": "application/json",
545
+ Authorization: `Bearer ${expectedToken}`,
546
+ },
547
+ body: JSON.stringify(Object.assign({ mac_address: "mockMacAddress" }, truePayload)),
548
+ });
549
+ }));
550
+ it(`should call fetch with correct payload for ${method}(false)`, () => __awaiter(void 0, void 0, void 0, function* () {
551
+ fetchStub.resolves(mockResponse({ success: true }));
552
+ const api = configure("https://example.com/api/");
553
+ yield call(api, expectedToken, "mockMacAddress", false);
554
+ assert.ok(fetchStub.calledOnce);
555
+ assert.deepEqual(fetchStub.firstCall.args[1], {
556
+ method: "PUT",
557
+ headers: {
558
+ "Content-Type": "application/json",
559
+ Authorization: `Bearer ${expectedToken}`,
560
+ },
561
+ body: JSON.stringify(Object.assign({ mac_address: "mockMacAddress" }, falsePayload)),
562
+ });
563
+ }));
564
+ });
330
565
  });
331
566
  describe("registerDevice", () => {
332
567
  it("should call POST /device with correct payload", () => __awaiter(void 0, void 0, void 0, function* () {
@@ -515,6 +750,37 @@ describe("library", () => {
515
750
  const result = yield api.getTargetTemperature(expectedToken, "mockMacAddress");
516
751
  assert.equal(result, 22);
517
752
  }));
753
+ it("should work with getPelletInReserve on compressed response", () => __awaiter(void 0, void 0, void 0, function* () {
754
+ const statusData = {
755
+ commands: { power: true },
756
+ temperatures: { enviroment: 19 },
757
+ flags: { is_pellet_in_reserve: true },
758
+ pellet: { autonomy_time: 120 },
759
+ };
760
+ const mockResponseData = {
761
+ status: createGzippedBuffer(statusData),
762
+ nvm: { user_parameters: { enviroment_1_temperature: 22 } },
763
+ };
764
+ fetchStub.resolves(mockResponse(mockResponseData));
765
+ const api = configure("https://example.com/api/");
766
+ const result = yield api.getPelletInReserve(expectedToken, "mockMacAddress");
767
+ assert.equal(result, true);
768
+ }));
769
+ it("should work with getPelletAutonomyTime on response", () => __awaiter(void 0, void 0, void 0, function* () {
770
+ const mockResponseData = {
771
+ status: {
772
+ commands: { power: true },
773
+ temperatures: { enviroment: 19 },
774
+ flags: { is_pellet_in_reserve: false },
775
+ pellet: { autonomy_time: 240 },
776
+ },
777
+ nvm: { user_parameters: { enviroment_1_temperature: 22 } },
778
+ };
779
+ fetchStub.resolves(mockResponse(mockResponseData));
780
+ const api = configure("https://example.com/api/");
781
+ const result = yield api.getPelletAutonomyTime(expectedToken, "mockMacAddress");
782
+ assert.equal(result, 240);
783
+ }));
518
784
  });
519
785
  describe("Error Handling", () => {
520
786
  const errorTests = [
@@ -13,9 +13,17 @@ interface TemperaturesType {
13
13
  board: number;
14
14
  enviroment: number;
15
15
  }
16
+ interface GeneralFlagsType {
17
+ is_pellet_in_reserve: boolean;
18
+ }
19
+ interface PelletAutonomyType {
20
+ autonomy_time: number;
21
+ }
16
22
  interface StatusType {
17
23
  commands: CommandsType;
18
24
  temperatures: TemperaturesType;
25
+ flags: GeneralFlagsType;
26
+ pellet: PelletAutonomyType;
19
27
  }
20
28
  interface UserParametersType {
21
29
  enviroment_1_temperature: number;
@@ -23,6 +31,14 @@ interface UserParametersType {
23
31
  enviroment_3_temperature: number;
24
32
  is_auto: boolean;
25
33
  is_sound_active: boolean;
34
+ manual_power: number;
35
+ fan_1_ventilation: number;
36
+ fan_2_ventilation: number;
37
+ fan_3_ventilation: number;
38
+ is_standby_active: boolean;
39
+ standby_waiting_time: number;
40
+ is_fahrenheit: boolean;
41
+ language: number;
26
42
  }
27
43
  interface DeviceInfoType {
28
44
  status: StatusType;
@@ -70,4 +86,17 @@ interface DeviceAssociationResponse {
70
86
  deviceRoom: string;
71
87
  serialNumber: string;
72
88
  }
73
- export type { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, };
89
+ /**
90
+ * Represents a discovered Edilkamin device from Bluetooth scanning.
91
+ */
92
+ interface DiscoveredDevice {
93
+ /** BLE MAC address as discovered */
94
+ bleMac: string;
95
+ /** WiFi MAC address (BLE MAC - 2), used for API calls */
96
+ wifiMac: string;
97
+ /** Device name (typically "EDILKAMIN_EP") */
98
+ name: string;
99
+ /** Signal strength in dBm (optional, not all platforms provide this) */
100
+ rssi?: number;
101
+ }
102
+ export type { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, DiscoveredDevice, EditDeviceAssociationBody, GeneralFlagsType, PelletAutonomyType, StatusType, TemperaturesType, UserParametersType, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.7.4",
3
+ "version": "1.9.0",
4
4
  "description": "",
5
5
  "main": "dist/cjs/src/index.js",
6
6
  "module": "dist/esm/src/index.js",
@@ -15,6 +15,16 @@
15
15
  "types": "./dist/cjs/src/index.d.ts",
16
16
  "default": "./dist/cjs/src/index.js"
17
17
  }
18
+ },
19
+ "./bluetooth": {
20
+ "import": {
21
+ "types": "./dist/esm/src/bluetooth.d.ts",
22
+ "default": "./dist/esm/src/bluetooth.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/cjs/src/bluetooth.d.ts",
26
+ "default": "./dist/cjs/src/bluetooth.js"
27
+ }
18
28
  }
19
29
  },
20
30
  "scripts": {
@@ -60,9 +70,10 @@
60
70
  "@eslint/eslintrc": "^3.2.0",
61
71
  "@eslint/js": "^9.16.0",
62
72
  "@types/mocha": "^10.0.10",
63
- "@types/node": "^25.0.2",
73
+ "@types/node": "^24",
64
74
  "@types/pako": "^2.0.4",
65
- "@types/sinon": "^17.0.3",
75
+ "@types/sinon": "^21.0.0",
76
+ "@types/web-bluetooth": "^0.0.21",
66
77
  "@typescript-eslint/eslint-plugin": "^8.17.0",
67
78
  "@typescript-eslint/parser": "^8.17.0",
68
79
  "esbuild": "^0.27.1",
@@ -73,12 +84,12 @@
73
84
  "mocha": "^11.7.5",
74
85
  "nyc": "^17.1.0",
75
86
  "prettier": "^3.7.4",
76
- "sinon": "^19.0.2",
87
+ "sinon": "^21.0.1",
77
88
  "ts-node": "^10.9.1",
78
89
  "typedoc": "^0.28.15",
79
90
  "typescript": "^5.7.2"
80
91
  },
81
92
  "optionalDependencies": {
82
- "commander": "^12.1.0"
93
+ "commander": "^14.0.2"
83
94
  }
84
95
  }
@@ -0,0 +1,46 @@
1
+ import { strict as assert } from "assert";
2
+
3
+ import { bleToWifiMac } from "./bluetooth-utils";
4
+
5
+ describe("bleToWifiMac", () => {
6
+ it("converts BLE MAC with colons to WiFi MAC", () => {
7
+ assert.equal(bleToWifiMac("A8:03:2A:FE:D5:0A"), "a8032afed508");
8
+ });
9
+
10
+ it("converts BLE MAC without separators", () => {
11
+ assert.equal(bleToWifiMac("a8032afed50a"), "a8032afed508");
12
+ });
13
+
14
+ it("converts BLE MAC with dashes", () => {
15
+ assert.equal(bleToWifiMac("A8-03-2A-FE-D5-0A"), "a8032afed508");
16
+ });
17
+
18
+ it("handles lowercase input", () => {
19
+ assert.equal(bleToWifiMac("a8:03:2a:fe:d5:0a"), "a8032afed508");
20
+ });
21
+
22
+ it("handles edge case where subtraction crosses byte boundary", () => {
23
+ // FF:FF:FF:FF:FF:01 - 2 = FF:FF:FF:FF:FE:FF
24
+ assert.equal(bleToWifiMac("FF:FF:FF:FF:FF:01"), "fffffffffeff");
25
+ });
26
+
27
+ it("handles minimum value edge case", () => {
28
+ // 00:00:00:00:00:02 - 2 = 00:00:00:00:00:00
29
+ assert.equal(bleToWifiMac("00:00:00:00:00:02"), "000000000000");
30
+ });
31
+
32
+ it("throws on invalid MAC format - too short", () => {
33
+ assert.throws(() => bleToWifiMac("A8:03:2A"), /Invalid MAC address format/);
34
+ });
35
+
36
+ it("throws on invalid MAC format - invalid characters", () => {
37
+ assert.throws(
38
+ () => bleToWifiMac("G8:03:2A:FE:D5:0A"),
39
+ /Invalid MAC address format/,
40
+ );
41
+ });
42
+
43
+ it("throws on empty string", () => {
44
+ assert.throws(() => bleToWifiMac(""), /Invalid MAC address format/);
45
+ });
46
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Converts a BLE MAC address to WiFi MAC address.
3
+ * The WiFi MAC is the BLE MAC minus 2 in hexadecimal.
4
+ *
5
+ * @param bleMac - BLE MAC address (with or without colons/dashes)
6
+ * @returns WiFi MAC address in lowercase without separators
7
+ *
8
+ * @example
9
+ * bleToWifiMac("A8:03:2A:FE:D5:0A") // returns "a8032afed508"
10
+ * bleToWifiMac("a8032afed50a") // returns "a8032afed508"
11
+ */
12
+ const bleToWifiMac = (bleMac: string): string => {
13
+ // Remove colons, dashes, and convert to lowercase
14
+ const normalized = bleMac.replace(/[:-]/g, "").toLowerCase();
15
+
16
+ // Validate MAC address format (12 hex characters)
17
+ if (!/^[0-9a-f]{12}$/.test(normalized)) {
18
+ throw new Error(`Invalid MAC address format: ${bleMac}`);
19
+ }
20
+
21
+ // Convert to number, subtract 2, convert back to hex
22
+ const bleValue = BigInt(`0x${normalized}`);
23
+ const wifiValue = bleValue - BigInt(2);
24
+
25
+ // Pad to 12 characters and return lowercase
26
+ return wifiValue.toString(16).padStart(12, "0");
27
+ };
28
+
29
+ export { bleToWifiMac };
@@ -0,0 +1,115 @@
1
+ import { bleToWifiMac } from "./bluetooth-utils";
2
+ import { DiscoveredDevice } from "./types";
3
+
4
+ /** Device name broadcast by Edilkamin stoves */
5
+ const EDILKAMIN_DEVICE_NAME = "EDILKAMIN_EP";
6
+
7
+ /** GATT Service UUID for Edilkamin devices (0xABF0) */
8
+ const EDILKAMIN_SERVICE_UUID = 0xabf0;
9
+
10
+ /**
11
+ * Check if Web Bluetooth API is available in the current browser.
12
+ *
13
+ * @returns true if Web Bluetooth is supported
14
+ */
15
+ const isWebBluetoothSupported = (): boolean => {
16
+ return typeof navigator !== "undefined" && "bluetooth" in navigator;
17
+ };
18
+
19
+ /**
20
+ * Scan for nearby Edilkamin stoves using the Web Bluetooth API.
21
+ *
22
+ * This function triggers the browser's Bluetooth device picker dialog,
23
+ * filtered to show only devices named "EDILKAMIN_EP".
24
+ *
25
+ * Note: Web Bluetooth requires:
26
+ * - HTTPS or localhost
27
+ * - User gesture (button click)
28
+ * - Chrome/Edge/Opera (not Firefox/Safari)
29
+ *
30
+ * @returns Promise resolving to array of discovered devices
31
+ * @throws Error if Web Bluetooth is not supported or user cancels
32
+ *
33
+ * @example
34
+ * const devices = await scanForDevices();
35
+ * console.log(devices[0].wifiMac); // Use this for API calls
36
+ */
37
+ const scanForDevices = async (): Promise<DiscoveredDevice[]> => {
38
+ if (!isWebBluetoothSupported()) {
39
+ throw new Error(
40
+ "Web Bluetooth API is not supported in this browser. " +
41
+ "Use Chrome, Edge, or Opera on desktop/Android. " +
42
+ "On iOS, use the Bluefy browser app.",
43
+ );
44
+ }
45
+
46
+ try {
47
+ // Request device - this opens the browser's device picker
48
+ const device = await navigator.bluetooth.requestDevice({
49
+ filters: [{ name: EDILKAMIN_DEVICE_NAME }],
50
+ optionalServices: [EDILKAMIN_SERVICE_UUID],
51
+ });
52
+
53
+ // Extract BLE MAC from device ID if available
54
+ // Note: device.id format varies by platform, may need adjustment
55
+ const bleMac = device.id || "";
56
+ const name = device.name || EDILKAMIN_DEVICE_NAME;
57
+
58
+ // Calculate WiFi MAC for API calls
59
+ let wifiMac = "";
60
+ if (bleMac && /^[0-9a-f:-]{12,17}$/i.test(bleMac)) {
61
+ try {
62
+ wifiMac = bleToWifiMac(bleMac);
63
+ } catch {
64
+ // device.id may not be a valid MAC format on all platforms
65
+ wifiMac = "";
66
+ }
67
+ }
68
+
69
+ const discoveredDevice: DiscoveredDevice = {
70
+ bleMac,
71
+ wifiMac,
72
+ name,
73
+ // RSSI not directly available from requestDevice
74
+ };
75
+
76
+ return [discoveredDevice];
77
+ } catch (error) {
78
+ if (error instanceof Error) {
79
+ if (error.name === "NotFoundError") {
80
+ // User cancelled the device picker
81
+ return [];
82
+ }
83
+ throw error;
84
+ }
85
+ throw new Error("Unknown error during Bluetooth scan");
86
+ }
87
+ };
88
+
89
+ /**
90
+ * Scan for devices with a custom filter.
91
+ * Advanced function for users who need more control over device selection.
92
+ *
93
+ * @param options - Web Bluetooth requestDevice options
94
+ * @returns Promise resolving to the selected BluetoothDevice
95
+ */
96
+ const scanWithOptions = async (
97
+ options: RequestDeviceOptions,
98
+ ): Promise<BluetoothDevice> => {
99
+ if (!isWebBluetoothSupported()) {
100
+ throw new Error("Web Bluetooth API is not supported in this browser.");
101
+ }
102
+
103
+ return navigator.bluetooth.requestDevice(options);
104
+ };
105
+
106
+ export {
107
+ EDILKAMIN_DEVICE_NAME,
108
+ EDILKAMIN_SERVICE_UUID,
109
+ isWebBluetoothSupported,
110
+ scanForDevices,
111
+ scanWithOptions,
112
+ };
113
+
114
+ // Re-export DiscoveredDevice for convenience
115
+ export type { DiscoveredDevice } from "./types";