@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.
@@ -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, cleanDeviceConfig, createPlatformLogger, formatDeviceIdAsMac, hs2rgb, isSuccessfulStatusCode, makeBLESender, makeOpenAPISender, mergeByDeviceId, normalizeDeviceId, rgb2hs, sleep } from './utils.js'
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
- // Normalize deviceConfig to remove UI-inserted defaults
134
- try {
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 two arrays by deviceId. For each item in a1 (user-provided devices list),
423
- * find matching item in a2 (discovered devices) and merge them with user overrides last.
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 device config or per-device config, return discovered as-is
437
- if (!this.config.options?.devices && !this.config.options?.deviceConfig) {
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
- // Step 1: Assign missing deviceType from configDeviceType and merge deviceType-level configs
442
- const devicesWithTypeConfig = await Promise.all(discovered.map(async (deviceObj) => {
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
- const deviceTypeConfig = this.config.options?.deviceConfig?.[deviceObj.deviceType] || {}
448
- return Object.assign({}, deviceObj, deviceTypeConfig)
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
- // For any entries in merged (which are based on config.options.devices), ensure final per-device merges include deviceId-specific config
455
- const final: any[] = []
456
- for (const device of merged) {
457
- // Find per-device config entry by deviceId (config.options.devices is an array)
458
- const deviceIdConfig = (this.config.options?.devices || []).find((d: any) => this.normalizeDeviceId(d.deviceId) === this.normalizeDeviceId(device.deviceId)) || {}
459
- const deviceWithConfig = Object.assign({}, device, deviceIdConfig)
460
- final.push(deviceWithConfig)
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
- // Also include any discovered devices that weren't present in the user devices list
464
- const userDeviceIds = new Set((this.config.options?.devices || []).map((d: any) => this.normalizeDeviceId(d.deviceId)))
465
- for (const d of devicesWithTypeConfig) {
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 final
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
  */