diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..67e1454c --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,165 @@ +# Node-SwitchBot Development Instructions + +Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. + +## Working Effectively + +### Bootstrap and Setup +- Install Node.js (^20 || ^22 || ^24): Use the existing version if available +- Install dependencies: `npm install` -- takes ~5-25 seconds (varies by cache). **NEVER CANCEL** +- Build the project: `npm run build` -- takes ~5 seconds. **NEVER CANCEL** +- Run tests: `npm run test` -- takes ~1 second with 12 tests. **NEVER CANCEL** + +### Development Workflow +- **ALWAYS run these commands in order when starting work:** + 1. `npm install` + 2. `npm run build` + 3. `npm run test` + 4. `npm run lint` +- **Build and validate EVERY change**: After any code modification, always run `npm run build && npm run test && npm run lint` +- **NEVER skip linting**: Run `npm run lint` before committing or the CI (.github/workflows/build.yml) will fail +- **Use lint:fix for automatic fixes**: `npm run lint:fix` to automatically fix ESLint issues + +### Platform Requirements and Constraints +- **BLE functionality requires Linux-based OS only** (Raspbian, Ubuntu, etc.) +- **Windows and macOS are NOT supported** for BLE operations (use OpenAPI instead) +- **Node.js versions**: Must use ^20, ^22, or ^24 (currently using v20.19.4) +- **ES Modules**: This project uses `"type": "module"` - always use ES import/export syntax + +### Testing and Validation +- **Basic functionality test**: After any changes to core classes, run this validation: + ```javascript + const { SwitchBotBLE, SwitchBotOpenAPI } = require('./dist/index.js'); + const ble = new SwitchBotBLE(); // Should not throw + const api = new SwitchBotOpenAPI('test', 'test'); // Should not throw + ``` +- **Complete test suite**: `npm run test` (12 tests) -- should always pass before committing +- **Test coverage**: `npm run test-coverage` to see coverage report (~15% coverage is normal) +- **Documentation generation**: `npm run docs` generates TypeDoc documentation in ./docs/ + +### Build and Timing Expectations +- **npm install**: ~5-25 seconds (varies by cache) -- **NEVER CANCEL**. Set timeout to 5+ minutes for safety +- **npm run build**: ~5 seconds -- **NEVER CANCEL**. Set timeout to 2+ minutes for safety +- **npm run test**: ~1 second (12 tests) -- **NEVER CANCEL**. Set timeout to 2+ minutes for safety +- **npm run lint**: ~3 seconds -- **NEVER CANCEL**. Set timeout to 2+ minutes for safety +- **npm run docs**: ~2 seconds -- **NEVER CANCEL**. Set timeout to 2+ minutes for safety + +## Project Structure and Key Files + +### Source Code Organization +- **src/index.ts**: Main export file - exports SwitchBotBLE, SwitchBotOpenAPI, and device classes +- **src/switchbot-ble.ts**: Bluetooth Low Energy interface for direct device control +- **src/switchbot-openapi.ts**: HTTP API interface for cloud-based SwitchBot control +- **src/device.ts**: Individual device classes (WoHand, WoCurtain, WoSmartLock, etc.) +- **src/types/**: TypeScript type definitions for all device interfaces +- **src/settings.ts**: Configuration constants and API URLs +- **dist/**: Compiled JavaScript output (generated by `npm run build`) + +### Configuration Files +- **package.json**: Main project config - scripts, dependencies, ES module config +- **tsconfig.json**: TypeScript compilation settings (target: ES2022, module: ES2022) +- **eslint.config.js**: ESLint configuration using @antfu/eslint-config +- **jest.config.js**: Test configuration (uses Vitest, not Jest) +- **.gitignore**: Excludes dist/, node_modules/, coverage/, and build artifacts + +### Documentation +- **README.md**: Main project documentation and installation instructions +- **BLE.md**: Comprehensive BLE usage documentation and device examples +- **OpenAPI.md**: OpenAPI usage documentation and authentication setup +- **CHANGELOG.md**: Version history and release notes +- **docs/**: Generated TypeDoc API documentation (HTML format) + +## Common Development Tasks + +### Adding New Device Support +- **Add device class**: Create new class in src/device.ts extending SwitchbotDevice +- **Update exports**: Add export to src/index.ts +- **Add type definitions**: Create types in src/types/ if needed +- **Test basic instantiation**: Ensure the device class can be imported and instantiated +- **Always run full build and test cycle**: `npm run build && npm run test && npm run lint` + +### API Changes and Extensions +- **OpenAPI changes**: Modify src/switchbot-openapi.ts +- **BLE changes**: Modify src/switchbot-ble.ts +- **Update type definitions**: Modify corresponding files in src/types/ +- **Always verify exports**: Check that new functionality is exported in src/index.ts +- **Test import functionality**: Verify new exports can be imported correctly + +### Working with Dependencies +- **Noble (BLE)**: @stoprocent/noble for Bluetooth functionality - Linux only +- **HTTP requests**: Uses undici for HTTP calls (not axios or fetch) +- **Async operations**: Uses async-mutex for concurrency control +- **Adding dependencies**: Use `npm install --save` for runtime deps, `--save-dev` for dev deps + +## Validation and Quality Assurance + +### Pre-commit Checklist +1. **Build succeeds**: `npm run build` completes without errors +2. **All tests pass**: `npm run test` shows all 12 tests passing +3. **Linting passes**: `npm run lint` shows no errors +4. **Documentation builds**: `npm run docs` generates without warnings +5. **Basic import works**: Can import and instantiate main classes + +### Manual Testing Scenarios +- **SwitchBotBLE instantiation**: `new SwitchBotBLE()` should not throw errors +- **SwitchBotOpenAPI instantiation**: `new SwitchBotOpenAPI('token', 'secret')` should not throw +- **Module exports**: All exported classes should be importable from main package +- **TypeScript compilation**: No TypeScript errors in dist/ output +- **Documentation completeness**: Check that new public APIs appear in generated docs + +### CI/CD Integration +- **GitHub Actions**: Build runs on push to 'latest' branch and PRs +- **External workflows**: Uses OpenWonderLabs/.github/.github/workflows/nodejs-build-and-test.yml +- **Required checks**: Build, test, and lint must all pass +- **Coverage reporting**: Test coverage is tracked and reported + +## Troubleshooting Common Issues + +### BLE Not Working +- **Check OS**: BLE only works on Linux-based systems +- **Install noble prerequisites**: May need additional system libraries for @stoprocent/noble +- **Use OpenAPI instead**: For Windows/macOS development, use SwitchBotOpenAPI class + +### Build Failures +- **Check Node.js version**: Must be ^20, ^22, or ^24 +- **Clean and rebuild**: `npm run clean && npm install && npm run build` +- **TypeScript errors**: Check tsconfig.json settings and type definitions + +### Test Failures +- **Run individual tests**: Use `npm run test:watch` for interactive testing +- **Check imports**: Ensure all imports use correct ES module syntax +- **Verify exports**: Check that src/index.ts exports all necessary components + +### Linting Errors +- **Auto-fix**: Use `npm run lint:fix` to automatically fix many issues +- **ESLint config**: Review eslint.config.js for current rules +- **Import sorting**: ESLint enforces specific import order - use lint:fix + +## Frequently Referenced Information + +### Package Scripts (from package.json) +```bash +npm run build # Clean and compile TypeScript +npm run test # Run test suite with Vitest +npm run lint # Run ESLint on src/**/*.ts +npm run lint:fix # Auto-fix ESLint issues +npm run docs # Generate TypeDoc documentation +npm run clean # Remove dist/ directory +npm run watch # Build and link for development +``` + +### Main Exports (from src/index.ts) +- **SwitchBotBLE**: Bluetooth interface class +- **SwitchBotOpenAPI**: HTTP API interface class +- **SwitchbotDevice**: Base device class +- **Device classes**: WoHand, WoCurtain, WoSmartLock, etc. +- **Type definitions**: All device status and response types + +### Dependencies Summary +- **@stoprocent/noble**: BLE functionality (Linux only) +- **undici**: HTTP client for API requests +- **async-mutex**: Concurrency control +- **TypeScript**: Language and compilation +- **Vitest**: Testing framework +- **ESLint**: Code linting with @antfu/eslint-config +- **TypeDoc**: API documentation generation \ No newline at end of file diff --git a/docs/classes/WoHub3.html b/docs/classes/WoHub3.html new file mode 100644 index 00000000..39c4a397 --- /dev/null +++ b/docs/classes/WoHub3.html @@ -0,0 +1,52 @@ +WoHub3 | node-switchbot

Class representing a WoHub3 device.

+

Hierarchy (View Summary)

Constructors

Accessors

  • get address(): string

    Returns string

  • get connectionState(): string

    Returns string

  • get id(): string

    Returns string

  • get onConnectHandler(): () => Promise<void>

    Returns () => Promise<void>

  • set onConnectHandler(func: () => Promise<void>): void

    Parameters

    • func: () => Promise<void>

    Returns void

  • get onDisconnectHandler(): () => Promise<void>

    Returns () => Promise<void>

  • set onDisconnectHandler(func: () => Promise<void>): void

    Parameters

    • func: () => Promise<void>

    Returns void

Methods

  • Sends a command to the device and awaits a response.

    +

    Parameters

    • reqBuf: Buffer

      The command buffer.

      +

    Returns Promise<Buffer<ArrayBufferLike>>

    A Promise that resolves with the response buffer.

    +
  • Connects to the device.

    +

    Returns Promise<void>

    A Promise that resolves when the connection is complete.

    +
  • Disconnects from the device.

    +

    Returns Promise<void>

    A Promise that resolves when the disconnection is complete.

    +
  • Internal method to handle the connection process.

    +

    Returns Promise<void>

    A Promise that resolves when the connection is complete.

    +
  • Logs a message with the specified log level.

    +

    Parameters

    • level: string

      The severity level of the log (e.g., 'info', 'warn', 'error').

      +
    • message: string

      The log message to be emitted.

      +

    Returns Promise<void>

  • Sets the device name.

    +

    Parameters

    • name: string

      The new device name.

      +

    Returns Promise<void>

    A Promise that resolves when the name is set.

    +
  • Parses the service data for WoHub3.

    +

    Parameters

    • manufacturerData: Buffer

      The manufacturer data buffer.

      +
    • emitLog: (level: string, message: string) => void

      The function to emit log messages.

      +

    Returns Promise<null | hub3ServiceData>

      +
    • Parsed service data or null if invalid.
    • +
    +
diff --git a/docs/types/hub3.html b/docs/types/hub3.html new file mode 100644 index 00000000..f8847991 --- /dev/null +++ b/docs/types/hub3.html @@ -0,0 +1 @@ +hub3 | node-switchbot

Type Alias hub3

hub3: device & {}
diff --git a/docs/types/hub3ServiceData.html b/docs/types/hub3ServiceData.html new file mode 100644 index 00000000..e30211c1 --- /dev/null +++ b/docs/types/hub3ServiceData.html @@ -0,0 +1 @@ +hub3ServiceData | node-switchbot

Type Alias hub3ServiceData

hub3ServiceData: serviceData & {
    celsius: number;
    fahrenheit: number;
    fahrenheit_mode: boolean;
    humidity: number;
    lightLevel: number;
    model: Hub3;
    modelFriendlyName: Hub3;
    modelName: Hub3;
}
diff --git a/docs/types/hub3Status.html b/docs/types/hub3Status.html new file mode 100644 index 00000000..d312cefe --- /dev/null +++ b/docs/types/hub3Status.html @@ -0,0 +1 @@ +hub3Status | node-switchbot

Type Alias hub3Status

hub3Status: deviceStatus & {
    humidity: number;
    lightLevel: number;
    temperature: number;
}
diff --git a/docs/types/hub3WebhookContext.html b/docs/types/hub3WebhookContext.html new file mode 100644 index 00000000..7ec34c91 --- /dev/null +++ b/docs/types/hub3WebhookContext.html @@ -0,0 +1 @@ +hub3WebhookContext | node-switchbot

Type Alias hub3WebhookContext

hub3WebhookContext: deviceWebhookContext & {
    humidity: number;
    lightLevel: number;
    scale: "CELSIUS" | "FAHRENHEIT";
    temperature: number;
}
diff --git a/src/device.ts b/src/device.ts index ee1f59e5..8cbd9629 100644 --- a/src/device.ts +++ b/src/device.ts @@ -4,7 +4,7 @@ */ import type { Characteristic, Noble, Peripheral, Service } from '@stoprocent/noble' -import type { airPurifierServiceData, airPurifierTableServiceData, batteryCirculatorFanServiceData, blindTiltServiceData, botServiceData, ceilingLightProServiceData, ceilingLightServiceData, colorBulbServiceData, contactSensorServiceData, curtain3ServiceData, curtainServiceData, hub2ServiceData, humidifier2ServiceData, humidifierServiceData, keypadDetectorServiceData, lockProServiceData, lockServiceData, meterPlusServiceData, meterProCO2ServiceData, meterProServiceData, meterServiceData, motionSensorServiceData, outdoorMeterServiceData, plugMiniJPServiceData, plugMiniUSServiceData, relaySwitch1PMServiceData, relaySwitch1ServiceData, remoteServiceData, robotVacuumCleanerServiceData, stripLightServiceData, waterLeakDetectorServiceData } from './types/ble.js' +import type { airPurifierServiceData, airPurifierTableServiceData, batteryCirculatorFanServiceData, blindTiltServiceData, botServiceData, ceilingLightProServiceData, ceilingLightServiceData, colorBulbServiceData, contactSensorServiceData, curtain3ServiceData, curtainServiceData, hub2ServiceData, hub3ServiceData, humidifier2ServiceData, humidifierServiceData, keypadDetectorServiceData, lockProServiceData, lockServiceData, meterPlusServiceData, meterProCO2ServiceData, meterProServiceData, meterServiceData, motionSensorServiceData, outdoorMeterServiceData, plugMiniJPServiceData, plugMiniUSServiceData, relaySwitch1PMServiceData, relaySwitch1ServiceData, remoteServiceData, robotVacuumCleanerServiceData, stripLightServiceData, waterLeakDetectorServiceData } from './types/ble.js' import { Buffer } from 'node:buffer' import * as Crypto from 'node:crypto' @@ -93,7 +93,7 @@ export interface ad { id: string address: string rssi: number - serviceData: botServiceData | colorBulbServiceData | contactSensorServiceData | curtainServiceData | curtain3ServiceData | stripLightServiceData | ServiceData | lockProServiceData | ServiceData | meterPlusServiceData | meterProServiceData | meterProCO2ServiceData | motionSensorServiceData | outdoorMeterServiceData | plugMiniUSServiceData | plugMiniJPServiceData | blindTiltServiceData | ceilingLightServiceData | ceilingLightProServiceData | hub2ServiceData | batteryCirculatorFanServiceData | waterLeakDetectorServiceData | humidifierServiceData | humidifier2ServiceData | robotVacuumCleanerServiceData | keypadDetectorServiceData | relaySwitch1PMServiceData | relaySwitch1ServiceData | remoteServiceData + serviceData: airPurifierServiceData | airPurifierTableServiceData | botServiceData | colorBulbServiceData | contactSensorServiceData | curtainServiceData | curtain3ServiceData | stripLightServiceData | lockServiceData | lockProServiceData | meterServiceData | meterPlusServiceData | meterProServiceData | meterProCO2ServiceData | motionSensorServiceData | outdoorMeterServiceData | plugMiniUSServiceData | plugMiniJPServiceData | blindTiltServiceData | ceilingLightServiceData | ceilingLightProServiceData | hub2ServiceData | hub3ServiceData | batteryCirculatorFanServiceData | waterLeakDetectorServiceData | humidifierServiceData | humidifier2ServiceData | robotVacuumCleanerServiceData | keypadDetectorServiceData | relaySwitch1PMServiceData | relaySwitch1ServiceData | remoteServiceData [key: string]: unknown } @@ -118,6 +118,7 @@ export declare interface SwitchBotBLEDevice { MeterPro: DeviceInfo MeterProCO2: DeviceInfo Hub2: DeviceInfo + Hub3: DeviceInfo OutdoorMeter: DeviceInfo MotionSensor: DeviceInfo ContactSensor: DeviceInfo @@ -139,6 +140,7 @@ export enum SwitchBotModel { HubMini = 'W0202200', HubPlus = 'SwitchBot Hub S1', Hub2 = 'W3202100', + Hub3 = 'W3302100', Bot = 'SwitchBot S1', Curtain = 'W0701600', Curtain3 = 'W2400000', @@ -197,6 +199,7 @@ export enum SwitchBotBLEModel { MeterPro = '4', MeterProCO2 = '5', Hub2 = 'v', + Hub3 = 'V', OutdoorMeter = 'w', MotionSensor = 's', ContactSensor = 'd', @@ -222,6 +225,7 @@ export enum SwitchBotBLEModel { export enum SwitchBotBLEModelName { Bot = 'WoHand', Hub2 = 'WoHub2', + Hub3 = 'WoHub3', ColorBulb = 'WoBulb', Curtain = 'WoCurtain', Curtain3 = 'WoCurtain3', @@ -254,6 +258,7 @@ export enum SwitchBotBLEModelName { export enum SwitchBotBLEModelFriendlyName { Bot = 'Bot', Hub2 = 'Hub 2', + Hub3 = 'Hub 3', ColorBulb = 'Color Bulb', Curtain = 'Curtain', Curtain3 = 'Curtain 3', @@ -1051,6 +1056,8 @@ export class Advertising { return WoSensorTHProCO2.parseServiceData(serviceData, manufacturerData, emitLog) case SwitchBotBLEModel.Hub2: return WoHub2.parseServiceData(manufacturerData, emitLog) + case SwitchBotBLEModel.Hub3: + return WoHub3.parseServiceData(manufacturerData, emitLog) case SwitchBotBLEModel.OutdoorMeter: return WoIOSensorTH.parseServiceData(serviceData, manufacturerData, emitLog) case SwitchBotBLEModel.AirPurifier: @@ -2010,6 +2017,52 @@ export class WoHub2 extends SwitchbotDevice { } } +/** + * Class representing a WoHub3 device. + * @see https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/latest/devicetypes/meter.md + */ +export class WoHub3 extends SwitchbotDevice { + /** + * Parses the service data for WoHub3. + * @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. + */ + static async parseServiceData( + manufacturerData: Buffer, + emitLog: (level: string, message: string) => void, + ): Promise { + if (manufacturerData.length !== 16) { + emitLog('debugerror', `[parseServiceDataForWoHub3] Buffer length ${manufacturerData.length} !== 16!`) + return null + } + + const [byte0, byte1, byte2, , , , , , , , , , byte12] = manufacturerData + + const tempSign = byte1 & 0b10000000 ? 1 : -1 + const tempC = tempSign * ((byte1 & 0b01111111) + (byte0 & 0b00001111) / 10) + const tempF = Math.round(((tempC * 9) / 5 + 32) * 10) / 10 + const lightLevel = byte12 & 0b11111 + + const data: hub3ServiceData = { + model: SwitchBotBLEModel.Hub3, + modelName: SwitchBotBLEModelName.Hub3, + modelFriendlyName: SwitchBotBLEModelFriendlyName.Hub3, + celsius: tempC, + fahrenheit: tempF, + fahrenheit_mode: !!(byte2 & 0b10000000), + humidity: byte2 & 0b01111111, + lightLevel, + } + + return data + } + + constructor(peripheral: NobleTypes['peripheral'], noble: NobleTypes['noble']) { + super(peripheral, noble) + } +} + /** * Class representing a WoHumi device. * @see https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/tree/latest/devicetypes diff --git a/src/types/ble.ts b/src/types/ble.ts index 53742e6f..0948a86f 100644 --- a/src/types/ble.ts +++ b/src/types/ble.ts @@ -267,6 +267,17 @@ export type hub2ServiceData = BLEServiceData & { lightLevel: number } +export type hub3ServiceData = BLEServiceData & { + model: SwitchBotBLEModel.Hub3 + modelName: SwitchBotBLEModelName.Hub3 + modelFriendlyName: SwitchBotBLEModelFriendlyName.Hub3 + celsius: number + fahrenheit: number + fahrenheit_mode: boolean + humidity: number + lightLevel: number +} + export type batteryCirculatorFanServiceData = BLEServiceData & { model: SwitchBotBLEModel.Unknown modelName: SwitchBotBLEModelName.Unknown @@ -403,6 +414,7 @@ export type BLEDeviceServiceData | curtain3ServiceData | curtainServiceData | hub2ServiceData + | hub3ServiceData | keypadDetectorServiceData | lockProServiceData | lockServiceData