@switchbot/homebridge-switchbot 5.0.0-beta.34 → 5.0.0-beta.36
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 +760 -13806
- package/dist/platform-hap.d.ts +0 -5
- package/dist/platform-hap.d.ts.map +1 -1
- package/dist/platform-hap.js +25 -54
- package/dist/platform-hap.js.map +1 -1
- package/dist/platform-matter.d.ts +2 -8
- package/dist/platform-matter.d.ts.map +1 -1
- package/dist/platform-matter.js +16 -55
- package/dist/platform-matter.js.map +1 -1
- package/dist/settings.d.ts +12 -6
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js.map +1 -1
- package/dist/utils.d.ts +11 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +48 -43
- package/dist/utils.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +1 -1
- package/src/platform-hap.ts +35 -55
- package/src/platform-matter.ts +22 -56
- package/src/settings.ts +12 -2
- package/src/utils.ts +61 -49
package/src/platform-matter.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
WindowBlindAccessory,
|
|
43
43
|
} from './devices-matter/index.js'
|
|
44
44
|
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'
|
|
45
|
-
import { ApiRequestTracker,
|
|
45
|
+
import { ApiRequestTracker, applyDeviceTypeTemplates, createPlatformLogger, formatDeviceIdAsMac, hs2rgb, isSuccessfulStatusCode, makeBLESender, makeOpenAPISender, mergeByDeviceId, normalizeDeviceId, rgb2hs, sleep } from './utils.js'
|
|
46
46
|
|
|
47
47
|
export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
48
48
|
// Track restored HAP cached accessories (required for DynamicPlatformPlugin)
|
|
@@ -130,19 +130,8 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
130
130
|
|
|
131
131
|
this.debugLog('Finished initializing platform:', this.config.name)
|
|
132
132
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
if ((this.config as any).options) {
|
|
136
|
-
const cleaned = cleanDeviceConfig((this.config as any).options.deviceConfig)
|
|
137
|
-
if (cleaned) {
|
|
138
|
-
;(this.config as any).options.deviceConfig = cleaned
|
|
139
|
-
} else {
|
|
140
|
-
delete (this.config as any).options.deviceConfig
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} catch (e) {
|
|
144
|
-
this.debugLog('Failed to clean deviceConfig: %s', e)
|
|
145
|
-
}
|
|
133
|
+
// Note: deviceConfig and irdeviceConfig have been removed from the platform.
|
|
134
|
+
// All device-specific configuration should be done via options.devices arrays.
|
|
146
135
|
|
|
147
136
|
// Does the user have a version of Homebridge that is compatible with matter?
|
|
148
137
|
if (!this.api.isMatterAvailable?.()) {
|
|
@@ -419,56 +408,37 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
419
408
|
}
|
|
420
409
|
|
|
421
410
|
/**
|
|
422
|
-
* Merge
|
|
423
|
-
*
|
|
424
|
-
* @deprecated Use shared mergeByDeviceId from utils.js instead
|
|
425
|
-
*/
|
|
426
|
-
private mergeByDeviceId(a1: { deviceId: string }[], a2: any[]) {
|
|
427
|
-
const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
|
|
428
|
-
return mergeByDeviceId(a1, a2, allowConfigOnly)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Merge discovered devices with deviceConfig (per deviceType) and per-device overrides
|
|
433
|
-
* from `config.options.devices`, matching the behavior used in platform-hap.
|
|
411
|
+
* Merge discovered devices with per-device overrides from `config.options.devices`.
|
|
412
|
+
* Also applies device-type templates when applyToAllDevicesOfType is set.
|
|
434
413
|
*/
|
|
435
414
|
private async mergeDiscoveredDevices(discovered: device[]): Promise<any[]> {
|
|
436
|
-
// If there's no
|
|
437
|
-
if (!this.config.options?.devices
|
|
415
|
+
// If there's no per-device config, return discovered as-is
|
|
416
|
+
if (!this.config.options?.devices) {
|
|
438
417
|
return discovered
|
|
439
418
|
}
|
|
440
419
|
|
|
441
|
-
//
|
|
442
|
-
const
|
|
420
|
+
// Assign missing deviceType from configDeviceType if needed
|
|
421
|
+
const devicesWithTypeAssigned = discovered.map((deviceObj) => {
|
|
443
422
|
if (!deviceObj.deviceType) {
|
|
444
423
|
deviceObj.deviceType = (deviceObj as any).configDeviceType !== undefined ? (deviceObj as any).configDeviceType : 'Unknown'
|
|
445
424
|
this.debugLog(`API missing deviceType for ${deviceObj.deviceId}, using configDeviceType: ${(deviceObj as any).configDeviceType}`)
|
|
446
425
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}))
|
|
450
|
-
|
|
451
|
-
// Merge per-device overrides by matching deviceId
|
|
452
|
-
const merged = this.mergeByDeviceId(this.config.options?.devices ?? [], devicesWithTypeConfig ?? [])
|
|
426
|
+
return deviceObj
|
|
427
|
+
})
|
|
453
428
|
|
|
454
|
-
//
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
429
|
+
// Apply device-type templates from config entries with applyToAllDevicesOfType=true
|
|
430
|
+
const devicesWithTemplates = applyDeviceTypeTemplates(
|
|
431
|
+
devicesWithTypeAssigned,
|
|
432
|
+
this.config.options.devices,
|
|
433
|
+
'deviceType',
|
|
434
|
+
msg => this.debugLog(msg),
|
|
435
|
+
)
|
|
462
436
|
|
|
463
|
-
//
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
if (!userDeviceIds.has(this.normalizeDeviceId(d.deviceId))) {
|
|
467
|
-
final.push(d)
|
|
468
|
-
}
|
|
469
|
-
}
|
|
437
|
+
// Merge per-device overrides by matching deviceId
|
|
438
|
+
const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
|
|
439
|
+
const merged = mergeByDeviceId(this.config.options?.devices ?? [], devicesWithTemplates ?? [], allowConfigOnly)
|
|
470
440
|
|
|
471
|
-
return
|
|
441
|
+
return merged
|
|
472
442
|
}
|
|
473
443
|
|
|
474
444
|
/**
|
|
@@ -1714,10 +1684,6 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
1714
1684
|
* Register all Matter accessories
|
|
1715
1685
|
*/
|
|
1716
1686
|
private async registerMatterAccessories() {
|
|
1717
|
-
this.debugLog('═'.repeat(80))
|
|
1718
|
-
this.infoLog('Homebridge Matter Plugin')
|
|
1719
|
-
this.debugLog('═'.repeat(80))
|
|
1720
|
-
|
|
1721
1687
|
// Remove accessories that are disabled in config
|
|
1722
1688
|
await this.removeDisabledAccessories()
|
|
1723
1689
|
|
package/src/settings.ts
CHANGED
|
@@ -32,9 +32,7 @@ interface credentials {
|
|
|
32
32
|
|
|
33
33
|
export interface options {
|
|
34
34
|
devices?: devicesConfig[]
|
|
35
|
-
deviceConfig?: { [deviceType: string]: devicesConfig }
|
|
36
35
|
irdevices?: irDevicesConfig[]
|
|
37
|
-
irdeviceConfig?: { [remoteType: string]: irDevicesConfig }
|
|
38
36
|
allowInvalidCharacters?: boolean
|
|
39
37
|
// When true, devices declared in config.options.devices that are not
|
|
40
38
|
// discovered via the SwitchBot OpenAPI will still be included (config-only
|
|
@@ -100,6 +98,12 @@ export interface BaseDeviceConfig extends device {
|
|
|
100
98
|
mqttPubOptions?: IClientOptions
|
|
101
99
|
history?: boolean
|
|
102
100
|
webhook?: boolean
|
|
101
|
+
/**
|
|
102
|
+
* When true, applies this device's configuration to all other devices
|
|
103
|
+
* of the same deviceType/configDeviceType (e.g., all Humidifiers).
|
|
104
|
+
* Specific per-device settings will override these template settings.
|
|
105
|
+
*/
|
|
106
|
+
applyToAllDevicesOfType?: boolean
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
export interface botConfig extends BaseDeviceConfig {
|
|
@@ -259,6 +263,12 @@ export interface irBaseDeviceConfig extends irdevice {
|
|
|
259
263
|
logging?: string
|
|
260
264
|
customOn?: string
|
|
261
265
|
customOff?: string
|
|
266
|
+
/**
|
|
267
|
+
* When true, applies this IR device's configuration to all other IR devices
|
|
268
|
+
* of the same remoteType/configRemoteType (e.g., all IR Fans).
|
|
269
|
+
* Specific per-device settings will override these template settings.
|
|
270
|
+
*/
|
|
271
|
+
applyToAllDevicesOfType?: boolean
|
|
262
272
|
customize?: boolean
|
|
263
273
|
commandType?: string
|
|
264
274
|
disablePushOn?: boolean
|
package/src/utils.ts
CHANGED
|
@@ -672,55 +672,6 @@ export function m2hs(m) {
|
|
|
672
672
|
return [Math.round(toReturn[1]), Math.round(toReturn[0])]
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
-
/**
|
|
676
|
-
* Remove deviceConfig entries that contain only default/empty values.
|
|
677
|
-
* Returns undefined if nothing meaningful remains.
|
|
678
|
-
*/
|
|
679
|
-
export function cleanDeviceConfig(deviceConfig?: Record<string, Record<string, any>>): Record<string, Record<string, any>> | undefined {
|
|
680
|
-
if (!deviceConfig || typeof deviceConfig !== 'object') {
|
|
681
|
-
return undefined
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
const cleaned: Record<string, Record<string, any>> = {}
|
|
685
|
-
|
|
686
|
-
for (const [deviceName, cfg] of Object.entries(deviceConfig)) {
|
|
687
|
-
if (!cfg || typeof cfg !== 'object') {
|
|
688
|
-
continue
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const hasMeaningful = Object.values(cfg).some((v) => {
|
|
692
|
-
if (v === null || v === undefined) {
|
|
693
|
-
return false
|
|
694
|
-
}
|
|
695
|
-
if (typeof v === 'boolean') {
|
|
696
|
-
return v === true
|
|
697
|
-
}
|
|
698
|
-
if (typeof v === 'string') {
|
|
699
|
-
return v.trim().length > 0
|
|
700
|
-
}
|
|
701
|
-
if (typeof v === 'number') {
|
|
702
|
-
return Number.isFinite(v)
|
|
703
|
-
}
|
|
704
|
-
if (Array.isArray(v)) {
|
|
705
|
-
return v.length > 0
|
|
706
|
-
}
|
|
707
|
-
if (typeof v === 'object') {
|
|
708
|
-
return Object.keys(v).length > 0
|
|
709
|
-
}
|
|
710
|
-
return true
|
|
711
|
-
})
|
|
712
|
-
|
|
713
|
-
if (hasMeaningful) {
|
|
714
|
-
cleaned[deviceName] = cfg
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (Object.keys(cleaned).length > 0) {
|
|
719
|
-
return cleaned
|
|
720
|
-
}
|
|
721
|
-
return undefined
|
|
722
|
-
}
|
|
723
|
-
|
|
724
675
|
/**
|
|
725
676
|
* Factory that returns a function to send OpenAPI commands using a retry wrapper.
|
|
726
677
|
*
|
|
@@ -1157,6 +1108,67 @@ export function mergeByDeviceId(a1: { deviceId: string }[], a2: any[], allowConf
|
|
|
1157
1108
|
return result
|
|
1158
1109
|
}
|
|
1159
1110
|
|
|
1111
|
+
/**
|
|
1112
|
+
* Apply device-type or remote-type templates to an array of devices.
|
|
1113
|
+
* Templates are config entries with applyToAllDevicesOfType=true.
|
|
1114
|
+
*
|
|
1115
|
+
* @param devices - Array of devices to apply templates to
|
|
1116
|
+
* @param configDevices - User config array that may contain template entries
|
|
1117
|
+
* @param typeKey - Property name to match device types ('deviceType' for devices, 'remoteType' for IR devices)
|
|
1118
|
+
* @param debugLog - Optional debug logging function
|
|
1119
|
+
* @returns Array of devices with templates applied
|
|
1120
|
+
*/
|
|
1121
|
+
export function applyDeviceTypeTemplates(
|
|
1122
|
+
devices: any[],
|
|
1123
|
+
configDevices: any[],
|
|
1124
|
+
typeKey: string,
|
|
1125
|
+
debugLog?: (message: string) => void,
|
|
1126
|
+
): any[] {
|
|
1127
|
+
// Build a map of device-type templates from config devices with applyToAllDevicesOfType=true
|
|
1128
|
+
const typeTemplates = new Map<string, any>()
|
|
1129
|
+
|
|
1130
|
+
for (const configDevice of configDevices || []) {
|
|
1131
|
+
if (configDevice.applyToAllDevicesOfType) {
|
|
1132
|
+
// Get the type value from multiple possible sources
|
|
1133
|
+
const deviceType = configDevice[typeKey] || (configDevice as any).configDeviceType || (configDevice as any).configRemoteType
|
|
1134
|
+
if (!deviceType) {
|
|
1135
|
+
continue
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Store all config properties except deviceId and applyToAllDevicesOfType flag
|
|
1139
|
+
const template: any = { ...configDevice }
|
|
1140
|
+
delete template.deviceId
|
|
1141
|
+
delete template.applyToAllDevicesOfType
|
|
1142
|
+
|
|
1143
|
+
typeTemplates.set(deviceType, template)
|
|
1144
|
+
if (debugLog) {
|
|
1145
|
+
debugLog(`Device type template found for '${deviceType}': ${JSON.stringify(template)}`)
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// If no templates found, return original array
|
|
1151
|
+
if (typeTemplates.size === 0) {
|
|
1152
|
+
return devices
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Apply templates to devices
|
|
1156
|
+
return devices.map((device) => {
|
|
1157
|
+
const deviceType = device[typeKey] || (device as any).configDeviceType || (device as any).configRemoteType
|
|
1158
|
+
const template = typeTemplates.get(deviceType)
|
|
1159
|
+
|
|
1160
|
+
if (template) {
|
|
1161
|
+
if (debugLog) {
|
|
1162
|
+
debugLog(`Applying device type template to ${device.deviceId} (${deviceType})`)
|
|
1163
|
+
}
|
|
1164
|
+
// Template settings go first, then device data (device data takes precedence)
|
|
1165
|
+
return Object.assign({}, template, device)
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return device
|
|
1169
|
+
})
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1160
1172
|
/**
|
|
1161
1173
|
* Check if an API status code indicates success
|
|
1162
1174
|
*/
|