@switchbot/homebridge-switchbot 5.0.0-beta.99 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +14 -0
- package/.github/copilot-instructions.md +39 -0
- package/.github/workflows/ci.yml +4 -1
- package/.github/workflows/manual-e2e.yml +6 -3
- package/.github/workflows/release.yml +64 -15
- package/.github/workflows/stale.yml +2 -4
- package/.husky/pre-push +15 -0
- package/CHANGELOG.md +126 -134
- package/MIGRATION.md +16 -6
- package/README.md +84 -3
- package/TODO.md +263 -0
- package/config.schema.json +229 -36
- package/dist/SwitchBotHAPPlatform.d.ts +133 -0
- package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
- package/dist/SwitchBotHAPPlatform.js +555 -0
- package/dist/SwitchBotHAPPlatform.js.map +1 -0
- package/dist/SwitchBotMatterPlatform.d.ts +141 -0
- package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
- package/dist/SwitchBotMatterPlatform.js +536 -0
- package/dist/SwitchBotMatterPlatform.js.map +1 -0
- package/dist/device-types.d.ts +31 -0
- package/dist/device-types.d.ts.map +1 -0
- package/dist/device-types.js +246 -0
- package/dist/device-types.js.map +1 -0
- package/dist/deviceCommandMapper.d.ts +10 -0
- package/dist/deviceCommandMapper.d.ts.map +1 -0
- package/dist/deviceCommandMapper.js +319 -0
- package/dist/deviceCommandMapper.js.map +1 -0
- package/dist/deviceFactory.d.ts +3 -2
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +107 -29
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +59 -37
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +376 -78
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- package/dist/homebridge-ui/device-types.js +246 -0
- package/dist/homebridge-ui/device-types.js.map +1 -0
- package/dist/homebridge-ui/deviceCommandMapper.js +319 -0
- package/dist/homebridge-ui/deviceCommandMapper.js.map +1 -0
- package/dist/homebridge-ui/endpoints/config.d.ts +3 -0
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/config.js +90 -0
- package/dist/homebridge-ui/endpoints/config.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.js +144 -0
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.js +219 -0
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
- package/dist/homebridge-ui/errors.js +32 -0
- package/dist/homebridge-ui/errors.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +90 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js +144 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js +219 -0
- package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/server.js +11 -0
- package/dist/homebridge-ui/homebridge-ui/server.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +108 -0
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js +111 -0
- package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js.map +1 -0
- package/dist/homebridge-ui/homebridge-ui/utils/logger.js +17 -0
- package/dist/homebridge-ui/homebridge-ui/utils/logger.js.map +1 -0
- package/dist/homebridge-ui/public/css/styles.css +483 -0
- package/dist/homebridge-ui/public/index.html +197 -621
- package/dist/homebridge-ui/public/js/advanced-settings.d.ts +3 -0
- package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/advanced-settings.js +95 -0
- package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -0
- package/dist/homebridge-ui/public/js/advanced-settings.ts +94 -0
- package/dist/homebridge-ui/public/js/api.d.ts +66 -0
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/api.js +295 -0
- package/dist/homebridge-ui/public/js/api.js.map +1 -0
- package/dist/homebridge-ui/public/js/api.ts +355 -0
- package/dist/homebridge-ui/public/js/app.d.ts +2 -0
- package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/app.js +3722 -0
- package/dist/homebridge-ui/public/js/app.js.map +7 -0
- package/dist/homebridge-ui/public/js/app.ts +22 -0
- package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
- package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/constants.js +2 -0
- package/dist/homebridge-ui/public/js/constants.js.map +1 -0
- package/dist/homebridge-ui/public/js/constants.ts +1 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.js +99 -0
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.ts +105 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts +3 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.js +199 -0
- package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.ts +227 -0
- package/dist/homebridge-ui/public/js/devices.d.ts +9 -0
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices.js +98 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices.ts +106 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts +9 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.js +1201 -0
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.ts +1335 -0
- package/dist/homebridge-ui/public/js/logger.d.ts +7 -0
- package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/logger.js +17 -0
- package/dist/homebridge-ui/public/js/logger.js.map +1 -0
- package/dist/homebridge-ui/public/js/logger.ts +17 -0
- package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
- package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modal.js +35 -0
- package/dist/homebridge-ui/public/js/modal.js.map +1 -0
- package/dist/homebridge-ui/public/js/modal.ts +35 -0
- package/dist/homebridge-ui/public/js/modals.d.ts +15 -0
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modals.js +675 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -0
- package/dist/homebridge-ui/public/js/modals.ts +765 -0
- package/dist/homebridge-ui/public/js/render.d.ts +71 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/render.js +960 -0
- package/dist/homebridge-ui/public/js/render.js.map +1 -0
- package/dist/homebridge-ui/public/js/render.ts +1084 -0
- package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
- package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/toast.js +38 -0
- package/dist/homebridge-ui/public/js/toast.js.map +1 -0
- package/dist/homebridge-ui/public/js/toast.ts +44 -0
- package/dist/homebridge-ui/public/js/types.d.ts +23 -0
- package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/types.js +2 -0
- package/dist/homebridge-ui/public/js/types.js.map +1 -0
- package/dist/homebridge-ui/public/js/types.ts +26 -0
- package/dist/homebridge-ui/server.d.ts +1 -3
- package/dist/homebridge-ui/server.d.ts.map +1 -1
- package/dist/homebridge-ui/server.js +8 -471
- package/dist/homebridge-ui/server.js.map +1 -1
- package/dist/homebridge-ui/settings.js +8 -0
- package/dist/homebridge-ui/settings.js.map +1 -0
- package/dist/homebridge-ui/switchbotClient.js +247 -0
- package/dist/homebridge-ui/switchbotClient.js.map +1 -0
- package/dist/homebridge-ui/utils/config-parser.d.ts +39 -0
- package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/config-parser.js +108 -0
- package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
- package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
- package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/device-migration.js +111 -0
- package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
- package/dist/homebridge-ui/utils/logger.d.ts +7 -0
- package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/logger.js +17 -0
- package/dist/homebridge-ui/utils/logger.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/settings.d.ts +1 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +1 -0
- package/dist/settings.js.map +1 -1
- package/dist/switchbotClient.d.ts +12 -10
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +156 -103
- package/dist/switchbotClient.js.map +1 -1
- package/dist/utils.d.ts +76 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1121 -4
- package/dist/utils.js.map +1 -1
- package/docs/assets/highlight.css +16 -2
- package/docs/assets/main.js +1 -1
- package/docs/index.html +82 -5
- package/docs/variables/default.html +3 -1
- package/eslint.config.js +9 -5
- package/nodemon.json +2 -2
- package/package.json +34 -21
- package/scripts/build-ui.js +37 -0
- package/scripts/free-dev-ports.mjs +105 -0
- package/scripts/generate-matter-maps.js +34 -17
- package/scripts/sync-device-types.mjs +31 -0
- package/src/SwitchBotHAPPlatform.ts +558 -0
- package/src/SwitchBotMatterPlatform.ts +538 -0
- package/src/device-types.js +246 -0
- package/src/device-types.js.map +1 -0
- package/src/device-types.ts +261 -0
- package/src/deviceCommandMapper.js +319 -0
- package/src/deviceCommandMapper.js.map +1 -0
- package/src/deviceCommandMapper.ts +333 -0
- package/src/deviceFactory.ts +125 -45
- package/src/devices/genericDevice.ts +411 -69
- package/src/errors.js +32 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +35 -0
- package/src/homebridge-ui/endpoints/config.ts +110 -0
- package/src/homebridge-ui/endpoints/devices.ts +153 -0
- package/src/homebridge-ui/endpoints/discovery.ts +240 -0
- package/src/homebridge-ui/public/css/styles.css +483 -0
- package/src/homebridge-ui/public/index.html +197 -621
- package/src/homebridge-ui/public/js/advanced-settings.ts +94 -0
- package/src/homebridge-ui/public/js/api.ts +355 -0
- package/src/homebridge-ui/public/js/app.ts +22 -0
- package/src/homebridge-ui/public/js/constants.ts +1 -0
- package/src/homebridge-ui/public/js/credentials.ts +105 -0
- package/src/homebridge-ui/public/js/devices-delete.ts +227 -0
- package/src/homebridge-ui/public/js/devices.ts +106 -0
- package/src/homebridge-ui/public/js/discovery.ts +1335 -0
- package/src/homebridge-ui/public/js/logger.ts +17 -0
- package/src/homebridge-ui/public/js/modal.ts +35 -0
- package/src/homebridge-ui/public/js/modals.ts +765 -0
- package/src/homebridge-ui/public/js/render.ts +1084 -0
- package/src/homebridge-ui/public/js/toast.ts +44 -0
- package/src/homebridge-ui/public/js/types.ts +26 -0
- package/src/homebridge-ui/server.ts +9 -554
- package/src/homebridge-ui/utils/config-parser.ts +125 -0
- package/src/homebridge-ui/utils/device-migration.ts +144 -0
- package/src/homebridge-ui/utils/logger.ts +17 -0
- package/src/index.ts +12 -2
- package/src/settings.js +8 -0
- package/src/settings.js.map +1 -0
- package/src/settings.ts +2 -0
- package/src/switchbotClient.js +247 -0
- package/src/switchbotClient.js.map +1 -0
- package/src/switchbotClient.ts +177 -114
- package/src/utils.ts +1133 -5
- package/test/client/switchbot-client-debounce.spec.ts +35 -0
- package/test/client/switchbot-client-openapi.spec.ts +19 -0
- package/test/client/switchbotClient.spec.ts +64 -0
- package/test/device/device-mapping.spec.ts +23 -0
- package/test/device/deviceBase.spec.ts +26 -0
- package/test/device/deviceFactory-edge.spec.ts +15 -0
- package/test/device/deviceFactory.spec.ts +33 -0
- package/test/device/fan-swing.spec.ts +34 -0
- package/test/device/genericDevice-blepoll.spec.ts +47 -0
- package/test/device/irdevice.spec.ts +9 -0
- package/test/device/lock-users.spec.ts +35 -0
- package/test/device/matter-descriptors.spec.ts +22 -0
- package/test/device/matter-device-state.spec.ts +37 -0
- package/test/e2e/run-e2e.spec.ts +18 -19
- package/test/errors/errors.spec.ts +10 -0
- package/test/helpers/matter-harness.ts +20 -9
- package/test/homebridge-ui/server.spec.ts +9 -0
- package/test/platform/accessory-restore.spec.ts +37 -0
- package/test/platform/matter-childbridge.spec.ts +34 -0
- package/test/platform/matter-integration.spec.ts +33 -0
- package/test/platform/platform-edge.spec.ts +73 -0
- package/test/platform/platform.integration.spec.ts +34 -0
- package/test/utils/utils-extra.spec.ts +10 -0
- package/test/utils/utils.spec.ts +53 -0
- package/todo/TODO.md +80 -0
- package/tsconfig.ui.json +11 -0
- package/.github/npm-version-script-esm.js +0 -97
- package/.github/workflows/beta-release.yml +0 -52
- package/dist/platform.d.ts +0 -35
- package/dist/platform.d.ts.map +0 -1
- package/dist/platform.js +0 -945
- package/dist/platform.js.map +0 -1
- package/src/platform.ts +0 -963
- package/test/accessory-restore.spec.ts +0 -73
- package/test/device-mapping.spec.ts +0 -37
- package/test/deviceFactory.spec.ts +0 -18
- package/test/fan-swing.spec.ts +0 -29
- package/test/lock-users.spec.ts +0 -44
- package/test/matter-childbridge.spec.ts +0 -55
- package/test/matter-descriptors.spec.ts +0 -97
- package/test/matter-device-state.spec.ts +0 -101
- package/test/matter-integration.spec.ts +0 -70
- package/test/platform.integration.spec.ts +0 -55
- package/test/switchbot-client-debounce.spec.ts +0 -131
- package/test/switchbot-client-openapi.spec.ts +0 -56
- package/test/switchbotClient.spec.ts +0 -10
- package/test/utils.spec.ts +0 -20
|
@@ -1,12 +1,189 @@
|
|
|
1
|
+
// Utility: Validate BLE response length before parsing
|
|
1
2
|
/* eslint-disable style/max-statements-per-line, unused-imports/no-unused-vars */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Status Update Strategy for BLE and OpenAPI
|
|
6
|
+
*
|
|
7
|
+
* BLE (Bluetooth Low Energy):
|
|
8
|
+
* - Primary: Subscribes to device notifications for real-time state updates using _subscribeBLENotifications().
|
|
9
|
+
* - Fallback: (Recommended) Optionally, a low-frequency polling timer (e.g., every 5–10 minutes) can call getState() to recover from missed notifications or connection loss.
|
|
10
|
+
* - This ensures state stays in sync even if notifications are unreliable or the device reconnects.
|
|
11
|
+
* - Polling should be infrequent to avoid battery drain and BLE congestion.
|
|
12
|
+
*
|
|
13
|
+
* BLE Polling Options (config & per-device):
|
|
14
|
+
* - blePollingEnabled (boolean): Enable/disable BLE polling fallback (default: true).
|
|
15
|
+
* - blePollIntervalMs (integer): Polling interval in ms (default: 600000, min: 60000).
|
|
16
|
+
* - These can be set globally in config or overridden per device.
|
|
17
|
+
* - Setting a lower interval increases update frequency but may drain battery faster.
|
|
18
|
+
* - Setting a higher interval reduces battery impact but may delay state recovery.
|
|
19
|
+
*
|
|
20
|
+
* OpenAPI (Cloud):
|
|
21
|
+
* - Uses periodic polling to fetch device status at a configurable interval (default: 300 seconds, can be set per device or platform).
|
|
22
|
+
* - Platform supports batched refresh (matterBatchEnabled, matterBatchRefreshRate, etc.) and per-device refreshRate overrides.
|
|
23
|
+
* - Rate limiting:
|
|
24
|
+
* - Default daily limit: 10,000 OpenAPI requests (configurable via options.dailyApiLimit).
|
|
25
|
+
* - Reserve: 1,000 requests for user commands (options.dailyApiReserveForCommands).
|
|
26
|
+
* - When the remaining budget reaches the reserve, background polling/discovery pauses, but user commands and webhooks continue.
|
|
27
|
+
* - Counter resets at local or UTC midnight (options.dailyApiResetLocalMidnight).
|
|
28
|
+
*
|
|
29
|
+
* Best Practices:
|
|
30
|
+
* - BLE: Use notifications for instant updates, add periodic polling as a safety net.
|
|
31
|
+
* - OpenAPI: Tune polling intervals to balance freshness and rate limit budget.
|
|
32
|
+
* - Both: Document and expose polling intervals and rate limit settings in config.
|
|
33
|
+
*
|
|
34
|
+
* See README.md and docs for more details.
|
|
35
|
+
*/
|
|
2
36
|
import type { SwitchBotPluginConfig } from '../settings.js'
|
|
3
37
|
|
|
38
|
+
import { Buffer } from 'node:buffer'
|
|
39
|
+
|
|
4
40
|
import { MATTER_ATTRIBUTE_IDS, MATTER_CLUSTER_IDS } from '../utils.js'
|
|
5
41
|
import { DeviceBase } from './deviceBase.js'
|
|
6
42
|
|
|
43
|
+
function validateBLEResponseLength(buf: Buffer | Uint8Array | any[], expected: number, context = '', log: import('homebridge').Logger): boolean {
|
|
44
|
+
if (!buf || typeof buf.length !== 'number' || buf.length !== expected) {
|
|
45
|
+
log.warn(`[BLE] Invalid response length${context ? ` for ${context}` : ''}: expected ${expected}, got ${buf?.length}`)
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// BLE notification handling: per-command notification futures and unsolicited notification logging
|
|
52
|
+
const BLE_NOTIFICATION_HANDLERS = new Map<string, (payload: Buffer) => void>()
|
|
53
|
+
|
|
54
|
+
// Module-scope regex pattern to avoid recompilation
|
|
55
|
+
const HEX_COLOR_REGEX = /^#?[0-9A-F]{6}$/i
|
|
56
|
+
|
|
7
57
|
export class GenericDevice extends DeviceBase {
|
|
58
|
+
protected log: import('homebridge').Logger
|
|
59
|
+
private _blePollTimer: NodeJS.Timeout | null = null
|
|
60
|
+
private _blePollIntervalMs: number
|
|
61
|
+
private _blePollingEnabled: boolean
|
|
62
|
+
|
|
8
63
|
constructor(opts: any, cfg: SwitchBotPluginConfig) {
|
|
9
64
|
super(opts, cfg)
|
|
65
|
+
// Require logger from opts or cfg
|
|
66
|
+
this.log = opts?.log || cfg?.log
|
|
67
|
+
if (!this.log) {
|
|
68
|
+
throw new Error('Device requires a logger (Homebridge logger) in opts or cfg')
|
|
69
|
+
}
|
|
70
|
+
// If BLE encryptionKey/keyId provided, set on node-switchbot device instance if possible
|
|
71
|
+
if (opts.encryptionKey && this.client && typeof this.client.devices?.get === 'function') {
|
|
72
|
+
try {
|
|
73
|
+
const dev = this.client.devices.get(opts.id)
|
|
74
|
+
if (dev && typeof dev.setKey === 'function') {
|
|
75
|
+
dev.setKey({
|
|
76
|
+
encryptionKey: opts.encryptionKey,
|
|
77
|
+
keyId: opts.keyId || undefined,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// ignore if device not found or setKey not available
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// BLE polling config: allow override via opts.blePollingEnabled/blePollIntervalMs or cfg.blePollingEnabled/blePollIntervalMs
|
|
85
|
+
this._blePollingEnabled = opts?.blePollingEnabled ?? cfg?.blePollingEnabled ?? true
|
|
86
|
+
let pollMs = opts?.blePollIntervalMs ?? cfg?.blePollIntervalMs ?? 10 * 60 * 1000 // default: 10 min
|
|
87
|
+
if (typeof pollMs !== 'number' || Number.isNaN(pollMs) || pollMs < 60000) {
|
|
88
|
+
this.log.warn(`[BLE] Invalid blePollIntervalMs (${pollMs}), using minimum 60000ms`)
|
|
89
|
+
pollMs = 60000
|
|
90
|
+
}
|
|
91
|
+
this._blePollIntervalMs = pollMs
|
|
92
|
+
// Subscribe to BLE notifications if supported (node-switchbot v4+)
|
|
93
|
+
this._subscribeBLENotifications()
|
|
94
|
+
// Start BLE polling fallback if enabled
|
|
95
|
+
if (this._blePollingEnabled) {
|
|
96
|
+
this._startBlePolling()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Start periodic BLE polling as a fallback to notifications.
|
|
102
|
+
*/
|
|
103
|
+
private _startBlePolling() {
|
|
104
|
+
if (this._blePollTimer) {
|
|
105
|
+
clearInterval(this._blePollTimer)
|
|
106
|
+
}
|
|
107
|
+
this._blePollTimer = setInterval(async () => {
|
|
108
|
+
try {
|
|
109
|
+
this.log.debug(`[BLE] Polling getState() for device ${this.opts.id}`)
|
|
110
|
+
await this.getState()
|
|
111
|
+
} catch (e) {
|
|
112
|
+
this.log.debug(`[BLE] Polling getState() failed for device ${this.opts.id}:`, (e as Error)?.message)
|
|
113
|
+
}
|
|
114
|
+
}, this._blePollIntervalMs)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clean up BLE polling timer on destroy.
|
|
119
|
+
*/
|
|
120
|
+
async destroy(): Promise<void> {
|
|
121
|
+
if (this._blePollTimer) {
|
|
122
|
+
clearInterval(this._blePollTimer)
|
|
123
|
+
this._blePollTimer = null
|
|
124
|
+
}
|
|
125
|
+
// Only call super.destroy if DeviceBase.prototype.destroy is a function and not this method itself
|
|
126
|
+
const baseProto = Object.getPrototypeOf(GenericDevice.prototype)
|
|
127
|
+
if (typeof baseProto.destroy === 'function' && baseProto.destroy !== GenericDevice.prototype.destroy) {
|
|
128
|
+
await super.destroy()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Subscribe to BLE notifications for this device (if supported by node-switchbot)
|
|
134
|
+
* Logs unsolicited notifications and enables per-command notification futures.
|
|
135
|
+
*/
|
|
136
|
+
private async _subscribeBLENotifications() {
|
|
137
|
+
if (!this.client || typeof this.client.devices?.get !== 'function') { return }
|
|
138
|
+
const dev = this.client.devices.get(this.opts.id)
|
|
139
|
+
if (!dev || typeof dev.mac !== 'string' || !dev.mac) { return }
|
|
140
|
+
// Only subscribe once per device
|
|
141
|
+
if (BLE_NOTIFICATION_HANDLERS.has(dev.mac)) { return }
|
|
142
|
+
if (typeof dev.subscribeNotifications === 'function') {
|
|
143
|
+
const handler = (payload: Buffer) => {
|
|
144
|
+
// If a per-command notification future is waiting, let node-switchbot handle it
|
|
145
|
+
// Otherwise, log unsolicited notification
|
|
146
|
+
if (payload && payload.length > 0) {
|
|
147
|
+
// Unsolicited notification logging
|
|
148
|
+
// (node-switchbot will resolve per-command futures internally)
|
|
149
|
+
this.log.debug(`[BLE] Unsolicited notification from ${dev.mac}: ${payload.toString('hex')}`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
// Subscribe and remember handler for possible cleanup
|
|
154
|
+
await dev.subscribeNotifications(handler)
|
|
155
|
+
BLE_NOTIFICATION_HANDLERS.set(dev.mac, handler)
|
|
156
|
+
} catch (e) {
|
|
157
|
+
// ignore if subscription fails
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Await a BLE notification for this device (for advanced use in subclasses)
|
|
164
|
+
* Returns the notification payload or throws on timeout.
|
|
165
|
+
*/
|
|
166
|
+
protected async _awaitBLENotification(timeoutMs = 5000): Promise<Buffer> {
|
|
167
|
+
if (!this.client || typeof this.client.devices?.get !== 'function') { throw new Error('No BLE client/device') }
|
|
168
|
+
const dev = this.client.devices.get(this.opts.id)
|
|
169
|
+
if (!dev || typeof dev.mac !== 'string' || !dev.mac) { throw new Error('No BLE MAC for device') }
|
|
170
|
+
if (typeof dev.bleConnection?.sendCommand !== 'function') { throw new TypeError('BLE connection does not support sendCommand') }
|
|
171
|
+
// This is a low-level utility; in most cases, node-switchbot handles notification futures for commands
|
|
172
|
+
// Here, we expose a direct await for advanced use
|
|
173
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
174
|
+
let timer: NodeJS.Timeout | undefined
|
|
175
|
+
const handler = (payload: Buffer) => {
|
|
176
|
+
clearTimeout(timer)
|
|
177
|
+
dev.bleConnection?.unsubscribeNotifications(dev.mac, handler)
|
|
178
|
+
resolve(payload)
|
|
179
|
+
}
|
|
180
|
+
dev.bleConnection?.subscribeNotifications(dev.mac, handler).then(() => {
|
|
181
|
+
timer = setTimeout(() => {
|
|
182
|
+
dev.bleConnection?.unsubscribeNotifications(dev.mac, handler)
|
|
183
|
+
reject(new Error('BLE notification timeout'))
|
|
184
|
+
}, timeoutMs)
|
|
185
|
+
}).catch(reject)
|
|
186
|
+
})
|
|
10
187
|
}
|
|
11
188
|
|
|
12
189
|
async getState(): Promise<any> {
|
|
@@ -14,6 +191,13 @@ export class GenericDevice extends DeviceBase {
|
|
|
14
191
|
if (this.client && typeof this.client.getDevice === 'function') {
|
|
15
192
|
try {
|
|
16
193
|
const raw = await this.client.getDevice(this.opts.id)
|
|
194
|
+
// If this is a BLE buffer/array, validate length (common BLE status: 12 bytes, but may vary by device)
|
|
195
|
+
if (raw && (raw instanceof Buffer || Array.isArray(raw) || raw instanceof Uint8Array)) {
|
|
196
|
+
// Default to 12, override per device if needed
|
|
197
|
+
if (!validateBLEResponseLength(raw, 12, this.opts.type, this.log)) {
|
|
198
|
+
return { id: this.opts.id, type: this.opts.type, error: 'invalid_ble_response_length', raw }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
17
201
|
// Normalize common response shapes
|
|
18
202
|
try {
|
|
19
203
|
const device = raw?.body ?? raw
|
|
@@ -127,62 +311,163 @@ export class GenericDevice extends DeviceBase {
|
|
|
127
311
|
// platform can construct a Matter accessory representation when
|
|
128
312
|
// Homebridge Matter APIs are available. Device subclasses may override
|
|
129
313
|
// this to provide Matter-specific clusters/attributes if desired.
|
|
130
|
-
createMatterAccessory(api: any): any {
|
|
131
|
-
|
|
132
|
-
|
|
314
|
+
async createMatterAccessory(api: any): Promise<any> {
|
|
315
|
+
// Dynamically detect features from getState()
|
|
316
|
+
const state = await this.getState()
|
|
133
317
|
const clusters: any[] = []
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return { attr: 'colorTemperature', get: async () => {
|
|
146
|
-
const s = await this.getState(); if (typeof s.colorTemperature === 'number') { return s.colorTemperature } if (typeof s.kelvin === 'number') { return Math.round(1000000 / s.kelvin) } return 400
|
|
147
|
-
}, set: async (v: any) => this.setState({ colorTemperature: Number(v) }) }
|
|
148
|
-
case 'RotationSpeed':
|
|
149
|
-
return { attr: 'rotationSpeed', get: async () => { const s = await this.getState(); return typeof s.speed === 'number' ? s.speed : 0 }, set: async (v: any) => this.setState({ speed: Number(v) }) }
|
|
150
|
-
case 'TargetPosition':
|
|
151
|
-
case 'CurrentPosition':
|
|
152
|
-
return { attr: 'position', get: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, set: async (v: any) => this.setState({ position: Number(v) }) }
|
|
153
|
-
case 'LockCurrentState':
|
|
154
|
-
case 'LockTargetState':
|
|
155
|
-
return { attr: 'locked', get: async () => { const s = await this.getState(); return !!(s && s.locked) }, set: async (v: any) => this.setState({ locked: !!v }) }
|
|
156
|
-
case 'MotionDetected':
|
|
157
|
-
return { attr: 'motionDetected', get: async () => { const s = await this.getState(); return !!(s && s.motion === true) }, set: undefined }
|
|
158
|
-
case 'ContactSensorState':
|
|
159
|
-
return { attr: 'contact', get: async () => { const s = await this.getState(); return s && s.open ? 1 : 0 }, set: undefined }
|
|
160
|
-
default:
|
|
161
|
-
return null
|
|
162
|
-
}
|
|
318
|
+
|
|
319
|
+
// On/Off (Switch/Plug/Generic)
|
|
320
|
+
if ('on' in state || 'power' in state || 'state' in state) {
|
|
321
|
+
clusters.push({
|
|
322
|
+
type: 'OnOff',
|
|
323
|
+
clusterId: MATTER_CLUSTER_IDS.OnOff,
|
|
324
|
+
attributes: {
|
|
325
|
+
onOff: { read: async () => !!(await this.getState()).on || (await this.getState()).power === 'on' || (await this.getState()).state === 'on', write: async (v: any) => this.setState({ on: !!v }) },
|
|
326
|
+
[MATTER_ATTRIBUTE_IDS.OnOff.OnOff]: { read: async () => !!(await this.getState()).on || (await this.getState()).power === 'on' || (await this.getState()).state === 'on', write: async (v: any) => this.setState({ on: !!v }) },
|
|
327
|
+
},
|
|
328
|
+
})
|
|
163
329
|
}
|
|
164
330
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
})()
|
|
331
|
+
// Brightness (Light)
|
|
332
|
+
if ('brightness' in state) {
|
|
333
|
+
clusters.push({
|
|
334
|
+
type: 'LevelControl',
|
|
335
|
+
clusterId: MATTER_CLUSTER_IDS.LevelControl,
|
|
336
|
+
attributes: {
|
|
337
|
+
currentLevel: { read: async () => (await this.getState()).brightness ?? 100, write: async (v: any) => this.setState({ brightness: Number(v) }) },
|
|
338
|
+
[MATTER_ATTRIBUTE_IDS.LevelControl.CurrentLevel]: { read: async () => (await this.getState()).brightness ?? 100, write: async (v: any) => this.setState({ brightness: Number(v) }) },
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
}
|
|
178
342
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
343
|
+
// Color (Light)
|
|
344
|
+
if ('hue' in state && 'saturation' in state) {
|
|
345
|
+
clusters.push({
|
|
346
|
+
type: 'ColorControl',
|
|
347
|
+
clusterId: MATTER_CLUSTER_IDS.ColorControl,
|
|
348
|
+
attributes: {
|
|
349
|
+
colorMode: { read: async () => 0 },
|
|
350
|
+
colorHue: { read: async () => (await this.getState()).hue ?? 0, write: async (v: any) => this.setState({ hue: Number(v) }) },
|
|
351
|
+
[MATTER_ATTRIBUTE_IDS.ColorControl.CurrentHue]: { read: async () => (await this.getState()).hue ?? 0, write: async (v: any) => this.setState({ hue: Number(v) }) },
|
|
352
|
+
colorSaturation: { read: async () => (await this.getState()).saturation ?? 0, write: async (v: any) => this.setState({ saturation: Number(v) }) },
|
|
353
|
+
[MATTER_ATTRIBUTE_IDS.ColorControl.CurrentSaturation]: { read: async () => (await this.getState()).saturation ?? 0, write: async (v: any) => this.setState({ saturation: Number(v) }) },
|
|
354
|
+
...(typeof state.colorTemperature === 'number' || typeof state.kelvin === 'number'
|
|
355
|
+
? {
|
|
356
|
+
colorTemperature: { read: async () => typeof (await this.getState()).colorTemperature === 'number' ? (await this.getState()).colorTemperature : Math.round(1000000 / ((await this.getState()).kelvin ?? 2500)), write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
|
|
357
|
+
[MATTER_ATTRIBUTE_IDS.ColorControl.ColorTemperatureMireds]: { read: async () => typeof (await this.getState()).colorTemperature === 'number' ? (await this.getState()).colorTemperature : Math.round(1000000 / ((await this.getState()).kelvin ?? 2500)), write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
|
|
358
|
+
}
|
|
359
|
+
: {}),
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Temperature sensor
|
|
365
|
+
if ('temperature' in state) {
|
|
366
|
+
clusters.push({
|
|
367
|
+
type: 'TemperatureMeasurement',
|
|
368
|
+
// No clusterId, not present in MATTER_CLUSTER_IDS
|
|
369
|
+
attributes: {
|
|
370
|
+
measuredValue: { read: async () => (await this.getState()).temperature ?? 0 },
|
|
371
|
+
},
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Humidity sensor
|
|
376
|
+
if ('humidity' in state) {
|
|
377
|
+
clusters.push({
|
|
378
|
+
type: 'RelativeHumidityMeasurement',
|
|
379
|
+
clusterId: MATTER_CLUSTER_IDS.RelativeHumidityMeasurement,
|
|
380
|
+
attributes: {
|
|
381
|
+
measuredValue: { read: async () => (await this.getState()).humidity ?? 0 },
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// CO2 sensor
|
|
387
|
+
if ('CO2' in state) {
|
|
388
|
+
clusters.push({
|
|
389
|
+
type: 'AirQuality',
|
|
390
|
+
attributes: {
|
|
391
|
+
CO2: { read: async () => (await this.getState()).CO2 ?? 0 },
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Lock
|
|
397
|
+
if ('lockState' in state || 'locked' in state) {
|
|
398
|
+
clusters.push({
|
|
399
|
+
type: 'DoorLock',
|
|
400
|
+
clusterId: MATTER_CLUSTER_IDS.DoorLock,
|
|
401
|
+
attributes: {
|
|
402
|
+
lockState: { read: async () => (await this.getState()).lockState ?? (await this.getState()).locked ? 1 : 0, write: async (v: any) => this.setState({ locked: !!v }) },
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Motion sensor
|
|
408
|
+
if ('moveDetected' in state || 'motion' in state) {
|
|
409
|
+
clusters.push({
|
|
410
|
+
type: 'OccupancySensing',
|
|
411
|
+
// No clusterId, not present in MATTER_CLUSTER_IDS
|
|
412
|
+
attributes: {
|
|
413
|
+
occupancy: { read: async () => (await this.getState()).moveDetected === true || (await this.getState()).motion === true ? 1 : 0 },
|
|
414
|
+
},
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Contact sensor
|
|
419
|
+
if ('openState' in state || 'contact' in state || 'open' in state) {
|
|
420
|
+
clusters.push({
|
|
421
|
+
type: 'BooleanState',
|
|
422
|
+
// No clusterId, not present in MATTER_CLUSTER_IDS
|
|
423
|
+
attributes: {
|
|
424
|
+
stateValue: { read: async () => (await this.getState()).openState === 'open' || (await this.getState()).open === true ? 1 : 0 },
|
|
425
|
+
},
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Leak sensor
|
|
430
|
+
if ('leak' in state || 'status' in state) {
|
|
431
|
+
clusters.push({
|
|
432
|
+
type: 'LeakSensor',
|
|
433
|
+
attributes: {
|
|
434
|
+
leakDetected: { read: async () => (await this.getState()).leak === true || (await this.getState()).status === 1 ? 1 : 0 },
|
|
435
|
+
},
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Energy monitoring (Plug)
|
|
440
|
+
if ('voltage' in state || 'power' in state || 'electricCurrent' in state) {
|
|
441
|
+
clusters.push({
|
|
442
|
+
type: 'ElectricalMeasurement',
|
|
443
|
+
// No clusterId, not present in MATTER_CLUSTER_IDS
|
|
444
|
+
attributes: {
|
|
445
|
+
voltage: { read: async () => (await this.getState()).voltage ?? 0 },
|
|
446
|
+
power: { read: async () => (await this.getState()).power ?? 0 },
|
|
447
|
+
electricCurrent: { read: async () => (await this.getState()).electricCurrent ?? 0 },
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Fan
|
|
453
|
+
if ('speed' in state || 'fanSpeed' in state) {
|
|
454
|
+
clusters.push({
|
|
455
|
+
type: 'FanControl',
|
|
456
|
+
clusterId: MATTER_CLUSTER_IDS.FanControl,
|
|
457
|
+
attributes: {
|
|
458
|
+
speedCurrent: { read: async () => (await this.getState()).speed ?? (await this.getState()).fanSpeed ?? 0, write: async (v: any) => this.setState({ speed: Number(v) }) },
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
}
|
|
184
462
|
|
|
185
|
-
|
|
463
|
+
// Vacuum
|
|
464
|
+
if ('workingStatus' in state) {
|
|
465
|
+
clusters.push({
|
|
466
|
+
type: 'RobotVacuumCleaner',
|
|
467
|
+
attributes: {
|
|
468
|
+
workingStatus: { read: async () => (await this.getState()).workingStatus ?? 'StandBy' },
|
|
469
|
+
},
|
|
470
|
+
})
|
|
186
471
|
}
|
|
187
472
|
|
|
188
473
|
return {
|
|
@@ -225,25 +510,80 @@ export class CurtainDevice extends GenericDevice {
|
|
|
225
510
|
}
|
|
226
511
|
}
|
|
227
512
|
|
|
228
|
-
// Matter-specific descriptor for Curtain (
|
|
229
|
-
createMatterAccessory(api: any): any {
|
|
513
|
+
// Matter-specific descriptor for Curtain (WindowCovering cluster) with new attributes
|
|
514
|
+
async createMatterAccessory(api: any): Promise<any> {
|
|
515
|
+
// Get current state for dynamic attributes
|
|
516
|
+
const state = await this.getState()
|
|
517
|
+
// Compose attributes for Matter WindowCovering cluster
|
|
518
|
+
const attributes: Record<string, any> = {
|
|
519
|
+
currentPositionLiftPercent100ths: {
|
|
520
|
+
read: async () => {
|
|
521
|
+
const s = await this.getState()
|
|
522
|
+
return typeof s.position === 'number' ? Math.round(s.position * 100) : 0
|
|
523
|
+
},
|
|
524
|
+
write: undefined,
|
|
525
|
+
},
|
|
526
|
+
targetPositionLiftPercent100ths: {
|
|
527
|
+
read: async () => {
|
|
528
|
+
const s = await this.getState()
|
|
529
|
+
return typeof s.position === 'number' ? Math.round(s.position * 100) : 0
|
|
530
|
+
},
|
|
531
|
+
write: async (v: any) => this.setState({ position: Math.round(Number(v) / 100) }),
|
|
532
|
+
},
|
|
533
|
+
operationalStatus: {
|
|
534
|
+
read: async () => state.operationalStatus ?? { global: 0, lift: 0, tilt: 0 },
|
|
535
|
+
write: undefined,
|
|
536
|
+
},
|
|
537
|
+
endProductType: {
|
|
538
|
+
read: async () => state.endProductType ?? 0,
|
|
539
|
+
write: undefined,
|
|
540
|
+
},
|
|
541
|
+
configStatus: {
|
|
542
|
+
read: async () => state.configStatus ?? {
|
|
543
|
+
operational: true,
|
|
544
|
+
onlineReserved: true,
|
|
545
|
+
liftMovementReversed: false,
|
|
546
|
+
liftPositionAware: true,
|
|
547
|
+
tiltPositionAware: false,
|
|
548
|
+
liftEncoderControlled: true,
|
|
549
|
+
tiltEncoderControlled: false,
|
|
550
|
+
},
|
|
551
|
+
write: undefined,
|
|
552
|
+
},
|
|
553
|
+
}
|
|
554
|
+
// If tilt is supported, add tilt attributes
|
|
555
|
+
if (typeof state.tilt === 'number') {
|
|
556
|
+
attributes.currentPositionTiltPercent100ths = {
|
|
557
|
+
read: async () => {
|
|
558
|
+
const s = await this.getState()
|
|
559
|
+
return typeof s.tilt === 'number' ? Math.round(s.tilt * 100) : 0
|
|
560
|
+
},
|
|
561
|
+
write: undefined,
|
|
562
|
+
}
|
|
563
|
+
attributes.targetPositionTiltPercent100ths = {
|
|
564
|
+
read: async () => {
|
|
565
|
+
const s = await this.getState()
|
|
566
|
+
return typeof s.tilt === 'number' ? Math.round(s.tilt * 100) : 0
|
|
567
|
+
},
|
|
568
|
+
write: async (v: any) => this.setState({ tilt: Math.round(Number(v) / 100) }),
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const windowCoveringCluster = {
|
|
572
|
+
type: 'WindowCovering',
|
|
573
|
+
clusterId: MATTER_CLUSTER_IDS.WindowCovering,
|
|
574
|
+
attributes,
|
|
575
|
+
}
|
|
576
|
+
// Provide both array and named property for clusters for compatibility with test expectations
|
|
577
|
+
const clustersArr: any[] = [windowCoveringCluster]
|
|
578
|
+
const clusters: any = [...clustersArr]
|
|
579
|
+
// Always set clusters.windowCovering to the WindowCovering cluster by clusterId
|
|
580
|
+
const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
|
|
581
|
+
clusters.windowCovering = foundWC || null
|
|
230
582
|
return {
|
|
231
583
|
id: this.opts.id,
|
|
232
584
|
name: this.opts.name ?? this.opts.type,
|
|
233
585
|
protocol: 'matter',
|
|
234
|
-
clusters
|
|
235
|
-
{
|
|
236
|
-
// Shade cluster
|
|
237
|
-
type: 'Shade',
|
|
238
|
-
clusterId: MATTER_CLUSTER_IDS.WindowCovering,
|
|
239
|
-
attributes: {
|
|
240
|
-
currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
|
|
241
|
-
[MATTER_ATTRIBUTE_IDS.WindowCovering.CurrentPosition]: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
|
|
242
|
-
targetPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
|
|
243
|
-
[MATTER_ATTRIBUTE_IDS.WindowCovering.TargetPosition]: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
],
|
|
586
|
+
clusters,
|
|
247
587
|
}
|
|
248
588
|
}
|
|
249
589
|
}
|
|
@@ -384,7 +724,7 @@ export class LightDevice extends GenericDevice {
|
|
|
384
724
|
if (s && typeof s.hue === 'number') { return s.hue }
|
|
385
725
|
// try HSV from color hex
|
|
386
726
|
const hex = s?.color || s?.colorHex || s?.colour
|
|
387
|
-
if (typeof hex === 'string' &&
|
|
727
|
+
if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
|
|
388
728
|
const h = (() => {
|
|
389
729
|
const hsl = (h: number, s: number, l: number) => ({ h, s, l })
|
|
390
730
|
// convert hex -> rgb -> hsv
|
|
@@ -420,7 +760,7 @@ export class LightDevice extends GenericDevice {
|
|
|
420
760
|
if (s && typeof s.saturation === 'number') { return s.saturation }
|
|
421
761
|
// if color hex is available, derive saturation from rgb
|
|
422
762
|
const hex = s?.color || s?.colorHex || s?.colour
|
|
423
|
-
if (typeof hex === 'string' &&
|
|
763
|
+
if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
|
|
424
764
|
const cleaned = hex.replace('#', '')
|
|
425
765
|
const r = Number.parseInt(cleaned.substr(0, 2), 16) / 255
|
|
426
766
|
const g = Number.parseInt(cleaned.substr(2, 2), 16) / 255
|
|
@@ -537,6 +877,8 @@ export class LightDevice extends GenericDevice {
|
|
|
537
877
|
type: 'ColorControl',
|
|
538
878
|
clusterId: MATTER_CLUSTER_IDS.ColorControl,
|
|
539
879
|
attributes: {
|
|
880
|
+
// Required colorMode attribute for Matter conformance (0 = currentHueAndSaturation)
|
|
881
|
+
colorMode: { read: async () => 0 },
|
|
540
882
|
colorHue: { read: async () => { const s = await this.getState(); return typeof s.hue === 'number' ? s.hue : 0 }, write: async (v: any) => this.setState({ hue: Number(v) }) },
|
|
541
883
|
[MATTER_ATTRIBUTE_IDS.ColorControl.CurrentHue]: { read: async () => { const s = await this.getState(); return typeof s.hue === 'number' ? s.hue : 0 }, write: async (v: any) => this.setState({ hue: Number(v) }) },
|
|
542
884
|
colorSaturation: { read: async () => { const s = await this.getState(); return typeof s.saturation === 'number' ? s.saturation : 0 }, write: async (v: any) => this.setState({ saturation: Number(v) }) },
|
|
@@ -844,7 +1186,7 @@ export class HumidifierDevice extends GenericDevice {
|
|
|
844
1186
|
|
|
845
1187
|
// Provide Matter descriptor for humidifier (humidity and on/off)
|
|
846
1188
|
export class HumidifierMatterDevice extends HumidifierDevice {
|
|
847
|
-
createMatterAccessory(api: any) {
|
|
1189
|
+
async createMatterAccessory(api: any): Promise<any> {
|
|
848
1190
|
return {
|
|
849
1191
|
id: this.opts.id,
|
|
850
1192
|
name: this.opts.name ?? this.opts.type,
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Error classes for node-switchbot v4 compatibility
|
|
2
|
+
// Always use local fallback classes for compatibility; upstream error classes are not guaranteed to exist
|
|
3
|
+
const SwitchbotOperationError = class extends Error {
|
|
4
|
+
code;
|
|
5
|
+
cause;
|
|
6
|
+
constructor(message, code, cause) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'SwitchbotOperationError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const SwitchbotAuthenticationError = class extends Error {
|
|
14
|
+
code;
|
|
15
|
+
cause;
|
|
16
|
+
constructor(message, code, cause) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'SwitchbotAuthenticationError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const CharacteristicMissingError = class extends Error {
|
|
24
|
+
characteristic;
|
|
25
|
+
constructor(message, characteristic) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'CharacteristicMissingError';
|
|
28
|
+
this.characteristic = characteristic;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
export { CharacteristicMissingError, SwitchbotAuthenticationError, SwitchbotOperationError };
|
|
32
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,0GAA0G;AAE1G,MAAM,uBAAuB,GAAG,KAAM,SAAQ,KAAK;IACjD,IAAI,CAAS;IACb,KAAK,CAAQ;IACb,YAAY,OAAe,EAAE,IAAa,EAAE,KAAa;QACvD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAA;QACrC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF,CAAA;AAED,MAAM,4BAA4B,GAAG,KAAM,SAAQ,KAAK;IACtD,IAAI,CAAS;IACb,KAAK,CAAQ;IACb,YAAY,OAAe,EAAE,IAAa,EAAE,KAAa;QACvD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAA;QAC1C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF,CAAA;AAED,MAAM,0BAA0B,GAAG,KAAM,SAAQ,KAAK;IACpD,cAAc,CAAQ;IACtB,YAAY,OAAe,EAAE,cAAsB;QACjD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAA;QACxC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAA;IACtC,CAAC;CACF,CAAA;AAED,OAAO,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,CAAA"}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Error classes for node-switchbot v4 compatibility
|
|
2
|
+
// Always use local fallback classes for compatibility; upstream error classes are not guaranteed to exist
|
|
3
|
+
|
|
4
|
+
const SwitchbotOperationError = class extends Error {
|
|
5
|
+
code?: string
|
|
6
|
+
cause?: Error
|
|
7
|
+
constructor(message: string, code?: string, cause?: Error) {
|
|
8
|
+
super(message)
|
|
9
|
+
this.name = 'SwitchbotOperationError'
|
|
10
|
+
this.code = code
|
|
11
|
+
this.cause = cause
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SwitchbotAuthenticationError = class extends Error {
|
|
16
|
+
code?: string
|
|
17
|
+
cause?: Error
|
|
18
|
+
constructor(message: string, code?: string, cause?: Error) {
|
|
19
|
+
super(message)
|
|
20
|
+
this.name = 'SwitchbotAuthenticationError'
|
|
21
|
+
this.code = code
|
|
22
|
+
this.cause = cause
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const CharacteristicMissingError = class extends Error {
|
|
27
|
+
characteristic: string
|
|
28
|
+
constructor(message: string, characteristic: string) {
|
|
29
|
+
super(message)
|
|
30
|
+
this.name = 'CharacteristicMissingError'
|
|
31
|
+
this.characteristic = characteristic
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { CharacteristicMissingError, SwitchbotAuthenticationError, SwitchbotOperationError }
|