@switchbot/homebridge-switchbot 5.0.0-beta.20 → 5.0.0-beta.22

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.
@@ -0,0 +1,101 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { SwitchBotMatterPlatform } from './platform-matter.js'
4
+ import { formatDeviceIdAsMac } from './utils.js'
5
+
6
+ describe('platform-matter lifecycle cleanup', () => {
7
+ it('clearDeviceResources removes timers, instances and BLE handler entries', async () => {
8
+ // Setup stubbed API and logs
9
+ const handlers: Record<string, (...args: any[]) => any> = {}
10
+ const api: any = {
11
+ matter: {
12
+ uuid: { generate: (s: string) => `uuid-${s}` },
13
+ registerPlatformAccessories: vi.fn(),
14
+ unregisterPlatformAccessories: vi.fn(),
15
+ clusterNames: { OnOff: 'OnOff' },
16
+ },
17
+ isMatterAvailable: () => true,
18
+ isMatterEnabled: () => true,
19
+ on: (ev: string, fn: (...args: any[]) => any) => { handlers[ev] = fn },
20
+ _handlers: handlers,
21
+ }
22
+
23
+ const log = { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() }
24
+
25
+ const platform = new SwitchBotMatterPlatform(log as any, {} as any, api)
26
+
27
+ // Insert a fake timer, accessory instance, and BLE handler
28
+ const deviceId = 'AA:BB:CC:11:22:33'
29
+ const nid = (platform as any).normalizeDeviceId(deviceId)
30
+
31
+ const timer = setInterval(() => {}, 100000)
32
+ ;(platform as any).refreshTimers.set(nid, timer)
33
+ ;(platform as any).accessoryInstances.set(nid, { dummy: true })
34
+ ;(platform as any).bleEventHandler[deviceId.toLowerCase()] = () => {}
35
+
36
+ // Ensure they exist prior
37
+ expect((platform as any).refreshTimers.get(nid)).toBeDefined()
38
+ expect((platform as any).accessoryInstances.get(nid)).toBeDefined()
39
+ expect((platform as any).bleEventHandler[deviceId.toLowerCase()]).toBeDefined()
40
+
41
+ // Call the private helper
42
+ ;(platform as any).clearDeviceResources(deviceId)
43
+
44
+ // Now they should be removed
45
+ expect((platform as any).refreshTimers.get(nid)).toBeUndefined()
46
+ expect((platform as any).accessoryInstances.get(nid)).toBeUndefined()
47
+ expect((platform as any).bleEventHandler[deviceId.toLowerCase()]).toBeUndefined()
48
+ })
49
+
50
+ it('shutdown handler clears all timers and handlers when invoked', async () => {
51
+ // Setup stubbed API and logs
52
+ const handlers: Record<string, (...args: any[]) => any> = {}
53
+ const api: any = {
54
+ matter: {
55
+ uuid: { generate: (s: string) => `uuid-${s}` },
56
+ registerPlatformAccessories: vi.fn(),
57
+ unregisterPlatformAccessories: vi.fn(),
58
+ clusterNames: { OnOff: 'OnOff' },
59
+ },
60
+ isMatterAvailable: () => true,
61
+ isMatterEnabled: () => true,
62
+ on: (ev: string, fn: (...args: any[]) => any) => { handlers[ev] = fn },
63
+ _handlers: handlers,
64
+ }
65
+
66
+ const log = { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() }
67
+
68
+ const platform = new SwitchBotMatterPlatform(log as any, {} as any, api)
69
+
70
+ // Add two timers to the platform (using normalized ids)
71
+ const ids = ['devA', 'devB']
72
+ for (const id of ids) {
73
+ const nid = (platform as any).normalizeDeviceId(id)
74
+ const t = setInterval(() => {}, 100000)
75
+ ;(platform as any).refreshTimers.set(nid, t)
76
+ ;(platform as any).accessoryInstances.set(nid, { dummy: true })
77
+ try {
78
+ const mac = formatDeviceIdAsMac(id).toLowerCase()
79
+ ;(platform as any).bleEventHandler[mac] = () => {}
80
+ } catch {
81
+ // ignore formatting errors in this test
82
+ }
83
+ }
84
+
85
+ // Invoke didFinishLaunching to ensure the platform registered its shutdown handler
86
+ await Promise.resolve(api._handlers.didFinishLaunching?.())
87
+
88
+ // Shutdown handler should now be registered on api._handlers.shutdown
89
+ expect(typeof api._handlers.shutdown).toBe('function')
90
+
91
+ // Call the shutdown handler
92
+ await Promise.resolve(api._handlers.shutdown())
93
+
94
+ // All refresh timers should be cleared
95
+ for (const id of ids) {
96
+ const nid = (platform as any).normalizeDeviceId(id)
97
+ expect((platform as any).refreshTimers.get(nid)).toBeUndefined()
98
+ expect((platform as any).accessoryInstances.get(nid)).toBeUndefined()
99
+ }
100
+ })
101
+ })
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { SwitchBotMatterPlatform } from './platform-matter.js'
4
+
5
+ describe('platform-matter mapping helper', () => {
6
+ it('prefers accessory instance update methods for battery and falls back to api.matter.updateAccessoryState', async () => {
7
+ const updateAccessoryState = vi.fn()
8
+ const api: any = {
9
+ matter: {
10
+ uuid: { generate: (s: string) => `uuid-${s}` },
11
+ updateAccessoryState,
12
+ clusterNames: { OnOff: 'OnOff', LevelControl: 'LevelControl', ColorControl: 'ColorControl', PowerSource: 'powerSource' },
13
+ },
14
+ isMatterAvailable: () => true,
15
+ isMatterEnabled: () => true,
16
+ on: () => {},
17
+ }
18
+
19
+ const log: any = { info: vi.fn(), warn: vi.fn(), debug: vi.fn(), error: vi.fn() }
20
+ const platform = new SwitchBotMatterPlatform(log as any, {} as any, api)
21
+
22
+ // Create a fake accessory instance with updateBatteryPercentage
23
+ const deviceId = 'DEV123'
24
+ const nid = (platform as any).normalizeDeviceId(deviceId)
25
+ const fakeInstance = { updateBatteryPercentage: vi.fn() }
26
+ ;(platform as any).accessoryInstances.set(nid, fakeInstance)
27
+
28
+ // Call the private helper with different battery field names
29
+ await (platform as any).applyStatusToAccessory('uuid-test', { deviceId } as any, { batteryPercentage: 55 })
30
+ expect(fakeInstance.updateBatteryPercentage).toHaveBeenCalled()
31
+
32
+ // Remove instance to force fallback
33
+ ;(platform as any).accessoryInstances.delete(nid)
34
+ await (platform as any).applyStatusToAccessory('uuid-test', { deviceId } as any, { battery: 30 })
35
+ expect(updateAccessoryState).toHaveBeenCalled()
36
+ })
37
+
38
+ it('handles temperature and humidity synonyms', async () => {
39
+ const updateAccessoryState = vi.fn()
40
+ const api: any = {
41
+ matter: {
42
+ uuid: { generate: (s: string) => `uuid-${s}` },
43
+ updateAccessoryState,
44
+ clusterNames: {},
45
+ },
46
+ isMatterAvailable: () => true,
47
+ isMatterEnabled: () => true,
48
+ on: () => {},
49
+ }
50
+
51
+ const log: any = { info: vi.fn(), warn: vi.fn(), debug: vi.fn(), error: vi.fn() }
52
+ const platform = new SwitchBotMatterPlatform(log as any, {} as any, api)
53
+
54
+ const deviceId = 'DEV-TEMP'
55
+ await (platform as any).applyStatusToAccessory('uuid-temp', { deviceId } as any, { temp: 21.5, humid: 48 })
56
+
57
+ // Expect updateAccessoryState to have been called for temperature and humidity
58
+ expect(updateAccessoryState).toHaveBeenCalled()
59
+ })
60
+ })
@@ -978,6 +978,12 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
978
978
  if (c.includes(':')) {
979
979
  const parts = c.split(':').map(Number)
980
980
  ;[r, g, b] = parts
981
+ } else if (c.includes(',')) {
982
+ const parts = c.split(',').map(s => Number(s.trim()))
983
+ ;[r, g, b] = parts
984
+ } else if (c.includes(' ')) {
985
+ const parts = c.split(' ').map(s => Number(s.trim()))
986
+ ;[r, g, b] = parts
981
987
  } else if (c.startsWith('#')) {
982
988
  const hex = c.replace('#', '')
983
989
  r = Number.parseInt(hex.substring(0, 2), 16)
@@ -997,6 +1003,85 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
997
1003
  result.battery = Number(battery)
998
1004
  }
999
1005
 
1006
+ // VOC / TVOC (some air quality devices report total volatile organic compounds)
1007
+ const voc = sd.voc ?? sd.tvoc
1008
+ if (voc !== undefined) {
1009
+ result.voc = Number(voc)
1010
+ }
1011
+
1012
+ // PM10 (some devices report PM10 alongside PM2.5)
1013
+ const pm10 = sd.pm10
1014
+ if (pm10 !== undefined) {
1015
+ result.pm10 = Number(pm10)
1016
+ }
1017
+
1018
+ // PM2.5 (some BLE adverts use pm25 / pm_2_5)
1019
+ const pm25 = sd.pm2_5 ?? sd.pm25 ?? sd.pm_2_5
1020
+ if (pm25 !== undefined) {
1021
+ result.pm25 = Number(pm25)
1022
+ }
1023
+
1024
+ // CO2 (carbon dioxide ppm)
1025
+ const co2 = sd.co2 ?? sd.co2ppm ?? sd.carbonDioxide
1026
+ if (co2 !== undefined) {
1027
+ result.co2 = Number(co2)
1028
+ }
1029
+
1030
+ // Temperature (C) and Humidity (%) — support common shorthand keys
1031
+ const temperature = sd.temperature ?? sd.temp ?? sd.t
1032
+ if (temperature !== undefined) {
1033
+ result.temperature = Number(temperature)
1034
+ }
1035
+
1036
+ const humidity = sd.humidity ?? sd.h ?? sd.humid
1037
+ if (humidity !== undefined) {
1038
+ result.humidity = Number(humidity)
1039
+ }
1040
+
1041
+ // Motion, Contact, Leak
1042
+ const motion = sd.motion ?? sd.m
1043
+ if (motion !== undefined) {
1044
+ result.motion = Boolean(motion)
1045
+ }
1046
+
1047
+ const contact = sd.contact ?? sd.open
1048
+ if (contact !== undefined) {
1049
+ result.contact = contact
1050
+ }
1051
+
1052
+ const leak = sd.leak ?? sd.water
1053
+ if (leak !== undefined) {
1054
+ result.leak = Boolean(leak)
1055
+ }
1056
+
1057
+ // Position / Cover / Curtain synonyms
1058
+ const position = sd.position ?? sd.percent ?? sd.slidePosition ?? sd.curtainPosition
1059
+ if (position !== undefined) {
1060
+ result.position = Number(position)
1061
+ }
1062
+
1063
+ // Fan speed/speed
1064
+ const fanSpeed = sd.fanSpeed ?? sd.speed
1065
+ if (fanSpeed !== undefined) {
1066
+ result.fanSpeed = Number(fanSpeed)
1067
+ }
1068
+
1069
+ // Lock state
1070
+ const lock = sd.lock
1071
+ if (lock !== undefined) {
1072
+ result.lock = lock
1073
+ }
1074
+
1075
+ // Robot vacuum fields
1076
+ const rvcRunMode = sd.rvcRunMode
1077
+ if (rvcRunMode !== undefined) {
1078
+ result.rvcRunMode = rvcRunMode
1079
+ }
1080
+ const rvcOperationalState = sd.rvcOperationalState
1081
+ if (rvcOperationalState !== undefined) {
1082
+ result.rvcOperationalState = rvcOperationalState
1083
+ }
1084
+
1000
1085
  return result
1001
1086
  } catch (e: any) {
1002
1087
  this.debugLog(`parseAdvertisementForDevice failed for ${dev.deviceId}: ${e?.message ?? e}`)
@@ -1055,6 +1140,12 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1055
1140
  if (color.includes(':')) {
1056
1141
  const parts = color.split(':').map(Number)
1057
1142
  ;[r, g, b] = parts
1143
+ } else if (color.includes(',')) {
1144
+ const parts = color.split(',').map(s => Number(s.trim()))
1145
+ ;[r, g, b] = parts
1146
+ } else if (color.includes(' ')) {
1147
+ const parts = color.split(' ').map(s => Number(s.trim()))
1148
+ ;[r, g, b] = parts
1058
1149
  } else if (color.startsWith('#')) {
1059
1150
  const hex = color.replace('#', '')
1060
1151
  r = Number.parseInt(hex.substring(0, 2), 16)
@@ -1065,10 +1156,10 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1065
1156
  await safeUpdate(this.api.matter.clusterNames.ColorControl, { currentHue: Math.round((h / 360) * 254), currentSaturation: Math.round((s / 100) * 254) }, 'updateHueSaturation')
1066
1157
  }
1067
1158
 
1068
- // Battery/powerSource
1069
- if (status?.battery !== undefined || status?.batt !== undefined) {
1159
+ // Battery/powerSource (support many possible field names)
1160
+ if (status?.battery !== undefined || status?.batt !== undefined || status?.batteryLevel !== undefined || status?.batteryPercentage !== undefined || status?.battery_level !== undefined) {
1070
1161
  try {
1071
- const percentage = Number(status?.battery ?? status?.batt)
1162
+ const percentage = Number(status?.battery ?? status?.batt ?? status?.batteryPercentage ?? status?.batteryLevel ?? status?.battery_level)
1072
1163
  const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)))
1073
1164
  let batChargeLevel = 0
1074
1165
  if (percentage < 20) {
@@ -1076,7 +1167,8 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1076
1167
  } else if (percentage < 40) {
1077
1168
  batChargeLevel = 1
1078
1169
  }
1079
- await safeUpdate('powerSource', { batPercentRemaining, batChargeLevel }, 'updateBatteryPercentage')
1170
+ const powerCluster = (this.api.matter?.clusterNames && (this.api.matter.clusterNames as any).PowerSource) ? (this.api.matter.clusterNames as any).PowerSource : 'powerSource'
1171
+ await safeUpdate(powerCluster, { batPercentRemaining, batChargeLevel }, 'updateBatteryPercentage')
1080
1172
  } catch (e: any) {
1081
1173
  this.debugLog(`Failed to apply battery status for ${dev.deviceId}: ${e?.message ?? e}`)
1082
1174
  }
@@ -1099,10 +1191,10 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1099
1191
  }
1100
1192
  }
1101
1193
 
1102
- // Humidity
1103
- if (status?.humidity !== undefined || status?.h !== undefined) {
1194
+ // Humidity (support different keys)
1195
+ if (status?.humidity !== undefined || status?.h !== undefined || status?.humid !== undefined) {
1104
1196
  try {
1105
- const percent = Number(status?.humidity ?? status?.h)
1197
+ const percent = Number(status?.humidity ?? status?.h ?? status?.humid)
1106
1198
  const measured = Math.round(percent * 100)
1107
1199
  await safeUpdate('relativeHumidityMeasurement', { measuredValue: measured }, 'updateHumidity')
1108
1200
  } catch (e: any) {
@@ -1186,6 +1278,44 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1186
1278
  this.debugLog(`Failed to apply rvcRunMode for ${dev.deviceId}: ${e?.message ?? e}`)
1187
1279
  }
1188
1280
  }
1281
+ // CO2 (carbon dioxide) - support common synonyms
1282
+ if (status?.co2 !== undefined || status?.co2ppm !== undefined || status?.carbonDioxide !== undefined) {
1283
+ try {
1284
+ const val = Number(status?.co2 ?? status?.co2ppm ?? status?.carbonDioxide)
1285
+ await safeUpdate('carbonDioxide', { carbonDioxideLevel: val }, 'updateCO2')
1286
+ } catch (e: any) {
1287
+ this.debugLog(`Failed to apply CO2 for ${dev.deviceId}: ${e?.message ?? e}`)
1288
+ }
1289
+ }
1290
+
1291
+ // PM2.5 / particulate matter
1292
+ if (status?.pm2_5 !== undefined || status?.pm25 !== undefined || status?.pm_2_5 !== undefined) {
1293
+ try {
1294
+ const pm = Number(status?.pm2_5 ?? status?.pm25 ?? status?.pm_2_5)
1295
+ await safeUpdate('pm2_5', { pm25: pm }, 'updatePM25')
1296
+ } catch (e: any) {
1297
+ this.debugLog(`Failed to apply PM2.5 for ${dev.deviceId}: ${e?.message ?? e}`)
1298
+ }
1299
+ }
1300
+ // PM10 (some devices report pm10)
1301
+ if (status?.pm10 !== undefined) {
1302
+ try {
1303
+ const pm10 = Number(status?.pm10)
1304
+ await safeUpdate('pm10', { pm10 }, 'updatePM10')
1305
+ } catch (e: any) {
1306
+ this.debugLog(`Failed to apply PM10 for ${dev.deviceId}: ${e?.message ?? e}`)
1307
+ }
1308
+ }
1309
+
1310
+ // VOC / TVOC - volatile organic compounds
1311
+ if (status?.voc !== undefined || status?.tvoc !== undefined) {
1312
+ try {
1313
+ const val = Number(status?.voc ?? status?.tvoc)
1314
+ await safeUpdate('voc', { voc: val }, 'updateVOC')
1315
+ } catch (e: any) {
1316
+ this.debugLog(`Failed to apply VOC for ${dev.deviceId}: ${e?.message ?? e}`)
1317
+ }
1318
+ }
1189
1319
  if (status?.rvcOperationalState !== undefined) {
1190
1320
  try {
1191
1321
  await safeUpdate('rvcOperationalState', { operationalState: Number(status.rvcOperationalState) }, 'updateOperationalState')
@@ -1237,6 +1367,52 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1237
1367
  // Merge device config (deviceConfig per deviceType and per-device overrides) to match HAP behavior
1238
1368
  const devicesToProcess = await this.mergeDiscoveredDevices(this.discoveredDevices)
1239
1369
 
1370
+ // By default, automatically remove previously-registered Matter
1371
+ // accessories whose deviceId is not present in the merged discovered
1372
+ // list. If the user explicitly sets `options.keepStaleAccessories` to
1373
+ // true, then we will keep previously-registered accessories (legacy
1374
+ // behavior).
1375
+ if ((this.config as any).options?.keepStaleAccessories) {
1376
+ this.debugLog('Keeping previously-registered stale accessories because options.keepStaleAccessories=true')
1377
+ } else {
1378
+ try {
1379
+ const desiredIds = new Set((devicesToProcess || []).map((d: any) => this.normalizeDeviceId(d.deviceId)))
1380
+ const toUnregister: Array<MatterAccessory<Record<string, unknown>>> = []
1381
+ for (const [uuid, acc] of Array.from(this.matterAccessories.entries())) {
1382
+ try {
1383
+ const deviceId = (acc as any)?.context?.deviceId
1384
+ if (!deviceId) {
1385
+ continue
1386
+ }
1387
+ const nid = this.normalizeDeviceId(deviceId)
1388
+ if (!desiredIds.has(nid)) {
1389
+ // Accessory exists but is no longer desired -> schedule for removal
1390
+ this.infoLog(`Removing previously-registered accessory for deviceId=${deviceId} (no longer discovered or configured)`)
1391
+ try {
1392
+ this.clearDeviceResources(deviceId)
1393
+ } catch (e: any) {
1394
+ this.debugLog(`Failed to clear resources for ${deviceId} before unregister: ${e?.message ?? e}`)
1395
+ }
1396
+ toUnregister.push(acc as unknown as MatterAccessory<Record<string, unknown>>)
1397
+ this.matterAccessories.delete(uuid)
1398
+ }
1399
+ } catch (e: any) {
1400
+ this.debugLog(`Error while checking existing accessory ${uuid}: ${e?.message ?? e}`)
1401
+ }
1402
+ }
1403
+
1404
+ if (toUnregister.length > 0) {
1405
+ try {
1406
+ await this.api.matter.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, toUnregister)
1407
+ } catch (e: any) {
1408
+ this.debugLog(`Failed to unregister accessories: ${e?.message ?? e}`)
1409
+ }
1410
+ }
1411
+ } catch (e: any) {
1412
+ this.debugLog(`Failed to remove stale accessories: ${e?.message ?? e}`)
1413
+ }
1414
+ }
1415
+
1240
1416
  // We'll separate discovered devices into two buckets:
1241
1417
  // - platformAccessories: accessories that will be hosted under the plugin's Matter bridge
1242
1418
  // - roboticAccessories: robot vacuum devices which require standalone commissioning behaviour
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { SwitchBotMatterPlatform } from './platform-matter.js'
4
+ import { formatDeviceIdAsMac } from './utils.js'
5
+
6
+ describe('removeDisabledAccessories and unregister edge cases', () => {
7
+ it('clears resources and unregisters accessory even with invalid timer and non-MAC deviceId', async () => {
8
+ // Prepare API stub
9
+ const unregister = vi.fn()
10
+ const api: any = {
11
+ matter: {
12
+ uuid: { generate: (s: string) => `uuid-${s}` },
13
+ unregisterPlatformAccessories: unregister,
14
+ },
15
+ isMatterAvailable: () => true,
16
+ isMatterEnabled: () => true,
17
+ on: () => {},
18
+ }
19
+
20
+ const log: any = { info: vi.fn(), warn: vi.fn(), debug: vi.fn(), error: vi.fn(), success: vi.fn() }
21
+ const platform = new SwitchBotMatterPlatform(log as any, { enableOnOffLight: false } as any, api)
22
+
23
+ // Build a fake serialized accessory matching the generated UUID for OnOff Light
24
+ const uuid = api.matter.uuid.generate('matter-onoff-light')
25
+ const accessory: any = { uuid, displayName: 'OnOff Light', context: { deviceId: 'NOT-A-MAC' } }
26
+
27
+ // Put invalid timer object into refreshTimers to ensure code handles it
28
+ const nid = (platform as any).normalizeDeviceId(accessory.context.deviceId)
29
+ ;(platform as any).refreshTimers.set(nid, null as any)
30
+ ;(platform as any).accessoryInstances.set(nid, { dummy: true })
31
+
32
+ // Insert accessory into matterAccessories so removeDisabledAccessories will see it
33
+ ;(platform as any).matterAccessories.set(uuid, accessory)
34
+
35
+ // Spy on clearDeviceResources
36
+ const spyClear = vi.spyOn(platform as any, 'clearDeviceResources')
37
+
38
+ // Call removeDisabledAccessories directly
39
+ await (platform as any).removeDisabledAccessories()
40
+
41
+ // Expect unregister was called and matterAccessories cleared
42
+ expect(unregister).toHaveBeenCalled()
43
+ expect((platform as any).matterAccessories.get(uuid)).toBeUndefined()
44
+
45
+ // clearDeviceResources should have been called with the accessory.deviceId
46
+ expect(spyClear).toHaveBeenCalledWith(accessory.context.deviceId)
47
+ })
48
+ })
package/src/settings.ts CHANGED
@@ -40,6 +40,12 @@ export interface options {
40
40
  // discovered via the SwitchBot OpenAPI will still be included (config-only
41
41
  // devices). Default: false.
42
42
  allowConfigOnlyDevices?: boolean
43
+ /**
44
+ * When true, previously-registered accessories for devices that are no
45
+ * longer discovered or configured will be kept on the bridge. Default: false.
46
+ * When false (default), stale accessories are removed automatically.
47
+ */
48
+ keepStaleAccessories?: boolean
43
49
  mqttURL?: string
44
50
  mqttOptions?: IClientOptions
45
51
  mqttPubOptions?: IClientOptions