From ef463ecf75f54c83193ae97ffc1cf3a26d262f0b Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Sat, 5 Oct 2024 10:42:30 -0500 Subject: [PATCH 01/10] export device --- .github/workflows/release.yml | 2 +- package-lock.json | 1 + src/device.ts | 1 - src/index.ts | 2 +- src/parameter-checker.ts | 11 +++- src/test/advertising.test.ts | 6 +- src/test/index.test.ts | 38 ++--------- src/test/switchbot-openapi.test.ts | 101 ++++++++++++++++++++++++----- 8 files changed, 104 insertions(+), 58 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7a7d9e6..0454331b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: if: ${{ github.repository == 'OpenWonderLabs/node-switchbot' }} uses: OpenWonderLabs/.github/.github/workflows/discord-webhooks.yml@latest with: - title: "Node-SwitchBot Beta Release" + title: "Node-SwitchBot Release" description: | Version `v${{ needs.publish.outputs.NPM_VERSION }}` url: "https://github.com/homebridge/camera-utils/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}" diff --git a/package-lock.json b/package-lock.json index 5adbd7d3..c5dfb0c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -842,6 +842,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { diff --git a/src/device.ts b/src/device.ts index ff3c9bf8..9a67ce5a 100644 --- a/src/device.ts +++ b/src/device.ts @@ -19,7 +19,6 @@ import { SERV_UUID_PRIMARY, WRITE_TIMEOUT_MSEC, } from './settings.js' -import { SwitchBotBLE } from './switchbot-ble.js' /** * Represents a Switchbot Device. diff --git a/src/index.ts b/src/index.ts index 77118bce..246bb317 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ * * index.ts: Switchbot BLE API registration. */ +export * from './device.js' export * from './switchbot-ble.js' export * from './switchbot-openapi.js' export * from './types/bledevicestatus.js' @@ -11,4 +12,3 @@ export * from './types/deviceresponse.js' export * from './types/devicestatus.js' export * from './types/devicewebhookstatus.js' export * from './types/irdevicelist.js' -export * from './types/types.js' diff --git a/src/parameter-checker.ts b/src/parameter-checker.ts index de1d86cf..4c66dc98 100644 --- a/src/parameter-checker.ts +++ b/src/parameter-checker.ts @@ -49,7 +49,15 @@ export class ParameterChecker extends EventEmitter { */ async check(obj: Record, rules: Record, required: boolean = false): Promise { this._error = null - this.emitLog('debug', `Using rules: ${JSON.stringify(rules)}`) + // eslint-disable-next-line no-console + console.log('Checking object:', obj) + // eslint-disable-next-line no-console + console.log('With rules:', rules) + // eslint-disable-next-line no-console + console.log('Is required:', required) + this.emitLog('debug', `Checking object: ${JSON.stringify(obj)}`) + this.emitLog('debug', `With rules: ${JSON.stringify(rules)}`) + this.emitLog('debug', `Is required: ${JSON.stringify(required)}`) if (required && !this.isSpecified(obj)) { this._error = { code: 'MISSING_REQUIRED', message: 'The first argument is missing.' } @@ -96,6 +104,7 @@ export class ParameterChecker extends EventEmitter { } } + this.emitLog('debug', 'All checks passed.') return true } diff --git a/src/test/advertising.test.ts b/src/test/advertising.test.ts index 377aa239..6f802dcf 100644 --- a/src/test/advertising.test.ts +++ b/src/test/advertising.test.ts @@ -75,7 +75,7 @@ describe('advertising', () => { const result = await Advertising.parse(peripheral, mockLog) expect(result).toBeNull() - expect(mockLog).toHaveBeenCalledWith('[parseAdvertising.test-id.\x01] return null, parsed serviceData empty!') + expect(mockLog).toHaveBeenCalledWith('debugerror', '[parseAdvertising.test-id.\x01] return null, parsed serviceData empty!') }) }) @@ -86,10 +86,10 @@ describe('advertising', () => { expect(result).toBe(true) }) - it('should return false for invalid buffer', () => { + it('should return null for invalid buffer', () => { const buffer = null const result = (Advertising as any).validateBuffer(buffer) - expect(result).toBe(false) + expect(result).toBe(null) }) }) diff --git a/src/test/index.test.ts b/src/test/index.test.ts index aa43f6dd..85b82b70 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -3,43 +3,15 @@ import { describe, expect, it } from 'vitest' import * as index from '../index.js' describe('index module exports', () => { - it('should export switchbot', () => { - expect(index).toHaveProperty('switchbot') + it('should export switchbot-ble', () => { + expect(index.SwitchBotBLE).toBeDefined() }) it('should export switchbot-openapi', () => { - expect(index).toHaveProperty('switchbot-openapi') + expect(index.SwitchBotOpenAPI).toBeDefined() }) - it('should export bledevicestatus', () => { - expect(index).toHaveProperty('bledevicestatus') - }) - - it('should export devicelist', () => { - expect(index).toHaveProperty('devicelist') - }) - - it('should export devicepush', () => { - expect(index).toHaveProperty('devicepush') - }) - - it('should export deviceresponse', () => { - expect(index).toHaveProperty('deviceresponse') - }) - - it('should export devicestatus', () => { - expect(index).toHaveProperty('devicestatus') - }) - - it('should export devicewebhookstatus', () => { - expect(index).toHaveProperty('devicewebhookstatus') - }) - - it('should export irdevicelist', () => { - expect(index).toHaveProperty('irdevicelist') - }) - - it('should export types', () => { - expect(index).toHaveProperty('types') + it('should export SwitchbotDevice', () => { + expect(index.SwitchbotDevice).toBeDefined() }) }) diff --git a/src/test/switchbot-openapi.test.ts b/src/test/switchbot-openapi.test.ts index 7d5c52b0..ce92085c 100644 --- a/src/test/switchbot-openapi.test.ts +++ b/src/test/switchbot-openapi.test.ts @@ -1,31 +1,44 @@ +import type { Mock } from 'vitest' + +import { createServer } from 'node:http' + import { request } from 'undici' -import { describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { SwitchBotOpenAPI } from '../switchbot-openapi.js' -jest.mock('undici', () => ({ - request: jest.fn(), +vi.mock('undici', () => ({ + request: vi.fn(), })) describe('switchBotOpenAPI', () => { let switchBotAPI: SwitchBotOpenAPI const token = 'test-token' const secret = 'test-secret' + const port = 3000 + let server: any beforeEach(() => { switchBotAPI = new SwitchBotOpenAPI(token, secret) + if (server && typeof server.close === 'function') { + server.close() + } + server = startServer(port) }) afterEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() + if (server && typeof server.close === 'function') { + server.close() + } }) describe('getDevices', () => { it('should retrieve the list of devices', async () => { const mockDevicesResponse = { body: { devices: [] }, statusCode: 200 }; - (request as jest.Mock).mockResolvedValue({ + (request as Mock).mockResolvedValue({ body: { - json: jest.fn().mockResolvedValue(mockDevicesResponse.body), + json: vi.fn().mockResolvedValue(mockDevicesResponse.body), }, statusCode: mockDevicesResponse.statusCode, }) @@ -38,7 +51,7 @@ describe('switchBotOpenAPI', () => { it('should throw an error if the request fails', async () => { const errorMessage = 'Failed to get devices'; - (request as jest.Mock).mockRejectedValue(new Error(errorMessage)) + (request as Mock).mockRejectedValue(new Error(errorMessage)) await expect(switchBotAPI.getDevices()).rejects.toThrow(`Failed to get devices: ${errorMessage}`) }) @@ -47,9 +60,9 @@ describe('switchBotOpenAPI', () => { describe('controlDevice', () => { it('should control a device by sending a command', async () => { const mockControlResponse = { body: {}, statusCode: 200 }; - (request as jest.Mock).mockResolvedValue({ + (request as Mock).mockResolvedValue({ body: { - json: jest.fn().mockResolvedValue(mockControlResponse.body), + json: vi.fn().mockResolvedValue(mockControlResponse.body), }, statusCode: mockControlResponse.statusCode, }) @@ -62,7 +75,7 @@ describe('switchBotOpenAPI', () => { it('should throw an error if the device control fails', async () => { const errorMessage = 'Failed to control device'; - (request as jest.Mock).mockRejectedValue(new Error(errorMessage)) + (request as Mock).mockRejectedValue(new Error(errorMessage)) await expect(switchBotAPI.controlDevice('device-id', 'turnOn', 'default')).rejects.toThrow(`Failed to control device: ${errorMessage}`) }) @@ -71,9 +84,9 @@ describe('switchBotOpenAPI', () => { describe('getDeviceStatus', () => { it('should retrieve the status of a specific device', async () => { const mockStatusResponse = { body: {}, statusCode: 200 }; - (request as jest.Mock).mockResolvedValue({ + (request as Mock).mockResolvedValue({ body: { - json: jest.fn().mockResolvedValue(mockStatusResponse.body), + json: vi.fn().mockResolvedValue(mockStatusResponse.body), }, statusCode: mockStatusResponse.statusCode, }) @@ -86,7 +99,7 @@ describe('switchBotOpenAPI', () => { it('should throw an error if the request fails', async () => { const errorMessage = 'Failed to get device status'; - (request as jest.Mock).mockRejectedValue(new Error(errorMessage)) + (request as Mock).mockRejectedValue(new Error(errorMessage)) await expect(switchBotAPI.getDeviceStatus('device-id')).rejects.toThrow(`Failed to get device status: ${errorMessage}`) }) @@ -95,14 +108,14 @@ describe('switchBotOpenAPI', () => { describe('setupWebhook', () => { it('should set up a webhook listener and configure the webhook on the server', async () => { const mockWebhookResponse = { body: {}, statusCode: 200 }; - (request as jest.Mock).mockResolvedValue({ + (request as Mock).mockResolvedValue({ body: { - json: jest.fn().mockResolvedValue(mockWebhookResponse.body), + json: vi.fn().mockResolvedValue(mockWebhookResponse.body), }, statusCode: mockWebhookResponse.statusCode, }) - const url = 'http://localhost:3000/webhook' + const url = `http://localhost:${port}/webhook` await switchBotAPI.setupWebhook(url) expect(request).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) @@ -110,12 +123,64 @@ describe('switchBotOpenAPI', () => { it('should log an error if the webhook setup fails', async () => { const errorMessage = 'Failed to create webhook listener'; - (request as jest.Mock).mockRejectedValue(new Error(errorMessage)) + (request as Mock).mockRejectedValue(new Error(errorMessage)) - const url = 'http://localhost:3000/webhook' + const url = `http://localhost:${port}/webhook` await switchBotAPI.setupWebhook(url) expect(request).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) }) }) + + describe('deleteWebhook', () => { + it('should delete the webhook listener and remove the webhook from the server', async () => { + const mockDeleteResponse = { body: {}, statusCode: 200 }; + (request as Mock).mockResolvedValue({ + body: { + json: vi.fn().mockResolvedValue(mockDeleteResponse.body), + }, + statusCode: mockDeleteResponse.statusCode, + }) + + const url = `http://localhost:${port}/webhook` + await switchBotAPI.deleteWebhook(url) + + expect(request).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + }) + + it('should log an error if the webhook deletion fails', async () => { + const errorMessage = 'Failed to delete webhook listener'; + (request as Mock).mockRejectedValue(new Error(errorMessage)) + + const url = `http://localhost:${port}/webhook` + await switchBotAPI.deleteWebhook(url) + + expect(request).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) + }) + }) }) + +function startServer(port: number): any { + const server = createServer((req, res) => { + if (req.method === 'POST' && req.url === '/webhook') { + req.on('data', () => { + // Process the chunk if needed + }) + req.on('end', () => { + // Log the webhook received event + // console.log('Webhook received:', body) + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ message: 'Webhook received' })) + }) + } else { + res.writeHead(404, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ message: 'Not Found' })) + } + }) + + server.listen(port, () => { + // Server is listening on port ${port} + }) + + return server +} From 2b47f7e81352d58b6c5fc2743d91396f2d161cec Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Sat, 5 Oct 2024 21:32:50 -0500 Subject: [PATCH 02/10] more changes --- BLE.md | 6 +- OpenAPI.md | 4 +- docs/media/BLE.md | 6 +- src/advertising.ts | 2 +- src/device.ts | 15 +- src/index.ts | 1 + src/switchbot-ble.ts | 411 +++++++++++++++++++-------------------- src/switchbot-openapi.ts | 14 +- 8 files changed, 228 insertions(+), 231 deletions(-) diff --git a/BLE.md b/BLE.md index 4bd7633f..72a57d92 100644 --- a/BLE.md +++ b/BLE.md @@ -80,7 +80,7 @@ const switchBotBLE = new SwitchBotBLE() try { await switchBotBLE.startScan() } catch (e: any) { - console.error(`Failed to start BLE scanning. Error:${e}`) + console.error(`Failed to start BLE scanning, Error: ${e.message ?? e}`) } ``` @@ -230,7 +230,7 @@ switchBotBLE.onadvertisement = async (ad: any) => { try { this.bleEventHandler[ad.address]?.(ad.serviceData) } catch (e: any) { - await this.errorLog(`Failed to handle BLE event. Error:${e}`) + await this.errorLog(`Failed to handle BLE event, Error: ${e.message ?? e}`) } } ``` @@ -246,7 +246,7 @@ try { switchBotBLE.stopScan() console.log('Stopped BLE scanning to close listening.') } catch (e: any) { - console.error(`Failed to stop BLE scanning, error:${e.message}`) + console.error(`Failed to stop BLE scanning, Error: ${e.message ?? e}`) } ``` diff --git a/OpenAPI.md b/OpenAPI.md index 39c3894c..3869513c 100644 --- a/OpenAPI.md +++ b/OpenAPI.md @@ -47,8 +47,8 @@ async function getDevices() { try { const devices = await switchBotAPI.getDevices() console.log('Devices:', devices) - } catch (error) { - console.error('Error getting devices:', error) + } catch (e: any) { + console.error(`failed to get devices, Error: ${e.message ?? e}`) } } diff --git a/docs/media/BLE.md b/docs/media/BLE.md index 4bd7633f..72a57d92 100644 --- a/docs/media/BLE.md +++ b/docs/media/BLE.md @@ -80,7 +80,7 @@ const switchBotBLE = new SwitchBotBLE() try { await switchBotBLE.startScan() } catch (e: any) { - console.error(`Failed to start BLE scanning. Error:${e}`) + console.error(`Failed to start BLE scanning, Error: ${e.message ?? e}`) } ``` @@ -230,7 +230,7 @@ switchBotBLE.onadvertisement = async (ad: any) => { try { this.bleEventHandler[ad.address]?.(ad.serviceData) } catch (e: any) { - await this.errorLog(`Failed to handle BLE event. Error:${e}`) + await this.errorLog(`Failed to handle BLE event, Error: ${e.message ?? e}`) } } ``` @@ -246,7 +246,7 @@ try { switchBotBLE.stopScan() console.log('Stopped BLE scanning to close listening.') } catch (e: any) { - console.error(`Failed to stop BLE scanning, error:${e.message}`) + console.error(`Failed to stop BLE scanning, Error: ${e.message ?? e}`) } ``` diff --git a/src/advertising.ts b/src/advertising.ts index 4de43480..0014bb2a 100644 --- a/src/advertising.ts +++ b/src/advertising.ts @@ -95,7 +95,7 @@ export class Advertising { * @param {Function} emitLog - The function to emit log messages. * @returns {Promise} - The parsed service data. */ - private static async parseServiceData( + public static async parseServiceData( model: string, serviceData: Buffer, manufacturerData: Buffer, diff --git a/src/device.ts b/src/device.ts index 9a67ce5a..9f4c8406 100644 --- a/src/device.ts +++ b/src/device.ts @@ -199,12 +199,17 @@ export class SwitchbotDevice extends EventEmitter { * @returns A Promise that resolves with the list of services. */ public async discoverServices(): Promise { - const services = await this._peripheral.discoverServicesAsync([]) - const primaryServices = services.filter(s => s.uuid === SERV_UUID_PRIMARY) - if (primaryServices.length === 0) { - throw new Error('No service was found.') + try { + const services = await this._peripheral.discoverServicesAsync([]) + const primaryServices = services.filter(s => s.uuid === SERV_UUID_PRIMARY) + + if (primaryServices.length === 0) { + throw new Error('No service was found.') + } + return primaryServices + } catch (e: any) { + throw new Error(`Failed to discover services, Error: ${e.message ?? e}`) } - return primaryServices } /** diff --git a/src/index.ts b/src/index.ts index 246bb317..8765b462 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,3 +12,4 @@ export * from './types/deviceresponse.js' export * from './types/devicestatus.js' export * from './types/devicewebhookstatus.js' export * from './types/irdevicelist.js' +export * from './types/types.js' diff --git a/src/switchbot-ble.ts b/src/switchbot-ble.ts index 1b7635cb..e461b353 100644 --- a/src/switchbot-ble.ts +++ b/src/switchbot-ble.ts @@ -2,12 +2,12 @@ * * switchbot.ts: Switchbot BLE API registration. */ -import type * as Noble from '@stoprocent/noble' - import type { Ad, Params } from './types/types.js' import { EventEmitter } from 'node:events' +import * as Noble from '@stoprocent/noble' + import { Advertising } from './advertising.js' import { SwitchbotDevice } from './device.js' import { WoBlindTilt } from './device/woblindtilt.js' @@ -33,9 +33,9 @@ import { SwitchBotBLEModel } from './types/types.js' */ export class SwitchBotBLE extends EventEmitter { private ready: Promise - noble!: typeof Noble - ondiscover?: (device: SwitchbotDevice) => void - onadvertisement?: (ad: Ad) => void + public noble!: typeof Noble + ondiscover?: (device: SwitchbotDevice) => Promise | void + onadvertisement?: (ad: Ad) => Promise | void /** * Constructor @@ -64,15 +64,7 @@ export class SwitchBotBLE extends EventEmitter { * @returns {Promise} - Resolves when initialization is complete */ async init(params?: Params): Promise { - let noble: typeof Noble - if (params && params.noble) { - noble = params.noble - } else { - noble = (await import('@stoprocent/noble')).default as typeof Noble - } - - // Public properties - this.noble = noble + this.noble = params && params.noble ? params.noble : Noble } /** @@ -82,54 +74,11 @@ export class SwitchBotBLE extends EventEmitter { * @returns {Promise} - A promise that resolves with a list of discovered devices. */ async discover(params: Params = {}): Promise { - const promise = new Promise((resolve, reject) => { - // Check the parameters - const valid = parameterChecker.check( - params as Record, - { - duration: { required: false, type: 'integer', min: 1, max: 60000 }, - model: { - required: false, - type: 'string', - enum: [ - SwitchBotBLEModel.Bot, - SwitchBotBLEModel.Curtain, - SwitchBotBLEModel.Curtain3, - SwitchBotBLEModel.Humidifier, - SwitchBotBLEModel.Meter, - SwitchBotBLEModel.MeterPlus, - SwitchBotBLEModel.Hub2, - SwitchBotBLEModel.OutdoorMeter, - SwitchBotBLEModel.MotionSensor, - SwitchBotBLEModel.ContactSensor, - SwitchBotBLEModel.ColorBulb, - SwitchBotBLEModel.CeilingLight, - SwitchBotBLEModel.CeilingLightPro, - SwitchBotBLEModel.StripLight, - SwitchBotBLEModel.PlugMiniUS, - SwitchBotBLEModel.PlugMiniJP, - SwitchBotBLEModel.Lock, - SwitchBotBLEModel.LockPro, - SwitchBotBLEModel.BlindTilt, - ], - }, - id: { required: false, type: 'string', min: 12, max: 17 }, - quick: { required: false, type: 'boolean' }, - }, - false, - ) - - if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) - reject(new Error(parameterChecker.error!.message)) - return - } + await this.ready - if (!params) { - params = {} - } + try { + await this.validateParams(params) - // Determine the values of the parameters const p = { duration: params.duration ?? DEFAULT_DISCOVERY_DURATION, model: params.model ?? '', @@ -137,64 +86,109 @@ export class SwitchBotBLE extends EventEmitter { quick: !!params.quick, } - // Initialize the noble object - this._init() - .then(() => { - if (this.noble === null) { - return reject(new Error('noble failed to initialize')) - } - const peripherals: Record = {} - let timer: NodeJS.Timeout = setTimeout(() => { }, 0) - const finishDiscovery = () => { - if (timer) { - clearTimeout(timer) - } - - this.noble.removeAllListeners('discover') - this.noble.stopScanningAsync() - - const device_list: SwitchbotDevice[] = [] - for (const addr in peripherals) { - device_list.push(peripherals[addr]) - } - - resolve(device_list) - } + await this.initNoble() - // Set a handler for the 'discover' event - this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const device = await this.getDeviceObject(peripheral, p.id, p.model) - if (!device) { - return - } - const id = device.id - peripherals[id!] = device - - if (this.ondiscover && typeof this.ondiscover === 'function') { - this.ondiscover(device) - } - - if (p.quick) { - finishDiscovery() - } - }) - // Start scanning - this.noble.startScanningAsync( - PRIMARY_SERVICE_UUID_LIST, - false, - ).then(() => { - timer = setTimeout(() => { - finishDiscovery() - }, p.duration) - }).catch((error: Error) => { - reject(error) - }) - }) - .catch((error) => { - reject(error) + if (this.noble === null) { + throw new Error('noble failed to initialize') + } + + const peripherals: Record = {} + let timer: NodeJS.Timeout + + const finishDiscovery = (): SwitchbotDevice[] => { + if (timer) { + clearTimeout(timer) + } + + this.noble.removeAllListeners('discover') + this.noble.stopScanningAsync() + + const deviceList: SwitchbotDevice[] = Object.values(peripherals) + return deviceList + } + + this.noble.on('discover', async (peripheral: Noble.Peripheral) => { + const device = await this.getDeviceObject(peripheral, p.id, p.model) + if (!device) { + return + } + const id = device.id + peripherals[id!] = device + + if (this.ondiscover && typeof this.ondiscover === 'function') { + this.ondiscover(device) + } + + if (p.quick) { + return finishDiscovery() + } + }) + + await this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, false) + timer = setTimeout(() => { + return finishDiscovery() + }, p.duration) + + await new Promise((resolve, reject) => { + this.noble.once('stateChange', (state: typeof Noble._state) => { + switch (state) { + case 'unsupported': + case 'unauthorized': + case 'poweredOff': + reject(new Error(`Failed to initialize the Noble object: ${state}`)) + break + case 'resetting': + case 'unknown': + reject(new Error(`Adapter is not ready: ${state}`)) + break + case 'poweredOn': + resolve() + break + default: + reject(new Error(`Unknown state: ${state}`)) + } }) + }) + + return finishDiscovery() + } catch (e: any) { + this.emitLog('error', e.message ?? e) + throw e + } + } + + /** + * Initializes the noble object and waits for it to be powered on. + * + * @returns {Promise} - Resolves when the noble object is powered on. + */ + async initNoble(): Promise { + await this.ready + + if (this.noble._state === 'poweredOn') { + return + } + + return new Promise((resolve, reject) => { + this.noble.once('stateChange', (state: typeof Noble._state) => { + switch (state) { + case 'unsupported': + case 'unauthorized': + case 'poweredOff': + reject(new Error(`Failed to initialize the Noble object: ${state}`)) + break + case 'resetting': + case 'unknown': + reject(new Error(`Adapter is not ready: ${state}`)) + break + case 'poweredOn': + resolve() + break + default: + reject(new Error(`Unknown state: ${state}`)) + } + }) }) - return promise } /** @@ -205,7 +199,6 @@ export class SwitchBotBLE extends EventEmitter { async _init(): Promise { await this.ready const promise = new Promise((resolve, reject) => { - let err if (this.noble._state === 'poweredOn') { resolve() return @@ -215,26 +208,16 @@ export class SwitchBotBLE extends EventEmitter { case 'unsupported': case 'unauthorized': case 'poweredOff': - err = new Error( - `Failed to initialize the Noble object: ${this.noble._state}`, - ) - reject(err) + reject(new Error(`Failed to initialize the Noble object: ${this.noble._state}`)) return case 'resetting': case 'unknown': - err = new Error( - `Adapter is not ready: ${this.noble._state}`, - ) - reject(err) + reject(new Error(`Adapter is not ready: ${this.noble._state}`)) return case 'poweredOn': - resolve() return default: - err = new Error( - `Unknown state: ${this.noble._state}`, - ) - reject(err) + reject(new Error(`Unknown state: ${this.noble._state}`)) } }) }) @@ -345,6 +328,81 @@ export class SwitchBotBLE extends EventEmitter { return true } + /** + * Validates the provided parameters against a predefined schema. + * + * @param params - The parameters to validate. + * @returns A promise that resolves if the parameters are valid, otherwise it throws an error. + * + * @throws {Error} If the parameters do not conform to the expected schema. + * + * The expected schema for `params` is: + * - `duration` (optional): An integer between 1 and 60000. + * - `model` (optional): A string that must be one of the following values: + * - `SwitchBotBLEModel.Bot` + * - `SwitchBotBLEModel.Curtain` + * - `SwitchBotBLEModel.Curtain3` + * - `SwitchBotBLEModel.Humidifier` + * - `SwitchBotBLEModel.Meter` + * - `SwitchBotBLEModel.MeterPlus` + * - `SwitchBotBLEModel.Hub2` + * - `SwitchBotBLEModel.OutdoorMeter` + * - `SwitchBotBLEModel.MotionSensor` + * - `SwitchBotBLEModel.ContactSensor` + * - `SwitchBotBLEModel.ColorBulb` + * - `SwitchBotBLEModel.CeilingLight` + * - `SwitchBotBLEModel.CeilingLightPro` + * - `SwitchBotBLEModel.StripLight` + * - `SwitchBotBLEModel.PlugMiniUS` + * - `SwitchBotBLEModel.PlugMiniJP` + * - `SwitchBotBLEModel.Lock` + * - `SwitchBotBLEModel.LockPro` + * - `SwitchBotBLEModel.BlindTilt` + * - `id` (optional): A string with a length between 12 and 17 characters. + * - `quick` (optional): A boolean value. + */ + async validateParams(params: Params): Promise { + const valid = parameterChecker.check( + params as Record, + { + duration: { required: false, type: 'integer', min: 1, max: 60000 }, + model: { + required: false, + type: 'string', + enum: [ + SwitchBotBLEModel.Bot, + SwitchBotBLEModel.Curtain, + SwitchBotBLEModel.Curtain3, + SwitchBotBLEModel.Humidifier, + SwitchBotBLEModel.Meter, + SwitchBotBLEModel.MeterPlus, + SwitchBotBLEModel.Hub2, + SwitchBotBLEModel.OutdoorMeter, + SwitchBotBLEModel.MotionSensor, + SwitchBotBLEModel.ContactSensor, + SwitchBotBLEModel.ColorBulb, + SwitchBotBLEModel.CeilingLight, + SwitchBotBLEModel.CeilingLightPro, + SwitchBotBLEModel.StripLight, + SwitchBotBLEModel.PlugMiniUS, + SwitchBotBLEModel.PlugMiniJP, + SwitchBotBLEModel.Lock, + SwitchBotBLEModel.LockPro, + SwitchBotBLEModel.BlindTilt, + ], + }, + id: { required: false, type: 'string', min: 12, max: 17 }, + quick: { required: false, type: 'boolean' }, + }, + false, + ) + + if (!valid) { + this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) + throw new Error(parameterChecker.error!.message) + } + } + /** * Starts scanning for SwitchBot devices. * @@ -352,89 +410,22 @@ export class SwitchBotBLE extends EventEmitter { * @returns {Promise} - Resolves when scanning starts successfully. */ async startScan(params: Params = {}): Promise { - const promise = new Promise((resolve, reject) => { - // Check the parameters - const valid = parameterChecker.check( - params as Record, - { - model: { - required: false, - type: 'string', - enum: [ - SwitchBotBLEModel.Bot, - SwitchBotBLEModel.Curtain, - SwitchBotBLEModel.Curtain3, - SwitchBotBLEModel.Humidifier, - SwitchBotBLEModel.Meter, - SwitchBotBLEModel.MeterPlus, - SwitchBotBLEModel.Hub2, - SwitchBotBLEModel.OutdoorMeter, - SwitchBotBLEModel.MotionSensor, - SwitchBotBLEModel.ContactSensor, - SwitchBotBLEModel.ColorBulb, - SwitchBotBLEModel.CeilingLight, - SwitchBotBLEModel.CeilingLightPro, - SwitchBotBLEModel.StripLight, - SwitchBotBLEModel.PlugMiniUS, - SwitchBotBLEModel.PlugMiniJP, - SwitchBotBLEModel.Lock, - SwitchBotBLEModel.LockPro, - SwitchBotBLEModel.BlindTilt, - ], - }, - id: { required: false, type: 'string', min: 12, max: 17 }, - }, - false, - ) - if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) - reject(new Error(parameterChecker.error!.message)) - return - } - - // Initialize the noble object - this._init() - .then(() => { - if (this.noble === null) { - return reject(new Error('noble object failed to initialize')) - } - // Determine the values of the parameters - const p = { - model: params.model || '', - id: params.id || '', - } + const p = { + model: params.model || '', + id: params.id || '', + } - // Set a handler for the 'discover' event - this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) - if (ad && await this.filterAdvertising(ad, p.id, p.model)) { - if ( - this.onadvertisement - && typeof this.onadvertisement === 'function' - ) { - this.onadvertisement(ad) - } - } - }) - - // Start scanning - this.noble.startScanningAsync( - PRIMARY_SERVICE_UUID_LIST, - true, - ).then(() => { - this.emitLog('info', 'Started Scanning for SwitchBot BLE devices.') - resolve() - }).catch((error: Error) => { - this.emitLog('error', `startScanning error: ${JSON.stringify(error!.message)}`) - reject(error) - }) - }) - .catch((error) => { - this.emitLog('error', `startScanning error: ${JSON.stringify(error!.message)}`) - reject(error) - }) + this.noble.on('discover', async (peripheral: Noble.Peripheral) => { + const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) + if (ad && await this.filterAdvertising(ad, p.id, p.model)) { + if (this.onadvertisement && typeof this.onadvertisement === 'function') { + this.onadvertisement(ad) + } + } }) - return promise + + await this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, true) + this.emitLog('info', 'Started Scanning for SwitchBot BLE devices.') } /** diff --git a/src/switchbot-openapi.ts b/src/switchbot-openapi.ts index 890a221b..85a3e4e9 100644 --- a/src/switchbot-openapi.ts +++ b/src/switchbot-openapi.ts @@ -213,7 +213,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('debug', `Received Webhook: ${JSON.stringify(body)}`) this.emit('webhookEvent', body) } catch (e: any) { - await this.emitLog('error', `Failed to handle webhook event data. Error:${e}`) + await this.emitLog('error', `Failed to handle webhook event data, Error: ${e.message ?? e}`) } }) response.writeHead(200, { 'Content-Type': 'text/plain' }) @@ -224,11 +224,11 @@ export class SwitchBotOpenAPI extends EventEmitter { response.end(`NG`) } } catch (e: any) { - await this.emitLog('error', `Failed to handle webhook event. Error:${e}`) + await this.emitLog('error', `Failed to handle webhook event, Error: ${e.message ?? e}`) } }).listen(port || 80) } catch (e: any) { - await this.emitLog('error', `Failed to create webhook listener. Error:${e.message}`) + await this.emitLog('error', `Failed to create webhook listener, Error: ${e.message ?? e}`) return } @@ -248,7 +248,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('error', `Failed to configure webhook. Existing webhook well be overridden. HTTP:${statusCode} API:${response?.statusCode} message:${response?.message}`) } } catch (e: any) { - await this.emitLog('error', `Failed to configure webhook. Error: ${e.message}`) + await this.emitLog('error', `Failed to configure webhook, Error: ${e.message ?? e}`) } try { @@ -269,7 +269,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('error', `Failed to update webhook. HTTP:${statusCode} API:${response?.statusCode} message:${response?.message}`) } } catch (e: any) { - await this.emitLog('error', `Failed to update webhook. Error:${e.message}`) + await this.emitLog('error', `Failed to update webhook, Error: ${e.message ?? e}`) } try { @@ -288,7 +288,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('info', `Listening webhook on ${response?.body?.urls[0]}`) } } catch (e: any) { - await this.emitLog('error', `Failed to query webhook. Error:${e}`) + await this.emitLog('error', `Failed to query webhook, Error: ${e.message ?? e}`) } } @@ -318,7 +318,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('info', 'Unregistered webhook to close listening.') } } catch (e: any) { - await this.emitLog('error', `Failed to delete webhook. Error:${e.message}`) + await this.emitLog('error', `Failed to delete webhook, Error: ${e.message ?? e}`) } } } From 28579b20d24215b9abb8f4e23bba100b437050ac Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Sun, 6 Oct 2024 05:16:54 -0500 Subject: [PATCH 03/10] fix --- src/index.ts | 1 - src/parameter-checker.ts | 10 +- src/switchbot-ble.ts | 385 ++++++++++++++--------------- src/test/advertising.test.ts | 6 +- src/test/switchbot-openapi.test.ts | 101 ++------ 5 files changed, 210 insertions(+), 293 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8765b462..77118bce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ * * index.ts: Switchbot BLE API registration. */ -export * from './device.js' export * from './switchbot-ble.js' export * from './switchbot-openapi.js' export * from './types/bledevicestatus.js' diff --git a/src/parameter-checker.ts b/src/parameter-checker.ts index 4c66dc98..aca25548 100644 --- a/src/parameter-checker.ts +++ b/src/parameter-checker.ts @@ -49,15 +49,7 @@ export class ParameterChecker extends EventEmitter { */ async check(obj: Record, rules: Record, required: boolean = false): Promise { this._error = null - // eslint-disable-next-line no-console - console.log('Checking object:', obj) - // eslint-disable-next-line no-console - console.log('With rules:', rules) - // eslint-disable-next-line no-console - console.log('Is required:', required) - this.emitLog('debug', `Checking object: ${JSON.stringify(obj)}`) - this.emitLog('debug', `With rules: ${JSON.stringify(rules)}`) - this.emitLog('debug', `Is required: ${JSON.stringify(required)}`) + this.emitLog('debug', `Using rules: ${JSON.stringify(rules)}`) if (required && !this.isSpecified(obj)) { this._error = { code: 'MISSING_REQUIRED', message: 'The first argument is missing.' } diff --git a/src/switchbot-ble.ts b/src/switchbot-ble.ts index e461b353..2bebcb07 100644 --- a/src/switchbot-ble.ts +++ b/src/switchbot-ble.ts @@ -1,11 +1,11 @@ -/* Copyright(C) 2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. - * - * switchbot.ts: Switchbot BLE API registration. - */ import type { Ad, Params } from './types/types.js' import { EventEmitter } from 'node:events' +/* Copyright(C) 2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * switchbot.ts: Switchbot BLE API registration. + */ import * as Noble from '@stoprocent/noble' import { Advertising } from './advertising.js' @@ -64,7 +64,7 @@ export class SwitchBotBLE extends EventEmitter { * @returns {Promise} - Resolves when initialization is complete */ async init(params?: Params): Promise { - this.noble = params && params.noble ? params.noble : Noble + this.noble = params && params.noble ? params.noble : Noble.default as typeof Noble } /** @@ -74,11 +74,54 @@ export class SwitchBotBLE extends EventEmitter { * @returns {Promise} - A promise that resolves with a list of discovered devices. */ async discover(params: Params = {}): Promise { - await this.ready + const promise = new Promise((resolve, reject) => { + // Check the parameters + const valid = parameterChecker.check( + params as Record, + { + duration: { required: false, type: 'integer', min: 1, max: 60000 }, + model: { + required: false, + type: 'string', + enum: [ + SwitchBotBLEModel.Bot, + SwitchBotBLEModel.Curtain, + SwitchBotBLEModel.Curtain3, + SwitchBotBLEModel.Humidifier, + SwitchBotBLEModel.Meter, + SwitchBotBLEModel.MeterPlus, + SwitchBotBLEModel.Hub2, + SwitchBotBLEModel.OutdoorMeter, + SwitchBotBLEModel.MotionSensor, + SwitchBotBLEModel.ContactSensor, + SwitchBotBLEModel.ColorBulb, + SwitchBotBLEModel.CeilingLight, + SwitchBotBLEModel.CeilingLightPro, + SwitchBotBLEModel.StripLight, + SwitchBotBLEModel.PlugMiniUS, + SwitchBotBLEModel.PlugMiniJP, + SwitchBotBLEModel.Lock, + SwitchBotBLEModel.LockPro, + SwitchBotBLEModel.BlindTilt, + ], + }, + id: { required: false, type: 'string', min: 12, max: 17 }, + quick: { required: false, type: 'boolean' }, + }, + false, + ) - try { - await this.validateParams(params) + if (!valid) { + this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) + reject(new Error(parameterChecker.error!.message)) + return + } + + if (!params) { + params = {} + } + // Determine the values of the parameters const p = { duration: params.duration ?? DEFAULT_DISCOVERY_DURATION, model: params.model ?? '', @@ -86,109 +129,64 @@ export class SwitchBotBLE extends EventEmitter { quick: !!params.quick, } - await this.initNoble() - - if (this.noble === null) { - throw new Error('noble failed to initialize') - } - - const peripherals: Record = {} - let timer: NodeJS.Timeout - - const finishDiscovery = (): SwitchbotDevice[] => { - if (timer) { - clearTimeout(timer) - } - - this.noble.removeAllListeners('discover') - this.noble.stopScanningAsync() - - const deviceList: SwitchbotDevice[] = Object.values(peripherals) - return deviceList - } - - this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const device = await this.getDeviceObject(peripheral, p.id, p.model) - if (!device) { - return - } - const id = device.id - peripherals[id!] = device - - if (this.ondiscover && typeof this.ondiscover === 'function') { - this.ondiscover(device) - } - - if (p.quick) { - return finishDiscovery() - } - }) - - await this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, false) - timer = setTimeout(() => { - return finishDiscovery() - }, p.duration) - - await new Promise((resolve, reject) => { - this.noble.once('stateChange', (state: typeof Noble._state) => { - switch (state) { - case 'unsupported': - case 'unauthorized': - case 'poweredOff': - reject(new Error(`Failed to initialize the Noble object: ${state}`)) - break - case 'resetting': - case 'unknown': - reject(new Error(`Adapter is not ready: ${state}`)) - break - case 'poweredOn': - resolve() - break - default: - reject(new Error(`Unknown state: ${state}`)) + // Initialize the noble object + this._init() + .then(() => { + if (this.noble === null) { + return reject(new Error('noble failed to initialize')) + } + const peripherals: Record = {} + let timer: NodeJS.Timeout = setTimeout(() => { }, 0) + const finishDiscovery = () => { + if (timer) { + clearTimeout(timer) + } + + this.noble.removeAllListeners('discover') + this.noble.stopScanningAsync() + + const device_list: SwitchbotDevice[] = [] + for (const addr in peripherals) { + device_list.push(peripherals[addr]) + } + + resolve(device_list) } - }) - }) - - return finishDiscovery() - } catch (e: any) { - this.emitLog('error', e.message ?? e) - throw e - } - } - - /** - * Initializes the noble object and waits for it to be powered on. - * - * @returns {Promise} - Resolves when the noble object is powered on. - */ - async initNoble(): Promise { - await this.ready - - if (this.noble._state === 'poweredOn') { - return - } - return new Promise((resolve, reject) => { - this.noble.once('stateChange', (state: typeof Noble._state) => { - switch (state) { - case 'unsupported': - case 'unauthorized': - case 'poweredOff': - reject(new Error(`Failed to initialize the Noble object: ${state}`)) - break - case 'resetting': - case 'unknown': - reject(new Error(`Adapter is not ready: ${state}`)) - break - case 'poweredOn': - resolve() - break - default: - reject(new Error(`Unknown state: ${state}`)) - } - }) + // Set a handler for the 'discover' event + this.noble.on('discover', async (peripheral: Noble.Peripheral) => { + const device = await this.getDeviceObject(peripheral, p.id, p.model) + if (!device) { + return + } + const id = device.id + peripherals[id!] = device + + if (this.ondiscover && typeof this.ondiscover === 'function') { + this.ondiscover(device) + } + + if (p.quick) { + finishDiscovery() + } + }) + // Start scanning + this.noble.startScanningAsync( + PRIMARY_SERVICE_UUID_LIST, + false, + ).then(() => { + timer = setTimeout(() => { + finishDiscovery() + }, p.duration) + }).catch((error: Error) => { + reject(error) + }) + }) + .catch((error) => { + reject(error) + }) }) + return promise } /** @@ -215,6 +213,7 @@ export class SwitchBotBLE extends EventEmitter { reject(new Error(`Adapter is not ready: ${this.noble._state}`)) return case 'poweredOn': + resolve() return default: reject(new Error(`Unknown state: ${this.noble._state}`)) @@ -328,81 +327,6 @@ export class SwitchBotBLE extends EventEmitter { return true } - /** - * Validates the provided parameters against a predefined schema. - * - * @param params - The parameters to validate. - * @returns A promise that resolves if the parameters are valid, otherwise it throws an error. - * - * @throws {Error} If the parameters do not conform to the expected schema. - * - * The expected schema for `params` is: - * - `duration` (optional): An integer between 1 and 60000. - * - `model` (optional): A string that must be one of the following values: - * - `SwitchBotBLEModel.Bot` - * - `SwitchBotBLEModel.Curtain` - * - `SwitchBotBLEModel.Curtain3` - * - `SwitchBotBLEModel.Humidifier` - * - `SwitchBotBLEModel.Meter` - * - `SwitchBotBLEModel.MeterPlus` - * - `SwitchBotBLEModel.Hub2` - * - `SwitchBotBLEModel.OutdoorMeter` - * - `SwitchBotBLEModel.MotionSensor` - * - `SwitchBotBLEModel.ContactSensor` - * - `SwitchBotBLEModel.ColorBulb` - * - `SwitchBotBLEModel.CeilingLight` - * - `SwitchBotBLEModel.CeilingLightPro` - * - `SwitchBotBLEModel.StripLight` - * - `SwitchBotBLEModel.PlugMiniUS` - * - `SwitchBotBLEModel.PlugMiniJP` - * - `SwitchBotBLEModel.Lock` - * - `SwitchBotBLEModel.LockPro` - * - `SwitchBotBLEModel.BlindTilt` - * - `id` (optional): A string with a length between 12 and 17 characters. - * - `quick` (optional): A boolean value. - */ - async validateParams(params: Params): Promise { - const valid = parameterChecker.check( - params as Record, - { - duration: { required: false, type: 'integer', min: 1, max: 60000 }, - model: { - required: false, - type: 'string', - enum: [ - SwitchBotBLEModel.Bot, - SwitchBotBLEModel.Curtain, - SwitchBotBLEModel.Curtain3, - SwitchBotBLEModel.Humidifier, - SwitchBotBLEModel.Meter, - SwitchBotBLEModel.MeterPlus, - SwitchBotBLEModel.Hub2, - SwitchBotBLEModel.OutdoorMeter, - SwitchBotBLEModel.MotionSensor, - SwitchBotBLEModel.ContactSensor, - SwitchBotBLEModel.ColorBulb, - SwitchBotBLEModel.CeilingLight, - SwitchBotBLEModel.CeilingLightPro, - SwitchBotBLEModel.StripLight, - SwitchBotBLEModel.PlugMiniUS, - SwitchBotBLEModel.PlugMiniJP, - SwitchBotBLEModel.Lock, - SwitchBotBLEModel.LockPro, - SwitchBotBLEModel.BlindTilt, - ], - }, - id: { required: false, type: 'string', min: 12, max: 17 }, - quick: { required: false, type: 'boolean' }, - }, - false, - ) - - if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) - throw new Error(parameterChecker.error!.message) - } - } - /** * Starts scanning for SwitchBot devices. * @@ -410,22 +334,89 @@ export class SwitchBotBLE extends EventEmitter { * @returns {Promise} - Resolves when scanning starts successfully. */ async startScan(params: Params = {}): Promise { - const p = { - model: params.model || '', - id: params.id || '', - } - - this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) - if (ad && await this.filterAdvertising(ad, p.id, p.model)) { - if (this.onadvertisement && typeof this.onadvertisement === 'function') { - this.onadvertisement(ad) - } + const promise = new Promise((resolve, reject) => { + // Check the parameters + const valid = parameterChecker.check( + params as Record, + { + model: { + required: false, + type: 'string', + enum: [ + SwitchBotBLEModel.Bot, + SwitchBotBLEModel.Curtain, + SwitchBotBLEModel.Curtain3, + SwitchBotBLEModel.Humidifier, + SwitchBotBLEModel.Meter, + SwitchBotBLEModel.MeterPlus, + SwitchBotBLEModel.Hub2, + SwitchBotBLEModel.OutdoorMeter, + SwitchBotBLEModel.MotionSensor, + SwitchBotBLEModel.ContactSensor, + SwitchBotBLEModel.ColorBulb, + SwitchBotBLEModel.CeilingLight, + SwitchBotBLEModel.CeilingLightPro, + SwitchBotBLEModel.StripLight, + SwitchBotBLEModel.PlugMiniUS, + SwitchBotBLEModel.PlugMiniJP, + SwitchBotBLEModel.Lock, + SwitchBotBLEModel.LockPro, + SwitchBotBLEModel.BlindTilt, + ], + }, + id: { required: false, type: 'string', min: 12, max: 17 }, + }, + false, + ) + if (!valid) { + this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) + reject(new Error(parameterChecker.error!.message)) + return } - }) - await this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, true) - this.emitLog('info', 'Started Scanning for SwitchBot BLE devices.') + // Initialize the noble object + this._init() + .then(() => { + if (this.noble === null) { + return reject(new Error('noble object failed to initialize')) + } + // Determine the values of the parameters + const p = { + model: params.model || '', + id: params.id || '', + } + + // Set a handler for the 'discover' event + this.noble.on('discover', async (peripheral: Noble.Peripheral) => { + const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) + if (ad && await this.filterAdvertising(ad, p.id, p.model)) { + if ( + this.onadvertisement + && typeof this.onadvertisement === 'function' + ) { + this.onadvertisement(ad) + } + } + }) + + // Start scanning + this.noble.startScanningAsync( + PRIMARY_SERVICE_UUID_LIST, + true, + ).then(() => { + this.emitLog('info', 'Started Scanning for SwitchBot BLE devices.') + resolve() + }).catch((error: Error) => { + this.emitLog('error', `startScanning error: ${JSON.stringify(error!.message)}`) + reject(error) + }) + }) + .catch((error) => { + this.emitLog('error', `startScanning error: ${JSON.stringify(error!.message)}`) + reject(error) + }) + }) + return promise } /** diff --git a/src/test/advertising.test.ts b/src/test/advertising.test.ts index 6f802dcf..377aa239 100644 --- a/src/test/advertising.test.ts +++ b/src/test/advertising.test.ts @@ -75,7 +75,7 @@ describe('advertising', () => { const result = await Advertising.parse(peripheral, mockLog) expect(result).toBeNull() - expect(mockLog).toHaveBeenCalledWith('debugerror', '[parseAdvertising.test-id.\x01] return null, parsed serviceData empty!') + expect(mockLog).toHaveBeenCalledWith('[parseAdvertising.test-id.\x01] return null, parsed serviceData empty!') }) }) @@ -86,10 +86,10 @@ describe('advertising', () => { expect(result).toBe(true) }) - it('should return null for invalid buffer', () => { + it('should return false for invalid buffer', () => { const buffer = null const result = (Advertising as any).validateBuffer(buffer) - expect(result).toBe(null) + expect(result).toBe(false) }) }) diff --git a/src/test/switchbot-openapi.test.ts b/src/test/switchbot-openapi.test.ts index ce92085c..7d5c52b0 100644 --- a/src/test/switchbot-openapi.test.ts +++ b/src/test/switchbot-openapi.test.ts @@ -1,44 +1,31 @@ -import type { Mock } from 'vitest' - -import { createServer } from 'node:http' - import { request } from 'undici' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { SwitchBotOpenAPI } from '../switchbot-openapi.js' -vi.mock('undici', () => ({ - request: vi.fn(), +jest.mock('undici', () => ({ + request: jest.fn(), })) describe('switchBotOpenAPI', () => { let switchBotAPI: SwitchBotOpenAPI const token = 'test-token' const secret = 'test-secret' - const port = 3000 - let server: any beforeEach(() => { switchBotAPI = new SwitchBotOpenAPI(token, secret) - if (server && typeof server.close === 'function') { - server.close() - } - server = startServer(port) }) afterEach(() => { - vi.clearAllMocks() - if (server && typeof server.close === 'function') { - server.close() - } + jest.clearAllMocks() }) describe('getDevices', () => { it('should retrieve the list of devices', async () => { const mockDevicesResponse = { body: { devices: [] }, statusCode: 200 }; - (request as Mock).mockResolvedValue({ + (request as jest.Mock).mockResolvedValue({ body: { - json: vi.fn().mockResolvedValue(mockDevicesResponse.body), + json: jest.fn().mockResolvedValue(mockDevicesResponse.body), }, statusCode: mockDevicesResponse.statusCode, }) @@ -51,7 +38,7 @@ describe('switchBotOpenAPI', () => { it('should throw an error if the request fails', async () => { const errorMessage = 'Failed to get devices'; - (request as Mock).mockRejectedValue(new Error(errorMessage)) + (request as jest.Mock).mockRejectedValue(new Error(errorMessage)) await expect(switchBotAPI.getDevices()).rejects.toThrow(`Failed to get devices: ${errorMessage}`) }) @@ -60,9 +47,9 @@ describe('switchBotOpenAPI', () => { describe('controlDevice', () => { it('should control a device by sending a command', async () => { const mockControlResponse = { body: {}, statusCode: 200 }; - (request as Mock).mockResolvedValue({ + (request as jest.Mock).mockResolvedValue({ body: { - json: vi.fn().mockResolvedValue(mockControlResponse.body), + json: jest.fn().mockResolvedValue(mockControlResponse.body), }, statusCode: mockControlResponse.statusCode, }) @@ -75,7 +62,7 @@ describe('switchBotOpenAPI', () => { it('should throw an error if the device control fails', async () => { const errorMessage = 'Failed to control device'; - (request as Mock).mockRejectedValue(new Error(errorMessage)) + (request as jest.Mock).mockRejectedValue(new Error(errorMessage)) await expect(switchBotAPI.controlDevice('device-id', 'turnOn', 'default')).rejects.toThrow(`Failed to control device: ${errorMessage}`) }) @@ -84,9 +71,9 @@ describe('switchBotOpenAPI', () => { describe('getDeviceStatus', () => { it('should retrieve the status of a specific device', async () => { const mockStatusResponse = { body: {}, statusCode: 200 }; - (request as Mock).mockResolvedValue({ + (request as jest.Mock).mockResolvedValue({ body: { - json: vi.fn().mockResolvedValue(mockStatusResponse.body), + json: jest.fn().mockResolvedValue(mockStatusResponse.body), }, statusCode: mockStatusResponse.statusCode, }) @@ -99,7 +86,7 @@ describe('switchBotOpenAPI', () => { it('should throw an error if the request fails', async () => { const errorMessage = 'Failed to get device status'; - (request as Mock).mockRejectedValue(new Error(errorMessage)) + (request as jest.Mock).mockRejectedValue(new Error(errorMessage)) await expect(switchBotAPI.getDeviceStatus('device-id')).rejects.toThrow(`Failed to get device status: ${errorMessage}`) }) @@ -108,14 +95,14 @@ describe('switchBotOpenAPI', () => { describe('setupWebhook', () => { it('should set up a webhook listener and configure the webhook on the server', async () => { const mockWebhookResponse = { body: {}, statusCode: 200 }; - (request as Mock).mockResolvedValue({ + (request as jest.Mock).mockResolvedValue({ body: { - json: vi.fn().mockResolvedValue(mockWebhookResponse.body), + json: jest.fn().mockResolvedValue(mockWebhookResponse.body), }, statusCode: mockWebhookResponse.statusCode, }) - const url = `http://localhost:${port}/webhook` + const url = 'http://localhost:3000/webhook' await switchBotAPI.setupWebhook(url) expect(request).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) @@ -123,64 +110,12 @@ describe('switchBotOpenAPI', () => { it('should log an error if the webhook setup fails', async () => { const errorMessage = 'Failed to create webhook listener'; - (request as Mock).mockRejectedValue(new Error(errorMessage)) + (request as jest.Mock).mockRejectedValue(new Error(errorMessage)) - const url = `http://localhost:${port}/webhook` + const url = 'http://localhost:3000/webhook' await switchBotAPI.setupWebhook(url) expect(request).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) }) }) - - describe('deleteWebhook', () => { - it('should delete the webhook listener and remove the webhook from the server', async () => { - const mockDeleteResponse = { body: {}, statusCode: 200 }; - (request as Mock).mockResolvedValue({ - body: { - json: vi.fn().mockResolvedValue(mockDeleteResponse.body), - }, - statusCode: mockDeleteResponse.statusCode, - }) - - const url = `http://localhost:${port}/webhook` - await switchBotAPI.deleteWebhook(url) - - expect(request).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) - }) - - it('should log an error if the webhook deletion fails', async () => { - const errorMessage = 'Failed to delete webhook listener'; - (request as Mock).mockRejectedValue(new Error(errorMessage)) - - const url = `http://localhost:${port}/webhook` - await switchBotAPI.deleteWebhook(url) - - expect(request).toHaveBeenCalledWith(expect.any(String), expect.any(Object)) - }) - }) }) - -function startServer(port: number): any { - const server = createServer((req, res) => { - if (req.method === 'POST' && req.url === '/webhook') { - req.on('data', () => { - // Process the chunk if needed - }) - req.on('end', () => { - // Log the webhook received event - // console.log('Webhook received:', body) - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ message: 'Webhook received' })) - }) - } else { - res.writeHead(404, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ message: 'Not Found' })) - } - }) - - server.listen(port, () => { - // Server is listening on port ${port} - }) - - return server -} From d47f46c956cc47949e28661a20a409bcd1c7cb1c Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Sun, 6 Oct 2024 07:08:49 -0500 Subject: [PATCH 04/10] working --- src/switchbot-ble.ts | 424 +++++++++++++++++++------------------------ 1 file changed, 188 insertions(+), 236 deletions(-) diff --git a/src/switchbot-ble.ts b/src/switchbot-ble.ts index 2bebcb07..c779bb88 100644 --- a/src/switchbot-ble.ts +++ b/src/switchbot-ble.ts @@ -1,11 +1,7 @@ -import type { Ad, Params } from './types/types.js' +import type { Ad, Params, Rule } from './types/types.js' import { EventEmitter } from 'node:events' -/* Copyright(C) 2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. - * - * switchbot.ts: Switchbot BLE API registration. - */ import * as Noble from '@stoprocent/noble' import { Advertising } from './advertising.js' @@ -28,6 +24,7 @@ import { WoStrip } from './device/wostrip.js' import { parameterChecker } from './parameter-checker.js' import { DEFAULT_DISCOVERY_DURATION, PRIMARY_SERVICE_UUID_LIST } from './settings.js' import { SwitchBotBLEModel } from './types/types.js' + /** * SwitchBot class to interact with SwitchBot devices. */ @@ -68,125 +65,18 @@ export class SwitchBotBLE extends EventEmitter { } /** - * Discover SwitchBot devices based on the provided parameters. + * Validates the parameters. * - * @param {Params} params - The parameters for discovery. - * @returns {Promise} - A promise that resolves with a list of discovered devices. + * @param {Params} params - The parameters to validate. + * @param {Record} schema - The schema to validate against. + * @returns {Promise} - Resolves if parameters are valid, otherwise throws an error. */ - async discover(params: Params = {}): Promise { - const promise = new Promise((resolve, reject) => { - // Check the parameters - const valid = parameterChecker.check( - params as Record, - { - duration: { required: false, type: 'integer', min: 1, max: 60000 }, - model: { - required: false, - type: 'string', - enum: [ - SwitchBotBLEModel.Bot, - SwitchBotBLEModel.Curtain, - SwitchBotBLEModel.Curtain3, - SwitchBotBLEModel.Humidifier, - SwitchBotBLEModel.Meter, - SwitchBotBLEModel.MeterPlus, - SwitchBotBLEModel.Hub2, - SwitchBotBLEModel.OutdoorMeter, - SwitchBotBLEModel.MotionSensor, - SwitchBotBLEModel.ContactSensor, - SwitchBotBLEModel.ColorBulb, - SwitchBotBLEModel.CeilingLight, - SwitchBotBLEModel.CeilingLightPro, - SwitchBotBLEModel.StripLight, - SwitchBotBLEModel.PlugMiniUS, - SwitchBotBLEModel.PlugMiniJP, - SwitchBotBLEModel.Lock, - SwitchBotBLEModel.LockPro, - SwitchBotBLEModel.BlindTilt, - ], - }, - id: { required: false, type: 'string', min: 12, max: 17 }, - quick: { required: false, type: 'boolean' }, - }, - false, - ) - - if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) - reject(new Error(parameterChecker.error!.message)) - return - } - - if (!params) { - params = {} - } - - // Determine the values of the parameters - const p = { - duration: params.duration ?? DEFAULT_DISCOVERY_DURATION, - model: params.model ?? '', - id: params.id ?? '', - quick: !!params.quick, - } - - // Initialize the noble object - this._init() - .then(() => { - if (this.noble === null) { - return reject(new Error('noble failed to initialize')) - } - const peripherals: Record = {} - let timer: NodeJS.Timeout = setTimeout(() => { }, 0) - const finishDiscovery = () => { - if (timer) { - clearTimeout(timer) - } - - this.noble.removeAllListeners('discover') - this.noble.stopScanningAsync() - - const device_list: SwitchbotDevice[] = [] - for (const addr in peripherals) { - device_list.push(peripherals[addr]) - } - - resolve(device_list) - } - - // Set a handler for the 'discover' event - this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const device = await this.getDeviceObject(peripheral, p.id, p.model) - if (!device) { - return - } - const id = device.id - peripherals[id!] = device - - if (this.ondiscover && typeof this.ondiscover === 'function') { - this.ondiscover(device) - } - - if (p.quick) { - finishDiscovery() - } - }) - // Start scanning - this.noble.startScanningAsync( - PRIMARY_SERVICE_UUID_LIST, - false, - ).then(() => { - timer = setTimeout(() => { - finishDiscovery() - }, p.duration) - }).catch((error: Error) => { - reject(error) - }) - }) - .catch((error) => { - reject(error) - }) - }) - return promise + private async validateParams(params: Params, schema: Record): Promise { + const valid = parameterChecker.check(params as Record, schema as Record, false) + if (!valid) { + this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) + throw new Error(parameterChecker.error!.message) + } } /** @@ -196,31 +86,129 @@ export class SwitchBotBLE extends EventEmitter { */ async _init(): Promise { await this.ready - const promise = new Promise((resolve, reject) => { - if (this.noble._state === 'poweredOn') { - resolve() - return - } + if (this.noble._state === 'poweredOn') { + return + } + return new Promise((resolve, reject) => { this.noble.once('stateChange', (state: typeof Noble._state) => { switch (state) { case 'unsupported': case 'unauthorized': case 'poweredOff': - reject(new Error(`Failed to initialize the Noble object: ${this.noble._state}`)) - return + reject(new Error(`Failed to initialize the Noble object: ${state}`)) + break case 'resetting': case 'unknown': - reject(new Error(`Adapter is not ready: ${this.noble._state}`)) - return + reject(new Error(`Adapter is not ready: ${state}`)) + break case 'poweredOn': resolve() - return + break default: - reject(new Error(`Unknown state: ${this.noble._state}`)) + reject(new Error(`Unknown state: ${state}`)) + } + }) + }) + } + + /** + * Discover SwitchBot devices based on the provided parameters. + * + * @param {Params} params - The parameters for discovery. + * @returns {Promise} - A promise that resolves with a list of discovered devices. + */ + async discover(params: Params = {}): Promise { + await this.ready + await this.validateParams(params, { + duration: { required: false, type: 'integer', min: 1, max: 60000 }, + model: { + required: false, + type: 'string', + enum: [ + SwitchBotBLEModel.Bot, + SwitchBotBLEModel.Curtain, + SwitchBotBLEModel.Curtain3, + SwitchBotBLEModel.Humidifier, + SwitchBotBLEModel.Meter, + SwitchBotBLEModel.MeterPlus, + SwitchBotBLEModel.Hub2, + SwitchBotBLEModel.OutdoorMeter, + SwitchBotBLEModel.MotionSensor, + SwitchBotBLEModel.ContactSensor, + SwitchBotBLEModel.ColorBulb, + SwitchBotBLEModel.CeilingLight, + SwitchBotBLEModel.CeilingLightPro, + SwitchBotBLEModel.StripLight, + SwitchBotBLEModel.PlugMiniUS, + SwitchBotBLEModel.PlugMiniJP, + SwitchBotBLEModel.Lock, + SwitchBotBLEModel.LockPro, + SwitchBotBLEModel.BlindTilt, + ], + }, + id: { required: false, type: 'string', min: 12, max: 17 }, + quick: { required: false, type: 'boolean' }, + }) + + await this._init() + + if (this.noble === null) { + throw new Error('noble failed to initialize') + } + + const p = { + duration: params.duration ?? DEFAULT_DISCOVERY_DURATION, + model: params.model ?? '', + id: params.id ?? '', + quick: !!params.quick, + } + + const peripherals: Record = {} + let timer: NodeJS.Timeout + + const finishDiscovery = async () => { + if (timer) { + clearTimeout(timer) + } + + this.noble.removeAllListeners('discover') + try { + await this.noble.stopScanningAsync() + this.emitLog('info', 'Stopped Scanning for SwitchBot BLE devices.') + } catch (e: any) { + this.emitLog('error', `discover stopScanningAsync error: ${JSON.stringify(e.message ?? e)}`) + } + + return Object.values(peripherals) + } + + return new Promise((resolve, reject) => { + this.noble.on('discover', async (peripheral: Noble.Peripheral) => { + const device = await this.getDeviceObject(peripheral, p.id, p.model) + if (!device) { + return + } + peripherals[device.id!] = device + + if (this.ondiscover && typeof this.ondiscover === 'function') { + this.ondiscover(device) + } + + if (p.quick) { + resolve(await finishDiscovery()) } }) + + this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, false) + .then(() => { + timer = setTimeout(async () => { + resolve(await finishDiscovery()) + }, p.duration) + }) + .catch((error: Error) => { + reject(error) + }) }) - return promise } /** @@ -290,7 +278,7 @@ export class SwitchBotBLE extends EventEmitter { case SwitchBotBLEModel.BlindTilt: device = new WoBlindTilt(peripheral, this.noble) break - default: // 'resetting', 'unknown' + default: device = new SwitchbotDevice(peripheral, this.noble) } } @@ -334,89 +322,62 @@ export class SwitchBotBLE extends EventEmitter { * @returns {Promise} - Resolves when scanning starts successfully. */ async startScan(params: Params = {}): Promise { - const promise = new Promise((resolve, reject) => { - // Check the parameters - const valid = parameterChecker.check( - params as Record, - { - model: { - required: false, - type: 'string', - enum: [ - SwitchBotBLEModel.Bot, - SwitchBotBLEModel.Curtain, - SwitchBotBLEModel.Curtain3, - SwitchBotBLEModel.Humidifier, - SwitchBotBLEModel.Meter, - SwitchBotBLEModel.MeterPlus, - SwitchBotBLEModel.Hub2, - SwitchBotBLEModel.OutdoorMeter, - SwitchBotBLEModel.MotionSensor, - SwitchBotBLEModel.ContactSensor, - SwitchBotBLEModel.ColorBulb, - SwitchBotBLEModel.CeilingLight, - SwitchBotBLEModel.CeilingLightPro, - SwitchBotBLEModel.StripLight, - SwitchBotBLEModel.PlugMiniUS, - SwitchBotBLEModel.PlugMiniJP, - SwitchBotBLEModel.Lock, - SwitchBotBLEModel.LockPro, - SwitchBotBLEModel.BlindTilt, - ], - }, - id: { required: false, type: 'string', min: 12, max: 17 }, - }, - false, - ) - if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) - reject(new Error(parameterChecker.error!.message)) - return - } + await this.ready + await this.validateParams(params, { + model: { + required: false, + type: 'string', + enum: [ + SwitchBotBLEModel.Bot, + SwitchBotBLEModel.Curtain, + SwitchBotBLEModel.Curtain3, + SwitchBotBLEModel.Humidifier, + SwitchBotBLEModel.Meter, + SwitchBotBLEModel.MeterPlus, + SwitchBotBLEModel.Hub2, + SwitchBotBLEModel.OutdoorMeter, + SwitchBotBLEModel.MotionSensor, + SwitchBotBLEModel.ContactSensor, + SwitchBotBLEModel.ColorBulb, + SwitchBotBLEModel.CeilingLight, + SwitchBotBLEModel.CeilingLightPro, + SwitchBotBLEModel.StripLight, + SwitchBotBLEModel.PlugMiniUS, + SwitchBotBLEModel.PlugMiniJP, + SwitchBotBLEModel.Lock, + SwitchBotBLEModel.LockPro, + SwitchBotBLEModel.BlindTilt, + ], + }, + id: { required: false, type: 'string', min: 12, max: 17 }, + }) - // Initialize the noble object - this._init() - .then(() => { - if (this.noble === null) { - return reject(new Error('noble object failed to initialize')) - } - // Determine the values of the parameters - const p = { - model: params.model || '', - id: params.id || '', - } - - // Set a handler for the 'discover' event - this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) - if (ad && await this.filterAdvertising(ad, p.id, p.model)) { - if ( - this.onadvertisement - && typeof this.onadvertisement === 'function' - ) { - this.onadvertisement(ad) - } - } - }) - - // Start scanning - this.noble.startScanningAsync( - PRIMARY_SERVICE_UUID_LIST, - true, - ).then(() => { - this.emitLog('info', 'Started Scanning for SwitchBot BLE devices.') - resolve() - }).catch((error: Error) => { - this.emitLog('error', `startScanning error: ${JSON.stringify(error!.message)}`) - reject(error) - }) - }) - .catch((error) => { - this.emitLog('error', `startScanning error: ${JSON.stringify(error!.message)}`) - reject(error) - }) + await this._init() + + if (this.noble === null) { + throw new Error('noble object failed to initialize') + } + + const p = { + model: params.model || '', + id: params.id || '', + } + + this.noble.on('discover', async (peripheral: Noble.Peripheral) => { + const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) + if (ad && await this.filterAdvertising(ad, p.id, p.model)) { + if (this.onadvertisement && typeof this.onadvertisement === 'function') { + this.onadvertisement(ad) + } + } }) - return promise + + try { + await this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, true) + this.emitLog('info', 'Started Scanning for SwitchBot BLE devices.') + } catch (e: any) { + this.emitLog('error', `startScanningAsync error: ${JSON.stringify(e.message ?? e)}`) + } } /** @@ -430,8 +391,12 @@ export class SwitchBotBLE extends EventEmitter { } this.noble.removeAllListeners('discover') - this.noble.stopScanningAsync() - this.emitLog('info', 'Stopped Scanning for SwitchBot BLE devices.') + try { + await this.noble.stopScanningAsync() + this.emitLog('info', 'Stopped Scanning for SwitchBot BLE devices.') + } catch (e: any) { + this.emitLog('error', `stopScanningAsync error: ${JSON.stringify(e.message ?? e)}`) + } } /** @@ -441,24 +406,11 @@ export class SwitchBotBLE extends EventEmitter { * @returns {Promise} - Resolves after the specified time. */ async wait(msec: number): Promise { - return new Promise((resolve, reject) => { - // Check the parameters - const valid = parameterChecker.check( - { - msec, - }, - { - msec: { required: true, type: 'integer', min: 0 }, - }, - true, // Add the required argument - ) - - if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) - reject(new Error(parameterChecker.error!.message)) - return - } - // Set a timer + if (typeof msec !== 'number' || msec < 0) { + throw new Error('Invalid parameter: msec must be a non-negative integer.') + } + + return new Promise((resolve) => { setTimeout(resolve, msec) }) } From e4f4e889dd628d08ef418366e66a9be2740f89ee Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Sun, 6 Oct 2024 21:56:39 -0500 Subject: [PATCH 05/10] others --- src/advertising.ts | 2 +- src/device.ts | 198 +++++++++++++------------- src/device/woblindtilt.ts | 16 ++- src/device/wobulb.ts | 27 ++-- src/device/woceilinglight.ts | 36 +++-- src/device/wocontact.ts | 18 ++- src/device/wocurtain.ts | 54 ++++--- src/device/wohand.ts | 33 +++-- src/device/wohub2.ts | 16 ++- src/device/wohumi.ts | 16 ++- src/device/woiosensorth.ts | 16 ++- src/device/woplugmini.ts | 31 ++-- src/device/wopresence.ts | 6 + src/device/wosensorth.ts | 6 + src/device/wostrip.ts | 6 + src/index.ts | 17 +++ src/switchbot-ble.ts | 251 ++++++++++----------------------- src/test/switchbot-ble.test.ts | 52 +++++++ src/test/switchbot.test.ts | 118 ---------------- src/types/bledevicestatus.ts | 8 +- 20 files changed, 453 insertions(+), 474 deletions(-) create mode 100644 src/test/switchbot-ble.test.ts delete mode 100644 src/test/switchbot.test.ts diff --git a/src/advertising.ts b/src/advertising.ts index 0014bb2a..3131ddfb 100644 --- a/src/advertising.ts +++ b/src/advertising.ts @@ -60,7 +60,7 @@ export class Advertising { const model = serviceData.subarray(0, 1).toString('utf8') const sd = await Advertising.parseServiceData(model, serviceData, manufacturerData, emitLog) if (!sd) { - emitLog('debugerror', `[parseAdvertising.${peripheral.id}.${model}] return null, parsed serviceData empty!`) + // emitLog('debugerror', `[parseAdvertising.${peripheral.id}.${model}] return null, parsed serviceData empty!`) return null } diff --git a/src/device.ts b/src/device.ts index 9f4c8406..21fc56ab 100644 --- a/src/device.ts +++ b/src/device.ts @@ -4,39 +4,33 @@ */ import type * as Noble from '@stoprocent/noble' -import type { Chars, SwitchBotBLEModel, SwitchBotBLEModelName } from './types/types.js' +import type { Chars, SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from './types/types.js' import { Buffer } from 'node:buffer' import { EventEmitter } from 'node:events' import { Advertising } from './advertising.js' import { parameterChecker } from './parameter-checker.js' -import { - CHAR_UUID_DEVICE, - CHAR_UUID_NOTIFY, - CHAR_UUID_WRITE, - READ_TIMEOUT_MSEC, - SERV_UUID_PRIMARY, - WRITE_TIMEOUT_MSEC, -} from './settings.js' +import { CHAR_UUID_DEVICE, CHAR_UUID_NOTIFY, CHAR_UUID_WRITE, READ_TIMEOUT_MSEC, SERV_UUID_PRIMARY, WRITE_TIMEOUT_MSEC } from './settings.js' /** * Represents a Switchbot Device. */ export class SwitchbotDevice extends EventEmitter { - private _noble: typeof Noble - private _peripheral: Noble.Peripheral - private _characteristics: Chars | null = null - private _id!: string - private _address!: string - private _model!: SwitchBotBLEModel - private _modelName!: SwitchBotBLEModelName - private _explicitly = false - private _connected = false - private onnotify_internal: (buf: Buffer) => void = () => {} - - private ondisconnect_internal: () => Promise = async () => {} - private onconnect_internal: () => Promise = async () => {} + [x: string]: any + private noble: typeof Noble + private peripheral: Noble.Peripheral + private characteristics: Chars | null = null + private deviceId!: string + private deviceAddress!: string + private deviceModel!: SwitchBotBLEModel + private deviceModelName!: SwitchBotBLEModelName + private deviceFriendlyName!: SwitchBotBLEModelFriendlyName + private explicitlyConnected = false + private isConnected = false + private onNotify: (buf: Buffer) => void = () => {} + private onDisconnect: () => Promise = async () => {} + private onConnect: () => Promise = async () => {} /** * Initializes a new instance of the SwitchbotDevice class. @@ -45,70 +39,74 @@ export class SwitchbotDevice extends EventEmitter { */ constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { super() - this._peripheral = peripheral - this._noble = noble - - Advertising.parse(peripheral, this.emitLog.bind(this)).then((ad) => { - this._id = ad?.id ?? '' - this._address = ad?.address ?? '' - this._model = ad?.serviceData.model as SwitchBotBLEModel ?? '' - this._modelName = ad?.serviceData.modelName as SwitchBotBLEModelName ?? '' + this.peripheral = peripheral + this.noble = noble + + Advertising.parse(peripheral, this.log.bind(this)).then((ad) => { + this.deviceId = ad?.id ?? '' + this.deviceAddress = ad?.address ?? '' + this.deviceModel = ad?.serviceData.model as SwitchBotBLEModel ?? '' + this.deviceModelName = ad?.serviceData.modelName as SwitchBotBLEModelName ?? '' + this.deviceFriendlyName = ad?.serviceData.modelFriendlyName as SwitchBotBLEModelFriendlyName ?? '' }) } /** - * Emits a log event with the specified log level and message. - * - * @param level - The severity level of the log (e.g., 'info', 'warn', 'error'). - * @param message - The log message to be emitted. + * Logs a message with the specified log level. + * @param level The severity level of the log (e.g., 'info', 'warn', 'error'). + * @param message The log message to be emitted. */ - public async emitLog(level: string, message: string): Promise { + public async log(level: string, message: string): Promise { this.emit('log', { level, message }) } // Getters get id(): string { - return this._id + return this.deviceId } get address(): string { - return this._address + return this.deviceAddress } get model(): SwitchBotBLEModel { - return this._model + return this.deviceModel } get modelName(): SwitchBotBLEModelName { - return this._modelName + return this.deviceModelName + } + + get friendlyName(): SwitchBotBLEModelFriendlyName { + return this.deviceFriendlyName } get connectionState(): string { - return this._connected ? 'connected' : this._peripheral.state + return this.isConnected ? 'connected' : this.peripheral.state } - get onconnect(): () => Promise { - return this.onconnect_internal + get onConnectHandler(): () => Promise { + return this.onConnect } - set onconnect(func: () => Promise) { + set onConnectHandler(func: () => Promise) { if (typeof func !== 'function') { - throw new TypeError('The `onconnect` must be a function that returns a Promise.') + throw new TypeError('The `onConnectHandler` must be a function that returns a Promise.') } - this.onconnect_internal = async () => { + this.onConnect = async () => { await func() } } - get ondisconnect(): () => Promise { - return this.ondisconnect_internal + get onDisconnectHandler(): () => Promise { + return this.onDisconnect } - set ondisconnect(func: () => Promise) { + set onDisconnectHandler(func: () => Promise) { if (typeof func !== 'function') { - throw new TypeError('The `ondisconnect` must be a function that returns a Promise.') + throw new TypeError('The `onDisconnectHandler` must be a function that returns a Promise.') } - this.ondisconnect_internal = async () => { + this.onDisconnect = async () => { await func() } } @@ -118,17 +116,17 @@ export class SwitchbotDevice extends EventEmitter { * @returns A Promise that resolves when the connection is complete. */ async connect(): Promise { - this._explicitly = true - await this.connect_internal() + this.explicitlyConnected = true + await this.internalConnect() } /** * Internal method to handle the connection process. * @returns A Promise that resolves when the connection is complete. */ - private async connect_internal(): Promise { - if (this._noble._state !== 'poweredOn') { - throw new Error(`The Bluetooth status is ${this._noble._state}, not poweredOn.`) + private async internalConnect(): Promise { + if (this.noble._state !== 'poweredOn') { + throw new Error(`The Bluetooth status is ${this.noble._state}, not poweredOn.`) } const state = this.connectionState @@ -139,21 +137,21 @@ export class SwitchbotDevice extends EventEmitter { throw new Error(`Now ${state}. Wait for a few seconds then try again.`) } - this._peripheral.once('connect', async () => { - this._connected = true - await this.onconnect() + this.peripheral.once('connect', async () => { + this.isConnected = true + await this.onConnect() }) - this._peripheral.once('disconnect', async () => { - this._connected = false - this._characteristics = null - this._peripheral.removeAllListeners() - await this.ondisconnect_internal() + this.peripheral.once('disconnect', async () => { + this.isConnected = false + this.characteristics = null + this.peripheral.removeAllListeners() + await this.onDisconnect() }) - await this._peripheral.connectAsync() - this._characteristics = await this.getCharacteristics() - await this.subscribe() + await this.peripheral.connectAsync() + this.characteristics = await this.getCharacteristics() + await this.subscribeToNotify() } /** @@ -200,7 +198,7 @@ export class SwitchbotDevice extends EventEmitter { */ public async discoverServices(): Promise { try { - const services = await this._peripheral.discoverServicesAsync([]) + const services = await this.peripheral.discoverServicesAsync([]) const primaryServices = services.filter(s => s.uuid === SERV_UUID_PRIMARY) if (primaryServices.length === 0) { @@ -225,21 +223,21 @@ export class SwitchbotDevice extends EventEmitter { * Subscribes to the notify characteristic. * @returns A Promise that resolves when the subscription is complete. */ - private async subscribe(): Promise { - const char = this._characteristics?.notify + private async subscribeToNotify(): Promise { + const char = this.characteristics?.notify if (!char) { throw new Error('No notify characteristic was found.') } await char.subscribeAsync() - char.on('data', this.onnotify_internal) + char.on('data', this.onNotify) } /** * Unsubscribes from the notify characteristic. * @returns A Promise that resolves when the unsubscription is complete. */ - async unsubscribe(): Promise { - const char = this._characteristics?.notify + async unsubscribeFromNotify(): Promise { + const char = this.characteristics?.notify if (!char) { return } @@ -252,8 +250,8 @@ export class SwitchbotDevice extends EventEmitter { * @returns A Promise that resolves when the disconnection is complete. */ async disconnect(): Promise { - this._explicitly = false - const state = this._peripheral.state + this.explicitlyConnected = false + const state = this.peripheral.state if (state === 'disconnected') { return @@ -262,18 +260,18 @@ export class SwitchbotDevice extends EventEmitter { throw new Error(`Now ${state}. Wait for a few seconds then try again.`) } - await this.unsubscribe() - await this._peripheral.disconnectAsync() + await this.unsubscribeFromNotify() + await this.peripheral.disconnectAsync() } /** * Internal method to handle disconnection if not explicitly initiated. * @returns A Promise that resolves when the disconnection is complete. */ - private async disconnect_internal(): Promise { - if (!this._explicitly) { + private async internalDisconnect(): Promise { + if (!this.explicitlyConnected) { await this.disconnect() - this._explicitly = true + this.explicitlyConnected = true } } @@ -282,12 +280,12 @@ export class SwitchbotDevice extends EventEmitter { * @returns A Promise that resolves with the device name. */ async getDeviceName(): Promise { - await this.connect_internal() - if (!this._characteristics?.device) { + await this.internalConnect() + if (!this.characteristics?.device) { throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`) } - const buf = await this.read(this._characteristics.device) - await this.disconnect_internal() + const buf = await this.readCharacteristic(this.characteristics.device) + await this.internalDisconnect() return buf.toString('utf8') } @@ -308,41 +306,41 @@ export class SwitchbotDevice extends EventEmitter { } const buf = Buffer.from(name, 'utf8') - await this.connect_internal() - if (!this._characteristics?.device) { + await this.internalConnect() + if (!this.characteristics?.device) { throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`) } - await this.write(this._characteristics.device, buf) - await this.disconnect_internal() + await this.writeCharacteristic(this.characteristics.device, buf) + await this.internalDisconnect() } /** * Sends a command to the device and awaits a response. - * @param req_buf The command buffer. + * @param reqBuf The command buffer. * @returns A Promise that resolves with the response buffer. */ - async command(req_buf: Buffer): Promise { - if (!Buffer.isBuffer(req_buf)) { + async command(reqBuf: Buffer): Promise { + if (!Buffer.isBuffer(reqBuf)) { throw new TypeError('The specified data is not acceptable for writing.') } - await this.connect_internal() - if (!this._characteristics?.write) { + await this.internalConnect() + if (!this.characteristics?.write) { throw new Error('No characteristics available.') } - await this.write(this._characteristics.write, req_buf) - const res_buf = await this._waitCommandResponseAsync() - await this.disconnect_internal() + await this.writeCharacteristic(this.characteristics.write, reqBuf) + const resBuf = await this.waitForCommandResponse() + await this.internalDisconnect() - return res_buf + return resBuf } /** * Waits for a response from the device after sending a command. * @returns A Promise that resolves with the response buffer. */ - private async _waitCommandResponseAsync(): Promise { + private async waitForCommandResponse(): Promise { const timeout = READ_TIMEOUT_MSEC let timer: NodeJS.Timeout | null = null @@ -351,7 +349,7 @@ export class SwitchbotDevice extends EventEmitter { }) const readPromise = new Promise((resolve) => { - this.onnotify_internal = (buf: Buffer) => { + this.onNotify = (buf: Buffer) => { if (timer) { clearTimeout(timer) } @@ -367,7 +365,7 @@ export class SwitchbotDevice extends EventEmitter { * @param char The characteristic to read from. * @returns A Promise that resolves with the data buffer. */ - private async read(char: Noble.Characteristic): Promise { + private async readCharacteristic(char: Noble.Characteristic): Promise { const timer = setTimeout(() => { throw new Error('READ_TIMEOUT') }, READ_TIMEOUT_MSEC) @@ -388,7 +386,7 @@ export class SwitchbotDevice extends EventEmitter { * @param buf The data buffer. * @returns A Promise that resolves when the write is complete. */ - private async write(char: Noble.Characteristic, buf: Buffer): Promise { + private async writeCharacteristic(char: Noble.Characteristic, buf: Buffer): Promise { const timer = setTimeout(() => { throw new Error('WRITE_TIMEOUT') }, WRITE_TIMEOUT_MSEC) diff --git a/src/device/woblindtilt.ts b/src/device/woblindtilt.ts index fad9b0e2..eb7d8cda 100644 --- a/src/device/woblindtilt.ts +++ b/src/device/woblindtilt.ts @@ -2,6 +2,10 @@ * * woblindtilt.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { blindTiltServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -20,14 +24,14 @@ export class WoBlindTilt extends SwitchbotDevice { * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. * @param {boolean} [reverse] - Whether to reverse the tilt percentage. - * @returns {Promise} - The parsed data object or null if the data is invalid. + * @returns {Promise} - The parsed data object or null if the data is invalid. */ static async parseServiceData( serviceData: Buffer, manufacturerData: Buffer, emitLog: (level: string, message: string) => void, reverse: boolean = false, - ): Promise { + ): Promise { if (![5, 6].includes(manufacturerData.length)) { emitLog('debugerror', `[parseServiceDataForWoBlindTilt] Buffer length ${manufacturerData.length} !== 5 or 6!`) return null @@ -43,7 +47,7 @@ export class WoBlindTilt extends SwitchbotDevice { const sequenceNumber = byte6.readUInt8(0) const battery = serviceData.length > 2 ? byte2 & 0b01111111 : null - return { + const data: blindTiltServiceData = { model: SwitchBotBLEModel.BlindTilt, modelName: SwitchBotBLEModelName.BlindTilt, modelFriendlyName: SwitchBotBLEModelFriendlyName.BlindTilt, @@ -54,6 +58,12 @@ export class WoBlindTilt extends SwitchbotDevice { lightLevel, sequenceNumber, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/wobulb.ts b/src/device/wobulb.ts index e503a694..9e93c0e2 100644 --- a/src/device/wobulb.ts +++ b/src/device/wobulb.ts @@ -2,6 +2,10 @@ * * wobulb.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { colorBulbServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -17,19 +21,20 @@ export class WoBulb extends SwitchbotDevice { * @param {Buffer} serviceData - The service data buffer. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, manufacturerData: Buffer, + // eslint-disable-next-line unused-imports/no-unused-vars emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 18) { - emitLog('debugerror', `[parseServiceDataForWoBulb] Buffer length ${serviceData.length} !== 18!`) + // emitLog('debugerror', `[parseServiceDataForWoBulb] Buffer length ${serviceData.length} !== 18!`) return null } if (manufacturerData.length !== 13) { - emitLog('debugerror', `[parseServiceDataForWoBulb] Buffer length ${manufacturerData.length} !== 13!`) + // emitLog('debugerror', `[parseServiceDataForWoBulb] Buffer length ${manufacturerData.length} !== 13!`) return null } @@ -45,23 +50,29 @@ export class WoBulb extends SwitchbotDevice { byte10, ] = manufacturerData - return { + const data: colorBulbServiceData = { model: SwitchBotBLEModel.ColorBulb, modelName: SwitchBotBLEModelName.ColorBulb, modelFriendlyName: SwitchBotBLEModelFriendlyName.ColorBulb, - power: byte1, + power: !!byte1, red: byte3, green: byte4, blue: byte5, color_temperature: byte6, state: !!(byte7 & 0b01111111), brightness: byte7 & 0b01111111, - delay: !!(byte8 & 0b10000000), - preset: !!(byte8 & 0b00001000), + delay: (byte8 & 0b10000000) >> 7, + preset: (byte8 & 0b00001000) >> 3, color_mode: byte8 & 0b00000111, speed: byte9 & 0b01111111, loop_index: byte10 & 0b11111110, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/woceilinglight.ts b/src/device/woceilinglight.ts index 8cd0c06a..c7e7a322 100644 --- a/src/device/woceilinglight.ts +++ b/src/device/woceilinglight.ts @@ -2,6 +2,10 @@ * * woceilinglight.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { ceilingLightProServiceData, ceilingLightServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -16,12 +20,12 @@ export class WoCeilingLight extends SwitchbotDevice { * Parses the service data for WoCeilingLight. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (manufacturerData.length !== 13) { emitLog('debugerror', `[parseServiceDataForWoCeilingLight] Buffer length ${manufacturerData.length} !== 13!`) return null @@ -39,35 +43,37 @@ export class WoCeilingLight extends SwitchbotDevice { byte10, ] = manufacturerData - return { + const data: ceilingLightServiceData = { model: SwitchBotBLEModel.CeilingLight, modelName: SwitchBotBLEModelName.CeilingLight, modelFriendlyName: SwitchBotBLEModelFriendlyName.CeilingLight, - power: byte1, + power: !!byte1, red: byte3, green: byte4, blue: byte5, color_temperature: byte6, state: !!(byte7 & 0b01111111), brightness: byte7 & 0b01111111, - delay: !!(byte8 & 0b10000000), - preset: !!(byte8 & 0b00001000), + delay: (byte8 & 0b10000000) ? 1 : 0, + preset: (byte8 & 0b00001000) ? 1 : 0, color_mode: byte8 & 0b00000111, speed: byte9 & 0b01111111, loop_index: byte10 & 0b11111110, } + + return data } /** * Parses the service data for WoCeilingLight Pro. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData_Pro( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (manufacturerData.length !== 13) { emitLog('debugerror', `[parseServiceDataForWoCeilingLightPro] Buffer length ${manufacturerData.length} !== 13!`) return null @@ -85,23 +91,29 @@ export class WoCeilingLight extends SwitchbotDevice { byte10, ] = manufacturerData - return { + const data: ceilingLightProServiceData = { model: SwitchBotBLEModel.CeilingLightPro, modelName: SwitchBotBLEModelName.CeilingLightPro, modelFriendlyName: SwitchBotBLEModelFriendlyName.CeilingLightPro, - power: byte1, + power: !!byte1, red: byte3, green: byte4, blue: byte5, color_temperature: byte6, state: !!(byte7 & 0b01111111), brightness: byte7 & 0b01111111, - delay: !!(byte8 & 0b10000000), - preset: !!(byte8 & 0b00001000), + delay: (byte8 & 0b10000000) ? 1 : 0, + preset: (byte8 & 0b00001000) ? 1 : 0, color_mode: byte8 & 0b00000111, speed: byte9 & 0b01111111, loop_index: byte10 & 0b11111110, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/wocontact.ts b/src/device/wocontact.ts index c00c03d7..eed8af15 100644 --- a/src/device/wocontact.ts +++ b/src/device/wocontact.ts @@ -1,8 +1,12 @@ +import type { Buffer } from 'node:buffer' + /* Copyright(C) 2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * * wocontact.ts: Switchbot BLE API registration. */ -import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + +import type { contactSensorServiceData } from '../types/bledevicestatus.js' import { SwitchbotDevice } from '../device.js' import { SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' @@ -16,12 +20,12 @@ export class WoContact extends SwitchbotDevice { * Parses the service data for WoContact. * @param {Buffer} serviceData - The service data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 9) { emitLog('debugerror', `[parseServiceDataForWoContact] Buffer length ${serviceData.length} !== 9!`) return null @@ -39,7 +43,7 @@ export class WoContact extends SwitchbotDevice { const button_count = byte8 & 0b00001111 const doorState = hallState === 0 ? 'close' : hallState === 1 ? 'open' : 'timeout no closed' - return { + const data: contactSensorServiceData = { model: SwitchBotBLEModel.ContactSensor, modelName: SwitchBotBLEModelName.ContactSensor, modelFriendlyName: SwitchBotBLEModelFriendlyName.ContactSensor, @@ -52,5 +56,11 @@ export class WoContact extends SwitchbotDevice { button_count, doorState, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } } diff --git a/src/device/wocurtain.ts b/src/device/wocurtain.ts index 49a5b517..001d4b3a 100644 --- a/src/device/wocurtain.ts +++ b/src/device/wocurtain.ts @@ -2,10 +2,14 @@ * * wocurtain.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { curtain3ServiceData, curtainServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' -import { SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' +import { SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' /** * Class representing a WoCurtain device. @@ -19,14 +23,14 @@ export class WoCurtain extends SwitchbotDevice { * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. * @param {boolean} [reverse] - Whether to reverse the position. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, manufacturerData: Buffer, emitLog: (level: string, message: string) => void, reverse: boolean = false, - ): Promise { + ): Promise { if (![5, 6].includes(serviceData.length)) { emitLog('debugerror', `[parseServiceDataForWoCurtain] Buffer length ${serviceData.length} !== 5 or 6!`) return null @@ -49,9 +53,7 @@ export class WoCurtain extends SwitchbotDevice { batteryData = byte2 } - const model = serviceData.subarray(0, 1).toString('utf8') - const modelName = model === 'c' ? SwitchBotBLEModelName.Curtain : SwitchBotBLEModelName.Curtain3 - const modelFriendlyName = model === 'c' ? SwitchBotBLEModelFriendlyName.Curtain : SwitchBotBLEModelFriendlyName.Curtain3 + const model = serviceData.subarray(0, 1).toString('utf8') as string ? SwitchBotBLEModel.Curtain : SwitchBotBLEModel.Curtain3 const calibration = Boolean(byte1 & 0b01000000) const position = Math.max(Math.min(deviceData.readUInt8(0) & 0b01111111, 100), 0) const inMotion = Boolean(deviceData.readUInt8(0) & 0b10000000) @@ -59,19 +61,39 @@ export class WoCurtain extends SwitchbotDevice { const deviceChain = deviceData.readUInt8(1) & 0b00000111 const battery = batteryData !== null ? batteryData & 0b01111111 : null - return { - model, - modelName, - modelFriendlyName, - calibration, - battery, - inMotion, - position: reverse ? 100 - position : position, - lightLevel, - deviceChain, + if (model === SwitchBotBLEModel.Curtain) { + const data: curtainServiceData = { + model: SwitchBotBLEModel.Curtain, + modelName: SwitchBotBLEModelName.Curtain, + modelFriendlyName: SwitchBotBLEModelFriendlyName.Curtain, + calibration, + battery: battery ?? 0, + inMotion, + position: reverse ? 100 - position : position, + lightLevel, + deviceChain, + } + return data + } else { + const data: curtain3ServiceData = { + model: SwitchBotBLEModel.Curtain3, + modelName: SwitchBotBLEModelName.Curtain3, + modelFriendlyName: SwitchBotBLEModelFriendlyName.Curtain3, + calibration, + battery: battery ?? 0, + inMotion, + position: reverse ? 100 - position : position, + lightLevel, + deviceChain, + } + return data } } + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) + } + /** * Opens the curtain. * @param {number} [mode] - Running mode (0x01 = QuietDrift, 0xFF = Default). diff --git a/src/device/wohand.ts b/src/device/wohand.ts index 68e93243..d5712ead 100644 --- a/src/device/wohand.ts +++ b/src/device/wohand.ts @@ -2,6 +2,10 @@ * * wohand.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { botServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -40,13 +44,16 @@ export class WoHand extends SwitchbotDevice { } } + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) + } + /** * Sends a command to the bot. - * @param {number[]} bytes - The command bytes. + * @param {number[]} reqBuf - The command bytes. * @returns {Promise} */ - private async operateBot(bytes: number[]): Promise { - const reqBuf = Buffer.from(bytes) + protected async sendCommand(reqBuf: Buffer): Promise { const resBuf = await this.command(reqBuf) const code = resBuf.readUInt8(0) @@ -59,39 +66,39 @@ export class WoHand extends SwitchbotDevice { * Presses the bot. * @returns {Promise} */ - async press(): Promise { - await this.operateBot([0x57, 0x01, 0x00]) + public async press(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x00])) } /** * Turns on the bot. * @returns {Promise} */ - async turnOn(): Promise { - await this.operateBot([0x57, 0x01, 0x01]) + public async turnOn(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x01])) } /** * Turns off the bot. * @returns {Promise} */ - async turnOff(): Promise { - await this.operateBot([0x57, 0x01, 0x02]) + public async turnOff(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x02])) } /** * Moves the bot down. * @returns {Promise} */ - async down(): Promise { - await this.operateBot([0x57, 0x01, 0x03]) + public async down(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x03])) } /** * Moves the bot up. * @returns {Promise} */ - async up(): Promise { - await this.operateBot([0x57, 0x01, 0x04]) + public async up(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x04])) } } diff --git a/src/device/wohub2.ts b/src/device/wohub2.ts index 8579be87..f8264027 100644 --- a/src/device/wohub2.ts +++ b/src/device/wohub2.ts @@ -4,6 +4,10 @@ */ import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + +import type { hub2ServiceData } from '../types/bledevicestatus.js' + import { SwitchbotDevice } from '../device.js' import { SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' @@ -16,12 +20,12 @@ export class WoHub2 extends SwitchbotDevice { * Parses the service data for WoHub2. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (manufacturerData.length !== 16) { emitLog('debugerror', `[parseServiceDataForWoHub2] Buffer length ${manufacturerData.length} !== 16!`) return null @@ -34,7 +38,7 @@ export class WoHub2 extends SwitchbotDevice { const tempF = Math.round(((tempC * 9) / 5 + 32) * 10) / 10 const lightLevel = byte12 & 0b11111 - return { + const data: hub2ServiceData = { model: SwitchBotBLEModel.Hub2, modelName: SwitchBotBLEModelName.Hub2, modelFriendlyName: SwitchBotBLEModelFriendlyName.Hub2, @@ -44,5 +48,11 @@ export class WoHub2 extends SwitchbotDevice { humidity: byte2 & 0b01111111, lightLevel, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } } diff --git a/src/device/wohumi.ts b/src/device/wohumi.ts index c8ea12df..dab37934 100644 --- a/src/device/wohumi.ts +++ b/src/device/wohumi.ts @@ -2,6 +2,10 @@ * * wohumi.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { humidifierServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -16,12 +20,12 @@ export class WoHumi extends SwitchbotDevice { * Parses the service data for WoHumi. * @param {Buffer} serviceData - The service data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 8) { emitLog('debugerror', `[parseServiceDataForWoHumi] Buffer length ${serviceData.length} !== 8!`) return null @@ -35,7 +39,7 @@ export class WoHumi extends SwitchbotDevice { const percentage = byte4 & 0b01111111 // 0-100%, 101/102/103 - Quick gear 1/2/3 const humidity = autoMode ? 0 : percentage === 101 ? 33 : percentage === 102 ? 66 : percentage === 103 ? 100 : percentage - return { + const data: humidifierServiceData = { model: SwitchBotBLEModel.Humidifier, modelName: SwitchBotBLEModelName.Humidifier, modelFriendlyName: SwitchBotBLEModelFriendlyName.Humidifier, @@ -44,6 +48,12 @@ export class WoHumi extends SwitchbotDevice { percentage: autoMode ? 0 : percentage, humidity, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/woiosensorth.ts b/src/device/woiosensorth.ts index 6fe25500..b1f82d38 100644 --- a/src/device/woiosensorth.ts +++ b/src/device/woiosensorth.ts @@ -4,6 +4,10 @@ */ import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + +import type { outdoorMeterServiceData } from '../types/bledevicestatus.js' + import { SwitchbotDevice } from '../device.js' import { SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' @@ -17,13 +21,13 @@ export class WoIOSensorTH extends SwitchbotDevice { * @param {Buffer} serviceData - The service data buffer. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 3) { emitLog('debugerror', `[parseServiceDataForWoIOSensorTH] Service Data Buffer length ${serviceData.length} !== 3!`) return null @@ -44,7 +48,7 @@ export class WoIOSensorTH extends SwitchbotDevice { const tempC = tempSign * ((mdByte11 & 0b01111111) + (mdByte10 & 0b00001111) / 10) const tempF = Math.round(((tempC * 9) / 5 + 32) * 10) / 10 - return { + const data: outdoorMeterServiceData = { model: SwitchBotBLEModel.OutdoorMeter, modelName: SwitchBotBLEModelName.OutdoorMeter, modelFriendlyName: SwitchBotBLEModelFriendlyName.OutdoorMeter, @@ -54,5 +58,11 @@ export class WoIOSensorTH extends SwitchbotDevice { humidity: mdByte12 & 0b01111111, battery: sdByte2 & 0b01111111, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } } diff --git a/src/device/woplugmini.ts b/src/device/woplugmini.ts index 9b3c8cfa..507c2311 100644 --- a/src/device/woplugmini.ts +++ b/src/device/woplugmini.ts @@ -2,6 +2,9 @@ * * woplugmini.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { plugMiniJPServiceData, plugMiniUSServiceData } from '../types/bledevicestatus.js' import { Buffer } from 'node:buffer' @@ -17,26 +20,26 @@ export class WoPlugMini extends SwitchbotDevice { * Parses the service data for WoPlugMini US. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData_US( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { - return this.parseServiceData(manufacturerData, SwitchBotBLEModel.PlugMiniUS, emitLog) + ): Promise { + return this.parseServiceData(manufacturerData, SwitchBotBLEModel.PlugMiniUS, emitLog) as Promise } /** * Parses the service data for WoPlugMini JP. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData_JP( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { - return this.parseServiceData(manufacturerData, SwitchBotBLEModel.PlugMiniJP, emitLog) + ): Promise { + return this.parseServiceData(manufacturerData, SwitchBotBLEModel.PlugMiniJP, emitLog) as Promise } /** @@ -44,13 +47,13 @@ export class WoPlugMini extends SwitchbotDevice { * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {SwitchBotBLEModel} model - The model of the plug mini. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ private static async parseServiceData( manufacturerData: Buffer, model: SwitchBotBLEModel, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (manufacturerData.length !== 14) { emitLog('debugerror', `[parseServiceDataForWoPlugMini] Buffer length ${manufacturerData.length} should be 14`) return null @@ -72,11 +75,11 @@ export class WoPlugMini extends SwitchbotDevice { const overload = !!(byte12 & 0b10000000) const currentPower = (((byte12 & 0b01111111) << 8) + byte13) / 10 // in watt - return { - model, + const data = { + model: model === SwitchBotBLEModel.PlugMiniUS ? SwitchBotBLEModel.PlugMiniUS : SwitchBotBLEModel.PlugMiniJP, modelName: SwitchBotBLEModelName.PlugMini, modelFriendlyName: SwitchBotBLEModelFriendlyName.PlugMini, - state, + state: state ?? 'unknown', delay, timer, syncUtcTime, @@ -84,6 +87,12 @@ export class WoPlugMini extends SwitchbotDevice { overload, currentPower, } + + return data as plugMiniUSServiceData | plugMiniJPServiceData + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/wopresence.ts b/src/device/wopresence.ts index b2ed2ed0..e07b97ea 100644 --- a/src/device/wopresence.ts +++ b/src/device/wopresence.ts @@ -4,6 +4,8 @@ */ import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + import type { motionSensorServiceData } from '../types/bledevicestatus.js' import { SwitchbotDevice } from '../device.js' @@ -47,4 +49,8 @@ export class WoPresence extends SwitchbotDevice { return data } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) + } } diff --git a/src/device/wosensorth.ts b/src/device/wosensorth.ts index 2d570709..a5bc4df9 100644 --- a/src/device/wosensorth.ts +++ b/src/device/wosensorth.ts @@ -4,6 +4,8 @@ */ import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + import type { meterPlusServiceData, meterServiceData } from '../types/bledevicestatus.js' import { SwitchbotDevice } from '../device.js' @@ -77,4 +79,8 @@ export class WoSensorTH extends SwitchbotDevice { battery: byte2 & 0b01111111, } } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) + } } diff --git a/src/device/wostrip.ts b/src/device/wostrip.ts index ac1ea667..22d77bd4 100644 --- a/src/device/wostrip.ts +++ b/src/device/wostrip.ts @@ -2,6 +2,8 @@ * * wostrip.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + import type { stripLightServiceData } from '../types/bledevicestatus.js' import { Buffer } from 'node:buffer' @@ -59,6 +61,10 @@ export class WoStrip extends SwitchbotDevice { return data } + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) + } + /** * Reads the state of the strip light. * @returns {Promise} - Resolves with true if the strip light is ON, false otherwise. diff --git a/src/index.ts b/src/index.ts index 77118bce..d37ba917 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,23 @@ * * index.ts: Switchbot BLE API registration. */ +export * from './advertising.js' +export * from './device.js' +export * from './device/woblindtilt.js' +export * from './device/wobulb.js' +export * from './device/woceilinglight.js' +export * from './device/wocontact.js' +export * from './device/wocurtain.js' +export * from './device/wohand.js' +export * from './device/wohub2.js' +export * from './device/wohumi.js' +export * from './device/woiosensorth.js' +export * from './device/woplugmini.js' +export * from './device/wopresence.js' +export * from './device/wosensorth.js' +export * from './device/wosmartlock.js' +export * from './device/wosmartlockpro.js' +export * from './device/wostrip.js' export * from './switchbot-ble.js' export * from './switchbot-openapi.js' export * from './types/bledevicestatus.js' diff --git a/src/switchbot-ble.ts b/src/switchbot-ble.ts index c779bb88..95a038cc 100644 --- a/src/switchbot-ble.ts +++ b/src/switchbot-ble.ts @@ -1,3 +1,7 @@ +/* Copyright(C) 2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * switchbot.ts: Switchbot BLE API registration. + */ import type { Ad, Params, Rule } from './types/types.js' import { EventEmitter } from 'node:events' @@ -26,10 +30,10 @@ import { DEFAULT_DISCOVERY_DURATION, PRIMARY_SERVICE_UUID_LIST } from './setting import { SwitchBotBLEModel } from './types/types.js' /** - * SwitchBot class to interact with SwitchBot devices. + * SwitchBotBLE class to interact with SwitchBot devices. */ export class SwitchBotBLE extends EventEmitter { - private ready: Promise + public ready: Promise public noble!: typeof Noble ondiscover?: (device: SwitchbotDevice) => Promise | void onadvertisement?: (ad: Ad) => Promise | void @@ -41,7 +45,7 @@ export class SwitchBotBLE extends EventEmitter { */ constructor(params?: Params) { super() - this.ready = this.init(params) + this.ready = this.initialize(params) } /** @@ -50,7 +54,7 @@ export class SwitchBotBLE extends EventEmitter { * @param level - The severity level of the log (e.g., 'info', 'warn', 'error'). * @param message - The log message to be emitted. */ - public async emitLog(level: string, message: string): Promise { + public async log(level: string, message: string): Promise { this.emit('log', { level, message }) } @@ -60,8 +64,8 @@ export class SwitchBotBLE extends EventEmitter { * @param {Params} [params] - Optional parameters * @returns {Promise} - Resolves when initialization is complete */ - async init(params?: Params): Promise { - this.noble = params && params.noble ? params.noble : Noble.default as typeof Noble + private async initialize(params?: Params): Promise { + this.noble = params?.noble ?? Noble.default as typeof Noble } /** @@ -71,24 +75,25 @@ export class SwitchBotBLE extends EventEmitter { * @param {Record} schema - The schema to validate against. * @returns {Promise} - Resolves if parameters are valid, otherwise throws an error. */ - private async validateParams(params: Params, schema: Record): Promise { + public async validate(params: Params, schema: Record): Promise { const valid = parameterChecker.check(params as Record, schema as Record, false) if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) + this.log('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) throw new Error(parameterChecker.error!.message) } } /** - * Initializes the noble object and waits for it to be powered on. + * Waits for the noble object to be powered on. * * @returns {Promise} - Resolves when the noble object is powered on. */ - async _init(): Promise { + private async waitForPowerOn(): Promise { await this.ready if (this.noble._state === 'poweredOn') { return } + return new Promise((resolve, reject) => { this.noble.once('stateChange', (state: typeof Noble._state) => { switch (state) { @@ -117,42 +122,18 @@ export class SwitchBotBLE extends EventEmitter { * @param {Params} params - The parameters for discovery. * @returns {Promise} - A promise that resolves with a list of discovered devices. */ - async discover(params: Params = {}): Promise { + public async discover(params: Params = {}): Promise { await this.ready - await this.validateParams(params, { + await this.validate(params, { duration: { required: false, type: 'integer', min: 1, max: 60000 }, - model: { - required: false, - type: 'string', - enum: [ - SwitchBotBLEModel.Bot, - SwitchBotBLEModel.Curtain, - SwitchBotBLEModel.Curtain3, - SwitchBotBLEModel.Humidifier, - SwitchBotBLEModel.Meter, - SwitchBotBLEModel.MeterPlus, - SwitchBotBLEModel.Hub2, - SwitchBotBLEModel.OutdoorMeter, - SwitchBotBLEModel.MotionSensor, - SwitchBotBLEModel.ContactSensor, - SwitchBotBLEModel.ColorBulb, - SwitchBotBLEModel.CeilingLight, - SwitchBotBLEModel.CeilingLightPro, - SwitchBotBLEModel.StripLight, - SwitchBotBLEModel.PlugMiniUS, - SwitchBotBLEModel.PlugMiniJP, - SwitchBotBLEModel.Lock, - SwitchBotBLEModel.LockPro, - SwitchBotBLEModel.BlindTilt, - ], - }, + model: { required: false, type: 'string', enum: Object.values(SwitchBotBLEModel) }, id: { required: false, type: 'string', min: 12, max: 17 }, quick: { required: false, type: 'boolean' }, }) - await this._init() + await this.waitForPowerOn() - if (this.noble === null) { + if (!this.noble) { throw new Error('noble failed to initialize') } @@ -170,30 +151,27 @@ export class SwitchBotBLE extends EventEmitter { if (timer) { clearTimeout(timer) } - this.noble.removeAllListeners('discover') try { await this.noble.stopScanningAsync() - this.emitLog('info', 'Stopped Scanning for SwitchBot BLE devices.') + this.log('info', 'Stopped Scanning for SwitchBot BLE devices.') } catch (e: any) { - this.emitLog('error', `discover stopScanningAsync error: ${JSON.stringify(e.message ?? e)}`) + this.log('error', `discover stopScanningAsync error: ${JSON.stringify(e.message ?? e)}`) } - return Object.values(peripherals) } return new Promise((resolve, reject) => { this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const device = await this.getDeviceObject(peripheral, p.id, p.model) + const device = await this.createDevice(peripheral, p.id, p.model) if (!device) { return } peripherals[device.id!] = device - if (this.ondiscover && typeof this.ondiscover === 'function') { + if (this.ondiscover) { this.ondiscover(device) } - if (p.quick) { resolve(await finishDiscovery()) } @@ -201,91 +179,47 @@ export class SwitchBotBLE extends EventEmitter { this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, false) .then(() => { - timer = setTimeout(async () => { - resolve(await finishDiscovery()) - }, p.duration) - }) - .catch((error: Error) => { - reject(error) + timer = setTimeout(async () => resolve(await finishDiscovery()), p.duration) }) + .catch(reject) }) } /** - * Gets the device object based on the peripheral, id, and model. + * Creates a device object based on the peripheral, id, and model. * * @param {Noble.Peripheral} peripheral - The peripheral object. * @param {string} id - The device id. * @param {string} model - The device model. * @returns {Promise} - The device object or null. */ - async getDeviceObject(peripheral: Noble.Peripheral, id: string, model: string): Promise { - const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) - if (ad && await this.filterAdvertising(ad, id, model)) { - let device - if (ad && ad.serviceData && ad.serviceData.model) { - switch (ad.serviceData.model) { - case SwitchBotBLEModel.Bot: - device = new WoHand(peripheral, this.noble) - break - case SwitchBotBLEModel.Curtain: - case SwitchBotBLEModel.Curtain3: - device = new WoCurtain(peripheral, this.noble) - break - case SwitchBotBLEModel.Humidifier: - device = new WoHumi(peripheral, this.noble) - break - case SwitchBotBLEModel.Meter: - device = new WoSensorTH(peripheral, this.noble) - break - case SwitchBotBLEModel.MeterPlus: - device = new WoSensorTH(peripheral, this.noble) - break - case SwitchBotBLEModel.Hub2: - device = new WoHub2(peripheral, this.noble) - break - case SwitchBotBLEModel.OutdoorMeter: - device = new WoIOSensorTH(peripheral, this.noble) - break - case SwitchBotBLEModel.MotionSensor: - device = new WoPresence(peripheral, this.noble) - break - case SwitchBotBLEModel.ContactSensor: - device = new WoContact(peripheral, this.noble) - break - case SwitchBotBLEModel.ColorBulb: - device = new WoBulb(peripheral, this.noble) - break - case SwitchBotBLEModel.CeilingLight: - device = new WoCeilingLight(peripheral, this.noble) - break - case SwitchBotBLEModel.CeilingLightPro: - device = new WoCeilingLight(peripheral, this.noble) - break - case SwitchBotBLEModel.StripLight: - device = new WoStrip(peripheral, this.noble) - break - case SwitchBotBLEModel.PlugMiniUS: - case SwitchBotBLEModel.PlugMiniJP: - device = new WoPlugMini(peripheral, this.noble) - break - case SwitchBotBLEModel.Lock: - device = new WoSmartLock(peripheral, this.noble) - break - case SwitchBotBLEModel.LockPro: - device = new WoSmartLockPro(peripheral, this.noble) - break - case SwitchBotBLEModel.BlindTilt: - device = new WoBlindTilt(peripheral, this.noble) - break - default: - device = new SwitchbotDevice(peripheral, this.noble) - } + private async createDevice(peripheral: Noble.Peripheral, id: string, model: string): Promise { + const ad = await Advertising.parse(peripheral, this.log.bind(this)) + if (ad && await this.filterAd(ad, id, model)) { + switch (ad.serviceData.model) { + case SwitchBotBLEModel.Bot: return new WoHand(peripheral, this.noble) + case SwitchBotBLEModel.Curtain: + case SwitchBotBLEModel.Curtain3: return new WoCurtain(peripheral, this.noble) + case SwitchBotBLEModel.Humidifier: return new WoHumi(peripheral, this.noble) + case SwitchBotBLEModel.Meter: + case SwitchBotBLEModel.MeterPlus: return new WoSensorTH(peripheral, this.noble) + case SwitchBotBLEModel.Hub2: return new WoHub2(peripheral, this.noble) + case SwitchBotBLEModel.OutdoorMeter: return new WoIOSensorTH(peripheral, this.noble) + case SwitchBotBLEModel.MotionSensor: return new WoPresence(peripheral, this.noble) + case SwitchBotBLEModel.ContactSensor: return new WoContact(peripheral, this.noble) + case SwitchBotBLEModel.ColorBulb: return new WoBulb(peripheral, this.noble) + case SwitchBotBLEModel.CeilingLight: + case SwitchBotBLEModel.CeilingLightPro: return new WoCeilingLight(peripheral, this.noble) + case SwitchBotBLEModel.StripLight: return new WoStrip(peripheral, this.noble) + case SwitchBotBLEModel.PlugMiniUS: + case SwitchBotBLEModel.PlugMiniJP: return new WoPlugMini(peripheral, this.noble) + case SwitchBotBLEModel.Lock: return new WoSmartLock(peripheral, this.noble) + case SwitchBotBLEModel.LockPro: return new WoSmartLockPro(peripheral, this.noble) + case SwitchBotBLEModel.BlindTilt: return new WoBlindTilt(peripheral, this.noble) + default: return new SwitchbotDevice(peripheral, this.noble) } - return device || null - } else { - return null } + return null } /** @@ -294,23 +228,17 @@ export class SwitchBotBLE extends EventEmitter { * @param {Ad} ad - The advertising data. * @param {string} id - The device id. * @param {string} model - The device model. - * @returns {boolean} - True if the advertising data matches the id and model, false otherwise. + * @returns {Promise} - True if the advertising data matches the id and model, false otherwise. */ - async filterAdvertising(ad: Ad, id: string, model: string): Promise { + private async filterAd(ad: Ad, id: string, model: string): Promise { if (!ad) { return false } - if (id) { - id = id.toLowerCase().replace(/:/g, '') - const ad_id = ad.address.toLowerCase().replace(/[^a-z0-9]/g, '') - if (ad_id !== id) { - return false - } + if (id && ad.address.toLowerCase().replace(/[^a-z0-9]/g, '') !== id.toLowerCase().replace(/:/g, '')) { + return false } - if (model) { - if (ad.serviceData.model !== model) { - return false - } + if (model && ad.serviceData.model !== model) { + return false } return true } @@ -321,52 +249,25 @@ export class SwitchBotBLE extends EventEmitter { * @param {Params} [params] - Optional parameters. * @returns {Promise} - Resolves when scanning starts successfully. */ - async startScan(params: Params = {}): Promise { + public async startScan(params: Params = {}): Promise { await this.ready - await this.validateParams(params, { - model: { - required: false, - type: 'string', - enum: [ - SwitchBotBLEModel.Bot, - SwitchBotBLEModel.Curtain, - SwitchBotBLEModel.Curtain3, - SwitchBotBLEModel.Humidifier, - SwitchBotBLEModel.Meter, - SwitchBotBLEModel.MeterPlus, - SwitchBotBLEModel.Hub2, - SwitchBotBLEModel.OutdoorMeter, - SwitchBotBLEModel.MotionSensor, - SwitchBotBLEModel.ContactSensor, - SwitchBotBLEModel.ColorBulb, - SwitchBotBLEModel.CeilingLight, - SwitchBotBLEModel.CeilingLightPro, - SwitchBotBLEModel.StripLight, - SwitchBotBLEModel.PlugMiniUS, - SwitchBotBLEModel.PlugMiniJP, - SwitchBotBLEModel.Lock, - SwitchBotBLEModel.LockPro, - SwitchBotBLEModel.BlindTilt, - ], - }, + await this.validate(params, { + model: { required: false, type: 'string', enum: Object.values(SwitchBotBLEModel) }, id: { required: false, type: 'string', min: 12, max: 17 }, }) - await this._init() + await this.waitForPowerOn() - if (this.noble === null) { + if (!this.noble) { throw new Error('noble object failed to initialize') } - const p = { - model: params.model || '', - id: params.id || '', - } + const p = { model: params.model || '', id: params.id || '' } this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) - if (ad && await this.filterAdvertising(ad, p.id, p.model)) { - if (this.onadvertisement && typeof this.onadvertisement === 'function') { + const ad = await Advertising.parse(peripheral, this.log.bind(this)) + if (ad && await this.filterAd(ad, p.id, p.model)) { + if (this.onadvertisement) { this.onadvertisement(ad) } } @@ -374,9 +275,9 @@ export class SwitchBotBLE extends EventEmitter { try { await this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, true) - this.emitLog('info', 'Started Scanning for SwitchBot BLE devices.') + this.log('info', 'Started Scanning for SwitchBot BLE devices.') } catch (e: any) { - this.emitLog('error', `startScanningAsync error: ${JSON.stringify(e.message ?? e)}`) + this.log('error', `startScanningAsync error: ${JSON.stringify(e.message ?? e)}`) } } @@ -385,17 +286,17 @@ export class SwitchBotBLE extends EventEmitter { * * @returns {Promise} - Resolves when scanning stops successfully. */ - async stopScan(): Promise { - if (this.noble === null) { + public async stopScan(): Promise { + if (!this.noble) { return } this.noble.removeAllListeners('discover') try { await this.noble.stopScanningAsync() - this.emitLog('info', 'Stopped Scanning for SwitchBot BLE devices.') + this.log('info', 'Stopped Scanning for SwitchBot BLE devices.') } catch (e: any) { - this.emitLog('error', `stopScanningAsync error: ${JSON.stringify(e.message ?? e)}`) + this.log('error', `stopScanningAsync error: ${JSON.stringify(e.message ?? e)}`) } } @@ -405,14 +306,12 @@ export class SwitchBotBLE extends EventEmitter { * @param {number} msec - The time to wait in milliseconds. * @returns {Promise} - Resolves after the specified time. */ - async wait(msec: number): Promise { + public async wait(msec: number): Promise { if (typeof msec !== 'number' || msec < 0) { throw new Error('Invalid parameter: msec must be a non-negative integer.') } - return new Promise((resolve) => { - setTimeout(resolve, msec) - }) + return new Promise(resolve => setTimeout(resolve, msec)) } } diff --git a/src/test/switchbot-ble.test.ts b/src/test/switchbot-ble.test.ts new file mode 100644 index 00000000..8ff5f5d1 --- /dev/null +++ b/src/test/switchbot-ble.test.ts @@ -0,0 +1,52 @@ +import * as Noble from '@stoprocent/noble' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { SwitchBotBLE } from '../switchbot-ble.js' + +describe('switchBotBLE', () => { + let switchBot: SwitchBotBLE + + beforeEach(() => { + switchBot = new SwitchBotBLE({ noble: Noble }) + }) + + it('should initialize noble object', async () => { + await switchBot.ready + expect(switchBot.noble).toBeTruthy() + }) + + it('should validate parameters', async () => { + const params = { duration: 5000, model: 'Bot', id: '123456789012', quick: true } + await switchBot.validate(params, { + duration: { required: false, type: 'integer', min: 1, max: 60000 }, + model: { required: false, type: 'string', enum: ['Bot'] }, + id: { required: false, type: 'string', min: 12, max: 17 }, + quick: { required: false, type: 'boolean' }, + }) + }) + + it('should start and stop scanning', async () => { + let discoverListenerCount = 0 + const discoverListener = () => { + discoverListenerCount++ + } + switchBot.noble.on('discover', discoverListener) + await switchBot.startScan() + expect(discoverListenerCount).toBe(1) + await switchBot.stopScan() + switchBot.noble.removeListener('discover', discoverListener) + expect(discoverListenerCount).toBe(0) + }) + + it('should wait for specified time', async () => { + const start = Date.now() + await switchBot.wait(1000) + const end = Date.now() + expect(end - start).toBeGreaterThanOrEqual(1000) + }) + + it('should discover devices', async () => { + const devices = await switchBot.discover({ duration: 1000, quick: true }) + expect(devices).toBeInstanceOf(Array) + }) +}) diff --git a/src/test/switchbot.test.ts b/src/test/switchbot.test.ts deleted file mode 100644 index 546281c8..00000000 --- a/src/test/switchbot.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Buffer } from 'node:buffer' - -import sinon from 'sinon' -import { expect } from 'vitest' - -import { WoHumi } from '../device/wohumi.js' -import { SwitchBotBLE } from '../switchbot-ble.js' -import { SwitchBotBLEModel } from '../types/types.js' - -describe('switchBot', () => { - let switchBot: SwitchBotBLE - let nobleMock: any - - beforeEach(() => { - nobleMock = { - on: sinon.stub(), - startScanningAsync: sinon.stub().resolves(), - stopScanningAsync: sinon.stub().resolves(), - removeAllListeners: sinon.stub(), - _state: 'poweredOn', - once: sinon.stub(), - } - switchBot = new SwitchBotBLE({ noble: nobleMock }) - }) - - afterEach(() => { - sinon.restore() - }) - - it('should initialize noble object', async () => { - await switchBot.init({ noble: nobleMock }) - expect(switchBot.noble).toBe(nobleMock) - }) - - it('should discover devices', async () => { - const peripheralMock = { - id: 'mock-id', - uuid: 'mock-uuid', - address: 'mock-address', - addressType: 'public', - connectable: true, - advertisement: { - serviceData: [{ uuid: 'mock-uuid', data: Buffer.from([0x01, 0x02]) }], - localName: 'mock-localName', - txPowerLevel: -59, - manufacturerData: Buffer.from([0x01, 0x02, 0x03, 0x04]), - serviceUuids: ['mock-service-uuid'], - }, - rssi: -50, - services: [], - state: 'disconnected' as const, - mtu: 23, - connect: sinon.stub(), - connectAsync: sinon.stub().resolves(), - disconnect: sinon.stub(), - disconnectAsync: sinon.stub().resolves(), - updateRssi: sinon.stub(), - updateRssiAsync: sinon.stub().resolves(), - discoverServices: sinon.stub(), - discoverServicesAsync: sinon.stub().resolves(), - discoverSomeServicesAndCharacteristics: sinon.stub(), - discoverSomeServicesAndCharacteristicsAsync: sinon.stub().resolves(), - discoverAllServicesAndCharacteristics: sinon.stub(), - discoverAllServicesAndCharacteristicsAsync: sinon.stub().resolves(), - readHandle: sinon.stub(), - readHandleAsync: sinon.stub().resolves(), - writeHandle: sinon.stub(), - writeHandleAsync: sinon.stub().resolves(), - cancelConnect: sinon.stub(), - on: sinon.stub(), - once: sinon.stub(), - addListener: sinon.stub(), - removeListener: sinon.stub(), - removeAllListeners: sinon.stub(), - emit: sinon.stub(), - listeners: sinon.stub(), - eventNames: sinon.stub(), - listenerCount: sinon.stub(), - off: sinon.stub(), - setMaxListeners: sinon.stub(), - getMaxListeners: sinon.stub(), - rawListeners: sinon.stub(), - prependListener: sinon.stub(), - prependOnceListener: sinon.stub(), - } - const getDeviceObjectStub = sinon.stub(switchBot, 'getDeviceObject').resolves(new WoHumi(peripheralMock, nobleMock)) - nobleMock.on.withArgs('discover').yields(peripheralMock) - - const devices = await switchBot.discover({ duration: 1000, model: SwitchBotBLEModel.Humidifier }) - - expect(devices).toHaveLength(1) - expect(devices[0]).toBeInstanceOf(WoHumi) - expect(getDeviceObjectStub.calledOnce).toBe(true) - }) - - it('should handle noble state changes', async () => { - nobleMock._state = 'poweredOff' - const initPromise = switchBot._init() - - nobleMock.once.withArgs('stateChange').yields('poweredOn') - await initPromise - - expect(nobleMock.once.calledWith('stateChange')).toBe(true) - }) - - it('should filter advertising data correctly', async () => { - const ad = { - id: 'mock-id', - address: 'mock-address', - rssi: -50, - serviceData: { - model: SwitchBotBLEModel.Humidifier, - }, - } - const result = await switchBot.filterAdvertising(ad, 'mock-id', SwitchBotBLEModel.Humidifier) - expect(result).toBe(true) - }) -}) diff --git a/src/types/bledevicestatus.ts b/src/types/bledevicestatus.ts index 9fc52a79..3233df6e 100644 --- a/src/types/bledevicestatus.ts +++ b/src/types/bledevicestatus.ts @@ -17,8 +17,9 @@ export interface ad { } interface serviceData { - model: string - modelName: string + model: SwitchBotBLEModel + modelName: SwitchBotBLEModelName + modelFriendlyName: SwitchBotBLEModelFriendlyName } export type botServiceData = serviceData & { @@ -213,10 +214,11 @@ export type blindTiltServiceData = serviceData & { modelName: SwitchBotBLEModelName.BlindTilt modelFriendlyName: SwitchBotBLEModelFriendlyName.BlindTilt calibration: boolean - battery: number + battery: number | null inMotion: boolean tilt: number lightLevel: number + sequenceNumber: number } export type ceilingLightServiceData = serviceData & { From 9d4294f3894264e508e1e8e4c30575704d745699 Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Sun, 6 Oct 2024 22:12:16 -0500 Subject: [PATCH 06/10] fix --- src/device/wohand.ts | 8 +++++--- src/test/switchbot-ble.test.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/device/wohand.ts b/src/device/wohand.ts index d5712ead..fe0e0d86 100644 --- a/src/device/wohand.ts +++ b/src/device/wohand.ts @@ -25,7 +25,7 @@ export class WoHand extends SwitchbotDevice { static async parseServiceData( serviceData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 3) { emitLog('debugerror', `[parseServiceData] Buffer length ${serviceData.length} !== 3!`) return null @@ -34,14 +34,16 @@ export class WoHand extends SwitchbotDevice { const byte1 = serviceData.readUInt8(1) const byte2 = serviceData.readUInt8(2) - return { + const data: botServiceData = { model: SwitchBotBLEModel.Bot, modelName: SwitchBotBLEModelName.Bot, modelFriendlyName: SwitchBotBLEModelFriendlyName.Bot, - mode: !!(byte1 & 0b10000000), // Whether the light switch Add-on is used or not. 0 = press, 1 = switch + mode: (!!(byte1 & 0b10000000)).toString(), // Whether the light switch Add-on is used or not. 0 = press, 1 = switch state: !(byte1 & 0b01000000), // Whether the switch status is ON or OFF. 0 = on, 1 = off battery: byte2 & 0b01111111, // % } + + return data } constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { diff --git a/src/test/switchbot-ble.test.ts b/src/test/switchbot-ble.test.ts index 8ff5f5d1..76c24274 100644 --- a/src/test/switchbot-ble.test.ts +++ b/src/test/switchbot-ble.test.ts @@ -1,5 +1,5 @@ import * as Noble from '@stoprocent/noble' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it } from 'vitest' import { SwitchBotBLE } from '../switchbot-ble.js' From db9670200050ee20dd4b5ce5bc791bdc01e022a3 Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Wed, 9 Oct 2024 00:57:15 -0500 Subject: [PATCH 07/10] add context --- src/switchbot-openapi.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/switchbot-openapi.ts b/src/switchbot-openapi.ts index 85a3e4e9..235c9802 100644 --- a/src/switchbot-openapi.ts +++ b/src/switchbot-openapi.ts @@ -109,12 +109,12 @@ export class SwitchBotOpenAPI extends EventEmitter { * @param command - The command to send to the device. * @param parameter - The parameter for the command. * @param commandType - The type of the command, defaults to 'command'. - * @returns {Promise<{ response: pushResponse['body'], statusCode: number }>} A promise that resolves to an object containing the API response. + * @returns {Promise<{ response: pushResponse['body'], statusCode: pushResponse['statusCode'], context: object }>} A promise that resolves to an object containing the API response. * @throws An error if the device control fails. */ - async controlDevice(deviceId: string, command: string, parameter: string, commandType: string = 'command'): Promise<{ response: pushResponse['body'], statusCode: number }> { + async controlDevice(deviceId: string, command: string, parameter: string, commandType: string = 'command'): Promise<{ response: pushResponse['body'], statusCode: pushResponse['statusCode'], context: object }> { try { - const { body, statusCode } = await request(`${this.baseURL}/devices/${deviceId}/commands`, { + const { body, statusCode, context } = await request(`${this.baseURL}/devices/${deviceId}/commands`, { method: 'POST', headers: this.generateHeaders(), body: JSON.stringify({ @@ -126,7 +126,8 @@ export class SwitchBotOpenAPI extends EventEmitter { const response = await body.json() as pushResponse['body'] this.emitLog('debug', `Controlled device: ${deviceId} with command: ${command} and parameter: ${parameter}`) this.emitLog('debug', `statusCode: ${statusCode}`) - return { response, statusCode } + this.emitLog('debug', `context: ${context}`) + return { response, statusCode, context } } catch (error: any) { this.emitLog('error', `Failed to control device: ${error.message}`) throw new Error(`Failed to control device: ${error.message}`) From 89e3a6b44ff749c19593682027433f455f531aba Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Wed, 9 Oct 2024 01:02:04 -0500 Subject: [PATCH 08/10] Update switchbot-openapi.ts --- src/switchbot-openapi.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/switchbot-openapi.ts b/src/switchbot-openapi.ts index 235c9802..460be71b 100644 --- a/src/switchbot-openapi.ts +++ b/src/switchbot-openapi.ts @@ -109,12 +109,12 @@ export class SwitchBotOpenAPI extends EventEmitter { * @param command - The command to send to the device. * @param parameter - The parameter for the command. * @param commandType - The type of the command, defaults to 'command'. - * @returns {Promise<{ response: pushResponse['body'], statusCode: pushResponse['statusCode'], context: object }>} A promise that resolves to an object containing the API response. + * @returns {Promise<{ response: pushResponse['body'], statusCode: pushResponse['statusCode'] }>} A promise that resolves to an object containing the API response. * @throws An error if the device control fails. */ - async controlDevice(deviceId: string, command: string, parameter: string, commandType: string = 'command'): Promise<{ response: pushResponse['body'], statusCode: pushResponse['statusCode'], context: object }> { + async controlDevice(deviceId: string, command: string, parameter: string, commandType: string = 'command'): Promise<{ response: pushResponse['body'], statusCode: pushResponse['statusCode'] }> { try { - const { body, statusCode, context } = await request(`${this.baseURL}/devices/${deviceId}/commands`, { + const { body, statusCode } = await request(`${this.baseURL}/devices/${deviceId}/commands`, { method: 'POST', headers: this.generateHeaders(), body: JSON.stringify({ @@ -126,8 +126,7 @@ export class SwitchBotOpenAPI extends EventEmitter { const response = await body.json() as pushResponse['body'] this.emitLog('debug', `Controlled device: ${deviceId} with command: ${command} and parameter: ${parameter}`) this.emitLog('debug', `statusCode: ${statusCode}`) - this.emitLog('debug', `context: ${context}`) - return { response, statusCode, context } + return { response, statusCode } } catch (error: any) { this.emitLog('error', `Failed to control device: ${error.message}`) throw new Error(`Failed to control device: ${error.message}`) From d13692e17ddd52b40c6729fef824535b2bedbc96 Mon Sep 17 00:00:00 2001 From: shizuka-na-kazushi <56832143+shizuka-na-kazushi@users.noreply.github.com> Date: Sat, 12 Oct 2024 12:41:30 +0900 Subject: [PATCH 09/10] add link to my script 'get-encryption-key' npm script in BLE.md (#259) ## :recycle: Current situation When using `WoSmartLock` object, `setKey()`, user need to obtain encryption key by `pySwitchBot` script which is written in `Python`. Developers who use `node-switchbot` are familiar with npm/NodeJS, but may have troublesome for using python script. I just developed **NodeJS based** script, switchbot-get-encryption-key which is just download by `npm`. This script may be more useful for `node-switchbot` users who want to obtain key ID and encryption key. ## :bulb: Proposed solution Just adding link to npm page for my script in `BLE.md` document for developer convenience. ### Testing Click the link in BLE.md file so that there is any link error. --- BLE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/BLE.md b/BLE.md index 72a57d92..2a9861c3 100644 --- a/BLE.md +++ b/BLE.md @@ -731,6 +731,7 @@ Actually, the `WoSmartLock ` is an object inherited from the [`SwitchbotDevice`] The `setKey()` method initialises the key information required for encrypted communication with the SmartLock This must be set before any control commands are sent to the device. To obtain the key information you will need to use an external tool - see [`pySwitchbot`](https://github.com/Danielhiversen/pySwitchbot/tree/master?tab=readme-ov-file#obtaining-locks-encryption-key) project for an example script. +Or, use [`switchbot-get-encryption-key`](https://www.npmjs.com/package/switchbot-get-encryption-key) npm script. | Property | Type | Description | | :-------------- | :----- | :----------------------------------------------------------------------------------------------- | From d7a3b2139283fa97fb5b26e8740d01d19b2e7c5b Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Fri, 11 Oct 2024 22:57:49 -0500 Subject: [PATCH 10/10] v3.1.0 --- CHANGELOG.md | 9 + package-lock.json | 381 +++++++++++++++++++-------------------- package.json | 10 +- src/switchbot-openapi.ts | 6 +- 4 files changed, 202 insertions(+), 204 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f72e127b..1c6ffdac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/) +## [3.1.0](https://github.com/OpenWonderLabs/node-switchbot/releases/tag/v3.1.0) (2024-10-11) + +### What's Changed +- Added support for emitting logs from this module from the `SwitchBotBLE` class. +- Housekeeping and update dependencies + +**Full Changelog**: https://github.com/OpenWonderLabs/node-switchbot/compare/v3.0.1...v3.1.0 + ## [3.0.1](https://github.com/OpenWonderLabs/node-switchbot/releases/tag/v3.0.1) (2024-10-05) ### What's Changed @@ -16,6 +24,7 @@ All notable changes to this project will be documented in this file. This projec #### ⚠️ Breaking Changes - Have added OpenAPI Functionality into `node-switchbot`, so now it is able to handle both APIs with just one module. - There are now two different Classes `SwitchBotBLE` and `SwitchBotOpenAPI` that can be used interact with the two different APIs + - `SwitchBotOpenApi` support emitting logs from this module. - You will need to update your current setup from`SwitchBot` to `SwitchBotBLE` to be able to interact with the BLE API - Updated the documents to explain both the `BLE` and the `OpenAPI` Functionality within `node-switchbot` diff --git a/package-lock.json b/package-lock.json index c5dfb0c9..646856bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "node-switchbot", - "version": "3.0.1", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-switchbot", - "version": "3.0.1", + "version": "3.1.0", "license": "MIT", "dependencies": { "@stoprocent/noble": "^1.15.1", "async-mutex": "^0.5.0", - "undici": "^6.19.8" + "undici": "^6.20.0" }, "devDependencies": { "@antfu/eslint-config": "^3.7.3", @@ -20,7 +20,7 @@ "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.13", "@types/mdast": "^4.0.4", - "@types/node": "^22.7.4", + "@types/node": "^22.7.5", "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", "@types/source-map-support": "^0.5.10", @@ -33,8 +33,8 @@ "shx": "^0.3.4", "sinon": "^19.0.2", "ts-node": "^10.9.2", - "typedoc": "^0.26.8", - "typescript": "^5.6.2", + "typedoc": "^0.26.9", + "typescript": "^5.6.3", "vitest": "^2.1.2" }, "engines": { @@ -204,9 +204,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", - "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "dev": true, "license": "MIT", "engines": { @@ -214,9 +214,9 @@ } }, "node_modules/@babel/core": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", - "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", "dev": true, "license": "MIT", "dependencies": { @@ -226,10 +226,10 @@ "@babel/helper-compilation-targets": "^7.25.7", "@babel/helper-module-transforms": "^7.25.7", "@babel/helpers": "^7.25.7", - "@babel/parser": "^7.25.7", + "@babel/parser": "^7.25.8", "@babel/template": "^7.25.7", "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.7", + "@babel/types": "^7.25.8", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -493,13 +493,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", - "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.25.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -792,9 +792,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", - "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "dev": true, "license": "MIT", "dependencies": { @@ -842,7 +842,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -2764,58 +2763,58 @@ "optional": true }, "node_modules/@shikijs/core": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.21.0.tgz", - "integrity": "sha512-zAPMJdiGuqXpZQ+pWNezQAk5xhzRXBNiECFPcJLtUdsFM3f//G95Z15EHTnHchYycU8kIIysqGgxp8OVSj1SPQ==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.0.tgz", + "integrity": "sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-javascript": "1.21.0", - "@shikijs/engine-oniguruma": "1.21.0", - "@shikijs/types": "1.21.0", - "@shikijs/vscode-textmate": "^9.2.2", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.3" } }, "node_modules/@shikijs/engine-javascript": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.21.0.tgz", - "integrity": "sha512-jxQHNtVP17edFW4/0vICqAVLDAxmyV31MQJL4U/Kg+heQALeKYVOWo0sMmEZ18FqBt+9UCdyqGKYE7bLRtk9mg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz", + "integrity": "sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "1.21.0", - "@shikijs/vscode-textmate": "^9.2.2", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", "oniguruma-to-js": "0.4.3" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.21.0.tgz", - "integrity": "sha512-AIZ76XocENCrtYzVU7S4GY/HL+tgHGbVU+qhiDyNw1qgCA5OSi4B4+HY4BtAoJSMGuD/L5hfTzoRVbzEm2WTvg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz", + "integrity": "sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "1.21.0", - "@shikijs/vscode-textmate": "^9.2.2" + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0" } }, "node_modules/@shikijs/types": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.21.0.tgz", - "integrity": "sha512-tzndANDhi5DUndBtpojEq/42+dpUF2wS7wdCDQaFtIXm3Rd1QkrcVgSSRLOvEwexekihOXfbYJINW37g96tJRw==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.0.tgz", + "integrity": "sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^9.2.2", + "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4" } }, "node_modules/@shikijs/vscode-textmate": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.2.2.tgz", - "integrity": "sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", "dev": true, "license": "MIT" }, @@ -3141,9 +3140,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3230,17 +3229,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", - "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", + "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/type-utils": "8.8.0", - "@typescript-eslint/utils": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/type-utils": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3264,16 +3263,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", - "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", + "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4" }, "engines": { @@ -3293,14 +3292,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", - "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0" + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3311,14 +3310,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", - "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", + "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/utils": "8.8.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3336,9 +3335,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", - "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", "dev": true, "license": "MIT", "engines": { @@ -3350,14 +3349,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", - "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3379,16 +3378,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", - "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0" + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3402,13 +3401,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", - "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/types": "8.8.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3473,9 +3472,9 @@ } }, "node_modules/@vitest/eslint-plugin": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.6.tgz", - "integrity": "sha512-sFuAnD9iycnOzLHHhNCULXeb6ejOSo5Lcq/ODhdlUOoUrXkQPcVeYqXurZMA3neOqf+wNCQ6YuU1zyoYH/WEcg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.7.tgz", + "integrity": "sha512-pTWGW3y6lH2ukCuuffpan6kFxG6nIuoesbhMiQxskyQMRcCN5t9SXsKrNHvEw3p8wcCsgJoRqFZVkOTn6TjclA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3487,6 +3486,9 @@ "peerDependenciesMeta": { "typescript": { "optional": true + }, + "vitest": { + "optional": true } } }, @@ -3615,45 +3617,45 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.11.tgz", - "integrity": "sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.11", + "@vue/shared": "3.5.12", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.11.tgz", - "integrity": "sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@vue/compiler-core": "3.5.11", - "@vue/shared": "3.5.11" + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.11.tgz", - "integrity": "sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.11", - "@vue/compiler-dom": "3.5.11", - "@vue/compiler-ssr": "3.5.11", - "@vue/shared": "3.5.11", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", "postcss": "^8.4.47", @@ -3661,21 +3663,21 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.11.tgz", - "integrity": "sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.11", - "@vue/shared": "3.5.11" + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/shared": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.11.tgz", - "integrity": "sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", "dev": true, "license": "MIT", "peer": true @@ -4198,9 +4200,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001667", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", - "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "dev": true, "funding": [ { @@ -4482,9 +4484,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true, "license": "MIT" }, @@ -4732,9 +4734,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.32", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz", - "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "dev": true, "license": "ISC" }, @@ -5154,9 +5156,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "50.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.3.1.tgz", - "integrity": "sha512-SY9oUuTMr6aWoJggUS40LtMjsRzJPB5ZT7F432xZIHK3EfHF+8i48GbUBpwanrtlL9l1gILNTHK9o8gEhYLcKA==", + "version": "50.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.3.2.tgz", + "integrity": "sha512-TjgZocG53N3a84PdCFGqVMWLWwDitOUuKjlJftwTu/iTiD7N/Q2Q3eEy/Q4GfJqpM4rTJCkzUYWQfol6RZNDcA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5249,9 +5251,9 @@ } }, "node_modules/eslint-plugin-n": { - "version": "17.10.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.10.3.tgz", - "integrity": "sha512-ySZBfKe49nQZWR1yFaA0v/GsH6Fgp8ah6XV0WDz6CN8WO0ek4McMzb7A2xnf4DCYV43frjCygvb9f/wx7UUxRw==", + "version": "17.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.11.1.tgz", + "integrity": "sha512-93IUD82N6tIEgjztVI/l3ElHtC2wTa9boJHrD8iN+NyDxjxz/daZUZKfkedjBZNdg6EqDk4irybUsiPwDqXAEA==", "dev": true, "license": "MIT", "dependencies": { @@ -5416,9 +5418,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz", - "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==", + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.0.tgz", + "integrity": "sha512-hamyjrBhNH6Li6R1h1VF9KHfshJlKgKEg3ARbGTn72CMNDSMhWbgC7NdkRDEh25AFW+4SDATzyNM+3gWuZii8g==", "dev": true, "license": "MIT", "dependencies": { @@ -5957,16 +5959,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -6083,9 +6075,9 @@ } }, "node_modules/globals": { - "version": "15.10.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.10.0.tgz", - "integrity": "sha512-tqFIbz83w4Y5TCbtgjZjApohbuh7K9BxGYFm7ifwDR240tvdb7P9x+/9VvUKlmkPoiknoJtanI8UOrqxS3a7lQ==", + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", "dev": true, "license": "MIT", "engines": { @@ -7556,14 +7548,11 @@ } }, "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } + "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -7583,9 +7572,9 @@ "license": "MIT" }, "node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "license": "MIT", "dependencies": { @@ -8599,16 +8588,16 @@ } }, "node_modules/mlly": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", - "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.11.3", + "acorn": "^8.12.1", "pathe": "^1.1.2", - "pkg-types": "^1.1.1", - "ufo": "^1.5.3" + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" } }, "node_modules/ms": { @@ -8681,9 +8670,9 @@ } }, "node_modules/node-addon-api": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.1.0.tgz", - "integrity": "sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.1.tgz", + "integrity": "sha512-vmEOvxwiH8tlOcv4SyE8RH34rI5/nWVaigUeAUPawC6f0+HoDthwI0vkMu4tbtsZrXq6QXFfrkhjofzKEs5tpA==", "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" @@ -9008,9 +8997,9 @@ "license": "BlueOak-1.0.0" }, "node_modules/package-manager-detector": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.0.tgz", - "integrity": "sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.2.tgz", + "integrity": "sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==", "dev": true, "license": "MIT" }, @@ -9315,14 +9304,14 @@ } }, "node_modules/pkg-types": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz", - "integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", "dev": true, "license": "MIT", "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.7.1", + "confbox": "^0.1.8", + "mlly": "^1.7.2", "pathe": "^1.1.2" } }, @@ -10065,17 +10054,17 @@ } }, "node_modules/shiki": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.21.0.tgz", - "integrity": "sha512-apCH5BoWTrmHDPGgg3RF8+HAAbEL/CdbYr8rMw7eIrdhCkZHdVGat5mMNlRtd1erNG01VPMIKHNQ0Pj2HMAiog==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.0.tgz", + "integrity": "sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/core": "1.21.0", - "@shikijs/engine-javascript": "1.21.0", - "@shikijs/engine-oniguruma": "1.21.0", - "@shikijs/types": "1.21.0", - "@shikijs/vscode-textmate": "^9.2.2", + "@shikijs/core": "1.22.0", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4" } }, @@ -10476,9 +10465,9 @@ } }, "node_modules/synckit": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", - "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, "license": "MIT", "dependencies": { @@ -10792,9 +10781,9 @@ } }, "node_modules/typedoc": { - "version": "0.26.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.8.tgz", - "integrity": "sha512-QBF0BMbnNeUc6U7pRHY7Jb8pjhmiNWZNQT8LU6uk9qP9t3goP9bJptdlNqMC0wBB2w9sQrxjZt835bpRSSq1LA==", + "version": "0.26.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.9.tgz", + "integrity": "sha512-Rc7QpWL7EtmrT8yxV0GmhOR6xHgFnnhphbD9Suti3fz3um7ZOrou6q/g9d6+zC5PssTLZmjaW4Upmzv8T1rCcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10815,9 +10804,9 @@ } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10850,9 +10839,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.0.tgz", + "integrity": "sha512-AITZfPuxubm31Sx0vr8bteSalEbs9wQb/BOBi9FPlD9Qpd6HxZ4Q0+hI742jBhkPb4RT2v5MQzaW5VhRVyj+9A==", "license": "MIT", "engines": { "node": ">=18.17" diff --git a/package.json b/package.json index 7627352b..a2636ee1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-switchbot", "type": "module", - "version": "3.0.1", + "version": "3.1.0", "description": "The node-switchbot is a Node.js module which allows you to control your Switchbot Devices through Bluetooth (BLE).", "author": "OpenWonderLabs (https://github.com/OpenWonderLabs)", "license": "MIT", @@ -50,7 +50,7 @@ "dependencies": { "@stoprocent/noble": "^1.15.1", "async-mutex": "^0.5.0", - "undici": "^6.19.8" + "undici": "^6.20.0" }, "optionalDependencies": { "@stoprocent/bluetooth-hci-socket": "^1.4.1" @@ -62,7 +62,7 @@ "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.13", "@types/mdast": "^4.0.4", - "@types/node": "^22.7.4", + "@types/node": "^22.7.5", "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", "@types/source-map-support": "^0.5.10", @@ -75,8 +75,8 @@ "shx": "^0.3.4", "sinon": "^19.0.2", "ts-node": "^10.9.2", - "typedoc": "^0.26.8", - "typescript": "^5.6.2", + "typedoc": "^0.26.9", + "typescript": "^5.6.3", "vitest": "^2.1.2" } } diff --git a/src/switchbot-openapi.ts b/src/switchbot-openapi.ts index 460be71b..13dc5c1d 100644 --- a/src/switchbot-openapi.ts +++ b/src/switchbot-openapi.ts @@ -6,7 +6,7 @@ import type { IncomingMessage, Server, ServerResponse } from 'node:http' import type { pushResponse } from './types/devicepush.js' import type { devices } from './types/deviceresponse.js' -import type { deviceStatus } from './types/devicestatus.js' +import type { deviceStatus, deviceStatusRequest } from './types/devicestatus.js' import type { deleteWebhookResponse, queryWebhookResponse, setupWebhookResponse, updateWebhookResponse } from './types/devicewebhookstatus.js' import { Buffer } from 'node:buffer' @@ -137,10 +137,10 @@ export class SwitchBotOpenAPI extends EventEmitter { * Retrieves the status of a specific device. * * @param deviceId - The unique identifier of the device. - * @returns {Promise<{ response: deviceStatus, statusCode: number }>} A promise that resolves to the device status. + * @returns {Promise<{ response: deviceStatus, statusCode: deviceStatusRequest['statusCode'] }>} A promise that resolves to the device status. * @throws An error if the request fails. */ - async getDeviceStatus(deviceId: string): Promise<{ response: deviceStatus, statusCode: number }> { + async getDeviceStatus(deviceId: string): Promise<{ response: deviceStatus, statusCode: deviceStatusRequest['statusCode'] }> { try { const { body, statusCode } = await request(`${this.baseURL}/devices/${deviceId}/status`, { method: 'GET',