@switchbot/homebridge-switchbot 5.0.0-beta.134 → 5.0.0-beta.136
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/config.schema.json +0 -11
- package/dist/deviceFactory.d.ts +3 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +3 -18
- 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 +79 -159
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/homebridge-ui/public/index.html +107 -124
- package/dist/homebridge-ui/public/js/api.d.ts +1 -4
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +1 -7
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +1 -8
- package/dist/homebridge-ui/public/js/app.js +201 -90
- package/dist/homebridge-ui/public/js/app.js.map +2 -2
- package/dist/homebridge-ui/public/js/app.ts +5 -6
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.js +1 -1
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.ts +7 -3
- package/dist/homebridge-ui/public/js/devices-delete.js +1 -1
- package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices-delete.ts +1 -1
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +0 -1
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +0 -2
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +6 -3
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +9 -3
- package/dist/homebridge-ui/public/js/logger.d.ts +1 -0
- package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/logger.js +8 -3
- package/dist/homebridge-ui/public/js/logger.js.map +1 -1
- package/dist/homebridge-ui/public/js/logger.ts +12 -6
- package/dist/homebridge-ui/public/js/modals.d.ts +4 -0
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +152 -59
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +187 -60
- package/dist/homebridge-ui/public/js/render.d.ts +3 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +63 -24
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +102 -51
- package/dist/homebridge-ui/utils/logger.js +1 -1
- package/dist/homebridge-ui/utils/logger.js.map +1 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +40 -32
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts +5 -5
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +21 -12
- package/dist/switchbotClient.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +1 -1
- package/scripts/build-ui.js +4 -3
- package/scripts/generate-matter-maps.js +34 -17
- package/scripts/sync-device-types.mjs +4 -1
- package/src/deviceFactory.ts +3 -18
- package/src/devices/genericDevice.ts +77 -164
- package/src/homebridge-ui/public/index.html +107 -124
- package/src/homebridge-ui/public/js/api.ts +1 -8
- package/src/homebridge-ui/public/js/app.ts +5 -6
- package/src/homebridge-ui/public/js/credentials.ts +7 -3
- package/src/homebridge-ui/public/js/devices-delete.ts +1 -1
- package/src/homebridge-ui/public/js/devices.ts +0 -2
- package/src/homebridge-ui/public/js/discovery.ts +9 -3
- package/src/homebridge-ui/public/js/logger.ts +12 -6
- package/src/homebridge-ui/public/js/modals.ts +187 -60
- package/src/homebridge-ui/public/js/render.ts +102 -51
- package/src/homebridge-ui/utils/logger.ts +1 -1
- package/src/platform.ts +40 -31
- package/src/switchbotClient.ts +42 -27
- package/test/accessory-restore.spec.ts +9 -8
- package/test/device-mapping.spec.ts +4 -3
- package/test/deviceFactory.spec.ts +4 -3
- package/test/e2e/run-e2e.spec.ts +6 -10
- package/test/fan-swing.spec.ts +7 -6
- package/test/helpers/matter-harness.ts +18 -7
- package/test/lock-users.spec.ts +5 -5
- package/test/matter-childbridge.spec.ts +10 -7
- package/test/matter-descriptors.spec.ts +8 -8
- package/test/matter-device-state.spec.ts +52 -36
- package/test/matter-integration.spec.ts +53 -6
- package/test/platform.integration.spec.ts +7 -5
- package/test/switchbot-client-debounce.spec.ts +36 -40
- package/test/switchbot-client-openapi.spec.ts +22 -15
- package/test/switchbotClient.spec.ts +4 -3
- package/test/utils.spec.ts +2 -1
package/src/deviceFactory.ts
CHANGED
|
@@ -139,16 +139,10 @@ function classForType(type: string) {
|
|
|
139
139
|
return DEVICE_CLASS_MAP[key] ?? GenericDevice
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
export async function createDevice(opts: DeviceOptions, cfg: SwitchBotPluginConfig, useMatter: boolean) {
|
|
142
|
+
export async function createDevice(opts: DeviceOptions, cfg: SwitchBotPluginConfig, useMatter: boolean, log?: { info: (...args: any[]) => void }) {
|
|
143
143
|
// Debug: Log the options passed to the device constructor
|
|
144
|
-
if (opts && opts.name) {
|
|
145
|
-
|
|
146
|
-
// eslint-disable-next-line no-console
|
|
147
|
-
console.info(`[Matter/Debug] createDevice: Passing opts for ${opts.name}:`, JSON.stringify(opts, null, 2))
|
|
148
|
-
} else {
|
|
149
|
-
// eslint-disable-next-line no-console
|
|
150
|
-
console.info(`[Matter/Debug] createDevice: Passing opts for ${opts.name} (no matterType):`, JSON.stringify(opts, null, 2))
|
|
151
|
-
}
|
|
144
|
+
if (opts && opts.name && log && typeof log.info === 'function') {
|
|
145
|
+
log.info(`[Matter/Debug] createDevice: Passing opts for ${opts.name}:`, JSON.stringify(opts, null, 2))
|
|
152
146
|
}
|
|
153
147
|
// Reuse existing client when provided via config to avoid creating
|
|
154
148
|
// a new SwitchBotClient per device (which can be expensive).
|
|
@@ -169,15 +163,6 @@ export async function createDevice(opts: DeviceOptions, cfg: SwitchBotPluginConf
|
|
|
169
163
|
deviceOpts.keyId = opts.keyId
|
|
170
164
|
}
|
|
171
165
|
|
|
172
|
-
// Ensure matterType override is set for curtain devices
|
|
173
|
-
const typeKey = (opts.type || '').toLowerCase()
|
|
174
|
-
if ((typeKey === 'curtain' || typeKey === 'curtain3' || typeKey === 'rollershade' || typeKey === 'blindtilt') && cfg && cfg.devices) {
|
|
175
|
-
const configDevice = cfg.devices.find((d: any) => d.id === opts.id)
|
|
176
|
-
if (configDevice && typeof configDevice.matterType !== 'undefined') {
|
|
177
|
-
deviceOpts.matterType = configDevice.matterType
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
166
|
const DeviceCtor = classForType(opts.type)
|
|
182
167
|
const device = new DeviceCtor(deviceOpts, mergedCfg)
|
|
183
168
|
await device.init()
|
|
@@ -7,9 +7,9 @@ import { Buffer } from 'node:buffer'
|
|
|
7
7
|
import { MATTER_ATTRIBUTE_IDS, MATTER_CLUSTER_IDS } from '../utils.js'
|
|
8
8
|
import { DeviceBase } from './deviceBase.js'
|
|
9
9
|
|
|
10
|
-
function validateBLEResponseLength(buf: Buffer | Uint8Array | any[], expected: number, context = ''): boolean {
|
|
10
|
+
function validateBLEResponseLength(buf: Buffer | Uint8Array | any[], expected: number, context = '', log: import('homebridge').Logger): boolean {
|
|
11
11
|
if (!buf || typeof buf.length !== 'number' || buf.length !== expected) {
|
|
12
|
-
|
|
12
|
+
log.warn(`[BLE] Invalid response length${context ? ` for ${context}` : ''}: expected ${expected}, got ${buf?.length}`)
|
|
13
13
|
return false
|
|
14
14
|
}
|
|
15
15
|
return true
|
|
@@ -22,12 +22,15 @@ const BLE_NOTIFICATION_HANDLERS = new Map<string, (payload: Buffer) => void>()
|
|
|
22
22
|
const HEX_COLOR_REGEX = /^#?[0-9A-F]{6}$/i
|
|
23
23
|
|
|
24
24
|
export class GenericDevice extends DeviceBase {
|
|
25
|
-
protected log:
|
|
25
|
+
protected log: import('homebridge').Logger
|
|
26
26
|
|
|
27
27
|
constructor(opts: any, cfg: SwitchBotPluginConfig) {
|
|
28
28
|
super(opts, cfg)
|
|
29
|
-
//
|
|
30
|
-
this.log = opts?.log || cfg?.log
|
|
29
|
+
// Require logger from opts or cfg
|
|
30
|
+
this.log = opts?.log || cfg?.log
|
|
31
|
+
if (!this.log) {
|
|
32
|
+
throw new Error('Device requires a logger (Homebridge logger) in opts or cfg')
|
|
33
|
+
}
|
|
31
34
|
// If BLE encryptionKey/keyId provided, set on node-switchbot device instance if possible
|
|
32
35
|
if (opts.encryptionKey && this.client && typeof this.client.devices?.get === 'function') {
|
|
33
36
|
try {
|
|
@@ -42,10 +45,7 @@ export class GenericDevice extends DeviceBase {
|
|
|
42
45
|
// ignore if device not found or setKey not available
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
|
-
//
|
|
46
|
-
if (!this.log) {
|
|
47
|
-
this.log = console
|
|
48
|
-
}
|
|
48
|
+
// ...existing code...
|
|
49
49
|
// Subscribe to BLE notifications if supported (node-switchbot v4+)
|
|
50
50
|
this._subscribeBLENotifications()
|
|
51
51
|
}
|
|
@@ -115,7 +115,7 @@ export class GenericDevice extends DeviceBase {
|
|
|
115
115
|
// If this is a BLE buffer/array, validate length (common BLE status: 12 bytes, but may vary by device)
|
|
116
116
|
if (raw && (raw instanceof Buffer || Array.isArray(raw) || raw instanceof Uint8Array)) {
|
|
117
117
|
// Default to 12, override per device if needed
|
|
118
|
-
if (!validateBLEResponseLength(raw, 12, this.opts.type)) {
|
|
118
|
+
if (!validateBLEResponseLength(raw, 12, this.opts.type, this.log)) {
|
|
119
119
|
return { id: this.opts.id, type: this.opts.type, error: 'invalid_ble_response_length', raw }
|
|
120
120
|
}
|
|
121
121
|
}
|
|
@@ -431,168 +431,81 @@ export class CurtainDevice extends GenericDevice {
|
|
|
431
431
|
}
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
-
// Matter-specific descriptor for Curtain (
|
|
434
|
+
// Matter-specific descriptor for Curtain (WindowCovering cluster) with new attributes
|
|
435
435
|
async createMatterAccessory(api: any): Promise<any> {
|
|
436
|
-
//
|
|
437
|
-
const
|
|
438
|
-
//
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
436
|
+
// Get current state for dynamic attributes
|
|
437
|
+
const state = await this.getState()
|
|
438
|
+
// Compose attributes for Matter WindowCovering cluster
|
|
439
|
+
const attributes: Record<string, any> = {
|
|
440
|
+
currentPositionLiftPercent100ths: {
|
|
441
|
+
read: async () => {
|
|
442
|
+
const s = await this.getState()
|
|
443
|
+
return typeof s.position === 'number' ? Math.round(s.position * 100) : 0
|
|
444
|
+
},
|
|
445
|
+
write: undefined,
|
|
446
|
+
},
|
|
447
|
+
targetPositionLiftPercent100ths: {
|
|
448
|
+
read: async () => {
|
|
449
|
+
const s = await this.getState()
|
|
450
|
+
return typeof s.position === 'number' ? Math.round(s.position * 100) : 0
|
|
451
|
+
},
|
|
452
|
+
write: async (v: any) => this.setState({ position: Math.round(Number(v) / 100) }),
|
|
453
|
+
},
|
|
454
|
+
operationalStatus: {
|
|
455
|
+
read: async () => state.operationalStatus ?? { global: 0, lift: 0, tilt: 0 },
|
|
456
|
+
write: undefined,
|
|
457
|
+
},
|
|
458
|
+
endProductType: {
|
|
459
|
+
read: async () => state.endProductType ?? 0,
|
|
460
|
+
write: undefined,
|
|
461
|
+
},
|
|
462
|
+
configStatus: {
|
|
463
|
+
read: async () => state.configStatus ?? {
|
|
464
|
+
operational: true,
|
|
465
|
+
onlineReserved: true,
|
|
466
|
+
liftMovementReversed: false,
|
|
467
|
+
liftPositionAware: true,
|
|
468
|
+
tiltPositionAware: false,
|
|
469
|
+
liftEncoderControlled: true,
|
|
470
|
+
tiltEncoderControlled: false,
|
|
471
|
+
},
|
|
472
|
+
write: undefined,
|
|
473
|
+
},
|
|
471
474
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const attributes: Record<string, any> = {
|
|
479
|
-
type: {
|
|
480
|
-
read: async () => {
|
|
481
|
-
this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} returning config-overridden type attribute value:`, matterType)
|
|
482
|
-
return matterType
|
|
483
|
-
},
|
|
484
|
-
write: undefined,
|
|
475
|
+
// If tilt is supported, add tilt attributes
|
|
476
|
+
if (typeof state.tilt === 'number') {
|
|
477
|
+
attributes.currentPositionTiltPercent100ths = {
|
|
478
|
+
read: async () => {
|
|
479
|
+
const s = await this.getState()
|
|
480
|
+
return typeof s.tilt === 'number' ? Math.round(s.tilt * 100) : 0
|
|
485
481
|
},
|
|
486
|
-
|
|
487
|
-
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) }) },
|
|
488
|
-
}
|
|
489
|
-
if (supportsTilt) {
|
|
490
|
-
attributes.currentTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: undefined }
|
|
491
|
-
attributes.targetTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: async (v: any) => this.setState({ tilt: Number(v) }) }
|
|
492
|
-
}
|
|
493
|
-
try {
|
|
494
|
-
this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes:`, JSON.stringify(attributes, null, 2))
|
|
495
|
-
} catch (e) {
|
|
496
|
-
this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes: [unserializable]`)
|
|
497
|
-
}
|
|
498
|
-
const windowCoveringCluster = {
|
|
499
|
-
type: 'WindowCovering', // for test compatibility
|
|
500
|
-
matterType, // actual Matter type number
|
|
501
|
-
clusterId: MATTER_CLUSTER_IDS.WindowCovering,
|
|
502
|
-
attributes,
|
|
482
|
+
write: undefined,
|
|
503
483
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
name: this.opts.name ?? this.opts.type,
|
|
511
|
-
protocol: 'matter',
|
|
512
|
-
clusters: clustersArr,
|
|
484
|
+
attributes.targetPositionTiltPercent100ths = {
|
|
485
|
+
read: async () => {
|
|
486
|
+
const s = await this.getState()
|
|
487
|
+
return typeof s.tilt === 'number' ? Math.round(s.tilt * 100) : 0
|
|
488
|
+
},
|
|
489
|
+
write: async (v: any) => this.setState({ tilt: Math.round(Number(v) / 100) }),
|
|
513
490
|
}
|
|
514
491
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
// - supportsLift && supportsTilt: RollerShade (0) (or Blind, but 0 is safe)
|
|
520
|
-
// - supportsLift && supportsTilt && typeKey.includes('drapery'): allow Drapery (4)
|
|
521
|
-
// - !supportsLift && !supportsTilt: skip
|
|
522
|
-
interface MatterCombo {
|
|
523
|
-
matterType: number
|
|
524
|
-
supportsLift: boolean
|
|
525
|
-
supportsTilt: boolean
|
|
526
|
-
}
|
|
527
|
-
const combos: MatterCombo[] = []
|
|
528
|
-
if (supportsLift && supportsTilt) {
|
|
529
|
-
// Both supported: allow RollerShade (0), optionally Drapery (4) if typeKey includes 'drapery'
|
|
530
|
-
combos.push({ matterType: 0, supportsLift: true, supportsTilt: true })
|
|
531
|
-
if (typeKey.includes('drapery')) {
|
|
532
|
-
combos.push({ matterType: 4, supportsLift: true, supportsTilt: true })
|
|
533
|
-
}
|
|
534
|
-
} else if (supportsLift && !supportsTilt) {
|
|
535
|
-
// Only lift: for SwitchBot Curtain, use Drapery (4) for conformance
|
|
536
|
-
if (typeKey.includes('curtain')) {
|
|
537
|
-
combos.push({ matterType: 4, supportsLift: true, supportsTilt: false })
|
|
538
|
-
} else {
|
|
539
|
-
combos.push({ matterType: 0, supportsLift: true, supportsTilt: false })
|
|
540
|
-
}
|
|
541
|
-
} else if (!supportsLift && supportsTilt) {
|
|
542
|
-
// Only tilt: allow BlindTilt (8)
|
|
543
|
-
combos.push({ matterType: 8, supportsLift: false, supportsTilt: true })
|
|
492
|
+
const windowCoveringCluster = {
|
|
493
|
+
type: 'WindowCovering',
|
|
494
|
+
clusterId: MATTER_CLUSTER_IDS.WindowCovering,
|
|
495
|
+
attributes,
|
|
544
496
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
},
|
|
557
|
-
write: undefined,
|
|
558
|
-
},
|
|
559
|
-
currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
|
|
560
|
-
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) }) },
|
|
561
|
-
}
|
|
562
|
-
if (combo.supportsTilt) {
|
|
563
|
-
attributes.currentTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: undefined }
|
|
564
|
-
attributes.targetTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: async (v: any) => this.setState({ tilt: Number(v) }) }
|
|
565
|
-
}
|
|
566
|
-
// Log the final attributes object
|
|
567
|
-
try {
|
|
568
|
-
this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes:`, JSON.stringify(attributes, null, 2))
|
|
569
|
-
} catch (e) {
|
|
570
|
-
this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes: [unserializable]`)
|
|
571
|
-
}
|
|
572
|
-
// Try to register with this combo
|
|
573
|
-
const windowCoveringCluster = {
|
|
574
|
-
type: 'WindowCovering', // for test compatibility
|
|
575
|
-
matterType: combo.matterType, // actual Matter type number
|
|
576
|
-
clusterId: MATTER_CLUSTER_IDS.WindowCovering,
|
|
577
|
-
attributes,
|
|
578
|
-
}
|
|
579
|
-
const clustersArr: any = [windowCoveringCluster]
|
|
580
|
-
// Always set clusters.windowCovering to the WindowCovering cluster by clusterId
|
|
581
|
-
const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
|
|
582
|
-
clustersArr.windowCovering = foundWC || null
|
|
583
|
-
return {
|
|
584
|
-
id: this.opts.id,
|
|
585
|
-
name: this.opts.name ?? this.opts.type,
|
|
586
|
-
protocol: 'matter',
|
|
587
|
-
clusters: clustersArr,
|
|
588
|
-
}
|
|
589
|
-
} catch (err) {
|
|
590
|
-
lastError = err
|
|
591
|
-
this.log.warn(`[Matter/Retry] Failed combo matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}: ${err}`)
|
|
592
|
-
}
|
|
497
|
+
// Provide both array and named property for clusters for compatibility with test expectations
|
|
498
|
+
const clustersArr: any[] = [windowCoveringCluster]
|
|
499
|
+
const clusters: any = [...clustersArr]
|
|
500
|
+
// Always set clusters.windowCovering to the WindowCovering cluster by clusterId
|
|
501
|
+
const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
|
|
502
|
+
clusters.windowCovering = foundWC || null
|
|
503
|
+
return {
|
|
504
|
+
id: this.opts.id,
|
|
505
|
+
name: this.opts.name ?? this.opts.type,
|
|
506
|
+
protocol: 'matter',
|
|
507
|
+
clusters,
|
|
593
508
|
}
|
|
594
|
-
// If all combos fail, throw last error
|
|
595
|
-
throw lastError || new Error('All Matter window covering type combos failed')
|
|
596
509
|
}
|
|
597
510
|
}
|
|
598
511
|
|
|
@@ -1,145 +1,128 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
-
<title>SwitchBot Plugin UI</title>
|
|
7
|
-
<link rel="stylesheet" href="css/styles.css" />
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<h1>🤖 SwitchBot Configuration</h1>
|
|
11
3
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<div class="status" id="tokenStatus"></div>
|
|
19
|
-
</div>
|
|
20
|
-
<div class="form-group">
|
|
21
|
-
<label for="secret">Secret:</label>
|
|
22
|
-
<input type="password" id="secret" placeholder="Enter secret" />
|
|
23
|
-
<div class="status" id="secretStatus"></div>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
<button id="saveBtn" onclick="saveCredentials()">Save</button>
|
|
27
|
-
<div id="saveStatus"></div>
|
|
28
|
-
</div>
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
7
|
+
<title>SwitchBot Plugin UI</title>
|
|
8
|
+
<link rel="stylesheet" href="css/styles.css" />
|
|
9
|
+
</head>
|
|
29
10
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</label>
|
|
41
|
-
<div style="margin-top: 2px; margin-left: 22px; font-size: 10px; opacity: 0.75; line-height: 1.25">
|
|
42
|
-
Adds all discovered devices automatically; already configured devices are skipped.
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
<div class="discovery-settings-row" style="margin-bottom: 2px">
|
|
46
|
-
<label id="bleScanSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
|
|
47
|
-
<span>Scan:</span>
|
|
48
|
-
<select id="bleScanDurationSelect" style="font-size: 11px; padding: 2px 6px">
|
|
49
|
-
<option value="3">3s</option>
|
|
50
|
-
<option value="5" selected>5s</option>
|
|
51
|
-
<option value="10">10s</option>
|
|
52
|
-
<option value="15">15s</option>
|
|
53
|
-
</select>
|
|
54
|
-
</label>
|
|
55
|
-
<label id="bleTimeoutSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
|
|
56
|
-
<span>Timeout:</span>
|
|
57
|
-
<input
|
|
58
|
-
id="bleTimeoutInput"
|
|
59
|
-
type="number"
|
|
60
|
-
min="3"
|
|
61
|
-
max="30"
|
|
62
|
-
step="1"
|
|
63
|
-
value="8"
|
|
64
|
-
style="width: 56px; font-size: 11px; padding: 2px 4px"
|
|
65
|
-
/>
|
|
66
|
-
<span>s</span>
|
|
67
|
-
</label>
|
|
68
|
-
<label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
|
|
69
|
-
<input type="checkbox" id="disableBleScanCheckbox" style="width: auto; margin: 0" />
|
|
70
|
-
<span>Disable BLE scan</span>
|
|
71
|
-
</label>
|
|
11
|
+
<body>
|
|
12
|
+
<h1>🤖 SwitchBot Configuration</h1>
|
|
13
|
+
|
|
14
|
+
<div class="card compact">
|
|
15
|
+
<h2>API Credentials</h2>
|
|
16
|
+
<div class="credentials-row">
|
|
17
|
+
<div class="form-group">
|
|
18
|
+
<label for="token">Token:</label>
|
|
19
|
+
<input type="password" id="token" placeholder="Enter token" />
|
|
20
|
+
<div class="status" id="tokenStatus"></div>
|
|
72
21
|
</div>
|
|
73
|
-
<div
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
Auto-refresh:
|
|
78
|
-
<select id="autoRefreshIntervalSelect" style="margin-left: 4px; font-size: 11px; padding: 2px 6px">
|
|
79
|
-
<option value="0" selected>Off</option>
|
|
80
|
-
<option value="30">30s</option>
|
|
81
|
-
<option value="60">1m</option>
|
|
82
|
-
<option value="300">5m</option>
|
|
83
|
-
</select>
|
|
84
|
-
</label>
|
|
85
|
-
<button id="refreshDiscoverBtn" class="secondary" style="padding: 4px 9px; font-size: 11px">Refresh</button>
|
|
22
|
+
<div class="form-group">
|
|
23
|
+
<label for="secret">Secret:</label>
|
|
24
|
+
<input type="password" id="secret" placeholder="Enter secret" />
|
|
25
|
+
<div class="status" id="secretStatus"></div>
|
|
86
26
|
</div>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
27
|
+
</div>
|
|
28
|
+
<button id="saveBtn" onclick="saveCredentials()">Save</button>
|
|
29
|
+
<div id="saveStatus"></div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="card compact">
|
|
33
|
+
<h2>Discover Devices</h2>
|
|
34
|
+
<div style="margin-bottom: 6px">
|
|
35
|
+
<label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
|
|
36
|
+
<input type="checkbox" id="autoAddAllCheckbox" style="width: auto; margin: 0" />
|
|
37
|
+
<span>Auto-add all</span>
|
|
38
|
+
<span
|
|
39
|
+
title="When enabled, Discover immediately adds all found devices to your config. Existing devices are skipped automatically."
|
|
40
|
+
style="font-size: 11px; opacity: 0.7">ⓘ</span>
|
|
41
|
+
</label>
|
|
42
|
+
<div style="margin-top: 2px; margin-left: 22px; font-size: 10px; opacity: 0.75; line-height: 1.25">
|
|
43
|
+
Adds all discovered devices automatically; already configured devices are skipped.
|
|
104
44
|
</div>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
45
|
+
</div>
|
|
46
|
+
<div class="discovery-settings-row" style="margin-bottom: 2px">
|
|
47
|
+
<label id="bleScanSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
|
|
48
|
+
<span>Scan:</span>
|
|
49
|
+
<select id="bleScanDurationSelect" style="font-size: 11px; padding: 2px 6px">
|
|
50
|
+
<option value="3">3s</option>
|
|
51
|
+
<option value="5" selected>5s</option>
|
|
52
|
+
<option value="10">10s</option>
|
|
53
|
+
<option value="15">15s</option>
|
|
54
|
+
</select>
|
|
55
|
+
</label>
|
|
56
|
+
<label id="bleTimeoutSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
|
|
57
|
+
<span>Timeout:</span>
|
|
58
|
+
<input id="bleTimeoutInput" type="number" min="3" max="30" step="1" value="8"
|
|
59
|
+
style="width: 56px; font-size: 11px; padding: 2px 4px" />
|
|
60
|
+
<span>s</span>
|
|
61
|
+
</label>
|
|
62
|
+
<label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
|
|
63
|
+
<input type="checkbox" id="disableBleScanCheckbox" style="width: auto; margin: 0" />
|
|
64
|
+
<span>Disable BLE scan</span>
|
|
65
|
+
</label>
|
|
66
|
+
</div>
|
|
67
|
+
<div id="bluetoothStatus" style="margin-bottom: 4px; font-size: 10px; opacity: 0.8">Bluetooth: checking...</div>
|
|
68
|
+
<div class="discovery-settings-row" style="margin-bottom: 8px">
|
|
69
|
+
<div id="lastScannedStatus" style="font-size: 10px; opacity: 0.8; flex: 1 1 auto">Last scanned: never</div>
|
|
70
|
+
<label style="font-size: 11px">
|
|
71
|
+
Auto-refresh:
|
|
72
|
+
<select id="autoRefreshIntervalSelect" style="margin-left: 4px; font-size: 11px; padding: 2px 6px">
|
|
73
|
+
<option value="0" selected>Off</option>
|
|
74
|
+
<option value="30">30s</option>
|
|
75
|
+
<option value="60">1m</option>
|
|
76
|
+
<option value="300">5m</option>
|
|
77
|
+
</select>
|
|
78
|
+
</label>
|
|
79
|
+
<button id="refreshDiscoverBtn" class="secondary" style="padding: 4px 9px; font-size: 11px">Refresh</button>
|
|
80
|
+
</div>
|
|
81
|
+
<button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 8px">
|
|
82
|
+
🔍 Discover
|
|
83
|
+
</button>
|
|
84
|
+
<button id="cancelDiscoverBtn"
|
|
85
|
+
style="margin-left: 6px; margin-bottom: 8px; display: none; padding: 6px 10px; font-size: 11px" class="secondary">
|
|
86
|
+
Cancel
|
|
87
|
+
</button>
|
|
88
|
+
<div id="discoverStatus" style="margin-bottom: 6px; font-size: 11px"></div>
|
|
89
|
+
<div id="discoverDevicesFound" style="margin-bottom: 6px; font-size: 11px; display: none"></div>
|
|
90
|
+
<div id="discoverPhaseProgress" class="discover-phase-progress" style="display: none">
|
|
91
|
+
<div class="discover-phase-track">
|
|
92
|
+
<div id="discoverPhaseFill" class="discover-phase-fill"></div>
|
|
111
93
|
</div>
|
|
94
|
+
<div id="discoverPhaseLabel" class="discover-phase-label"></div>
|
|
95
|
+
</div>
|
|
96
|
+
<div id="discoveredList" style="display: none">
|
|
97
|
+
<h3 style="margin-top: 0; font-size: 12px; margin-bottom: 6px; font-weight: 600">Available Devices</h3>
|
|
98
|
+
<ul id="discoveredDevices" style="max-height: 300px; overflow-y: auto"></ul>
|
|
112
99
|
</div>
|
|
100
|
+
</div>
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
id="removeAllContainer"
|
|
121
|
-
style="
|
|
102
|
+
<div class="card compact">
|
|
103
|
+
<h2>Configured Devices</h2>
|
|
104
|
+
<div id="status">Loading…</div>
|
|
105
|
+
<ul id="devices"></ul>
|
|
106
|
+
<!-- BLE encryption key fields will be rendered dynamically per device in the UI (see app.ts/devices.js) -->
|
|
107
|
+
<div id="removeAllContainer" style="
|
|
122
108
|
display: none;
|
|
123
109
|
margin-top: 12px;
|
|
124
110
|
padding-top: 10px;
|
|
125
111
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
126
|
-
"
|
|
127
|
-
|
|
128
|
-
<button
|
|
129
|
-
id="removeAllBtn"
|
|
130
|
-
style="
|
|
112
|
+
">
|
|
113
|
+
<button id="removeAllBtn" style="
|
|
131
114
|
background: #dc2626;
|
|
132
115
|
width: 100%;
|
|
133
116
|
padding: 7px 12px;
|
|
134
117
|
font-size: 12px;
|
|
135
118
|
font-weight: 500;
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</button>
|
|
140
|
-
</div>
|
|
119
|
+
">
|
|
120
|
+
🗑️ Remove All Devices
|
|
121
|
+
</button>
|
|
141
122
|
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<script type="module" src="js/app.js"></script>
|
|
126
|
+
</body>
|
|
142
127
|
|
|
143
|
-
|
|
144
|
-
</body>
|
|
145
|
-
</html>
|
|
128
|
+
</html>
|
|
@@ -192,7 +192,7 @@ export async function addDevice(
|
|
|
192
192
|
deviceId: string,
|
|
193
193
|
name: string,
|
|
194
194
|
type: string,
|
|
195
|
-
options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string
|
|
195
|
+
options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string },
|
|
196
196
|
): Promise<any> {
|
|
197
197
|
const payload: any = { deviceId, name, type }
|
|
198
198
|
if (options?.address) {
|
|
@@ -210,9 +210,6 @@ export async function addDevice(
|
|
|
210
210
|
if (options?.keyId) {
|
|
211
211
|
payload.keyId = options.keyId
|
|
212
212
|
}
|
|
213
|
-
if (options?.matterType !== undefined) {
|
|
214
|
-
payload.matterType = options.matterType
|
|
215
|
-
}
|
|
216
213
|
uiLog.info('Adding device to config:', payload)
|
|
217
214
|
const resp = await homebridge.request('/add-device', payload)
|
|
218
215
|
uiLog.info('Add device response:', resp)
|
|
@@ -237,7 +234,6 @@ export async function updateDevice(
|
|
|
237
234
|
deviceId: string,
|
|
238
235
|
configDeviceName?: string,
|
|
239
236
|
configDeviceType?: string,
|
|
240
|
-
options?: { matterType?: number },
|
|
241
237
|
): Promise<any> {
|
|
242
238
|
const params: any = { deviceId }
|
|
243
239
|
if (configDeviceName) {
|
|
@@ -246,9 +242,6 @@ export async function updateDevice(
|
|
|
246
242
|
if (configDeviceType) {
|
|
247
243
|
params.configDeviceType = configDeviceType
|
|
248
244
|
}
|
|
249
|
-
if (options?.matterType !== undefined) {
|
|
250
|
-
params.matterType = options.matterType
|
|
251
|
-
}
|
|
252
245
|
uiLog.info('[Update Device] Sending update request with params:', params)
|
|
253
246
|
const resp = await homebridge.request('/update-device', params)
|
|
254
247
|
uiLog.info('[Update Device] Update response:', resp)
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { loadCredentialStatus, saveCredentials } from './credentials.js'
|
|
2
|
-
import {
|
|
3
|
-
// Wire up global event handlers
|
|
2
|
+
import { initRemoveAllButton, loadConfiguredDevices } from './devices.js'
|
|
4
3
|
import { discoverDevices, initializeDiscoverySettings } from './discovery.js';
|
|
5
4
|
|
|
6
|
-
(window as any).loadCredentialStatus = loadCredentialStatus
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
(window as any).loadCredentialStatus = loadCredentialStatus;
|
|
6
|
+
(window as any).saveCredentials = saveCredentials;
|
|
7
|
+
(window as any).discoverDevices = discoverDevices
|
|
9
8
|
|
|
10
9
|
// Initialize on page load
|
|
11
10
|
async function init(): Promise<void> {
|
|
12
11
|
await loadCredentialStatus()
|
|
13
12
|
await initializeDiscoverySettings()
|
|
14
|
-
|
|
13
|
+
await loadConfiguredDevices()
|
|
15
14
|
await initRemoveAllButton()
|
|
16
15
|
}
|
|
17
16
|
|