@switchbot/homebridge-switchbot 5.0.0-beta.33 → 5.0.0-beta.35

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.
@@ -10,7 +10,7 @@
10
10
 
11
11
  import type { API, EndpointType, Logger, MatterAccessory } from 'homebridge'
12
12
 
13
- import { rgb2hs } from '../utils.js'
13
+ import { deviceLoggingEnabled, deviceLoggingIsDebug, logDeviceStatusCode, rgb2hs } from '../utils.js'
14
14
 
15
15
  export interface BaseMatterAccessoryConfig {
16
16
  uuid: string
@@ -139,20 +139,28 @@ export abstract class BaseMatterAccessory implements MatterAccessory {
139
139
  */
140
140
  public async loggingIsDebug(): Promise<boolean> {
141
141
  const ctx: any = this.context as any
142
- const deviceLogging = ctx?.deviceLogging
143
- // deviceLogging may be a string ('debug', 'debugMode', 'standard') or undefined
144
- return deviceLogging === 'debugMode' || deviceLogging === 'debug'
142
+ return deviceLoggingIsDebug(ctx?.deviceLogging)
145
143
  }
146
144
 
147
145
  public async enablingDeviceLogging(): Promise<boolean> {
148
146
  const ctx: any = this.context as any
149
- const deviceLogging = ctx?.deviceLogging
150
- // If deviceLogging isn't provided, fall back to the platform-wide flag
151
- // that indicates whether platform logging is enabled.
152
- if (deviceLogging === undefined) {
153
- return Boolean((ctx as any)?.platformLogging)
154
- }
155
- return deviceLogging === 'debugMode' || deviceLogging === 'debug' || deviceLogging === 'standard'
147
+ return deviceLoggingEnabled(ctx?.deviceLogging, ctx?.platformLogging)
148
+ }
149
+
150
+ /**
151
+ * Status code logging using shared helper
152
+ */
153
+ public async statusCode(statusCode: number, deviceId?: string, hubDeviceId?: string): Promise<void> {
154
+ await logDeviceStatusCode(
155
+ statusCode,
156
+ {
157
+ debugLog: this.logDebug.bind(this),
158
+ errorLog: this.logError.bind(this),
159
+ infoLog: this.logInfo.bind(this),
160
+ },
161
+ deviceId ?? (this.context as any)?.deviceId,
162
+ hubDeviceId ?? (this.context as any)?.hubDeviceId,
163
+ )
156
164
  }
157
165
 
158
166
  /**
@@ -55,7 +55,7 @@ import { TV } from './irdevice/tv.js'
55
55
  import { VacuumCleaner } from './irdevice/vacuumcleaner.js'
56
56
  import { WaterHeater } from './irdevice/waterheater.js'
57
57
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'
58
- import { ApiRequestTracker, cleanDeviceConfig, createPlatformLogger, formatDeviceIdAsMac, isBlindTiltDevice, isCurtainDevice, isSuccessfulStatusCode, logStatusCode, mergeByDeviceId, safeStringify, sleep } from './utils.js'
58
+ import { ApiRequestTracker, createPlatformLogger, formatDeviceIdAsMac, isBlindTiltDevice, isCurtainDevice, isSuccessfulStatusCode, logStatusCode, mergeByDeviceId, safeStringify, sleep } from './utils.js'
59
59
 
60
60
  /**
61
61
  * HomebridgePlatform
@@ -161,20 +161,8 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
161
161
  // swallow any logging errors — diagnostics are best-effort
162
162
  }
163
163
 
164
- // Normalize deviceConfig to remove UI-inserted defaults (lots of false/empty values)
165
- try {
166
- if ((this.config as any).options) {
167
- const cleaned = cleanDeviceConfig((this.config as any).options.deviceConfig)
168
- if (cleaned) {
169
- ;(this.config as any).options.deviceConfig = cleaned
170
- } else {
171
- // remove the empty deviceConfig so downstream checks treat it as absent
172
- delete (this.config as any).options.deviceConfig
173
- }
174
- }
175
- } catch (e) {
176
- this.debugErrorLog(`Failed to clean deviceConfig: ${e}`)
177
- }
164
+ // Note: deviceConfig and irdeviceConfig have been removed from the platform.
165
+ // All device-specific configuration should be done via options.devices and options.irdevices arrays.
178
166
 
179
167
  // Plugin Configuration
180
168
  this.getPlatformLogSettings()
@@ -552,7 +540,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
552
540
  }
553
541
 
554
542
  private async handleDevices(deviceLists: any[]) {
555
- if (!this.config.options?.devices && !this.config.options?.deviceConfig) {
543
+ if (!this.config.options?.devices) {
556
544
  this.debugLog(`SwitchBot Device Config Not Set: ${JSON.stringify(this.config.options?.devices)}`)
557
545
  if (deviceLists.length === 0) {
558
546
  this.debugLog('SwitchBot API Has No Devices With Cloud Services Enabled')
@@ -566,80 +554,62 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
566
554
  }
567
555
  }
568
556
  }
569
- } else if (this.config.options?.devices || this.config.options?.deviceConfig) {
557
+ } else {
570
558
  this.debugLog(`SwitchBot Device Config Set: ${JSON.stringify(this.config.options?.devices)}`)
571
559
 
572
- // Step 1: Check and assign configDeviceType to deviceType if deviceType is not present
573
- const devicesWithTypeConfigPromises = deviceLists.map(async (device) => {
560
+ // Check and assign configDeviceType to deviceType if deviceType is not present
561
+ const devicesWithTypeAssigned = deviceLists.map((device) => {
574
562
  if (!device.deviceType) {
575
563
  device.deviceType = device.configDeviceType !== undefined ? device.configDeviceType : 'Unknown'
576
564
  this.warnLog(`API is displaying no deviceType: ${device.deviceType}, So using configDeviceType: ${device.configDeviceType}`)
577
565
  }
578
-
579
- // Retrieve deviceTypeConfig for each device and merge it
580
- const deviceTypeConfig = this.config.options?.deviceConfig?.[device.deviceType] || {}
581
- return Object.assign({}, device, deviceTypeConfig)
566
+ return device
582
567
  })
583
568
 
584
- // Wait for all promises to resolve
585
- const devicesWithTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null) // Filter out skipped devices
586
-
587
- const devices = this.mergeByDeviceId(this.config.options.devices ?? [], devicesWithTypeConfig ?? [])
569
+ const devices = this.mergeByDeviceId(this.config.options.devices ?? [], devicesWithTypeAssigned ?? [])
588
570
 
589
571
  this.debugLog(`SwitchBot Devices: ${JSON.stringify(devices)}`)
590
572
 
591
573
  for (const device of devices) {
592
- const deviceIdConfig = this.config.options?.devices?.[device.deviceId] || {}
593
- const deviceWithConfig = Object.assign({}, device, deviceIdConfig)
594
-
595
574
  if (device.configDeviceName) {
596
575
  device.deviceName = device.configDeviceName
597
576
  }
598
- // Pass the merged device object to createDevice
599
- await this.createDevice(deviceWithConfig)
577
+ await this.createDevice(device)
600
578
  }
601
579
  }
602
580
  }
603
581
 
604
582
  private async handleIRDevices(irDeviceLists: any[]) {
605
- if (!this.config.options?.irdevices && !this.config.options?.irdeviceConfig) {
583
+ if (!this.config.options?.irdevices) {
606
584
  this.debugLog(`IR Device Config Not Set: ${JSON.stringify(this.config.options?.irdevices)}`)
607
585
  for (const device of irDeviceLists) {
608
586
  if (device.remoteType) {
609
587
  await this.createIRDevice(device)
610
588
  }
611
589
  }
612
- } else if (this.config.options?.irdevices || this.config.options?.irdeviceConfig) {
590
+ } else {
613
591
  this.debugLog(`IR Device Config Set: ${JSON.stringify(this.config.options?.irdevices)}`)
614
592
 
615
- // Step 1: Check and assign configRemoteType to remoteType if remoteType is not present
616
- const devicesWithTypeConfigPromises = irDeviceLists.map(async (device) => {
593
+ // Check and assign configRemoteType to remoteType if remoteType is not present
594
+ const devicesWithTypeAssigned = irDeviceLists.map((device) => {
617
595
  if (!device.remoteType && device.configRemoteType) {
618
596
  device.remoteType = device.configRemoteType
619
597
  this.warnLog(`API is displaying no remoteType: ${device.remoteType}, So using configRemoteType: ${device.configRemoteType}`)
620
598
  } else if (!device.remoteType && !device.configDeviceName) {
621
599
  this.errorLog('No remoteType or configRemoteType for device. No device will be created.')
622
- return null // Skip this device
600
+ return null
623
601
  }
602
+ return device
603
+ }).filter(device => device !== null) // Filter out skipped devices
624
604
 
625
- // Retrieve remoteTypeConfig for each device and merge it
626
- const remoteTypeConfig = this.config.options?.irdeviceConfig?.[device.remoteType] || {}
627
- return Object.assign({}, device, remoteTypeConfig)
628
- })
629
- // Wait for all promises to resolve
630
- const devicesWithRemoteTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null) // Filter out skipped devices
631
-
632
- const devices = this.mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithRemoteTypeConfig ?? [])
605
+ const devices = this.mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithTypeAssigned ?? [])
633
606
 
634
607
  this.debugLog(`IR Devices: ${JSON.stringify(devices)}`)
635
608
  for (const device of devices) {
636
- const irdeviceIdConfig = this.config.options?.irdevices?.[device.deviceId] || {}
637
- const irdeviceWithConfig = Object.assign({}, device, irdeviceIdConfig)
638
-
639
609
  if (device.configDeviceName) {
640
610
  device.deviceName = device.configDeviceName
641
611
  }
642
- await this.createIRDevice(irdeviceWithConfig)
612
+ await this.createIRDevice(device)
643
613
  }
644
614
  }
645
615
  }
@@ -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, 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?.()) {
@@ -429,46 +418,27 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
429
418
  }
430
419
 
431
420
  /**
432
- * Merge discovered devices with deviceConfig (per deviceType) and per-device overrides
433
- * from `config.options.devices`, matching the behavior used in platform-hap.
421
+ * Merge discovered devices with per-device overrides from `config.options.devices`
434
422
  */
435
423
  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) {
424
+ // If there's no per-device config, return discovered as-is
425
+ if (!this.config.options?.devices) {
438
426
  return discovered
439
427
  }
440
428
 
441
- // Step 1: Assign missing deviceType from configDeviceType and merge deviceType-level configs
442
- const devicesWithTypeConfig = await Promise.all(discovered.map(async (deviceObj) => {
429
+ // Assign missing deviceType from configDeviceType if needed
430
+ const devicesWithTypeAssigned = discovered.map((deviceObj) => {
443
431
  if (!deviceObj.deviceType) {
444
432
  deviceObj.deviceType = (deviceObj as any).configDeviceType !== undefined ? (deviceObj as any).configDeviceType : 'Unknown'
445
433
  this.debugLog(`API missing deviceType for ${deviceObj.deviceId}, using configDeviceType: ${(deviceObj as any).configDeviceType}`)
446
434
  }
447
- const deviceTypeConfig = this.config.options?.deviceConfig?.[deviceObj.deviceType] || {}
448
- return Object.assign({}, deviceObj, deviceTypeConfig)
449
- }))
435
+ return deviceObj
436
+ })
450
437
 
451
438
  // Merge per-device overrides by matching deviceId
452
- const merged = this.mergeByDeviceId(this.config.options?.devices ?? [], devicesWithTypeConfig ?? [])
453
-
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
- }
462
-
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
- }
439
+ const merged = this.mergeByDeviceId(this.config.options?.devices ?? [], devicesWithTypeAssigned ?? [])
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
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
  *
@@ -1205,3 +1156,85 @@ export async function logStatusCode(statusCode: number, log: {
1205
1156
  await log.errorLog(message)
1206
1157
  }
1207
1158
  }
1159
+
1160
+ /**
1161
+ * Shared device logging helpers
1162
+ */
1163
+
1164
+ /**
1165
+ * Check if device logging is in debug mode
1166
+ */
1167
+ export function deviceLoggingIsDebug(deviceLogging?: string): boolean {
1168
+ return deviceLogging === 'debugMode' || deviceLogging === 'debug'
1169
+ }
1170
+
1171
+ /**
1172
+ * Check if device logging is enabled
1173
+ */
1174
+ export function deviceLoggingEnabled(deviceLogging?: string, platformLogging?: string): boolean {
1175
+ // If deviceLogging isn't provided, fall back to platform-wide flag
1176
+ if (deviceLogging === undefined || deviceLogging === '') {
1177
+ return platformLogging === 'debugMode' || platformLogging === 'debug' || platformLogging === 'standard'
1178
+ }
1179
+ return deviceLogging === 'debugMode' || deviceLogging === 'debug' || deviceLogging === 'standard'
1180
+ }
1181
+
1182
+ /**
1183
+ * Device status code handler with comprehensive messages
1184
+ */
1185
+ export interface DeviceStatusCodeLogger {
1186
+ debugLog: (...args: any[]) => void | Promise<void>
1187
+ debugErrorLog?: (...args: any[]) => void | Promise<void>
1188
+ errorLog: (...args: any[]) => void | Promise<void>
1189
+ infoLog?: (...args: any[]) => void | Promise<void>
1190
+ }
1191
+
1192
+ export async function logDeviceStatusCode(
1193
+ statusCode: number,
1194
+ log: DeviceStatusCodeLogger,
1195
+ deviceId?: string,
1196
+ hubDeviceId?: string,
1197
+ ): Promise<void> {
1198
+ let adjustedStatusCode = statusCode
1199
+
1200
+ // Handle special case where device is its own hub
1201
+ if (statusCode === 171 && hubDeviceId && deviceId && (hubDeviceId === deviceId || hubDeviceId === '000000000000')) {
1202
+ if (log.debugErrorLog) {
1203
+ log.debugErrorLog(`statusCode 171 changed to 161: hubDeviceId ${hubDeviceId} matches deviceId ${deviceId}, device is its own hub.`)
1204
+ }
1205
+ adjustedStatusCode = 161
1206
+ }
1207
+
1208
+ const statusMessages: { [key: number]: string } = {
1209
+ 151: 'Command not supported by this device type',
1210
+ 152: 'Device not found',
1211
+ 160: 'Command is not supported',
1212
+ 161: 'Device is offline',
1213
+ 171: hubDeviceId ? `Hub Device is offline. Hub: ${hubDeviceId}` : 'Hub Device is offline',
1214
+ 190: 'Device internal error due to device states not synchronized with server, or command format is invalid',
1215
+ 100: 'Command successfully sent',
1216
+ 200: 'Request successful',
1217
+ 400: 'Bad Request, an invalid payload request',
1218
+ 401: 'Unauthorized, Authorization for the API is required, but the request has not been authenticated',
1219
+ 403: 'Forbidden, The request has been authenticated but does not have appropriate permissions, or a requested resource is not found',
1220
+ 404: 'Not Found, Specifies the requested path does not exist',
1221
+ 406: 'Not Acceptable, a MIME type has been requested via the Accept header for a value not supported by the server',
1222
+ 415: 'Unsupported Media Type, a contentType header has been defined that is not supported by the server',
1223
+ 422: 'Unprocessable Entity: The server cannot process the request, often due to exceeded API limits.',
1224
+ 429: 'Too Many Requests, exceeded the number of requests allowed for a given time window',
1225
+ 500: 'Internal Server Error, An unexpected error occurred. These errors should be rare',
1226
+ }
1227
+
1228
+ const logMessage = statusMessages[adjustedStatusCode] || `Unknown statusCode: ${adjustedStatusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`
1229
+ const fullMessage = `${logMessage}, statusCode: ${adjustedStatusCode}`
1230
+
1231
+ if ([100, 200].includes(adjustedStatusCode)) {
1232
+ await log.debugLog(fullMessage)
1233
+ } else if (statusMessages[adjustedStatusCode]) {
1234
+ await log.errorLog(fullMessage)
1235
+ } else if (log.infoLog) {
1236
+ await log.infoLog(fullMessage)
1237
+ } else {
1238
+ await log.errorLog(fullMessage)
1239
+ }
1240
+ }