@switchbot/homebridge-switchbot 5.0.0-beta.111 → 5.0.0-beta.113
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/MIGRATION.md +10 -0
- package/README.md +43 -0
- package/TODO.md +263 -0
- package/config.schema.json +27 -10
- package/dist/device-types.d.ts.map +1 -1
- package/dist/device-types.js +93 -63
- package/dist/device-types.js.map +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +55 -33
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +12 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +121 -1
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.js +80 -1
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.js +73 -41
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +209 -16
- package/dist/homebridge-ui/public/index.html +87 -56
- package/dist/homebridge-ui/public/js/api.d.ts +24 -1
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +33 -2
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +52 -3
- package/dist/homebridge-ui/public/js/app.js +1604 -143
- package/dist/homebridge-ui/public/js/app.js.map +3 -3
- package/dist/homebridge-ui/public/js/app.ts +2 -1
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +2 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +13 -1
- package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +1244 -124
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +1344 -142
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +42 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +50 -1
- package/dist/homebridge-ui/public/js/render.d.ts +3 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +389 -36
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +421 -38
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +3 -4
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +51 -5
- package/dist/switchbotClient.js.map +1 -1
- package/docs/assets/highlight.css +9 -2
- package/docs/index.html +28 -2
- package/docs/variables/default.html +1 -1
- package/package.json +9 -8
- package/src/device-types.ts +95 -64
- package/src/deviceFactory.ts +55 -33
- package/src/devices/genericDevice.ts +113 -1
- package/src/errors.ts +43 -0
- package/src/homebridge-ui/endpoints/devices.ts +82 -1
- package/src/homebridge-ui/endpoints/discovery.ts +78 -44
- package/src/homebridge-ui/public/css/styles.css +209 -16
- package/src/homebridge-ui/public/index.html +87 -56
- package/src/homebridge-ui/public/js/api.ts +52 -3
- package/src/homebridge-ui/public/js/app.ts +2 -1
- package/src/homebridge-ui/public/js/devices.ts +13 -1
- package/src/homebridge-ui/public/js/discovery.ts +1344 -142
- package/src/homebridge-ui/public/js/modals.ts +50 -1
- package/src/homebridge-ui/public/js/render.ts +421 -38
- package/src/platform.ts +3 -4
- package/src/switchbotClient.ts +44 -9
- package/test/e2e/run-e2e.spec.ts +12 -10
- package/test/helpers/matter-harness.ts +2 -2
- package/test/matter-integration.spec.ts +21 -0
- package/todo/COMPLETED.md +313 -0
- package/todo/TODO.md +80 -66
- package/switchbot-homebridge-switchbot-5.0.0.tgz +0 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@switchbot/homebridge-switchbot",
|
|
3
3
|
"displayName": "SwitchBot",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "5.0.0-beta.
|
|
5
|
+
"version": "5.0.0-beta.113",
|
|
6
6
|
"description": "The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.",
|
|
7
7
|
"author": "SwitchBot <support@wondertechlabs.com> (https://github.com/SwitchBot)",
|
|
8
8
|
"contributors": [
|
|
@@ -67,12 +67,13 @@
|
|
|
67
67
|
"check": "npm install && npm outdated",
|
|
68
68
|
"ports:free": "node scripts/free-dev-ports.mjs",
|
|
69
69
|
"typecheck": "tsc --noEmit",
|
|
70
|
-
"lint": "eslint src/**/*.ts src/homebridge-ui/**/*.html && npm run typecheck",
|
|
71
|
-
"lint:
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"watch
|
|
70
|
+
"lint": "eslint src/**/*.ts test/**/*.ts src/homebridge-ui/**/*.html && npm run typecheck",
|
|
71
|
+
"lint:src": "eslint src/**/*.ts",
|
|
72
|
+
"lint:test": "eslint test/**/*.ts",
|
|
73
|
+
"lint:ui": "eslint src/homebridge-ui/**/*.html",
|
|
74
|
+
"lint:fix": "eslint src/**/*.ts test/**/*.ts src/homebridge-ui/**/*.html --fix && npm run typecheck",
|
|
75
|
+
"watch": "npm run ports:free && npm run build && npm run plugin-ui && npm link && nodemon",
|
|
76
|
+
"watch:ui": "npm run ports:free && bash -c 'npx homebridge -I -U ~/.homebridge-dev & HB_PID=$!; sleep 6; UIX_STORAGE_PATH=~/.homebridge-dev node ./node_modules/homebridge-config-ui-x/dist/bin/standalone.js & npm run watch:files'",
|
|
76
77
|
"watch:files": "nodemon",
|
|
77
78
|
"plugin-ui": "mkdir -p ./dist/homebridge-ui/public && if [ -d ./src/homebridge-ui/public ]; then rsync -a ./src/homebridge-ui/public/ ./dist/homebridge-ui/public/; fi && node scripts/build-ui.js",
|
|
78
79
|
"build": "npm run sync:device-types && npm run clean && tsc && npm run plugin-ui",
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
"dependencies": {
|
|
92
93
|
"@homebridge/plugin-ui-utils": "^2.2.1",
|
|
93
94
|
"class-validator": "^0.15.1",
|
|
94
|
-
"node-switchbot": "^4.0.0-beta.
|
|
95
|
+
"node-switchbot": "^4.0.0-beta.14"
|
|
95
96
|
},
|
|
96
97
|
"devDependencies": {
|
|
97
98
|
"@antfu/eslint-config": "^7.7.0",
|
package/src/device-types.ts
CHANGED
|
@@ -95,83 +95,114 @@ export const DEVICE_TYPES = {
|
|
|
95
95
|
} as const
|
|
96
96
|
|
|
97
97
|
export const DEVICE_TYPE_NORMALIZATION_MAP: Record<string, string> = {
|
|
98
|
-
//
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
98
|
+
// --- node-switchbot beta new types normalization additions ---
|
|
99
|
+
'hub mini': 'Hub Mini',
|
|
100
|
+
'hub 3': 'Hub 3',
|
|
101
|
+
'keypad': 'Keypad', // node-switchbot beta
|
|
102
|
+
'plug mini': 'Plug Mini (US)', // fallback to US if region not specified
|
|
103
|
+
'art frame': 'AI Art Frame',
|
|
104
|
+
'rgbicww': 'RGBICWW Strip Light',
|
|
105
|
+
'lock vision': 'Lock Vision Pro', // alias for new lock vision
|
|
106
|
+
'lock pro': 'Smart Lock Pro',
|
|
107
|
+
'lock lite': 'Lock Lite', // node-switchbot beta
|
|
108
|
+
'circulator fan': 'Circulator Fan', // node-switchbot beta
|
|
109
|
+
'smart thermostat radiator': 'Smart Radiator Thermostat',
|
|
110
|
+
'climate panel': 'Home Climate Panel',
|
|
111
|
+
'evaporative humidifier': 'Humidifier',
|
|
112
|
+
// --- end node-switchbot beta additions ---
|
|
113
|
+
// Only keep the last occurrence for each key, all values canonical
|
|
114
|
+
'air purifier pm2.5': 'Air Purifier PM2.5',
|
|
115
|
+
'pan/tilt cam plus 3k': 'Pan/Tilt Cam Plus 3K',
|
|
116
|
+
'remote with screen': 'Remote with Screen',
|
|
117
|
+
'ai hub': 'AI Hub',
|
|
118
|
+
'water detector': 'Water Detector',
|
|
119
|
+
'video doorbell': 'Video Doorbell',
|
|
120
|
+
'smart radiator thermostat': 'Smart Radiator Thermostat',
|
|
121
|
+
'woiosensor': 'WoIOSensor',
|
|
122
|
+
'garage door opener': 'Garage Door Opener',
|
|
123
|
+
'air purifier table pm2.5': 'Air Purifier Table PM2.5',
|
|
124
|
+
'air purifier voc': 'Air Purifier VOC',
|
|
125
|
+
'air purifier table voc': 'Air Purifier Table VOC',
|
|
126
|
+
'plug mini (eu)': 'Plug Mini (EU)',
|
|
127
|
+
// Only last occurrence for each key is kept above. Removed duplicates here.
|
|
103
128
|
|
|
104
129
|
// Climate control conversions
|
|
105
|
-
'humidifier2': '
|
|
106
|
-
'circulator fan': '
|
|
107
|
-
'
|
|
108
|
-
'standing circulator fan': 'fan',
|
|
130
|
+
'humidifier2': 'Humidifier2',
|
|
131
|
+
'battery circulator fan': 'Battery Circulator Fan',
|
|
132
|
+
'standing circulator fan': 'Standing Circulator Fan',
|
|
109
133
|
|
|
110
134
|
// Lock/keypad conversions
|
|
111
|
-
'smart lock': '
|
|
112
|
-
'smart lock pro': '
|
|
113
|
-
'smart lock ultra': '
|
|
114
|
-
'
|
|
115
|
-
'keypad': '
|
|
116
|
-
'keypad
|
|
117
|
-
'keypad vision': 'lock',
|
|
118
|
-
'keypad vision pro': 'lock',
|
|
135
|
+
'smart lock': 'Smart Lock',
|
|
136
|
+
'smart lock pro': 'Smart Lock Pro',
|
|
137
|
+
'smart lock ultra': 'Smart Lock Ultra',
|
|
138
|
+
'keypad touch': 'Keypad Touch',
|
|
139
|
+
'keypad vision': 'Keypad Vision',
|
|
140
|
+
'keypad vision pro': 'Keypad Vision Pro',
|
|
119
141
|
|
|
120
142
|
// Light conversions
|
|
121
|
-
'color bulb': '
|
|
122
|
-
'ceiling light': '
|
|
123
|
-
'ceiling light pro': '
|
|
124
|
-
'candle warmer lamp': '
|
|
125
|
-
'floor lamp': '
|
|
126
|
-
'rgbic neon rope light': '
|
|
127
|
-
'rgbic neon wire rope light': '
|
|
128
|
-
'rgbicww floor lamp': '
|
|
129
|
-
'rgbicww strip light': '
|
|
130
|
-
'strip light': '
|
|
131
|
-
'strip light 3': '
|
|
143
|
+
'color bulb': 'Color Bulb',
|
|
144
|
+
'ceiling light': 'Ceiling Light',
|
|
145
|
+
'ceiling light pro': 'Ceiling Light Pro',
|
|
146
|
+
'candle warmer lamp': 'Candle Warmer Lamp',
|
|
147
|
+
'floor lamp': 'Floor Lamp',
|
|
148
|
+
'rgbic neon rope light': 'RGBIC Neon Rope Light',
|
|
149
|
+
'rgbic neon wire rope light': 'RGBIC Neon Wire Rope Light',
|
|
150
|
+
'rgbicww floor lamp': 'RGBICWW Floor Lamp',
|
|
151
|
+
'rgbicww strip light': 'RGBICWW Strip Light',
|
|
152
|
+
'strip light': 'Strip Light',
|
|
153
|
+
'strip light 3': 'Strip Light 3', // node-switchbot beta
|
|
132
154
|
|
|
133
155
|
// Vacuum conversions
|
|
134
|
-
'robot vacuum cleaner s1': '
|
|
135
|
-
'robot vacuum cleaner s1 plus': '
|
|
136
|
-
'robot vacuum cleaner s10': '
|
|
137
|
-
'robot vacuum cleaner s20': '
|
|
138
|
-
'robot vacuum cleaner k10+ pro combo': '
|
|
139
|
-
'robot vacuum cleaner k11+': '
|
|
140
|
-
'robot vacuum cleaner k20 plus pro': '
|
|
156
|
+
'robot vacuum cleaner s1': 'Robot Vacuum Cleaner S1',
|
|
157
|
+
'robot vacuum cleaner s1 plus': 'Robot Vacuum Cleaner S1 Plus',
|
|
158
|
+
'robot vacuum cleaner s10': 'Robot Vacuum Cleaner S10',
|
|
159
|
+
'robot vacuum cleaner s20': 'Robot Vacuum Cleaner S20',
|
|
160
|
+
'robot vacuum cleaner k10+ pro combo': 'Robot Vacuum Cleaner K10+ Pro Combo',
|
|
161
|
+
'robot vacuum cleaner k11+': 'Robot Vacuum Cleaner K11+',
|
|
162
|
+
'robot vacuum cleaner k20 plus pro': 'Robot Vacuum Cleaner K20 Plus Pro',
|
|
141
163
|
|
|
142
164
|
// Exact device type mappings (API format → canonical format)
|
|
143
|
-
'
|
|
144
|
-
'
|
|
145
|
-
'
|
|
146
|
-
'
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
152
|
-
'
|
|
153
|
-
'
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
'
|
|
163
|
-
'
|
|
164
|
-
'meter plus': 'meterplus',
|
|
165
|
-
'relay switch 1 pm': 'relay switch 1pm',
|
|
166
|
-
'relay switch 2 pm': 'relay switch 2pm',
|
|
167
|
-
'plug mini eu': 'plug mini (eu)',
|
|
168
|
-
'plug mini jp': 'plug mini (jp)',
|
|
169
|
-
'plug mini us': 'plug mini (us)',
|
|
165
|
+
'relay switch 1': 'Relay Switch 1',
|
|
166
|
+
'blind tilt': 'Blind Tilt',
|
|
167
|
+
'roller shade': 'Roller Shade',
|
|
168
|
+
'curtain3': 'Curtain3',
|
|
169
|
+
'hub 2': 'Hub 2',
|
|
170
|
+
'meterplus': 'MeterPlus',
|
|
171
|
+
'meterpro': 'MeterPro',
|
|
172
|
+
'meterpro(co2)': 'MeterPro(CO2)',
|
|
173
|
+
'walletfinder': 'WalletFinder',
|
|
174
|
+
'k10+': 'K10+',
|
|
175
|
+
'k10+ pro (wosweeperminipro)': 'K10+ Pro (wosweeperminipro)',
|
|
176
|
+
|
|
177
|
+
// Handle spaced variants from config files (normalize back to canonical type)
|
|
178
|
+
'meter pro': 'Meter Pro',
|
|
179
|
+
'meter pro (co2)': 'Meter Pro (CO2)',
|
|
180
|
+
'meter plus': 'Meter Plus',
|
|
181
|
+
'relay switch 1 pm': 'Relay Switch 1PM',
|
|
182
|
+
'relay switch 2 pm': 'Relay Switch 2PM',
|
|
183
|
+
'plug mini eu': 'Plug Mini (EU)',
|
|
184
|
+
'plug mini jp': 'Plug Mini (JP)',
|
|
185
|
+
'plug mini us': 'Plug Mini (US)',
|
|
170
186
|
|
|
171
187
|
// Migration mappings for invalid/legacy device types
|
|
172
|
-
'lock vision pro': '
|
|
173
|
-
'lock vision': 'Keypad Vision', // Invalid type
|
|
188
|
+
'lock vision pro': 'Lock Vision Pro', // Valid alias; map to canonical
|
|
189
|
+
// 'lock vision': 'Keypad Vision', // Invalid type (removed, now node-switchbot beta alias above)
|
|
174
190
|
'lock touch': 'Keypad Touch', // Invalid type
|
|
191
|
+
|
|
192
|
+
// Additional normalization for new/unknown types from logs
|
|
193
|
+
'woplugus': 'Plug Mini (US)',
|
|
194
|
+
// Removed duplicate keys below, only last occurrence kept
|
|
195
|
+
// 'plug mini us': 'plug mini (us)', // duplicate, removed
|
|
196
|
+
// 'plug us': 'plug mini (us)', // duplicate, removed
|
|
197
|
+
// 'plug': 'plug', // duplicate, removed
|
|
198
|
+
// 'air purifier pm2.5': 'air purifier pm2.5', // duplicate, removed
|
|
199
|
+
// 'rgbic neon wire rope light': 'rgbic neon wire rope light', // duplicate, removed
|
|
200
|
+
// 'candle warmer lamp': 'candle warmer lamp', // duplicate, removed
|
|
201
|
+
// 'pan/tilt cam plus 3k': 'pan/tilt cam plus 3k', // duplicate, removed
|
|
202
|
+
// 'remote with screen': 'remote with screen', // duplicate, removed
|
|
203
|
+
// 'ai hub': 'ai hub', // duplicate, removed
|
|
204
|
+
// 'lock vision pro': 'lock vision pro', // duplicate, remove this line
|
|
205
|
+
// Add any other device types from logs as needed
|
|
175
206
|
}
|
|
176
207
|
|
|
177
208
|
/**
|
package/src/deviceFactory.ts
CHANGED
|
@@ -44,77 +44,93 @@ const DEVICE_CLASS_MAP: Record<string, any> = {
|
|
|
44
44
|
// Primary device type keys (lowercase, simplified)
|
|
45
45
|
'bot': BotDevice,
|
|
46
46
|
'curtain': CurtainDevice,
|
|
47
|
+
'curtain3': Curtain3Device,
|
|
47
48
|
'fan': FanDevice,
|
|
48
49
|
'light': LightDevice,
|
|
49
50
|
'lightstrip': LightStripDevice,
|
|
50
51
|
'motion': MotionSensorDevice,
|
|
51
52
|
'contact': ContactSensorDevice,
|
|
52
|
-
'vacuum': VacuumDevice,
|
|
53
|
+
'vacuum': VacuumDevice, // node-switchbot beta
|
|
54
|
+
// Canonical, normalized device type keys (lowercase, mapped to device classes)
|
|
55
|
+
'video doorbell': GenericDevice,
|
|
56
|
+
'smart radiator thermostat': GenericDevice, // node-switchbot beta
|
|
57
|
+
'woiosensor': GenericDevice,
|
|
58
|
+
'garage door opener': GenericDevice, // node-switchbot beta
|
|
59
|
+
'air purifier table pm2.5': GenericDevice,
|
|
60
|
+
'air purifier voc': GenericDevice,
|
|
61
|
+
'air purifier table voc': GenericDevice,
|
|
62
|
+
'meterplus': MeterDevice,
|
|
63
|
+
'meterpro': MeterDevice,
|
|
64
|
+
'meterpro(co2)': MeterDevice,
|
|
65
|
+
'walletfinder': WalletFinderDevice,
|
|
66
|
+
'plug': PlugDevice,
|
|
67
|
+
'plug mini (eu)': PlugMiniDevice, // node-switchbot beta
|
|
68
|
+
'plug mini (jp)': PlugMiniDevice, // node-switchbot beta
|
|
69
|
+
'plug mini (us)': PlugMiniDevice, // node-switchbot beta
|
|
70
|
+
'relay switch 1pm': RelaySwitch1PMDevice,
|
|
71
|
+
'relay switch 2pm': RelaySwitch1PMDevice,
|
|
72
|
+
'k10+ pro': WoSweeperDevice,
|
|
73
|
+
'robot vacuum cleaner k10+ pro combo': WoSweeperDevice,
|
|
74
|
+
'robot vacuum cleaner k11+': WoSweeperDevice,
|
|
75
|
+
'robot vacuum cleaner k20 plus pro': WoSweeperDevice,
|
|
76
|
+
'ai hub': GenericDevice,
|
|
77
|
+
'hub': GenericDevice,
|
|
78
|
+
'hub 2': Hub2Device,
|
|
79
|
+
'hub 3': GenericDevice, // node-switchbot beta
|
|
80
|
+
'hub mini': GenericDevice, // node-switchbot beta
|
|
81
|
+
'hub plus': GenericDevice,
|
|
82
|
+
'indoor cam': GenericDevice,
|
|
83
|
+
'pan/tilt cam': GenericDevice,
|
|
84
|
+
'pan/tilt cam 2k': GenericDevice,
|
|
85
|
+
'pan/tilt cam plus 2k': GenericDevice,
|
|
86
|
+
'pan/tilt cam plus 3k': GenericDevice,
|
|
87
|
+
'humidifier': HumidifierDevice, // node-switchbot beta (Evaporative Humidifier)
|
|
88
|
+
'roller shade': RollerShadeDevice, // node-switchbot beta
|
|
89
|
+
'strip light 3': StripLightDevice, // node-switchbot beta
|
|
90
|
+
'circulator fan': FanDevice, // node-switchbot beta
|
|
91
|
+
'smart lock pro': LockDevice, // node-switchbot beta
|
|
92
|
+
'lock lite': LockDevice, // node-switchbot beta
|
|
93
|
+
'keypad': LockDevice, // node-switchbot beta
|
|
94
|
+
'lock vision pro': LockDevice, // node-switchbot beta
|
|
95
|
+
'floor lamp': LightDevice, // node-switchbot beta
|
|
96
|
+
'rgbicww floor lamp': LightStripDevice, // node-switchbot beta
|
|
97
|
+
'rgbicww strip light': LightStripDevice, // node-switchbot beta
|
|
98
|
+
'home climate panel': GenericDevice, // node-switchbot beta (Climate Panel)
|
|
53
99
|
'lock': LockDevice,
|
|
54
|
-
'
|
|
100
|
+
'humidifier2': HumidifierDevice,
|
|
55
101
|
'temperature': TemperatureSensorDevice,
|
|
56
102
|
'relay switch 1': RelaySwitchDevice,
|
|
57
|
-
'relay switch 1pm': RelaySwitch1PMDevice,
|
|
58
|
-
'relay switch 2pm': RelaySwitch1PMDevice,
|
|
59
|
-
'plug': PlugDevice,
|
|
60
|
-
'plug mini (jp)': PlugMiniDevice,
|
|
61
|
-
'plug mini (us)': PlugMiniDevice,
|
|
62
|
-
'plug mini (eu)': PlugMiniDevice,
|
|
63
103
|
'blind tilt': BlindTiltDevice,
|
|
64
|
-
'curtain3': Curtain3Device,
|
|
65
104
|
'worollershade': RollerShadeDevice,
|
|
66
105
|
'wo rollershade': RollerShadeDevice,
|
|
67
|
-
'roller shade': RollerShadeDevice,
|
|
68
106
|
'rollershade': RollerShadeDevice,
|
|
69
|
-
'hub 2': Hub2Device,
|
|
70
107
|
'meter': MeterDevice,
|
|
71
|
-
'meterplus': MeterDevice,
|
|
72
108
|
'meter plus (jp)': MeterDevice,
|
|
73
|
-
'meterpro': MeterDevice,
|
|
74
|
-
'meterpro(co2)': MeterDevice,
|
|
75
109
|
'water detector': WaterDetectorDevice,
|
|
76
110
|
'smart fan': SmartFanDevice,
|
|
77
111
|
'strip light': StripLightDevice,
|
|
78
|
-
'strip light 3': StripLightDevice,
|
|
79
|
-
'walletfinder': WalletFinderDevice,
|
|
80
112
|
'wosweeper': WoSweeperDevice,
|
|
81
113
|
'wosweepermini': WoSweeperMiniDevice,
|
|
82
114
|
'wosweeperminipro': WoSweeperMiniProDevice,
|
|
83
115
|
'k10+': WoSweeperDevice,
|
|
84
|
-
'k10+ pro': WoSweeperDevice,
|
|
85
116
|
'k10+ pro (wosweeperminipro)': WoSweeperMiniProDevice,
|
|
86
|
-
// API display name aliases (for UI compatibility)
|
|
87
|
-
'motion sensor': MotionSensorDevice,
|
|
88
|
-
'contact sensor': ContactSensorDevice,
|
|
89
|
-
'humidifier2': HumidifierDevice,
|
|
90
|
-
'circulator fan': FanDevice,
|
|
91
117
|
'battery circulator fan': FanDevice,
|
|
92
118
|
'standing circulator fan': FanDevice,
|
|
93
119
|
'smart lock': LockDevice,
|
|
94
|
-
'smart lock pro': LockDevice,
|
|
95
120
|
'smart lock ultra': LockDevice,
|
|
96
|
-
'lock lite': LockDevice,
|
|
97
|
-
'keypad': LockDevice,
|
|
98
121
|
'keypad touch': LockDevice,
|
|
99
122
|
'keypad vision': LockDevice,
|
|
100
123
|
'keypad vision pro': LockDevice,
|
|
101
|
-
'lock vision pro': LockDevice,
|
|
102
124
|
'color bulb': LightDevice,
|
|
103
125
|
'ceiling light': LightDevice,
|
|
104
126
|
'ceiling light pro': LightDevice,
|
|
105
127
|
'candle warmer lamp': LightDevice,
|
|
106
|
-
'floor lamp': LightDevice,
|
|
107
128
|
'rgbic neon rope light': LightStripDevice,
|
|
108
129
|
'rgbic neon wire rope light': LightStripDevice,
|
|
109
|
-
'rgbicww floor lamp': LightStripDevice,
|
|
110
|
-
'rgbicww strip light': LightStripDevice,
|
|
111
130
|
'robot vacuum cleaner s1': VacuumDevice,
|
|
112
131
|
'robot vacuum cleaner s1 plus': VacuumDevice,
|
|
113
132
|
'robot vacuum cleaner s10': VacuumDevice,
|
|
114
133
|
'robot vacuum cleaner s20': VacuumDevice,
|
|
115
|
-
'robot vacuum cleaner k10+ pro combo': WoSweeperDevice,
|
|
116
|
-
'robot vacuum cleaner k11+': WoSweeperDevice,
|
|
117
|
-
'robot vacuum cleaner k20 plus pro': WoSweeperDevice,
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
function classForType(type: string) {
|
|
@@ -124,6 +140,7 @@ function classForType(type: string) {
|
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
export async function createDevice(opts: DeviceOptions, cfg: SwitchBotPluginConfig, useMatter: boolean) {
|
|
143
|
+
|
|
127
144
|
// Reuse existing client when provided via config to avoid creating
|
|
128
145
|
// a new SwitchBotClient per device (which can be expensive).
|
|
129
146
|
let client: any = (cfg as any)?._client ?? null
|
|
@@ -135,8 +152,13 @@ export async function createDevice(opts: DeviceOptions, cfg: SwitchBotPluginConf
|
|
|
135
152
|
// Pass client via config so devices can access it
|
|
136
153
|
const mergedCfg = { ...(cfg as any), _client: client }
|
|
137
154
|
|
|
155
|
+
// Pass encryptionKey and keyId to device opts if present
|
|
156
|
+
const deviceOpts = { ...opts }
|
|
157
|
+
if (opts.encryptionKey) deviceOpts.encryptionKey = opts.encryptionKey
|
|
158
|
+
if (opts.keyId) deviceOpts.keyId = opts.keyId
|
|
159
|
+
|
|
138
160
|
const DeviceCtor = classForType(opts.type)
|
|
139
|
-
const device = new DeviceCtor(
|
|
161
|
+
const device = new DeviceCtor(deviceOpts, mergedCfg)
|
|
140
162
|
await device.init()
|
|
141
163
|
|
|
142
164
|
// Attach a simple getState delegator to the client where appropriate
|
|
@@ -1,15 +1,105 @@
|
|
|
1
|
+
// Utility: Validate BLE response length before parsing
|
|
1
2
|
/* eslint-disable style/max-statements-per-line, unused-imports/no-unused-vars */
|
|
2
3
|
import type { SwitchBotPluginConfig } from '../settings.js'
|
|
3
4
|
|
|
5
|
+
import { Buffer } from 'node:buffer'
|
|
6
|
+
|
|
7
|
+
import { DEVICE_MATTER_CLUSTERS } from '../platform.js'
|
|
4
8
|
import { MATTER_ATTRIBUTE_IDS, MATTER_CLUSTER_IDS } from '../utils.js'
|
|
5
9
|
import { DeviceBase } from './deviceBase.js'
|
|
6
10
|
|
|
11
|
+
function validateBLEResponseLength(buf: Buffer | Uint8Array | any[], expected: number, context = ''): boolean {
|
|
12
|
+
if (!buf || typeof buf.length !== 'number' || buf.length !== expected) {
|
|
13
|
+
console.warn(`[BLE] Invalid response length${context ? ` for ${context}` : ''}: expected ${expected}, got ${buf?.length}`)
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// BLE notification handling: per-command notification futures and unsolicited notification logging
|
|
20
|
+
const BLE_NOTIFICATION_HANDLERS = new Map<string, (payload: Buffer) => void>()
|
|
21
|
+
|
|
7
22
|
// Module-scope regex pattern to avoid recompilation
|
|
8
23
|
const HEX_COLOR_REGEX = /^#?[0-9A-F]{6}$/i
|
|
9
24
|
|
|
10
25
|
export class GenericDevice extends DeviceBase {
|
|
11
26
|
constructor(opts: any, cfg: SwitchBotPluginConfig) {
|
|
12
27
|
super(opts, cfg)
|
|
28
|
+
// If BLE encryptionKey/keyId provided, set on node-switchbot device instance if possible
|
|
29
|
+
if (opts.encryptionKey && this.client && typeof this.client.devices?.get === 'function') {
|
|
30
|
+
try {
|
|
31
|
+
const dev = this.client.devices.get(opts.id)
|
|
32
|
+
if (dev && typeof dev.setKey === 'function') {
|
|
33
|
+
dev.setKey({
|
|
34
|
+
encryptionKey: opts.encryptionKey,
|
|
35
|
+
keyId: opts.keyId || undefined,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// ignore if device not found or setKey not available
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Subscribe to BLE notifications if supported (node-switchbot v4+)
|
|
44
|
+
this._subscribeBLENotifications()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Subscribe to BLE notifications for this device (if supported by node-switchbot)
|
|
49
|
+
* Logs unsolicited notifications and enables per-command notification futures.
|
|
50
|
+
*/
|
|
51
|
+
private async _subscribeBLENotifications() {
|
|
52
|
+
if (!this.client || typeof this.client.devices?.get !== 'function') { return }
|
|
53
|
+
const dev = this.client.devices.get(this.opts.id)
|
|
54
|
+
if (!dev || typeof dev.mac !== 'string' || !dev.mac) { return }
|
|
55
|
+
// Only subscribe once per device
|
|
56
|
+
if (BLE_NOTIFICATION_HANDLERS.has(dev.mac)) { return }
|
|
57
|
+
if (typeof dev.subscribeNotifications === 'function') {
|
|
58
|
+
const handler = (payload: Buffer) => {
|
|
59
|
+
// If a per-command notification future is waiting, let node-switchbot handle it
|
|
60
|
+
// Otherwise, log unsolicited notification
|
|
61
|
+
if (payload && payload.length > 0) {
|
|
62
|
+
// Unsolicited notification logging
|
|
63
|
+
// (node-switchbot will resolve per-command futures internally)
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.info(`[BLE] Unsolicited notification from ${dev.mac}: ${payload.toString('hex')}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
// Subscribe and remember handler for possible cleanup
|
|
70
|
+
await dev.subscribeNotifications(handler)
|
|
71
|
+
BLE_NOTIFICATION_HANDLERS.set(dev.mac, handler)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// ignore if subscription fails
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Await a BLE notification for this device (for advanced use in subclasses)
|
|
80
|
+
* Returns the notification payload or throws on timeout.
|
|
81
|
+
*/
|
|
82
|
+
protected async _awaitBLENotification(timeoutMs = 5000): Promise<Buffer> {
|
|
83
|
+
if (!this.client || typeof this.client.devices?.get !== 'function') { throw new Error('No BLE client/device') }
|
|
84
|
+
const dev = this.client.devices.get(this.opts.id)
|
|
85
|
+
if (!dev || typeof dev.mac !== 'string' || !dev.mac) { throw new Error('No BLE MAC for device') }
|
|
86
|
+
if (typeof dev.bleConnection?.sendCommand !== 'function') { throw new TypeError('BLE connection does not support sendCommand') }
|
|
87
|
+
// This is a low-level utility; in most cases, node-switchbot handles notification futures for commands
|
|
88
|
+
// Here, we expose a direct await for advanced use
|
|
89
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
90
|
+
let timer: NodeJS.Timeout | undefined
|
|
91
|
+
const handler = (payload: Buffer) => {
|
|
92
|
+
clearTimeout(timer)
|
|
93
|
+
dev.bleConnection?.unsubscribeNotifications(dev.mac, handler)
|
|
94
|
+
resolve(payload)
|
|
95
|
+
}
|
|
96
|
+
dev.bleConnection?.subscribeNotifications(dev.mac, handler).then(() => {
|
|
97
|
+
timer = setTimeout(() => {
|
|
98
|
+
dev.bleConnection?.unsubscribeNotifications(dev.mac, handler)
|
|
99
|
+
reject(new Error('BLE notification timeout'))
|
|
100
|
+
}, timeoutMs)
|
|
101
|
+
}).catch(reject)
|
|
102
|
+
})
|
|
13
103
|
}
|
|
14
104
|
|
|
15
105
|
async getState(): Promise<any> {
|
|
@@ -17,6 +107,13 @@ export class GenericDevice extends DeviceBase {
|
|
|
17
107
|
if (this.client && typeof this.client.getDevice === 'function') {
|
|
18
108
|
try {
|
|
19
109
|
const raw = await this.client.getDevice(this.opts.id)
|
|
110
|
+
// If this is a BLE buffer/array, validate length (common BLE status: 12 bytes, but may vary by device)
|
|
111
|
+
if (raw && (raw instanceof Buffer || Array.isArray(raw) || raw instanceof Uint8Array)) {
|
|
112
|
+
// Default to 12, override per device if needed
|
|
113
|
+
if (!validateBLEResponseLength(raw, 12, this.opts.type)) {
|
|
114
|
+
return { id: this.opts.id, type: this.opts.type, error: 'invalid_ble_response_length', raw }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
20
117
|
// Normalize common response shapes
|
|
21
118
|
try {
|
|
22
119
|
const device = raw?.body ?? raw
|
|
@@ -228,8 +325,19 @@ export class CurtainDevice extends GenericDevice {
|
|
|
228
325
|
}
|
|
229
326
|
}
|
|
230
327
|
|
|
231
|
-
// Matter-specific descriptor for Curtain (Shade cluster)
|
|
328
|
+
// Matter-specific descriptor for Curtain (Shade cluster) with conformance type
|
|
232
329
|
createMatterAccessory(api: any): any {
|
|
330
|
+
// Use DEVICE_MATTER_CLUSTERS directly (static import)
|
|
331
|
+
let matterType = 0 // Default to Rollershade (lift-only)
|
|
332
|
+
try {
|
|
333
|
+
const typeKey = (this.opts.type || '').toLowerCase()
|
|
334
|
+
const clusterConf = DEVICE_MATTER_CLUSTERS[typeKey] || DEVICE_MATTER_CLUSTERS.curtain
|
|
335
|
+
if (clusterConf && clusterConf.windowCovering && typeof clusterConf.windowCovering.type === 'number') {
|
|
336
|
+
matterType = clusterConf.windowCovering.type
|
|
337
|
+
}
|
|
338
|
+
} catch (e) {
|
|
339
|
+
// fallback to default
|
|
340
|
+
}
|
|
233
341
|
return {
|
|
234
342
|
id: this.opts.id,
|
|
235
343
|
name: this.opts.name ?? this.opts.type,
|
|
@@ -240,6 +348,7 @@ export class CurtainDevice extends GenericDevice {
|
|
|
240
348
|
type: 'Shade',
|
|
241
349
|
clusterId: MATTER_CLUSTER_IDS.WindowCovering,
|
|
242
350
|
attributes: {
|
|
351
|
+
type: { read: async () => matterType, write: undefined },
|
|
243
352
|
currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
|
|
244
353
|
[MATTER_ATTRIBUTE_IDS.WindowCovering.CurrentPosition]: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
|
|
245
354
|
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) }) },
|
|
@@ -540,6 +649,8 @@ export class LightDevice extends GenericDevice {
|
|
|
540
649
|
type: 'ColorControl',
|
|
541
650
|
clusterId: MATTER_CLUSTER_IDS.ColorControl,
|
|
542
651
|
attributes: {
|
|
652
|
+
// Required colorMode attribute for Matter conformance (0 = currentHueAndSaturation)
|
|
653
|
+
colorMode: { read: async () => 0 },
|
|
543
654
|
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) }) },
|
|
544
655
|
[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) }) },
|
|
545
656
|
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) }) },
|
|
@@ -996,3 +1107,4 @@ export class WalletFinderDevice extends GenericDevice {}
|
|
|
996
1107
|
export class WoSweeperDevice extends VacuumDevice {}
|
|
997
1108
|
export class WoSweeperMiniDevice extends VacuumDevice {}
|
|
998
1109
|
export class WoSweeperMiniProDevice extends VacuumDevice {}
|
|
1110
|
+
// Add missing closing braces for MeterDevice and WaterDetectorDevice
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Error re-exports for node-switchbot v4 error compatibility
|
|
2
|
+
|
|
3
|
+
let SwitchbotOperationError: any = undefined;
|
|
4
|
+
let SwitchbotAuthenticationError: any = undefined;
|
|
5
|
+
let CharacteristicMissingError: any = undefined;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
// Dynamically import from node-switchbot if available
|
|
9
|
+
// (node-switchbot v4+ exports these error classes)
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
11
|
+
const mod = require('node-switchbot');
|
|
12
|
+
SwitchbotOperationError = mod.SwitchbotOperationError;
|
|
13
|
+
SwitchbotAuthenticationError = mod.SwitchbotAuthenticationError;
|
|
14
|
+
CharacteristicMissingError = mod.CharacteristicMissingError;
|
|
15
|
+
} catch {}
|
|
16
|
+
|
|
17
|
+
// Fallbacks (for OpenAPI or legacy code)
|
|
18
|
+
if (!SwitchbotOperationError) {
|
|
19
|
+
SwitchbotOperationError = class extends Error {
|
|
20
|
+
code?: string; cause?: Error;
|
|
21
|
+
constructor(message: string, code?: string, cause?: Error) {
|
|
22
|
+
super(message); this.name = 'SwitchbotOperationError'; this.code = code; this.cause = cause;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (!SwitchbotAuthenticationError) {
|
|
27
|
+
SwitchbotAuthenticationError = class extends Error {
|
|
28
|
+
code?: string; cause?: Error;
|
|
29
|
+
constructor(message: string, code?: string, cause?: Error) {
|
|
30
|
+
super(message); this.name = 'SwitchbotAuthenticationError'; this.code = code; this.cause = cause;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (!CharacteristicMissingError) {
|
|
35
|
+
CharacteristicMissingError = class extends Error {
|
|
36
|
+
characteristic: string;
|
|
37
|
+
constructor(message: string, characteristic: string) {
|
|
38
|
+
super(message); this.name = 'CharacteristicMissingError'; this.characteristic = characteristic;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { SwitchbotOperationError, SwitchbotAuthenticationError, CharacteristicMissingError };
|