@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.
- package/TODO.md +564 -0
- package/config.schema.json +100 -25
- package/dist/device-types.d.ts +15 -0
- package/dist/device-types.d.ts.map +1 -0
- package/dist/device-types.js +145 -0
- package/dist/device-types.js.map +1 -0
- package/dist/deviceFactory.d.ts +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +55 -17
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +1 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +4 -2
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/homebridge-ui/endpoints/config.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/config.js +73 -0
- package/dist/homebridge-ui/endpoints/config.js.map +1 -0
- package/dist/homebridge-ui/endpoints/credentials.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/credentials.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/credentials.js +65 -0
- package/dist/homebridge-ui/endpoints/credentials.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.js +285 -0
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.js +171 -0
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
- package/dist/homebridge-ui/public/index.html +144 -588
- package/dist/homebridge-ui/public/js/api.d.ts +15 -0
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/api.js +164 -0
- package/dist/homebridge-ui/public/js/api.js.map +1 -0
- package/dist/homebridge-ui/public/js/api.ts +189 -0
- package/dist/homebridge-ui/public/js/app.d.ts +2 -0
- package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/app.js +1069 -0
- package/dist/homebridge-ui/public/js/app.js.map +7 -0
- package/dist/homebridge-ui/public/js/app.ts +21 -0
- package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
- package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/constants.js +2 -0
- package/dist/homebridge-ui/public/js/constants.js.map +1 -0
- package/dist/homebridge-ui/public/js/constants.ts +1 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.js +96 -0
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.ts +100 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts +2 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.js +99 -0
- package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.ts +113 -0
- package/dist/homebridge-ui/public/js/devices.d.ts +8 -0
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices.js +71 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices.ts +78 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts +3 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.js +107 -0
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.ts +123 -0
- package/dist/homebridge-ui/public/js/logger.d.ts +6 -0
- package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/logger.js +12 -0
- package/dist/homebridge-ui/public/js/logger.js.map +1 -0
- package/dist/homebridge-ui/public/js/logger.ts +11 -0
- package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
- package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modal.js +26 -0
- package/dist/homebridge-ui/public/js/modal.js.map +1 -0
- package/dist/homebridge-ui/public/js/modal.ts +28 -0
- package/dist/homebridge-ui/public/js/modals.d.ts +2 -0
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modals.js +132 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -0
- package/dist/homebridge-ui/public/js/modals.ts +153 -0
- package/dist/homebridge-ui/public/js/render.d.ts +6 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/render.js +166 -0
- package/dist/homebridge-ui/public/js/render.js.map +1 -0
- package/dist/homebridge-ui/public/js/render.ts +190 -0
- package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
- package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/toast.js +29 -0
- package/dist/homebridge-ui/public/js/toast.js.map +1 -0
- package/dist/homebridge-ui/public/js/toast.ts +37 -0
- package/dist/homebridge-ui/public/js/types.d.ts +23 -0
- package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/types.js +2 -0
- package/dist/homebridge-ui/public/js/types.js.map +1 -0
- package/dist/homebridge-ui/public/js/types.ts +26 -0
- package/dist/homebridge-ui/server.d.ts +1 -3
- package/dist/homebridge-ui/server.d.ts.map +1 -1
- package/dist/homebridge-ui/server.js +10 -561
- package/dist/homebridge-ui/server.js.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.d.ts +35 -0
- package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/config-parser.js +87 -0
- package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
- package/dist/homebridge-ui/utils/logger.d.ts +7 -0
- package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/logger.js +17 -0
- package/dist/homebridge-ui/utils/logger.js.map +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +6 -2
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +2 -0
- package/dist/switchbotClient.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/eslint.config.js +5 -0
- package/nodemon.json +2 -2
- package/package.json +20 -10
- package/scripts/build-ui.js +36 -0
- package/scripts/free-dev-ports.mjs +105 -0
- package/scripts/sync-device-types.mjs +28 -0
- package/src/device-types.ts +145 -0
- package/src/deviceFactory.ts +71 -31
- package/src/devices/genericDevice.ts +5 -2
- package/src/homebridge-ui/endpoints/config.ts +79 -0
- package/src/homebridge-ui/endpoints/credentials.ts +74 -0
- package/src/homebridge-ui/endpoints/devices.ts +321 -0
- package/src/homebridge-ui/endpoints/discovery.ts +191 -0
- package/src/homebridge-ui/public/index.html +144 -588
- package/src/homebridge-ui/public/js/api.ts +189 -0
- package/src/homebridge-ui/public/js/app.ts +21 -0
- package/src/homebridge-ui/public/js/constants.ts +1 -0
- package/src/homebridge-ui/public/js/credentials.ts +100 -0
- package/src/homebridge-ui/public/js/devices-delete.ts +113 -0
- package/src/homebridge-ui/public/js/devices.ts +78 -0
- package/src/homebridge-ui/public/js/discovery.ts +123 -0
- package/src/homebridge-ui/public/js/logger.ts +11 -0
- package/src/homebridge-ui/public/js/modal.ts +28 -0
- package/src/homebridge-ui/public/js/modals.ts +153 -0
- package/src/homebridge-ui/public/js/render.ts +190 -0
- package/src/homebridge-ui/public/js/toast.ts +37 -0
- package/src/homebridge-ui/public/js/types.ts +26 -0
- package/src/homebridge-ui/server.ts +11 -647
- package/src/homebridge-ui/utils/config-parser.ts +108 -0
- package/src/homebridge-ui/utils/logger.ts +17 -0
- package/src/platform.ts +6 -2
- package/src/switchbotClient.ts +2 -0
- 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
|
+
}
|
package/src/deviceFactory.ts
CHANGED
|
@@ -1,35 +1,37 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { DeviceType, SwitchBotPluginConfig } from './settings.js'
|
|
2
|
+
|
|
3
|
+
import { DEVICE_TYPE_NORMALIZATION_MAP } from './device-types.js'
|
|
3
4
|
import {
|
|
4
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
RelaySwitchDevice,
|
|
17
|
-
RelaySwitch1PMDevice,
|
|
17
|
+
MeterDevice,
|
|
18
|
+
MotionSensorDevice,
|
|
18
19
|
PlugDevice,
|
|
19
20
|
PlugMiniDevice,
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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' &&
|
|
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' &&
|
|
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
|
+
}
|