@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.
Files changed (85) hide show
  1. package/MIGRATION.md +10 -0
  2. package/README.md +43 -0
  3. package/TODO.md +263 -0
  4. package/config.schema.json +27 -10
  5. package/dist/device-types.d.ts.map +1 -1
  6. package/dist/device-types.js +93 -63
  7. package/dist/device-types.js.map +1 -1
  8. package/dist/deviceFactory.d.ts.map +1 -1
  9. package/dist/deviceFactory.js +55 -33
  10. package/dist/deviceFactory.js.map +1 -1
  11. package/dist/devices/genericDevice.d.ts +12 -1
  12. package/dist/devices/genericDevice.d.ts.map +1 -1
  13. package/dist/devices/genericDevice.js +121 -1
  14. package/dist/devices/genericDevice.js.map +1 -1
  15. package/dist/errors.d.ts +5 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +51 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  20. package/dist/homebridge-ui/endpoints/devices.js +80 -1
  21. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  22. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
  23. package/dist/homebridge-ui/endpoints/discovery.js +73 -41
  24. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
  25. package/dist/homebridge-ui/public/css/styles.css +209 -16
  26. package/dist/homebridge-ui/public/index.html +87 -56
  27. package/dist/homebridge-ui/public/js/api.d.ts +24 -1
  28. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  29. package/dist/homebridge-ui/public/js/api.js +33 -2
  30. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  31. package/dist/homebridge-ui/public/js/api.ts +52 -3
  32. package/dist/homebridge-ui/public/js/app.js +1604 -143
  33. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  34. package/dist/homebridge-ui/public/js/app.ts +2 -1
  35. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  36. package/dist/homebridge-ui/public/js/devices.js +2 -0
  37. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  38. package/dist/homebridge-ui/public/js/devices.ts +13 -1
  39. package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
  40. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  41. package/dist/homebridge-ui/public/js/discovery.js +1244 -124
  42. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  43. package/dist/homebridge-ui/public/js/discovery.ts +1344 -142
  44. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  45. package/dist/homebridge-ui/public/js/modals.js +42 -0
  46. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  47. package/dist/homebridge-ui/public/js/modals.ts +50 -1
  48. package/dist/homebridge-ui/public/js/render.d.ts +3 -0
  49. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  50. package/dist/homebridge-ui/public/js/render.js +389 -36
  51. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  52. package/dist/homebridge-ui/public/js/render.ts +421 -38
  53. package/dist/platform.d.ts +1 -0
  54. package/dist/platform.d.ts.map +1 -1
  55. package/dist/platform.js +3 -4
  56. package/dist/platform.js.map +1 -1
  57. package/dist/switchbotClient.d.ts.map +1 -1
  58. package/dist/switchbotClient.js +51 -5
  59. package/dist/switchbotClient.js.map +1 -1
  60. package/docs/assets/highlight.css +9 -2
  61. package/docs/index.html +28 -2
  62. package/docs/variables/default.html +1 -1
  63. package/package.json +9 -8
  64. package/src/device-types.ts +95 -64
  65. package/src/deviceFactory.ts +55 -33
  66. package/src/devices/genericDevice.ts +113 -1
  67. package/src/errors.ts +43 -0
  68. package/src/homebridge-ui/endpoints/devices.ts +82 -1
  69. package/src/homebridge-ui/endpoints/discovery.ts +78 -44
  70. package/src/homebridge-ui/public/css/styles.css +209 -16
  71. package/src/homebridge-ui/public/index.html +87 -56
  72. package/src/homebridge-ui/public/js/api.ts +52 -3
  73. package/src/homebridge-ui/public/js/app.ts +2 -1
  74. package/src/homebridge-ui/public/js/devices.ts +13 -1
  75. package/src/homebridge-ui/public/js/discovery.ts +1344 -142
  76. package/src/homebridge-ui/public/js/modals.ts +50 -1
  77. package/src/homebridge-ui/public/js/render.ts +421 -38
  78. package/src/platform.ts +3 -4
  79. package/src/switchbotClient.ts +44 -9
  80. package/test/e2e/run-e2e.spec.ts +12 -10
  81. package/test/helpers/matter-harness.ts +2 -2
  82. package/test/matter-integration.spec.ts +21 -0
  83. package/todo/COMPLETED.md +313 -0
  84. package/todo/TODO.md +80 -66
  85. 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.111",
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:fix": "eslint src/**/*.ts src/homebridge-ui/**/*.html --fix && npm run typecheck",
72
- "watch:base": "npm run build && npm run plugin-ui && npm link && nodemon",
73
- "watch": "npm run ports:free && npm run watch:base",
74
- "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'",
75
- "watch:ui:dev": "npm run watch:ui",
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.8"
95
+ "node-switchbot": "^4.0.0-beta.14"
95
96
  },
96
97
  "devDependencies": {
97
98
  "@antfu/eslint-config": "^7.7.0",
@@ -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
- // Sensor type conversions
99
- 'motion sensor': 'motion',
100
- 'contact sensor': 'contact',
101
- 'presence sensor': 'contact',
102
- 'water detector': 'water detector',
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': 'humidifier',
106
- 'circulator fan': 'fan',
107
- 'battery circulator fan': 'fan',
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': 'lock',
112
- 'smart lock pro': 'lock',
113
- 'smart lock ultra': 'lock',
114
- 'lock lite': 'lock',
115
- 'keypad': 'lock',
116
- 'keypad touch': 'lock',
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': 'light',
122
- 'ceiling light': 'light',
123
- 'ceiling light pro': 'light',
124
- 'candle warmer lamp': 'light',
125
- 'floor lamp': 'light',
126
- 'rgbic neon rope light': 'lightstrip',
127
- 'rgbic neon wire rope light': 'lightstrip',
128
- 'rgbicww floor lamp': 'lightstrip',
129
- 'rgbicww strip light': 'lightstrip',
130
- 'strip light': 'strip light',
131
- 'strip light 3': 'strip light',
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': 'vacuum',
135
- 'robot vacuum cleaner s1 plus': 'vacuum',
136
- 'robot vacuum cleaner s10': 'vacuum',
137
- 'robot vacuum cleaner s20': 'vacuum',
138
- 'robot vacuum cleaner k10+ pro combo': 'wosweeper',
139
- 'robot vacuum cleaner k11+': 'wosweeper',
140
- 'robot vacuum cleaner k20 plus pro': 'wosweeper',
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
- 'plug mini (eu)': 'plug mini (eu)',
144
- 'plug mini (jp)': 'plug mini (jp)',
145
- 'plug mini (us)': 'plug mini (us)',
146
- 'relay switch 1': 'relay switch 1',
147
- 'relay switch 1pm': 'relay switch 1pm',
148
- 'relay switch 2pm': 'relay switch 2pm',
149
- 'blind tilt': 'blind tilt',
150
- 'roller shade': 'roller shade',
151
- 'curtain3': 'curtain3',
152
- 'hub 2': 'hub 2',
153
- 'meterplus': 'meterplus',
154
- 'meterpro': 'meterpro',
155
- 'meterpro(co2)': 'meterpro(co2)',
156
- 'walletfinder': 'walletfinder',
157
- 'k10+': 'k10+',
158
- 'k10+ pro': 'k10+ pro',
159
- 'k10+ pro (wosweeperminipro)': 'k10+ pro (wosweeperminipro)',
160
-
161
- // Handle spaced variants from config files (normalize back to API format)
162
- 'meter pro': 'meterpro',
163
- 'meter pro (co2)': 'meterpro(co2)',
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': 'lock', // Valid alias; map to lock behavior
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
  /**
@@ -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
- 'humidifier': HumidifierDevice,
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(opts, mergedCfg)
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 };