edilkamin 1.7.4 → 1.8.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/README.md CHANGED
@@ -115,6 +115,39 @@ const legacyApi = configure(OLD_API_URL);
115
115
 
116
116
  > **Note**: The legacy API uses AWS API Gateway and may be deprecated in the future.
117
117
 
118
+ ## Bluetooth Device Discovery
119
+
120
+ For automatic device discovery in web browsers, use the `edilkamin/bluetooth` subpath export.
121
+
122
+ ### Quick Example (Web)
123
+
124
+ ```javascript
125
+ import { scanForDevices } from "edilkamin/bluetooth";
126
+ import { deviceInfo, signIn } from "edilkamin";
127
+
128
+ // Scan for nearby stoves (requires user gesture)
129
+ const devices = await scanForDevices();
130
+ const { wifiMac } = devices[0];
131
+
132
+ // Use discovered MAC for API calls
133
+ const token = await signIn(username, password);
134
+ const info = await deviceInfo(token, wifiMac);
135
+ ```
136
+
137
+ ### MAC Address Helper
138
+
139
+ The core library includes a helper to convert BLE MAC to WiFi MAC:
140
+
141
+ ```javascript
142
+ import { bleToWifiMac } from "edilkamin";
143
+
144
+ // BLE MAC from Bluetooth scan
145
+ const bleMac = "A8:03:2A:FE:D5:0A";
146
+
147
+ // WiFi MAC for API calls (BLE - 2)
148
+ const wifiMac = bleToWifiMac(bleMac); // "a8032afed508"
149
+ ```
150
+
118
151
  ## Motivations
119
152
 
120
153
  - providing an open source web alternative
@@ -132,6 +165,6 @@ const legacyApi = configure(OLD_API_URL);
132
165
 
133
166
  ## Limitations
134
167
 
135
- It seems like there's no endpoint to list stoves associated to a user.
136
- The way the official app seem to work is by probing the stove via bluetooth.
137
- Then cache the stove MAC address to a local database for later use.
168
+ - **No server-side device listing**: The API doesn't provide an endpoint to list stoves associated to a user.
169
+ - **Bluetooth discovery available**: Use `edilkamin/bluetooth` for web browser device discovery, similar to the official app.
170
+ - **Manual MAC entry fallback**: For unsupported browsers or CLI, users can find the BLE MAC with different means and use `bleToWifiMac()` to calculate the WiFi MAC for API calls.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.7.4",
3
+ "version": "1.8.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": {
@@ -63,6 +73,7 @@
63
73
  "@types/node": "^25.0.2",
64
74
  "@types/pako": "^2.0.4",
65
75
  "@types/sinon": "^17.0.3",
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",
@@ -79,6 +90,6 @@
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,13 @@
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
+ declare const bleToWifiMac: (bleMac: string) => string;
13
+ export { bleToWifiMac };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bleToWifiMac = void 0;
4
+ /**
5
+ * Converts a BLE MAC address to WiFi MAC address.
6
+ * The WiFi MAC is the BLE MAC minus 2 in hexadecimal.
7
+ *
8
+ * @param bleMac - BLE MAC address (with or without colons/dashes)
9
+ * @returns WiFi MAC address in lowercase without separators
10
+ *
11
+ * @example
12
+ * bleToWifiMac("A8:03:2A:FE:D5:0A") // returns "a8032afed508"
13
+ * bleToWifiMac("a8032afed50a") // returns "a8032afed508"
14
+ */
15
+ const bleToWifiMac = (bleMac) => {
16
+ // Remove colons, dashes, and convert to lowercase
17
+ const normalized = bleMac.replace(/[:-]/g, "").toLowerCase();
18
+ // Validate MAC address format (12 hex characters)
19
+ if (!/^[0-9a-f]{12}$/.test(normalized)) {
20
+ throw new Error(`Invalid MAC address format: ${bleMac}`);
21
+ }
22
+ // Convert to number, subtract 2, convert back to hex
23
+ const bleValue = BigInt(`0x${normalized}`);
24
+ const wifiValue = bleValue - BigInt(2);
25
+ // Pad to 12 characters and return lowercase
26
+ return wifiValue.toString(16).padStart(12, "0");
27
+ };
28
+ exports.bleToWifiMac = bleToWifiMac;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const assert_1 = require("assert");
4
+ const bluetooth_utils_1 = require("./bluetooth-utils");
5
+ describe("bleToWifiMac", () => {
6
+ it("converts BLE MAC with colons to WiFi MAC", () => {
7
+ assert_1.strict.equal((0, bluetooth_utils_1.bleToWifiMac)("A8:03:2A:FE:D5:0A"), "a8032afed508");
8
+ });
9
+ it("converts BLE MAC without separators", () => {
10
+ assert_1.strict.equal((0, bluetooth_utils_1.bleToWifiMac)("a8032afed50a"), "a8032afed508");
11
+ });
12
+ it("converts BLE MAC with dashes", () => {
13
+ assert_1.strict.equal((0, bluetooth_utils_1.bleToWifiMac)("A8-03-2A-FE-D5-0A"), "a8032afed508");
14
+ });
15
+ it("handles lowercase input", () => {
16
+ assert_1.strict.equal((0, bluetooth_utils_1.bleToWifiMac)("a8:03:2a:fe:d5:0a"), "a8032afed508");
17
+ });
18
+ it("handles edge case where subtraction crosses byte boundary", () => {
19
+ // FF:FF:FF:FF:FF:01 - 2 = FF:FF:FF:FF:FE:FF
20
+ assert_1.strict.equal((0, bluetooth_utils_1.bleToWifiMac)("FF:FF:FF:FF:FF:01"), "fffffffffeff");
21
+ });
22
+ it("handles minimum value edge case", () => {
23
+ // 00:00:00:00:00:02 - 2 = 00:00:00:00:00:00
24
+ assert_1.strict.equal((0, bluetooth_utils_1.bleToWifiMac)("00:00:00:00:00:02"), "000000000000");
25
+ });
26
+ it("throws on invalid MAC format - too short", () => {
27
+ assert_1.strict.throws(() => (0, bluetooth_utils_1.bleToWifiMac)("A8:03:2A"), /Invalid MAC address format/);
28
+ });
29
+ it("throws on invalid MAC format - invalid characters", () => {
30
+ assert_1.strict.throws(() => (0, bluetooth_utils_1.bleToWifiMac)("G8:03:2A:FE:D5:0A"), /Invalid MAC address format/);
31
+ });
32
+ it("throws on empty string", () => {
33
+ assert_1.strict.throws(() => (0, bluetooth_utils_1.bleToWifiMac)(""), /Invalid MAC address format/);
34
+ });
35
+ });
@@ -0,0 +1,40 @@
1
+ import { DiscoveredDevice } from "./types";
2
+ /** Device name broadcast by Edilkamin stoves */
3
+ declare const EDILKAMIN_DEVICE_NAME = "EDILKAMIN_EP";
4
+ /** GATT Service UUID for Edilkamin devices (0xABF0) */
5
+ declare const EDILKAMIN_SERVICE_UUID = 44016;
6
+ /**
7
+ * Check if Web Bluetooth API is available in the current browser.
8
+ *
9
+ * @returns true if Web Bluetooth is supported
10
+ */
11
+ declare const isWebBluetoothSupported: () => boolean;
12
+ /**
13
+ * Scan for nearby Edilkamin stoves using the Web Bluetooth API.
14
+ *
15
+ * This function triggers the browser's Bluetooth device picker dialog,
16
+ * filtered to show only devices named "EDILKAMIN_EP".
17
+ *
18
+ * Note: Web Bluetooth requires:
19
+ * - HTTPS or localhost
20
+ * - User gesture (button click)
21
+ * - Chrome/Edge/Opera (not Firefox/Safari)
22
+ *
23
+ * @returns Promise resolving to array of discovered devices
24
+ * @throws Error if Web Bluetooth is not supported or user cancels
25
+ *
26
+ * @example
27
+ * const devices = await scanForDevices();
28
+ * console.log(devices[0].wifiMac); // Use this for API calls
29
+ */
30
+ declare const scanForDevices: () => Promise<DiscoveredDevice[]>;
31
+ /**
32
+ * Scan for devices with a custom filter.
33
+ * Advanced function for users who need more control over device selection.
34
+ *
35
+ * @param options - Web Bluetooth requestDevice options
36
+ * @returns Promise resolving to the selected BluetoothDevice
37
+ */
38
+ declare const scanWithOptions: (options: RequestDeviceOptions) => Promise<BluetoothDevice>;
39
+ export { EDILKAMIN_DEVICE_NAME, EDILKAMIN_SERVICE_UUID, isWebBluetoothSupported, scanForDevices, scanWithOptions, };
40
+ export type { DiscoveredDevice } from "./types";
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.scanWithOptions = exports.scanForDevices = exports.isWebBluetoothSupported = exports.EDILKAMIN_SERVICE_UUID = exports.EDILKAMIN_DEVICE_NAME = void 0;
13
+ const bluetooth_utils_1 = require("./bluetooth-utils");
14
+ /** Device name broadcast by Edilkamin stoves */
15
+ const EDILKAMIN_DEVICE_NAME = "EDILKAMIN_EP";
16
+ exports.EDILKAMIN_DEVICE_NAME = EDILKAMIN_DEVICE_NAME;
17
+ /** GATT Service UUID for Edilkamin devices (0xABF0) */
18
+ const EDILKAMIN_SERVICE_UUID = 0xabf0;
19
+ exports.EDILKAMIN_SERVICE_UUID = EDILKAMIN_SERVICE_UUID;
20
+ /**
21
+ * Check if Web Bluetooth API is available in the current browser.
22
+ *
23
+ * @returns true if Web Bluetooth is supported
24
+ */
25
+ const isWebBluetoothSupported = () => {
26
+ return typeof navigator !== "undefined" && "bluetooth" in navigator;
27
+ };
28
+ exports.isWebBluetoothSupported = isWebBluetoothSupported;
29
+ /**
30
+ * Scan for nearby Edilkamin stoves using the Web Bluetooth API.
31
+ *
32
+ * This function triggers the browser's Bluetooth device picker dialog,
33
+ * filtered to show only devices named "EDILKAMIN_EP".
34
+ *
35
+ * Note: Web Bluetooth requires:
36
+ * - HTTPS or localhost
37
+ * - User gesture (button click)
38
+ * - Chrome/Edge/Opera (not Firefox/Safari)
39
+ *
40
+ * @returns Promise resolving to array of discovered devices
41
+ * @throws Error if Web Bluetooth is not supported or user cancels
42
+ *
43
+ * @example
44
+ * const devices = await scanForDevices();
45
+ * console.log(devices[0].wifiMac); // Use this for API calls
46
+ */
47
+ const scanForDevices = () => __awaiter(void 0, void 0, void 0, function* () {
48
+ if (!isWebBluetoothSupported()) {
49
+ throw new Error("Web Bluetooth API is not supported in this browser. " +
50
+ "Use Chrome, Edge, or Opera on desktop/Android. " +
51
+ "On iOS, use the Bluefy browser app.");
52
+ }
53
+ try {
54
+ // Request device - this opens the browser's device picker
55
+ const device = yield navigator.bluetooth.requestDevice({
56
+ filters: [{ name: EDILKAMIN_DEVICE_NAME }],
57
+ optionalServices: [EDILKAMIN_SERVICE_UUID],
58
+ });
59
+ // Extract BLE MAC from device ID if available
60
+ // Note: device.id format varies by platform, may need adjustment
61
+ const bleMac = device.id || "";
62
+ const name = device.name || EDILKAMIN_DEVICE_NAME;
63
+ // Calculate WiFi MAC for API calls
64
+ let wifiMac = "";
65
+ if (bleMac && /^[0-9a-f:-]{12,17}$/i.test(bleMac)) {
66
+ try {
67
+ wifiMac = (0, bluetooth_utils_1.bleToWifiMac)(bleMac);
68
+ }
69
+ catch (_a) {
70
+ // device.id may not be a valid MAC format on all platforms
71
+ wifiMac = "";
72
+ }
73
+ }
74
+ const discoveredDevice = {
75
+ bleMac,
76
+ wifiMac,
77
+ name,
78
+ // RSSI not directly available from requestDevice
79
+ };
80
+ return [discoveredDevice];
81
+ }
82
+ catch (error) {
83
+ if (error instanceof Error) {
84
+ if (error.name === "NotFoundError") {
85
+ // User cancelled the device picker
86
+ return [];
87
+ }
88
+ throw error;
89
+ }
90
+ throw new Error("Unknown error during Bluetooth scan");
91
+ }
92
+ });
93
+ exports.scanForDevices = scanForDevices;
94
+ /**
95
+ * Scan for devices with a custom filter.
96
+ * Advanced function for users who need more control over device selection.
97
+ *
98
+ * @param options - Web Bluetooth requestDevice options
99
+ * @returns Promise resolving to the selected BluetoothDevice
100
+ */
101
+ const scanWithOptions = (options) => __awaiter(void 0, void 0, void 0, function* () {
102
+ if (!isWebBluetoothSupported()) {
103
+ throw new Error("Web Bluetooth API is not supported in this browser.");
104
+ }
105
+ return navigator.bluetooth.requestDevice(options);
106
+ });
107
+ exports.scanWithOptions = scanWithOptions;
@@ -1,6 +1,7 @@
1
+ export { bleToWifiMac } from "./bluetooth-utils";
1
2
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
2
3
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
3
4
  export { configure, getSession, signIn } from "./library";
4
5
  export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
5
- export { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, } from "./types";
6
+ export { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, DiscoveredDevice, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, } from "./types";
6
7
  export declare const deviceInfo: (jwtToken: string, macAddress: string) => Promise<import("./types").DeviceInfoType>, registerDevice: (jwtToken: string, macAddress: string, serialNumber: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, editDevice: (jwtToken: string, macAddress: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, setPower: (jwtToken: string, macAddress: string, value: number) => Promise<unknown>, setPowerOff: (jwtToken: string, macAddress: string) => Promise<unknown>, setPowerOn: (jwtToken: string, macAddress: string) => Promise<unknown>, getPower: (jwtToken: string, macAddress: string) => Promise<boolean>, getEnvironmentTemperature: (jwtToken: string, macAddress: string) => Promise<number>, getTargetTemperature: (jwtToken: string, macAddress: string) => Promise<number>, setTargetTemperature: (jwtToken: string, macAddress: string, temperature: number) => Promise<unknown>;
@@ -1,8 +1,10 @@
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.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 = void 0;
4
+ exports.setTargetTemperature = exports.getTargetTemperature = exports.getEnvironmentTemperature = exports.getPower = exports.setPowerOn = exports.setPowerOff = exports.setPower = exports.editDevice = exports.registerDevice = exports.deviceInfo = 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;
5
5
  const library_1 = require("./library");
6
+ var bluetooth_utils_1 = require("./bluetooth-utils");
7
+ Object.defineProperty(exports, "bleToWifiMac", { enumerable: true, get: function () { return bluetooth_utils_1.bleToWifiMac; } });
6
8
  var buffer_utils_1 = require("./buffer-utils");
7
9
  Object.defineProperty(exports, "decompressBuffer", { enumerable: true, get: function () { return buffer_utils_1.decompressBuffer; } });
8
10
  Object.defineProperty(exports, "isBuffer", { enumerable: true, get: function () { return buffer_utils_1.isBuffer; } });
@@ -70,4 +70,17 @@ interface DeviceAssociationResponse {
70
70
  deviceRoom: string;
71
71
  serialNumber: string;
72
72
  }
73
- export type { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, };
73
+ /**
74
+ * Represents a discovered Edilkamin device from Bluetooth scanning.
75
+ */
76
+ interface DiscoveredDevice {
77
+ /** BLE MAC address as discovered */
78
+ bleMac: string;
79
+ /** WiFi MAC address (BLE MAC - 2), used for API calls */
80
+ wifiMac: string;
81
+ /** Device name (typically "EDILKAMIN_EP") */
82
+ name: string;
83
+ /** Signal strength in dBm (optional, not all platforms provide this) */
84
+ rssi?: number;
85
+ }
86
+ export type { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, DiscoveredDevice, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.7.4",
3
+ "version": "1.8.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": {
@@ -63,6 +73,7 @@
63
73
  "@types/node": "^25.0.2",
64
74
  "@types/pako": "^2.0.4",
65
75
  "@types/sinon": "^17.0.3",
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",
@@ -79,6 +90,6 @@
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,13 @@
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
+ declare const bleToWifiMac: (bleMac: string) => string;
13
+ export { bleToWifiMac };
@@ -0,0 +1,25 @@
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) => {
13
+ // Remove colons, dashes, and convert to lowercase
14
+ const normalized = bleMac.replace(/[:-]/g, "").toLowerCase();
15
+ // Validate MAC address format (12 hex characters)
16
+ if (!/^[0-9a-f]{12}$/.test(normalized)) {
17
+ throw new Error(`Invalid MAC address format: ${bleMac}`);
18
+ }
19
+ // Convert to number, subtract 2, convert back to hex
20
+ const bleValue = BigInt(`0x${normalized}`);
21
+ const wifiValue = bleValue - BigInt(2);
22
+ // Pad to 12 characters and return lowercase
23
+ return wifiValue.toString(16).padStart(12, "0");
24
+ };
25
+ export { bleToWifiMac };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { strict as assert } from "assert";
2
+ import { bleToWifiMac } from "./bluetooth-utils";
3
+ describe("bleToWifiMac", () => {
4
+ it("converts BLE MAC with colons to WiFi MAC", () => {
5
+ assert.equal(bleToWifiMac("A8:03:2A:FE:D5:0A"), "a8032afed508");
6
+ });
7
+ it("converts BLE MAC without separators", () => {
8
+ assert.equal(bleToWifiMac("a8032afed50a"), "a8032afed508");
9
+ });
10
+ it("converts BLE MAC with dashes", () => {
11
+ assert.equal(bleToWifiMac("A8-03-2A-FE-D5-0A"), "a8032afed508");
12
+ });
13
+ it("handles lowercase input", () => {
14
+ assert.equal(bleToWifiMac("a8:03:2a:fe:d5:0a"), "a8032afed508");
15
+ });
16
+ it("handles edge case where subtraction crosses byte boundary", () => {
17
+ // FF:FF:FF:FF:FF:01 - 2 = FF:FF:FF:FF:FE:FF
18
+ assert.equal(bleToWifiMac("FF:FF:FF:FF:FF:01"), "fffffffffeff");
19
+ });
20
+ it("handles minimum value edge case", () => {
21
+ // 00:00:00:00:00:02 - 2 = 00:00:00:00:00:00
22
+ assert.equal(bleToWifiMac("00:00:00:00:00:02"), "000000000000");
23
+ });
24
+ it("throws on invalid MAC format - too short", () => {
25
+ assert.throws(() => bleToWifiMac("A8:03:2A"), /Invalid MAC address format/);
26
+ });
27
+ it("throws on invalid MAC format - invalid characters", () => {
28
+ assert.throws(() => bleToWifiMac("G8:03:2A:FE:D5:0A"), /Invalid MAC address format/);
29
+ });
30
+ it("throws on empty string", () => {
31
+ assert.throws(() => bleToWifiMac(""), /Invalid MAC address format/);
32
+ });
33
+ });
@@ -0,0 +1,40 @@
1
+ import { DiscoveredDevice } from "./types";
2
+ /** Device name broadcast by Edilkamin stoves */
3
+ declare const EDILKAMIN_DEVICE_NAME = "EDILKAMIN_EP";
4
+ /** GATT Service UUID for Edilkamin devices (0xABF0) */
5
+ declare const EDILKAMIN_SERVICE_UUID = 44016;
6
+ /**
7
+ * Check if Web Bluetooth API is available in the current browser.
8
+ *
9
+ * @returns true if Web Bluetooth is supported
10
+ */
11
+ declare const isWebBluetoothSupported: () => boolean;
12
+ /**
13
+ * Scan for nearby Edilkamin stoves using the Web Bluetooth API.
14
+ *
15
+ * This function triggers the browser's Bluetooth device picker dialog,
16
+ * filtered to show only devices named "EDILKAMIN_EP".
17
+ *
18
+ * Note: Web Bluetooth requires:
19
+ * - HTTPS or localhost
20
+ * - User gesture (button click)
21
+ * - Chrome/Edge/Opera (not Firefox/Safari)
22
+ *
23
+ * @returns Promise resolving to array of discovered devices
24
+ * @throws Error if Web Bluetooth is not supported or user cancels
25
+ *
26
+ * @example
27
+ * const devices = await scanForDevices();
28
+ * console.log(devices[0].wifiMac); // Use this for API calls
29
+ */
30
+ declare const scanForDevices: () => Promise<DiscoveredDevice[]>;
31
+ /**
32
+ * Scan for devices with a custom filter.
33
+ * Advanced function for users who need more control over device selection.
34
+ *
35
+ * @param options - Web Bluetooth requestDevice options
36
+ * @returns Promise resolving to the selected BluetoothDevice
37
+ */
38
+ declare const scanWithOptions: (options: RequestDeviceOptions) => Promise<BluetoothDevice>;
39
+ export { EDILKAMIN_DEVICE_NAME, EDILKAMIN_SERVICE_UUID, isWebBluetoothSupported, scanForDevices, scanWithOptions, };
40
+ export type { DiscoveredDevice } from "./types";
@@ -0,0 +1,100 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { bleToWifiMac } from "./bluetooth-utils";
11
+ /** Device name broadcast by Edilkamin stoves */
12
+ const EDILKAMIN_DEVICE_NAME = "EDILKAMIN_EP";
13
+ /** GATT Service UUID for Edilkamin devices (0xABF0) */
14
+ const EDILKAMIN_SERVICE_UUID = 0xabf0;
15
+ /**
16
+ * Check if Web Bluetooth API is available in the current browser.
17
+ *
18
+ * @returns true if Web Bluetooth is supported
19
+ */
20
+ const isWebBluetoothSupported = () => {
21
+ return typeof navigator !== "undefined" && "bluetooth" in navigator;
22
+ };
23
+ /**
24
+ * Scan for nearby Edilkamin stoves using the Web Bluetooth API.
25
+ *
26
+ * This function triggers the browser's Bluetooth device picker dialog,
27
+ * filtered to show only devices named "EDILKAMIN_EP".
28
+ *
29
+ * Note: Web Bluetooth requires:
30
+ * - HTTPS or localhost
31
+ * - User gesture (button click)
32
+ * - Chrome/Edge/Opera (not Firefox/Safari)
33
+ *
34
+ * @returns Promise resolving to array of discovered devices
35
+ * @throws Error if Web Bluetooth is not supported or user cancels
36
+ *
37
+ * @example
38
+ * const devices = await scanForDevices();
39
+ * console.log(devices[0].wifiMac); // Use this for API calls
40
+ */
41
+ const scanForDevices = () => __awaiter(void 0, void 0, void 0, function* () {
42
+ if (!isWebBluetoothSupported()) {
43
+ throw new Error("Web Bluetooth API is not supported in this browser. " +
44
+ "Use Chrome, Edge, or Opera on desktop/Android. " +
45
+ "On iOS, use the Bluefy browser app.");
46
+ }
47
+ try {
48
+ // Request device - this opens the browser's device picker
49
+ const device = yield navigator.bluetooth.requestDevice({
50
+ filters: [{ name: EDILKAMIN_DEVICE_NAME }],
51
+ optionalServices: [EDILKAMIN_SERVICE_UUID],
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
+ // Calculate WiFi MAC for API calls
58
+ let wifiMac = "";
59
+ if (bleMac && /^[0-9a-f:-]{12,17}$/i.test(bleMac)) {
60
+ try {
61
+ wifiMac = bleToWifiMac(bleMac);
62
+ }
63
+ catch (_a) {
64
+ // device.id may not be a valid MAC format on all platforms
65
+ wifiMac = "";
66
+ }
67
+ }
68
+ const discoveredDevice = {
69
+ bleMac,
70
+ wifiMac,
71
+ name,
72
+ // RSSI not directly available from requestDevice
73
+ };
74
+ return [discoveredDevice];
75
+ }
76
+ catch (error) {
77
+ if (error instanceof Error) {
78
+ if (error.name === "NotFoundError") {
79
+ // User cancelled the device picker
80
+ return [];
81
+ }
82
+ throw error;
83
+ }
84
+ throw new Error("Unknown error during Bluetooth scan");
85
+ }
86
+ });
87
+ /**
88
+ * Scan for devices with a custom filter.
89
+ * Advanced function for users who need more control over device selection.
90
+ *
91
+ * @param options - Web Bluetooth requestDevice options
92
+ * @returns Promise resolving to the selected BluetoothDevice
93
+ */
94
+ const scanWithOptions = (options) => __awaiter(void 0, void 0, void 0, function* () {
95
+ if (!isWebBluetoothSupported()) {
96
+ throw new Error("Web Bluetooth API is not supported in this browser.");
97
+ }
98
+ return navigator.bluetooth.requestDevice(options);
99
+ });
100
+ export { EDILKAMIN_DEVICE_NAME, EDILKAMIN_SERVICE_UUID, isWebBluetoothSupported, scanForDevices, scanWithOptions, };
@@ -1,6 +1,7 @@
1
+ export { bleToWifiMac } from "./bluetooth-utils";
1
2
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
2
3
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
3
4
  export { configure, getSession, signIn } from "./library";
4
5
  export { serialNumberDisplay, serialNumberFromHex, serialNumberToHex, } from "./serial-utils";
5
- export { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, } from "./types";
6
+ export { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, DiscoveredDevice, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, } from "./types";
6
7
  export declare const deviceInfo: (jwtToken: string, macAddress: string) => Promise<import("./types").DeviceInfoType>, registerDevice: (jwtToken: string, macAddress: string, serialNumber: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, editDevice: (jwtToken: string, macAddress: string, deviceName?: string, deviceRoom?: string) => Promise<import("./types").DeviceAssociationResponse>, setPower: (jwtToken: string, macAddress: string, value: number) => Promise<unknown>, setPowerOff: (jwtToken: string, macAddress: string) => Promise<unknown>, setPowerOn: (jwtToken: string, macAddress: string) => Promise<unknown>, getPower: (jwtToken: string, macAddress: string) => Promise<boolean>, getEnvironmentTemperature: (jwtToken: string, macAddress: string) => Promise<number>, getTargetTemperature: (jwtToken: string, macAddress: string) => Promise<number>, setTargetTemperature: (jwtToken: string, macAddress: string, temperature: number) => Promise<unknown>;
@@ -1,4 +1,5 @@
1
1
  import { configure } from "./library";
2
+ export { bleToWifiMac } from "./bluetooth-utils";
2
3
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
3
4
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
4
5
  export { configure, getSession, signIn } from "./library";
@@ -70,4 +70,17 @@ interface DeviceAssociationResponse {
70
70
  deviceRoom: string;
71
71
  serialNumber: string;
72
72
  }
73
- export type { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, EditDeviceAssociationBody, StatusType, TemperaturesType, UserParametersType, };
73
+ /**
74
+ * Represents a discovered Edilkamin device from Bluetooth scanning.
75
+ */
76
+ interface DiscoveredDevice {
77
+ /** BLE MAC address as discovered */
78
+ bleMac: string;
79
+ /** WiFi MAC address (BLE MAC - 2), used for API calls */
80
+ wifiMac: string;
81
+ /** Device name (typically "EDILKAMIN_EP") */
82
+ name: string;
83
+ /** Signal strength in dBm (optional, not all platforms provide this) */
84
+ rssi?: number;
85
+ }
86
+ export type { BufferEncodedType, CommandsType, DeviceAssociationBody, DeviceAssociationResponse, DeviceInfoRawType, DeviceInfoType, DiscoveredDevice, EditDeviceAssociationBody, 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.8.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": {
@@ -63,6 +73,7 @@
63
73
  "@types/node": "^25.0.2",
64
74
  "@types/pako": "^2.0.4",
65
75
  "@types/sinon": "^17.0.3",
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",
@@ -79,6 +90,6 @@
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";
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { configure } from "./library";
2
2
 
3
+ export { bleToWifiMac } from "./bluetooth-utils";
3
4
  export { decompressBuffer, isBuffer, processResponse } from "./buffer-utils";
4
5
  export { API_URL, NEW_API_URL, OLD_API_URL } from "./constants";
5
6
  export { configure, getSession, signIn } from "./library";
@@ -15,6 +16,7 @@ export {
15
16
  DeviceAssociationResponse,
16
17
  DeviceInfoRawType,
17
18
  DeviceInfoType,
19
+ DiscoveredDevice,
18
20
  EditDeviceAssociationBody,
19
21
  StatusType,
20
22
  TemperaturesType,
package/src/types.ts CHANGED
@@ -82,6 +82,20 @@ interface DeviceAssociationResponse {
82
82
  serialNumber: string;
83
83
  }
84
84
 
85
+ /**
86
+ * Represents a discovered Edilkamin device from Bluetooth scanning.
87
+ */
88
+ interface DiscoveredDevice {
89
+ /** BLE MAC address as discovered */
90
+ bleMac: string;
91
+ /** WiFi MAC address (BLE MAC - 2), used for API calls */
92
+ wifiMac: string;
93
+ /** Device name (typically "EDILKAMIN_EP") */
94
+ name: string;
95
+ /** Signal strength in dBm (optional, not all platforms provide this) */
96
+ rssi?: number;
97
+ }
98
+
85
99
  export type {
86
100
  BufferEncodedType,
87
101
  CommandsType,
@@ -89,6 +103,7 @@ export type {
89
103
  DeviceAssociationResponse,
90
104
  DeviceInfoRawType,
91
105
  DeviceInfoType,
106
+ DiscoveredDevice,
92
107
  EditDeviceAssociationBody,
93
108
  StatusType,
94
109
  TemperaturesType,
package/tsconfig.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "es6",
4
+ "lib": ["ES6", "DOM"],
4
5
  "moduleResolution": "node",
5
6
  "outDir": "dist",
6
7
  "rootDir": ".",