@switchbot/homebridge-switchbot 5.0.0-beta.107 → 5.0.0-beta.109

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 (148) hide show
  1. package/TODO.md +564 -0
  2. package/config.schema.json +100 -25
  3. package/dist/device-types.d.ts +15 -0
  4. package/dist/device-types.d.ts.map +1 -0
  5. package/dist/device-types.js +145 -0
  6. package/dist/device-types.js.map +1 -0
  7. package/dist/deviceFactory.d.ts +1 -1
  8. package/dist/deviceFactory.d.ts.map +1 -1
  9. package/dist/deviceFactory.js +55 -17
  10. package/dist/deviceFactory.js.map +1 -1
  11. package/dist/devices/genericDevice.d.ts +1 -1
  12. package/dist/devices/genericDevice.d.ts.map +1 -1
  13. package/dist/devices/genericDevice.js +4 -2
  14. package/dist/devices/genericDevice.js.map +1 -1
  15. package/dist/homebridge-ui/endpoints/config.d.ts +6 -0
  16. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
  17. package/dist/homebridge-ui/endpoints/config.js +73 -0
  18. package/dist/homebridge-ui/endpoints/config.js.map +1 -0
  19. package/dist/homebridge-ui/endpoints/credentials.d.ts +6 -0
  20. package/dist/homebridge-ui/endpoints/credentials.d.ts.map +1 -0
  21. package/dist/homebridge-ui/endpoints/credentials.js +65 -0
  22. package/dist/homebridge-ui/endpoints/credentials.js.map +1 -0
  23. package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
  24. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
  25. package/dist/homebridge-ui/endpoints/devices.js +285 -0
  26. package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
  27. package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
  28. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
  29. package/dist/homebridge-ui/endpoints/discovery.js +171 -0
  30. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
  31. package/dist/homebridge-ui/public/index.html +144 -588
  32. package/dist/homebridge-ui/public/js/api.d.ts +15 -0
  33. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
  34. package/dist/homebridge-ui/public/js/api.js +164 -0
  35. package/dist/homebridge-ui/public/js/api.js.map +1 -0
  36. package/dist/homebridge-ui/public/js/api.ts +189 -0
  37. package/dist/homebridge-ui/public/js/app.d.ts +2 -0
  38. package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
  39. package/dist/homebridge-ui/public/js/app.js +1069 -0
  40. package/dist/homebridge-ui/public/js/app.js.map +7 -0
  41. package/dist/homebridge-ui/public/js/app.ts +21 -0
  42. package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
  43. package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
  44. package/dist/homebridge-ui/public/js/constants.js +2 -0
  45. package/dist/homebridge-ui/public/js/constants.js.map +1 -0
  46. package/dist/homebridge-ui/public/js/constants.ts +1 -0
  47. package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
  48. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
  49. package/dist/homebridge-ui/public/js/credentials.js +96 -0
  50. package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
  51. package/dist/homebridge-ui/public/js/credentials.ts +100 -0
  52. package/dist/homebridge-ui/public/js/devices-delete.d.ts +2 -0
  53. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
  54. package/dist/homebridge-ui/public/js/devices-delete.js +99 -0
  55. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
  56. package/dist/homebridge-ui/public/js/devices-delete.ts +113 -0
  57. package/dist/homebridge-ui/public/js/devices.d.ts +8 -0
  58. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
  59. package/dist/homebridge-ui/public/js/devices.js +71 -0
  60. package/dist/homebridge-ui/public/js/devices.js.map +1 -0
  61. package/dist/homebridge-ui/public/js/devices.ts +78 -0
  62. package/dist/homebridge-ui/public/js/discovery.d.ts +3 -0
  63. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
  64. package/dist/homebridge-ui/public/js/discovery.js +107 -0
  65. package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
  66. package/dist/homebridge-ui/public/js/discovery.ts +123 -0
  67. package/dist/homebridge-ui/public/js/logger.d.ts +6 -0
  68. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
  69. package/dist/homebridge-ui/public/js/logger.js +12 -0
  70. package/dist/homebridge-ui/public/js/logger.js.map +1 -0
  71. package/dist/homebridge-ui/public/js/logger.ts +11 -0
  72. package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
  73. package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
  74. package/dist/homebridge-ui/public/js/modal.js +26 -0
  75. package/dist/homebridge-ui/public/js/modal.js.map +1 -0
  76. package/dist/homebridge-ui/public/js/modal.ts +28 -0
  77. package/dist/homebridge-ui/public/js/modals.d.ts +2 -0
  78. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
  79. package/dist/homebridge-ui/public/js/modals.js +132 -0
  80. package/dist/homebridge-ui/public/js/modals.js.map +1 -0
  81. package/dist/homebridge-ui/public/js/modals.ts +153 -0
  82. package/dist/homebridge-ui/public/js/render.d.ts +6 -0
  83. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
  84. package/dist/homebridge-ui/public/js/render.js +166 -0
  85. package/dist/homebridge-ui/public/js/render.js.map +1 -0
  86. package/dist/homebridge-ui/public/js/render.ts +190 -0
  87. package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
  88. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
  89. package/dist/homebridge-ui/public/js/toast.js +29 -0
  90. package/dist/homebridge-ui/public/js/toast.js.map +1 -0
  91. package/dist/homebridge-ui/public/js/toast.ts +37 -0
  92. package/dist/homebridge-ui/public/js/types.d.ts +23 -0
  93. package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
  94. package/dist/homebridge-ui/public/js/types.js +2 -0
  95. package/dist/homebridge-ui/public/js/types.js.map +1 -0
  96. package/dist/homebridge-ui/public/js/types.ts +26 -0
  97. package/dist/homebridge-ui/server.d.ts +1 -3
  98. package/dist/homebridge-ui/server.d.ts.map +1 -1
  99. package/dist/homebridge-ui/server.js +10 -561
  100. package/dist/homebridge-ui/server.js.map +1 -1
  101. package/dist/homebridge-ui/utils/config-parser.d.ts +35 -0
  102. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
  103. package/dist/homebridge-ui/utils/config-parser.js +87 -0
  104. package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
  105. package/dist/homebridge-ui/utils/logger.d.ts +7 -0
  106. package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
  107. package/dist/homebridge-ui/utils/logger.js +17 -0
  108. package/dist/homebridge-ui/utils/logger.js.map +1 -0
  109. package/dist/platform.d.ts.map +1 -1
  110. package/dist/platform.js +6 -2
  111. package/dist/platform.js.map +1 -1
  112. package/dist/switchbotClient.d.ts.map +1 -1
  113. package/dist/switchbotClient.js +2 -0
  114. package/dist/switchbotClient.js.map +1 -1
  115. package/docs/variables/default.html +1 -1
  116. package/eslint.config.js +5 -0
  117. package/nodemon.json +2 -2
  118. package/package.json +20 -10
  119. package/scripts/build-ui.js +36 -0
  120. package/scripts/free-dev-ports.mjs +105 -0
  121. package/scripts/sync-device-types.mjs +28 -0
  122. package/src/device-types.ts +145 -0
  123. package/src/deviceFactory.ts +71 -31
  124. package/src/devices/genericDevice.ts +5 -2
  125. package/src/homebridge-ui/endpoints/config.ts +79 -0
  126. package/src/homebridge-ui/endpoints/credentials.ts +74 -0
  127. package/src/homebridge-ui/endpoints/devices.ts +321 -0
  128. package/src/homebridge-ui/endpoints/discovery.ts +191 -0
  129. package/src/homebridge-ui/public/index.html +144 -588
  130. package/src/homebridge-ui/public/js/api.ts +189 -0
  131. package/src/homebridge-ui/public/js/app.ts +21 -0
  132. package/src/homebridge-ui/public/js/constants.ts +1 -0
  133. package/src/homebridge-ui/public/js/credentials.ts +100 -0
  134. package/src/homebridge-ui/public/js/devices-delete.ts +113 -0
  135. package/src/homebridge-ui/public/js/devices.ts +78 -0
  136. package/src/homebridge-ui/public/js/discovery.ts +123 -0
  137. package/src/homebridge-ui/public/js/logger.ts +11 -0
  138. package/src/homebridge-ui/public/js/modal.ts +28 -0
  139. package/src/homebridge-ui/public/js/modals.ts +153 -0
  140. package/src/homebridge-ui/public/js/render.ts +190 -0
  141. package/src/homebridge-ui/public/js/toast.ts +37 -0
  142. package/src/homebridge-ui/public/js/types.ts +26 -0
  143. package/src/homebridge-ui/server.ts +11 -647
  144. package/src/homebridge-ui/utils/config-parser.ts +108 -0
  145. package/src/homebridge-ui/utils/logger.ts +17 -0
  146. package/src/platform.ts +6 -2
  147. package/src/switchbotClient.ts +2 -0
  148. package/switchbot-homebridge-switchbot-5.0.0.tgz +0 -0
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ import esbuild from 'esbuild'
4
+ import path from 'path'
5
+ import fs from 'fs'
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ const srcDir = path.resolve(__dirname, '../src/homebridge-ui/public/js')
11
+ const outputDir = path.resolve(__dirname, '../dist/homebridge-ui/public/js')
12
+
13
+ // Ensure output directory exists
14
+ fs.mkdirSync(outputDir, { recursive: true })
15
+
16
+ // Transpile TypeScript to JavaScript with esbuild
17
+ esbuild
18
+ .build({
19
+ entryPoints: [path.join(srcDir, 'app.ts')],
20
+ outfile: path.join(outputDir, 'app.js'),
21
+ bundle: true,
22
+ target: 'es2020',
23
+ platform: 'browser',
24
+ format: 'esm',
25
+ sourcemap: true,
26
+ external: [],
27
+ minify: false,
28
+ logLevel: 'info',
29
+ })
30
+ .then(() => {
31
+ console.log('✓ UI bundling complete')
32
+ })
33
+ .catch((error) => {
34
+ console.error('✗ UI bundling failed:', error)
35
+ process.exit(1)
36
+ })
@@ -0,0 +1,105 @@
1
+ import { execSync } from 'node:child_process'
2
+ import { readFileSync } from 'node:fs'
3
+ import { homedir } from 'node:os'
4
+ import { resolve } from 'node:path'
5
+
6
+ function resolvePorts() {
7
+ let bridgePort = 51829
8
+ let uiPort = 8581
9
+
10
+ try {
11
+ const configPath = resolve(homedir(), '.homebridge-dev', 'config.json')
12
+ const config = JSON.parse(readFileSync(configPath, 'utf8'))
13
+
14
+ if (typeof config?.bridge?.port === 'number') {
15
+ bridgePort = config.bridge.port
16
+ }
17
+
18
+ const configUiPlatform = Array.isArray(config?.platforms)
19
+ ? config.platforms.find(platform => platform?.platform === 'config')
20
+ : undefined
21
+
22
+ if (typeof configUiPlatform?.port === 'number') {
23
+ uiPort = configUiPlatform.port
24
+ }
25
+ } catch {}
26
+
27
+ return [...new Set([bridgePort, uiPort])]
28
+ }
29
+
30
+ function pidsForPort(port) {
31
+ try {
32
+ const output = execSync(`lsof -ti tcp:${port}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
33
+ return output ? output.split('\n').filter(Boolean) : []
34
+ } catch {
35
+ return []
36
+ }
37
+ }
38
+
39
+ function freePort(port) {
40
+ const initialPids = pidsForPort(port)
41
+ if (initialPids.length === 0) {
42
+ return
43
+ }
44
+
45
+ try {
46
+ execSync(`lsof -ti tcp:${port} | xargs -r kill -TERM`, { stdio: ['ignore', 'ignore', 'ignore'] })
47
+ } catch {}
48
+
49
+ const remainingPids = pidsForPort(port)
50
+ if (remainingPids.length > 0) {
51
+ try {
52
+ execSync(`lsof -ti tcp:${port} | xargs -r kill -KILL`, { stdio: ['ignore', 'ignore', 'ignore'] })
53
+ } catch {}
54
+ }
55
+ }
56
+
57
+ function cleanupDevProcesses() {
58
+ let output = ''
59
+ try {
60
+ output = execSync('ps -axo pid=,command=', { stdio: ['ignore', 'pipe', 'ignore'] }).toString()
61
+ } catch {
62
+ return
63
+ }
64
+
65
+ const targetPids = output
66
+ .split('\n')
67
+ .map(line => line.trim())
68
+ .filter(Boolean)
69
+ .map((line) => {
70
+ const firstSpace = line.indexOf(' ')
71
+ if (firstSpace === -1) {
72
+ return null
73
+ }
74
+
75
+ const pid = Number(line.slice(0, firstSpace).trim())
76
+ const command = line.slice(firstSpace + 1)
77
+ return Number.isFinite(pid) ? { pid, command } : null
78
+ })
79
+ .filter((entry) => {
80
+ if (!entry) {
81
+ return false
82
+ }
83
+
84
+ if (entry.pid === process.pid || entry.pid === process.ppid) {
85
+ return false
86
+ }
87
+
88
+ const isNodemonProcess = entry.command.includes('homebridge-switchbot/node_modules/.bin/nodemon')
89
+ const isConfigUiProcess = entry.command.trim().startsWith('homebridge-config-ui-x')
90
+ return isNodemonProcess || isConfigUiProcess
91
+ })
92
+ .map(entry => entry.pid)
93
+
94
+ for (const pid of targetPids) {
95
+ try {
96
+ process.kill(pid, 'SIGTERM')
97
+ } catch {}
98
+ }
99
+ }
100
+
101
+ cleanupDevProcesses()
102
+
103
+ for (const port of resolvePorts()) {
104
+ freePort(port)
105
+ }
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ const root = process.cwd()
5
+ const sourcePath = path.join(root, 'src/device-types.ts')
6
+ const schemaPath = path.join(root, 'config.schema.json')
7
+
8
+ const source = fs.readFileSync(sourcePath, 'utf8')
9
+ const schemaRaw = fs.readFileSync(schemaPath, 'utf8')
10
+ const schema = JSON.parse(schemaRaw)
11
+
12
+ const match = source.match(/export const DEVICE_TYPES = (\{[\s\S]*?\}) as const/)
13
+ if (!match) {
14
+ throw new Error('Could not find DEVICE_TYPES in src/device-types.ts')
15
+ }
16
+
17
+ const deviceTypesByCategory = Function(`return (${match[1]})`)()
18
+ const enumValues = [...new Set(Object.values(deviceTypesByCategory).flat())]
19
+
20
+ const configDeviceType = schema?.schema?.properties?.devices?.items?.properties?.configDeviceType
21
+ if (!configDeviceType) {
22
+ throw new Error('Could not locate schema.properties.devices.items.properties.configDeviceType')
23
+ }
24
+
25
+ configDeviceType.enum = enumValues
26
+
27
+ fs.writeFileSync(schemaPath, `${JSON.stringify(schema, null, 2)}\n`, 'utf8')
28
+ console.log(`Synchronized ${enumValues.length} device types into config.schema.json`)
@@ -0,0 +1,145 @@
1
+ export const DEVICE_TYPES = {
2
+ 'Window Coverings': ['Blind Tilt', 'Curtain', 'Curtain3', 'Roller Shade'],
3
+ 'Locks & Access': [
4
+ 'Keypad',
5
+ 'Keypad Touch',
6
+ 'Keypad Vision',
7
+ 'Keypad Vision Pro',
8
+ 'Lock Lite',
9
+ 'Smart Lock',
10
+ 'Smart Lock Pro',
11
+ 'Smart Lock Ultra',
12
+ 'Video Doorbell',
13
+ ],
14
+ 'Sensors': ['Contact Sensor', 'Motion Sensor', 'Presence Sensor', 'Water Detector'],
15
+ 'Lighting': [
16
+ 'Candle Warmer Lamp',
17
+ 'Ceiling Light',
18
+ 'Ceiling Light Pro',
19
+ 'Color Bulb',
20
+ 'Floor Lamp',
21
+ 'RGBIC Neon Rope Light',
22
+ 'RGBIC Neon Wire Rope Light',
23
+ 'RGBICWW Floor Lamp',
24
+ 'RGBICWW Strip Light',
25
+ 'Strip Light',
26
+ 'Strip Light 3',
27
+ ],
28
+ 'Climate Control': [
29
+ 'Air Purifier PM2.5',
30
+ 'Air Purifier Table PM2.5',
31
+ 'Air Purifier VOC',
32
+ 'Air Purifier Table VOC',
33
+ 'Battery Circulator Fan',
34
+ 'Circulator Fan',
35
+ 'Humidifier',
36
+ 'Humidifier2',
37
+ 'Meter',
38
+ 'MeterPlus',
39
+ 'MeterPro',
40
+ 'MeterPro(CO2)',
41
+ 'Smart Radiator Thermostat',
42
+ 'Standing Circulator Fan',
43
+ 'WoIOSensor',
44
+ ],
45
+ 'Plugs & Switches': [
46
+ 'Garage Door Opener',
47
+ 'Plug',
48
+ 'Plug Mini (EU)',
49
+ 'Plug Mini (JP)',
50
+ 'Plug Mini (US)',
51
+ 'Relay Switch 1',
52
+ 'Relay Switch 1PM',
53
+ 'Relay Switch 2PM',
54
+ ],
55
+ 'Robot Vacuums': [
56
+ 'K10+',
57
+ 'K10+ Pro',
58
+ 'Robot Vacuum Cleaner K10+ Pro Combo',
59
+ 'Robot Vacuum Cleaner K11+',
60
+ 'Robot Vacuum Cleaner K20 Plus Pro',
61
+ 'Robot Vacuum Cleaner S1',
62
+ 'Robot Vacuum Cleaner S1 Plus',
63
+ 'Robot Vacuum Cleaner S10',
64
+ 'Robot Vacuum Cleaner S20',
65
+ ],
66
+ 'Hubs': ['AI Hub', 'Hub', 'Hub 2', 'Hub 3', 'Hub Mini', 'Hub Plus'],
67
+ 'Cameras': [
68
+ 'Indoor Cam',
69
+ 'Pan/Tilt Cam',
70
+ 'Pan/Tilt Cam 2K',
71
+ 'Pan/Tilt Cam Plus 2K',
72
+ 'Pan/Tilt Cam Plus 3K',
73
+ ],
74
+ 'IR Devices': [
75
+ 'Air Conditioner',
76
+ 'Air Purifier',
77
+ 'Camera',
78
+ 'DVD',
79
+ 'Fan',
80
+ 'Light',
81
+ 'Others',
82
+ 'Projector',
83
+ 'Set Top Box',
84
+ 'Speaker',
85
+ 'Streamer',
86
+ 'TV',
87
+ 'Vacuum Cleaner',
88
+ 'Water Heater',
89
+ ],
90
+ 'Other Devices': ['AI Art Frame', 'Bot', 'Home Climate Panel', 'Remote', 'remote with screen'],
91
+ } as const
92
+
93
+ export const DEVICE_TYPE_NORMALIZATION_MAP: Record<string, string> = {
94
+ 'motion sensor': 'motion',
95
+ 'contact sensor': 'contact',
96
+ 'presence sensor': 'contact',
97
+ 'water detector': 'water detector',
98
+ 'humidifier2': 'humidifier',
99
+ 'circulator fan': 'fan',
100
+ 'battery circulator fan': 'fan',
101
+ 'standing circulator fan': 'fan',
102
+ 'smart lock': 'lock',
103
+ 'smart lock pro': 'lock',
104
+ 'smart lock ultra': 'lock',
105
+ 'lock lite': 'lock',
106
+ 'keypad': 'lock',
107
+ 'keypad touch': 'lock',
108
+ 'keypad vision': 'lock',
109
+ 'keypad vision pro': 'lock',
110
+ 'color bulb': 'light',
111
+ 'ceiling light': 'light',
112
+ 'ceiling light pro': 'light',
113
+ 'candle warmer lamp': 'light',
114
+ 'floor lamp': 'light',
115
+ 'rgbic neon rope light': 'lightstrip',
116
+ 'rgbic neon wire rope light': 'lightstrip',
117
+ 'rgbicww floor lamp': 'lightstrip',
118
+ 'rgbicww strip light': 'lightstrip',
119
+ 'strip light': 'strip light',
120
+ 'strip light 3': 'strip light',
121
+ 'robot vacuum cleaner s1': 'vacuum',
122
+ 'robot vacuum cleaner s1 plus': 'vacuum',
123
+ 'robot vacuum cleaner s10': 'vacuum',
124
+ 'robot vacuum cleaner s20': 'vacuum',
125
+ 'robot vacuum cleaner k10+ pro combo': 'wosweeper',
126
+ 'robot vacuum cleaner k11+': 'wosweeper',
127
+ 'robot vacuum cleaner k20 plus pro': 'wosweeper',
128
+ 'plug mini (eu)': 'plug mini (eu)',
129
+ 'plug mini (jp)': 'plug mini (jp)',
130
+ 'plug mini (us)': 'plug mini (us)',
131
+ 'relay switch 1': 'relay switch 1',
132
+ 'relay switch 1pm': 'relay switch 1pm',
133
+ 'relay switch 2pm': 'relay switch 2pm',
134
+ 'blind tilt': 'blind tilt',
135
+ 'roller shade': 'roller shade',
136
+ 'curtain3': 'curtain3',
137
+ 'hub 2': 'hub 2',
138
+ 'meterplus': 'meterplus',
139
+ 'meterpro': 'meterpro',
140
+ 'meterpro(co2)': 'meterpro(co2)',
141
+ 'walletfinder': 'walletfinder',
142
+ 'k10+': 'k10+',
143
+ 'k10+ pro': 'k10+ pro',
144
+ 'k10+ pro (wosweeperminipro)': 'k10+ pro (wosweeperminipro)',
145
+ }
@@ -1,35 +1,37 @@
1
- import type { SwitchBotPluginConfig, DeviceType } from './settings.js'
2
- import { SwitchBotClient } from './switchbotClient.js'
1
+ import type { DeviceType, SwitchBotPluginConfig } from './settings.js'
2
+
3
+ import { DEVICE_TYPE_NORMALIZATION_MAP } from './device-types.js'
3
4
  import {
4
- GenericDevice,
5
+ BlindTiltDevice,
5
6
  BotDevice,
7
+ ContactSensorDevice,
8
+ Curtain3Device,
6
9
  CurtainDevice,
7
10
  FanDevice,
11
+ GenericDevice,
12
+ Hub2Device,
13
+ HumidifierDevice,
8
14
  LightDevice,
9
15
  LightStripDevice,
10
- MotionSensorDevice,
11
- ContactSensorDevice,
12
- VacuumDevice,
13
16
  LockDevice,
14
- HumidifierDevice,
15
- TemperatureSensorDevice,
16
- RelaySwitchDevice,
17
- RelaySwitch1PMDevice,
17
+ MeterDevice,
18
+ MotionSensorDevice,
18
19
  PlugDevice,
19
20
  PlugMiniDevice,
20
- BlindTiltDevice,
21
- Curtain3Device,
21
+ RelaySwitch1PMDevice,
22
+ RelaySwitchDevice,
22
23
  RollerShadeDevice,
23
- Hub2Device,
24
- MeterDevice,
25
- WaterDetectorDevice,
26
24
  SmartFanDevice,
27
25
  StripLightDevice,
26
+ TemperatureSensorDevice,
27
+ VacuumDevice,
28
28
  WalletFinderDevice,
29
+ WaterDetectorDevice,
29
30
  WoSweeperDevice,
30
31
  WoSweeperMiniDevice,
31
32
  WoSweeperMiniProDevice,
32
33
  } from './devices/genericDevice.js'
34
+ import { SwitchBotClient } from './switchbotClient.js'
33
35
 
34
36
  export interface DeviceOptions {
35
37
  id: string
@@ -39,30 +41,33 @@ export interface DeviceOptions {
39
41
  }
40
42
 
41
43
  const DEVICE_CLASS_MAP: Record<string, any> = {
42
- bot: BotDevice,
43
- curtain: CurtainDevice,
44
- fan: FanDevice,
45
- light: LightDevice,
46
- lightstrip: LightStripDevice,
47
- motion: MotionSensorDevice,
48
- contact: ContactSensorDevice,
49
- vacuum: VacuumDevice,
50
- lock: LockDevice,
51
- humidifier: HumidifierDevice,
52
- temperature: TemperatureSensorDevice,
44
+ // Primary device type keys (lowercase, simplified)
45
+ 'bot': BotDevice,
46
+ 'curtain': CurtainDevice,
47
+ 'fan': FanDevice,
48
+ 'light': LightDevice,
49
+ 'lightstrip': LightStripDevice,
50
+ 'motion': MotionSensorDevice,
51
+ 'contact': ContactSensorDevice,
52
+ 'vacuum': VacuumDevice,
53
+ 'lock': LockDevice,
54
+ 'humidifier': HumidifierDevice,
55
+ 'temperature': TemperatureSensorDevice,
53
56
  'relay switch 1': RelaySwitchDevice,
54
57
  'relay switch 1pm': RelaySwitch1PMDevice,
55
- plug: PlugDevice,
58
+ 'relay switch 2pm': RelaySwitch1PMDevice,
59
+ 'plug': PlugDevice,
56
60
  'plug mini (jp)': PlugMiniDevice,
57
61
  'plug mini (us)': PlugMiniDevice,
62
+ 'plug mini (eu)': PlugMiniDevice,
58
63
  'blind tilt': BlindTiltDevice,
59
- curtain3: Curtain3Device,
64
+ 'curtain3': Curtain3Device,
60
65
  'worollershade': RollerShadeDevice,
61
66
  'wo rollershade': RollerShadeDevice,
62
67
  'roller shade': RollerShadeDevice,
63
68
  'rollershade': RollerShadeDevice,
64
69
  'hub 2': Hub2Device,
65
- meter: MeterDevice,
70
+ 'meter': MeterDevice,
66
71
  'meterplus': MeterDevice,
67
72
  'meter plus (jp)': MeterDevice,
68
73
  'meterpro': MeterDevice,
@@ -70,6 +75,7 @@ const DEVICE_CLASS_MAP: Record<string, any> = {
70
75
  'water detector': WaterDetectorDevice,
71
76
  'smart fan': SmartFanDevice,
72
77
  'strip light': StripLightDevice,
78
+ 'strip light 3': StripLightDevice,
73
79
  'walletfinder': WalletFinderDevice,
74
80
  'wosweeper': WoSweeperDevice,
75
81
  'wosweepermini': WoSweeperMiniDevice,
@@ -77,10 +83,42 @@ const DEVICE_CLASS_MAP: Record<string, any> = {
77
83
  'k10+': WoSweeperDevice,
78
84
  'k10+ pro': WoSweeperDevice,
79
85
  '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
+ 'battery circulator fan': FanDevice,
92
+ 'standing circulator fan': FanDevice,
93
+ 'smart lock': LockDevice,
94
+ 'smart lock pro': LockDevice,
95
+ 'smart lock ultra': LockDevice,
96
+ 'lock lite': LockDevice,
97
+ 'keypad': LockDevice,
98
+ 'keypad touch': LockDevice,
99
+ 'keypad vision': LockDevice,
100
+ 'keypad vision pro': LockDevice,
101
+ 'color bulb': LightDevice,
102
+ 'ceiling light': LightDevice,
103
+ 'ceiling light pro': LightDevice,
104
+ 'candle warmer lamp': LightDevice,
105
+ 'floor lamp': LightDevice,
106
+ 'rgbic neon rope light': LightStripDevice,
107
+ 'rgbic neon wire rope light': LightStripDevice,
108
+ 'rgbicww floor lamp': LightStripDevice,
109
+ 'rgbicww strip light': LightStripDevice,
110
+ 'robot vacuum cleaner s1': VacuumDevice,
111
+ 'robot vacuum cleaner s1 plus': VacuumDevice,
112
+ 'robot vacuum cleaner s10': VacuumDevice,
113
+ 'robot vacuum cleaner s20': VacuumDevice,
114
+ 'robot vacuum cleaner k10+ pro combo': WoSweeperDevice,
115
+ 'robot vacuum cleaner k11+': WoSweeperDevice,
116
+ 'robot vacuum cleaner k20 plus pro': WoSweeperDevice,
80
117
  }
81
118
 
82
119
  function classForType(type: string) {
83
- const key = (type || '').toLowerCase()
120
+ const rawKey = (type || '').toLowerCase()
121
+ const key = DEVICE_TYPE_NORMALIZATION_MAP[rawKey] ?? rawKey
84
122
  return DEVICE_CLASS_MAP[key] ?? GenericDevice
85
123
  }
86
124
 
@@ -106,7 +144,9 @@ export async function createDevice(opts: DeviceOptions, cfg: SwitchBotPluginConf
106
144
  try {
107
145
  // Prefer client-backed getDevice when available
108
146
  const dev = await client.getDevice(opts.id)
109
- if (dev) return dev
147
+ if (dev) {
148
+ return dev
149
+ }
110
150
  } catch (e) {
111
151
  // ignore and fallback to device implementation
112
152
  }
@@ -4,6 +4,9 @@ import type { SwitchBotPluginConfig } from '../settings.js'
4
4
  import { MATTER_ATTRIBUTE_IDS, MATTER_CLUSTER_IDS } from '../utils.js'
5
5
  import { DeviceBase } from './deviceBase.js'
6
6
 
7
+ // Module-scope regex pattern to avoid recompilation
8
+ const HEX_COLOR_REGEX = /^#?[0-9A-F]{6}$/i
9
+
7
10
  export class GenericDevice extends DeviceBase {
8
11
  constructor(opts: any, cfg: SwitchBotPluginConfig) {
9
12
  super(opts, cfg)
@@ -384,7 +387,7 @@ export class LightDevice extends GenericDevice {
384
387
  if (s && typeof s.hue === 'number') { return s.hue }
385
388
  // try HSV from color hex
386
389
  const hex = s?.color || s?.colorHex || s?.colour
387
- if (typeof hex === 'string' && /^#?[0-9A-F]{6}$/i.test(hex)) {
390
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
388
391
  const h = (() => {
389
392
  const hsl = (h: number, s: number, l: number) => ({ h, s, l })
390
393
  // convert hex -> rgb -> hsv
@@ -420,7 +423,7 @@ export class LightDevice extends GenericDevice {
420
423
  if (s && typeof s.saturation === 'number') { return s.saturation }
421
424
  // if color hex is available, derive saturation from rgb
422
425
  const hex = s?.color || s?.colorHex || s?.colour
423
- if (typeof hex === 'string' && /^#?[0-9A-F]{6}$/i.test(hex)) {
426
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
424
427
  const cleaned = hex.replace('#', '')
425
428
  const r = Number.parseInt(cleaned.substr(0, 2), 16) / 255
426
429
  const g = Number.parseInt(cleaned.substr(2, 2), 16) / 255
@@ -0,0 +1,79 @@
1
+ import type { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'
2
+
3
+ import { RequestError } from '@homebridge/plugin-ui-utils'
4
+ import fs from 'node:fs/promises'
5
+
6
+ import { getAllDevices, getSwitchBotPlatformConfig, SWITCHBOT_PLATFORM_REGEX } from '../utils/config-parser.js'
7
+ import { uiLog } from '../utils/logger.js'
8
+
9
+ /**
10
+ * Register config-related endpoints
11
+ */
12
+ export function registerConfigEndpoints(server: HomebridgePluginUiServer) {
13
+ /**
14
+ * GET /devices - List all configured devices from Homebridge config
15
+ */
16
+ server.onRequest('/devices', async () => {
17
+ try {
18
+ const cfgPath = server.homebridgeConfigPath
19
+ if (!cfgPath) {
20
+ throw new Error('HOMEBRIDGE_CONFIG_PATH not set')
21
+ }
22
+
23
+ const raw = await fs.readFile(cfgPath, 'utf8')
24
+ const cfg = JSON.parse(raw)
25
+ const found: Array<{ id: string, name?: string, type?: string, connectionPreference?: string, room?: string }> = []
26
+
27
+ const platforms = Array.isArray(cfg.platforms) ? cfg.platforms : []
28
+ for (const p of platforms) {
29
+ try {
30
+ const platformName = p.platform || p.name || ''
31
+ // Match known SwitchBot platform identifiers
32
+ if (!platformName || !SWITCHBOT_PLATFORM_REGEX.test(String(platformName))) {
33
+ continue
34
+ }
35
+
36
+ const devices = getAllDevices(p)
37
+ for (const d of devices) {
38
+ const id = d.deviceId ?? d.id
39
+ if (!id) {
40
+ continue
41
+ }
42
+
43
+ found.push({
44
+ id,
45
+ name: d.configDeviceName ?? d.name ?? d.deviceName,
46
+ type: d.configDeviceType ?? d.type ?? d.deviceType,
47
+ connectionPreference: d.connectionPreference ?? d.connection ?? 'auto',
48
+ room: d.room || d.location || undefined,
49
+ })
50
+ }
51
+ } catch (e) {
52
+ // ignore malformed platform entries
53
+ }
54
+ }
55
+
56
+ uiLog.info(`GET /devices - Found ${found.length} devices in ${cfgPath}`)
57
+ return { success: true, data: found }
58
+ } catch (e) {
59
+ uiLog.error(`Error in /devices: ${e instanceof Error ? e.message : String(e)}`)
60
+ throw new RequestError('Failed to read Homebridge config', e)
61
+ }
62
+ })
63
+
64
+ /**
65
+ * GET /platform-config - Get SwitchBot platform configuration
66
+ */
67
+ server.onRequest('/platform-config', async () => {
68
+ try {
69
+ const { platform } = await getSwitchBotPlatformConfig(server)
70
+ return {
71
+ success: true,
72
+ data: JSON.parse(JSON.stringify(platform)),
73
+ }
74
+ } catch (e) {
75
+ uiLog.error(`Error in /platform-config: ${e instanceof Error ? e.message : String(e)}`)
76
+ throw new RequestError('Failed to read platform config', e)
77
+ }
78
+ })
79
+ }
@@ -0,0 +1,74 @@
1
+ import type { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'
2
+
3
+ import { RequestError } from '@homebridge/plugin-ui-utils'
4
+ import fs from 'node:fs/promises'
5
+
6
+ import { getCredential, getSwitchBotPlatformConfig, setCredential } from '../utils/config-parser.js'
7
+ import { uiLog } from '../utils/logger.js'
8
+
9
+ /**
10
+ * Register credentials endpoint
11
+ */
12
+ export function registerCredentialsEndpoint(server: HomebridgePluginUiServer) {
13
+ server.onRequest('/credentials', async (body: any) => {
14
+ try {
15
+ // Handle both GET and POST requests
16
+ if (!body || Object.keys(body).length === 0) {
17
+ // GET request - return current status
18
+ const { platform } = await getSwitchBotPlatformConfig(server)
19
+
20
+ const token = getCredential(platform, 'openApiToken')
21
+ const secret = getCredential(platform, 'openApiSecret')
22
+ const status = {
23
+ hasToken: !!token,
24
+ hasSecret: !!secret,
25
+ tokenLength: token ? String(token).length : 0,
26
+ secretLength: secret ? String(secret).length : 0,
27
+ }
28
+
29
+ uiLog.info(`GET /credentials - Status hasToken=${status.hasToken} hasSecret=${status.hasSecret}`)
30
+ return { success: true, data: status }
31
+ } else {
32
+ // POST request - save credentials
33
+ const { token, secret } = body
34
+
35
+ if (!token || !secret) {
36
+ throw new Error('Token and secret are required')
37
+ }
38
+
39
+ const { cfg, platform, cfgPath } = await getSwitchBotPlatformConfig(server)
40
+
41
+ uiLog.info(`POST /credentials - Saving to platform: ${platform.platform || platform.name}`)
42
+ uiLog.debug(`POST /credentials - Config path: ${cfgPath}`)
43
+ uiLog.debug(`POST /credentials - Token length: ${token.length}, Secret length: ${secret.length}`)
44
+
45
+ // Save token and secret
46
+ setCredential(platform, 'openApiToken', token)
47
+ setCredential(platform, 'openApiSecret', secret)
48
+
49
+ // Write config with fsync
50
+ const configJson = JSON.stringify(cfg, null, 2)
51
+ const fileHandle = await fs.open(cfgPath, 'w')
52
+ try {
53
+ await fileHandle.writeFile(configJson, 'utf8')
54
+ await fileHandle.sync()
55
+ uiLog.info('POST /credentials - Credentials saved and synced to disk')
56
+ } finally {
57
+ await fileHandle.close()
58
+ }
59
+
60
+ // Notify Homebridge UI that config changed
61
+ server.pushEvent('configChanged', { source: 'switchbot-plugin' })
62
+ uiLog.debug('POST /credentials - Sent configChanged event to UI')
63
+
64
+ return {
65
+ success: true,
66
+ data: { message: 'Credentials saved successfully' },
67
+ }
68
+ }
69
+ } catch (e) {
70
+ uiLog.error(`Error in /credentials: ${e instanceof Error ? e.message : String(e)}`)
71
+ throw new RequestError('Failed to handle credentials request', e)
72
+ }
73
+ })
74
+ }