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

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/config.schema.json +17 -2
  3. package/dist/devices-hap/device.d.ts +1 -0
  4. package/dist/devices-hap/device.d.ts.map +1 -1
  5. package/dist/devices-hap/device.js +70 -30
  6. package/dist/devices-hap/device.js.map +1 -1
  7. package/dist/devices-matter/BaseMatterAccessory.d.ts +23 -0
  8. package/dist/devices-matter/BaseMatterAccessory.d.ts.map +1 -1
  9. package/dist/devices-matter/BaseMatterAccessory.js +167 -5
  10. package/dist/devices-matter/BaseMatterAccessory.js.map +1 -1
  11. package/dist/devices-matter/ColorLightAccessory.d.ts.map +1 -1
  12. package/dist/devices-matter/ColorLightAccessory.js +12 -12
  13. package/dist/devices-matter/ColorLightAccessory.js.map +1 -1
  14. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts.map +1 -1
  15. package/dist/devices-matter/ColorTemperatureLightAccessory.js +5 -7
  16. package/dist/devices-matter/ColorTemperatureLightAccessory.js.map +1 -1
  17. package/dist/devices-matter/DimmableLightAccessory.js +9 -9
  18. package/dist/devices-matter/DimmableLightAccessory.js.map +1 -1
  19. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts.map +1 -1
  20. package/dist/devices-matter/ExtendedColorLightAccessory.js +14 -15
  21. package/dist/devices-matter/ExtendedColorLightAccessory.js.map +1 -1
  22. package/dist/devices-matter/OnOffLightAccessory.d.ts.map +1 -1
  23. package/dist/devices-matter/OnOffLightAccessory.js +8 -16
  24. package/dist/devices-matter/OnOffLightAccessory.js.map +1 -1
  25. package/dist/devices-matter/OnOffOutletAccessory.d.ts +2 -0
  26. package/dist/devices-matter/OnOffOutletAccessory.d.ts.map +1 -1
  27. package/dist/devices-matter/OnOffOutletAccessory.js +10 -7
  28. package/dist/devices-matter/OnOffOutletAccessory.js.map +1 -1
  29. package/dist/devices-matter/OnOffSwitchAccessory.js +2 -2
  30. package/dist/devices-matter/OnOffSwitchAccessory.js.map +1 -1
  31. package/dist/devices-matter/baseMatterAccessory.test.d.ts +2 -0
  32. package/dist/devices-matter/baseMatterAccessory.test.d.ts.map +1 -0
  33. package/dist/devices-matter/baseMatterAccessory.test.js +71 -0
  34. package/dist/devices-matter/baseMatterAccessory.test.js.map +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.test.js +7 -2
  39. package/dist/index.test.js.map +1 -1
  40. package/dist/irdevice/irdevice.d.ts +11 -10
  41. package/dist/irdevice/irdevice.d.ts.map +1 -1
  42. package/dist/irdevice/irdevice.js +76 -35
  43. package/dist/irdevice/irdevice.js.map +1 -1
  44. package/dist/platform-hap.d.ts +10 -14
  45. package/dist/platform-hap.d.ts.map +1 -1
  46. package/dist/platform-hap.js +38 -64
  47. package/dist/platform-hap.js.map +1 -1
  48. package/dist/platform-matter.d.ts +68 -6
  49. package/dist/platform-matter.d.ts.map +1 -1
  50. package/dist/platform-matter.js +1213 -70
  51. package/dist/platform-matter.js.map +1 -1
  52. package/dist/platform-matter.test.d.ts +2 -0
  53. package/dist/platform-matter.test.d.ts.map +1 -0
  54. package/dist/platform-matter.test.js +127 -0
  55. package/dist/platform-matter.test.js.map +1 -0
  56. package/dist/settings.d.ts +1 -0
  57. package/dist/settings.d.ts.map +1 -1
  58. package/dist/settings.js.map +1 -1
  59. package/dist/utils.d.ts +87 -0
  60. package/dist/utils.d.ts.map +1 -1
  61. package/dist/utils.js +254 -0
  62. package/dist/utils.js.map +1 -1
  63. package/dist/utils.test.d.ts +2 -0
  64. package/dist/utils.test.d.ts.map +1 -0
  65. package/dist/utils.test.js +95 -0
  66. package/dist/utils.test.js.map +1 -0
  67. package/dist/verifyconfig.test.js +2 -2
  68. package/dist/verifyconfig.test.js.map +1 -1
  69. package/docs/assets/main.js +2 -2
  70. package/docs/index.html +2 -2
  71. package/docs/variables/default.html +1 -1
  72. package/package.json +14 -14
  73. package/src/devices-hap/device.ts +68 -30
  74. package/src/devices-matter/BaseMatterAccessory.ts +168 -5
  75. package/src/devices-matter/ColorLightAccessory.ts +12 -12
  76. package/src/devices-matter/ColorTemperatureLightAccessory.ts +5 -7
  77. package/src/devices-matter/DimmableLightAccessory.ts +9 -9
  78. package/src/devices-matter/ExtendedColorLightAccessory.ts +14 -15
  79. package/src/devices-matter/OnOffLightAccessory.ts +8 -16
  80. package/src/devices-matter/OnOffOutletAccessory.ts +12 -7
  81. package/src/devices-matter/OnOffSwitchAccessory.ts +2 -2
  82. package/src/devices-matter/baseMatterAccessory.test.ts +88 -0
  83. package/src/index.test.ts +7 -2
  84. package/src/index.ts +4 -5
  85. package/src/irdevice/irdevice.ts +74 -35
  86. package/src/platform-hap.ts +39 -73
  87. package/src/platform-matter.test.ts +155 -0
  88. package/src/platform-matter.ts +1254 -73
  89. package/src/settings.ts +4 -0
  90. package/src/utils.test.ts +96 -0
  91. package/src/utils.ts +255 -0
  92. package/src/verifyconfig.test.ts +11 -10
@@ -1,12 +1,7 @@
1
+ import { SwitchBotBLE, SwitchBotOpenAPI } from 'node-switchbot';
1
2
  import { ColorLightAccessory, ColorTemperatureLightAccessory, ContactSensorAccessory, DimmableLightAccessory, DoorLockAccessory, ExtendedColorLightAccessory, FanAccessory, HumiditySensorAccessory, LeakSensorAccessory, LightSensorAccessory, OccupancySensorAccessory, OnOffLightAccessory, OnOffOutletAccessory, OnOffSwitchAccessory, PowerStripAccessory, RoboticVacuumAccessory, SmokeCOAlarmAccessory, TemperatureSensorAccessory, ThermostatAccessory, VenetianBlindAccessory, WindowBlindAccessory, } from './devices-matter/index.js';
2
3
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
3
- import { cleanDeviceConfig } from './utils.js';
4
- /**
5
- * MatterPlatform
6
- * Demonstrates all available Matter device types in Homebridge
7
- *
8
- * Organized by official Matter Specification v1.4.1 categories
9
- */
4
+ import { cleanDeviceConfig, createPlatformLogger, formatDeviceIdAsMac, hs2rgb, makeBLESender, makeOpenAPISender, rgb2hs, sleep } from './utils.js';
10
5
  export class SwitchBotMatterPlatform {
11
6
  log;
12
7
  config;
@@ -16,11 +11,48 @@ export class SwitchBotMatterPlatform {
16
11
  // public readonly accessories: Map<string, PlatformAccessory> = new Map()
17
12
  // Track restored Matter cached accessories
18
13
  matterAccessories = new Map();
14
+ // node-switchbot clients
15
+ switchBotAPI;
16
+ switchBotBLE;
17
+ // discovered devices cache
18
+ discoveredDevices = [];
19
+ // Registry of created accessory instances keyed by normalized deviceId
20
+ accessoryInstances = new Map();
21
+ // Refresh timers keyed by normalized deviceId
22
+ refreshTimers = new Map();
23
+ // BLE event handlers keyed by device MAC (formatted)
24
+ bleEventHandler = {};
25
+ // Platform logging toggle (can be controlled via UI or config)
26
+ platformLogging;
27
+ // Platform-provided logging helpers (attached in constructor)
28
+ infoLog;
29
+ successLog;
30
+ debugSuccessLog;
31
+ warnLog;
32
+ debugWarnLog;
33
+ errorLog;
34
+ debugErrorLog;
35
+ debugLog;
36
+ loggingIsDebug;
37
+ enablingPlatformLogging;
19
38
  constructor(log, config, api) {
20
39
  this.log = log;
21
40
  this.config = config;
22
41
  this.api = api;
23
- this.log.debug('Finished initializing platform:', this.config.name);
42
+ // Attach platform-wide logging helpers from utils so Matter and device
43
+ // classes can use consistent logging methods (infoLog/debugLog/etc.)
44
+ const _pl = createPlatformLogger(async () => this.platformLogging, this.log);
45
+ this.infoLog = _pl.infoLog;
46
+ this.successLog = _pl.successLog;
47
+ this.debugSuccessLog = _pl.debugSuccessLog;
48
+ this.warnLog = _pl.warnLog;
49
+ this.debugWarnLog = _pl.debugWarnLog;
50
+ this.errorLog = _pl.errorLog;
51
+ this.debugErrorLog = _pl.debugErrorLog;
52
+ this.debugLog = _pl.debugLog;
53
+ this.loggingIsDebug = _pl.loggingIsDebug;
54
+ this.enablingPlatformLogging = _pl.enablingPlatformLogging;
55
+ this.debugLog('Finished initializing platform:', this.config.name);
24
56
  // Normalize deviceConfig to remove UI-inserted defaults
25
57
  try {
26
58
  if (this.config.options) {
@@ -35,11 +67,11 @@ export class SwitchBotMatterPlatform {
35
67
  }
36
68
  }
37
69
  catch (e) {
38
- this.log.debug('Failed to clean deviceConfig: %s', e);
70
+ this.debugLog('Failed to clean deviceConfig: %s', e);
39
71
  }
40
72
  // Does the user have a version of Homebridge that is compatible with matter?
41
73
  if (!this.api.isMatterAvailable?.()) {
42
- this.log.warn('Matter is not available in this version of Homebridge. Please update Homebridge to use this plugin.');
74
+ this.warnLog('Matter is not available in this version of Homebridge. Please update Homebridge to use this plugin.');
43
75
  }
44
76
  // Check if the user has matter enabled, this means:
45
77
  // - If the plugin is running on the main bridge, then the user must have enabled matter in the Homebridge settings page in the UI
@@ -47,15 +79,1058 @@ export class SwitchBotMatterPlatform {
47
79
  // In reality, only the below check is needed, but they are both included here for completeness
48
80
  // Remember to use a '?.' optional chaining operator in case the user is running an older version of Homebridge that does not have these APIs
49
81
  if (!this.api.isMatterEnabled?.()) {
50
- this.log.warn('Matter is not enabled in Homebridge. Please enable Matter in the Homebridge settings to use this plugin.');
82
+ this.warnLog('Matter is not enabled in Homebridge. Please enable Matter in the Homebridge settings to use this plugin.');
51
83
  return;
52
84
  }
53
85
  // Register Matter accessories when Homebridge has finished launching
54
86
  this.api.on('didFinishLaunching', () => {
55
- this.log.debug('Executed didFinishLaunching callback');
56
- this.registerMatterAccessories();
87
+ this.debugLog('Executed didFinishLaunching callback');
88
+ // Initialize SwitchBot API clients
89
+ try {
90
+ if (this.config.credentials?.token && this.config.credentials?.secret) {
91
+ this.switchBotAPI = new SwitchBotOpenAPI(this.config.credentials.token, this.config.credentials.secret, this.config.options?.hostname);
92
+ // forward basic logs
93
+ if (!this.config.options?.disableLogsforOpenAPI && this.switchBotAPI?.on) {
94
+ this.switchBotAPI.on('log', (l) => this.debugLog('[SwitchBot OpenAPI]', l.message));
95
+ }
96
+ }
97
+ else {
98
+ this.debugLog('SwitchBot OpenAPI credentials not provided; cloud devices will be skipped');
99
+ }
100
+ }
101
+ catch (e) {
102
+ this.errorLog('Failed to initialize SwitchBot OpenAPI:', e?.message ?? e);
103
+ }
104
+ try {
105
+ this.switchBotBLE = new SwitchBotBLE();
106
+ if (!this.config.options?.disableLogsforBLE && this.switchBotBLE?.on) {
107
+ this.switchBotBLE.on('log', (l) => this.debugLog('[SwitchBot BLE]', l.message));
108
+ }
109
+ }
110
+ catch (e) {
111
+ this.errorLog('Failed to initialize SwitchBot BLE client:', e?.message ?? e);
112
+ }
113
+ // If BLE scanning is enabled, start scanning and route advertisements to registered handlers
114
+ if (this.config.options?.BLE && this.switchBotBLE) {
115
+ const ble = this.switchBotBLE;
116
+ (async () => {
117
+ try {
118
+ await ble.startScan();
119
+ }
120
+ catch (e) {
121
+ this.errorLog(`Failed to start BLE scanning: ${e?.message ?? e}`);
122
+ }
123
+ // route advertisements to our handlers
124
+ ble.onadvertisement = async (ad) => {
125
+ try {
126
+ const mac = (ad.address || '').toLowerCase();
127
+ const handler = this.bleEventHandler[mac];
128
+ if (handler) {
129
+ await handler(ad.serviceData);
130
+ }
131
+ }
132
+ catch (e) {
133
+ this.errorLog(`Failed to handle BLE advertisement: ${e?.message ?? e}`);
134
+ }
135
+ };
136
+ })();
137
+ }
138
+ // Ensure we clean up any per-device timers and BLE handlers when Homebridge shuts down
139
+ this.api.on('shutdown', async () => {
140
+ try {
141
+ this.infoLog('Homebridge shutting down: clearing refresh timers and BLE handlers');
142
+ // Clear all refresh timers
143
+ for (const [nid, t] of Array.from(this.refreshTimers.entries())) {
144
+ try {
145
+ clearInterval(t);
146
+ }
147
+ catch (e) {
148
+ this.debugLog(`Failed to clear timer for ${nid}: ${e?.message ?? e}`);
149
+ }
150
+ this.refreshTimers.delete(nid);
151
+ }
152
+ // Clear accessory instances registry
153
+ try {
154
+ this.accessoryInstances.clear();
155
+ }
156
+ catch (e) {
157
+ this.debugLog(`Failed to clear accessoryInstances: ${e?.message ?? e}`);
158
+ }
159
+ // Remove BLE handlers
160
+ try {
161
+ for (const k of Object.keys(this.bleEventHandler)) {
162
+ delete this.bleEventHandler[k];
163
+ }
164
+ }
165
+ catch (e) {
166
+ this.debugLog(`Failed to clear bleEventHandler: ${e?.message ?? e}`);
167
+ }
168
+ // Stop BLE scanning if available
169
+ try {
170
+ if (this.switchBotBLE && typeof this.switchBotBLE.stopScan === 'function') {
171
+ await this.switchBotBLE.stopScan();
172
+ this.infoLog('Stopped BLE scanning');
173
+ }
174
+ }
175
+ catch (e) {
176
+ this.debugLog(`Failed to stop BLE scanning: ${e?.message ?? e}`);
177
+ }
178
+ }
179
+ catch (e) {
180
+ this.debugLog('Shutdown cleanup failed: %s', e?.message ?? e);
181
+ }
182
+ });
183
+ (async () => {
184
+ try {
185
+ await this.discoverDevices();
186
+ }
187
+ catch (e) {
188
+ this.debugLog('Device discovery failed during startup: %s', e?.message ?? e);
189
+ }
190
+ try {
191
+ await this.registerMatterAccessories();
192
+ }
193
+ catch (e) {
194
+ this.errorLog('Failed to register Matter accessories: %s', e?.message ?? e);
195
+ }
196
+ })();
57
197
  });
58
198
  }
199
+ /**
200
+ * Normalize a deviceId for matching (uppercase alphanumerics only)
201
+ */
202
+ normalizeDeviceId(deviceId) {
203
+ return (deviceId ?? '').toUpperCase().replace(/[^A-Z0-9]+/g, '');
204
+ }
205
+ /**
206
+ * Clear per-device resources: refresh timers, accessory instance registry, BLE handlers
207
+ */
208
+ clearDeviceResources(deviceId) {
209
+ if (!deviceId) {
210
+ return;
211
+ }
212
+ try {
213
+ const nid = this.normalizeDeviceId(deviceId);
214
+ const existing = this.refreshTimers.get(nid);
215
+ if (existing) {
216
+ try {
217
+ clearInterval(existing);
218
+ }
219
+ catch (e) {
220
+ this.debugLog(`Failed to clear refresh timer for ${deviceId}: ${e?.message ?? e}`);
221
+ }
222
+ this.refreshTimers.delete(nid);
223
+ }
224
+ try {
225
+ this.accessoryInstances.delete(nid);
226
+ }
227
+ catch (e) {
228
+ this.debugLog(`Failed to delete accessory instance for ${deviceId}: ${e?.message ?? e}`);
229
+ }
230
+ try {
231
+ const mac = formatDeviceIdAsMac(deviceId).toLowerCase();
232
+ if (this.bleEventHandler[mac]) {
233
+ delete this.bleEventHandler[mac];
234
+ }
235
+ }
236
+ catch (e) {
237
+ // formatting failed (not a MAC-like id) — ignore
238
+ this.debugLog(`clearDeviceResources: failed to remove BLE handler for ${deviceId}: ${e?.message ?? e}`);
239
+ }
240
+ }
241
+ catch (e) {
242
+ this.debugLog(`clearDeviceResources top-level error for ${deviceId}: ${e?.message ?? e}`);
243
+ }
244
+ }
245
+ /**
246
+ * Merge two arrays by deviceId. For each item in a1 (user-provided devices list),
247
+ * find matching item in a2 (discovered devices) and merge them with user overrides last.
248
+ */
249
+ mergeByDeviceId(a1, a2) {
250
+ const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices);
251
+ const result = [];
252
+ for (const itm of (a1 || [])) {
253
+ const matchingItem = (a2 || []).find(item => this.normalizeDeviceId(item.deviceId) === this.normalizeDeviceId(itm.deviceId));
254
+ if (matchingItem) {
255
+ result.push(Object.assign({}, matchingItem, itm));
256
+ }
257
+ else if (allowConfigOnly) {
258
+ // include config-only device as-is when explicitly allowed
259
+ result.push(Object.assign({}, itm));
260
+ }
261
+ // otherwise skip config-only entries
262
+ }
263
+ return result;
264
+ }
265
+ /**
266
+ * Merge discovered devices with deviceConfig (per deviceType) and per-device overrides
267
+ * from `config.options.devices`, matching the behavior used in platform-hap.
268
+ */
269
+ async mergeDiscoveredDevices(discovered) {
270
+ // If there's no device config or per-device config, return discovered as-is
271
+ if (!this.config.options?.devices && !this.config.options?.deviceConfig) {
272
+ return discovered;
273
+ }
274
+ // Step 1: Assign missing deviceType from configDeviceType and merge deviceType-level configs
275
+ const devicesWithTypeConfig = await Promise.all(discovered.map(async (deviceObj) => {
276
+ if (!deviceObj.deviceType) {
277
+ deviceObj.deviceType = deviceObj.configDeviceType !== undefined ? deviceObj.configDeviceType : 'Unknown';
278
+ this.debugLog(`API missing deviceType for ${deviceObj.deviceId}, using configDeviceType: ${deviceObj.configDeviceType}`);
279
+ }
280
+ const deviceTypeConfig = this.config.options?.deviceConfig?.[deviceObj.deviceType] || {};
281
+ return Object.assign({}, deviceObj, deviceTypeConfig);
282
+ }));
283
+ // Merge per-device overrides by matching deviceId
284
+ const merged = this.mergeByDeviceId(this.config.options?.devices ?? [], devicesWithTypeConfig ?? []);
285
+ // For any entries in merged (which are based on config.options.devices), ensure final per-device merges include deviceId-specific config
286
+ const final = [];
287
+ for (const device of merged) {
288
+ // Find per-device config entry by deviceId (config.options.devices is an array)
289
+ const deviceIdConfig = (this.config.options?.devices || []).find((d) => this.normalizeDeviceId(d.deviceId) === this.normalizeDeviceId(device.deviceId)) || {};
290
+ const deviceWithConfig = Object.assign({}, device, deviceIdConfig);
291
+ final.push(deviceWithConfig);
292
+ }
293
+ // Also include any discovered devices that weren't present in the user devices list
294
+ const userDeviceIds = new Set((this.config.options?.devices || []).map((d) => this.normalizeDeviceId(d.deviceId)));
295
+ for (const d of devicesWithTypeConfig) {
296
+ if (!userDeviceIds.has(this.normalizeDeviceId(d.deviceId))) {
297
+ final.push(d);
298
+ }
299
+ }
300
+ return final;
301
+ }
302
+ /**
303
+ * Select effective connection type for a device: prefer explicit device.connectionType,
304
+ * otherwise prefer BLE when platform BLE is enabled and device provides a BLE model/id.
305
+ */
306
+ chooseConnectionType(deviceObj) {
307
+ if (deviceObj?.connectionType) {
308
+ return deviceObj.connectionType === 'BLE' ? 'BLE' : 'OpenAPI';
309
+ }
310
+ // If platform BLE is enabled and we have a bleModel or deviceId that formats to a MAC, prefer BLE
311
+ if (this.config.options?.BLE && (deviceObj?.bleModel || formatDeviceIdAsMac(deviceObj?.deviceId))) {
312
+ return 'BLE';
313
+ }
314
+ return 'OpenAPI';
315
+ }
316
+ /**
317
+ * Map a SwitchBot device object to a MatterAccessory using the device-specific
318
+ * Matter accessory classes in `src/devices-matter`.
319
+ */
320
+ async createAccessoryFromDevice(dev) {
321
+ // Basic metadata
322
+ const displayName = dev.deviceName ?? dev.deviceId ?? 'SwitchBot Device';
323
+ const serial = dev.deviceId ?? 'unknown';
324
+ const manufacturer = 'SwitchBot';
325
+ const model = dev.model ?? dev.deviceType ?? 'SwitchBot';
326
+ const firmware = dev.firmware ?? dev.version ?? '0.0.0';
327
+ // Helper to build a default opts object consumed by the matter device classes
328
+ const baseOpts = {
329
+ uuid: this.api.matter.uuid.generate(serial),
330
+ displayName,
331
+ serialNumber: serial,
332
+ manufacturer,
333
+ model,
334
+ firmwareRevision: String(firmware),
335
+ hardwareRevision: '1.0.0',
336
+ deviceId: dev.deviceId,
337
+ // Inject handy platform-side helpers into the accessory `context` so Matter
338
+ // accessory classes can perform OpenAPI/BLE actions without reaching into
339
+ // the platform implementation directly.
340
+ context: {
341
+ deviceId: dev.deviceId,
342
+ // Expose the display name so Matter accessory classes can read it from context
343
+ name: displayName,
344
+ // Provide device-level logging override (if present) and platform logging flag
345
+ // so accessories can decide how verbose they should be.
346
+ deviceLogging: dev?.logging,
347
+ platformLogging: this.platformLogging,
348
+ },
349
+ };
350
+ // Build platform-side helpers using shared factories so they can be reused/tested
351
+ const sendOpenAPI = makeOpenAPISender(this.retryCommand.bind(this), dev, { maxRetries: this.config.options?.maxRetries ?? 1, delayBetweenRetries: this.config.options?.delayBetweenRetries ?? 1000 });
352
+ const sendBLE = makeBLESender(this.switchBotBLE, dev, { bleRetries: this.config.options?.bleRetries ?? 2, bleRetryDelay: this.config.options?.bleRetryDelay ?? 500 });
353
+ // Log that we're initializing this device so it's visible in startup logs
354
+ try {
355
+ this.infoLog(`Initializing Matter device: ${displayName} (type=${dev.deviceType ?? 'Unknown'}) id=${dev.deviceId}`);
356
+ }
357
+ catch (e) {
358
+ // best-effort logging — swallow errors to avoid breaking initialization
359
+ this.debugLog('Failed to log initializing device:', e?.message ?? e);
360
+ }
361
+ const makeOnOffHandlers = (uuid, connectionType) => ({
362
+ onOff: {
363
+ on: async () => {
364
+ try {
365
+ if (connectionType === 'BLE' && this.switchBotBLE) {
366
+ await sendBLE('turnOn');
367
+ }
368
+ else {
369
+ await sendOpenAPI('turnOn');
370
+ }
371
+ await this.api.matter.updateAccessoryState(uuid, this.api.matter.clusterNames.OnOff, { onOff: true });
372
+ }
373
+ catch (e) {
374
+ this.errorLog(`Failed to turn on device ${dev.deviceId}: ${e?.message ?? e}`);
375
+ }
376
+ },
377
+ off: async () => {
378
+ try {
379
+ if (connectionType === 'BLE' && this.switchBotBLE) {
380
+ await sendBLE('turnOff');
381
+ }
382
+ else {
383
+ await sendOpenAPI('turnOff');
384
+ }
385
+ await this.api.matter.updateAccessoryState(uuid, this.api.matter.clusterNames.OnOff, { onOff: false });
386
+ }
387
+ catch (e) {
388
+ this.errorLog(`Failed to turn off device ${dev.deviceId}: ${e?.message ?? e}`);
389
+ }
390
+ },
391
+ },
392
+ });
393
+ // Mapping from SwitchBot deviceType -> constructor (expanded for parity with HAP)
394
+ const mapping = {
395
+ // Plugs / Outlets
396
+ 'Plug': OnOffOutletAccessory,
397
+ 'Plug Mini (US)': OnOffOutletAccessory,
398
+ 'Plug Mini (JP)': OnOffOutletAccessory,
399
+ 'Plug Mini': OnOffOutletAccessory,
400
+ 'WoPlug': OnOffOutletAccessory,
401
+ // Lighting
402
+ 'Color Bulb': ColorLightAccessory,
403
+ 'Color Bulb Mini': ColorLightAccessory,
404
+ 'Ceiling Light': ColorTemperatureLightAccessory,
405
+ 'Ceiling Light Pro': ColorTemperatureLightAccessory,
406
+ 'Strip Light': ExtendedColorLightAccessory,
407
+ 'Light Strip': ExtendedColorLightAccessory,
408
+ 'Light Strip Plus': ExtendedColorLightAccessory,
409
+ 'Strip Light Pro': ExtendedColorLightAccessory,
410
+ 'Dimmable Light': DimmableLightAccessory,
411
+ // Robot Vacuums
412
+ 'K10+': RoboticVacuumAccessory,
413
+ 'K10+ Pro': RoboticVacuumAccessory,
414
+ 'WoSweeper': RoboticVacuumAccessory,
415
+ 'WoSweeperMini': RoboticVacuumAccessory,
416
+ 'Robot Vacuum Cleaner S1': RoboticVacuumAccessory,
417
+ 'Robot Vacuum Cleaner S1 Plus': RoboticVacuumAccessory,
418
+ 'Robot Vacuum Cleaner S10': RoboticVacuumAccessory,
419
+ 'Robot Vacuum Cleaner S1 Pro': RoboticVacuumAccessory,
420
+ 'Robot Vacuum Cleaner S1 Mini': RoboticVacuumAccessory,
421
+ // Locks
422
+ 'Smart Lock': DoorLockAccessory,
423
+ 'Smart Lock Pro': DoorLockAccessory,
424
+ // Sensors
425
+ 'Motion Sensor': OccupancySensorAccessory,
426
+ 'Contact Sensor': ContactSensorAccessory,
427
+ 'Water Detector': LeakSensorAccessory,
428
+ 'Meter': TemperatureSensorAccessory,
429
+ 'MeterPlus': TemperatureSensorAccessory,
430
+ 'MeterPro': TemperatureSensorAccessory,
431
+ 'WoIOSensor': TemperatureSensorAccessory,
432
+ 'Air Purifier PM2.5': HumiditySensorAccessory,
433
+ 'Air Purifier Table PM2.5': HumiditySensorAccessory,
434
+ 'Air Purifier': HumiditySensorAccessory,
435
+ 'Air Purifier VOC': HumiditySensorAccessory,
436
+ 'Air Purifier Table VOC': HumiditySensorAccessory,
437
+ // Fans
438
+ 'Battery Circulator Fan': FanAccessory,
439
+ // Curtains / Blinds
440
+ 'Blind Tilt': VenetianBlindAccessory,
441
+ 'Curtain': WindowBlindAccessory,
442
+ 'Curtain2': WindowBlindAccessory,
443
+ 'Curtain3': WindowBlindAccessory,
444
+ 'Curtain 2': WindowBlindAccessory,
445
+ 'WoRollerShade': WindowBlindAccessory,
446
+ 'Roller Shade': WindowBlindAccessory,
447
+ 'Venetian Blind': VenetianBlindAccessory,
448
+ // Switches / Relays
449
+ 'Relay Switch 1': OnOffSwitchAccessory,
450
+ 'Relay Switch 1PM': OnOffSwitchAccessory,
451
+ 'Relay Switch 2': OnOffSwitchAccessory,
452
+ 'Relay Switch 3': OnOffSwitchAccessory,
453
+ // Misc / hubs / other
454
+ 'Hub 2': undefined,
455
+ 'Hub 3': undefined,
456
+ 'Hub Mini': undefined,
457
+ 'Bot': OnOffSwitchAccessory,
458
+ 'Smart Bot': OnOffSwitchAccessory,
459
+ 'Humidifier': HumiditySensorAccessory,
460
+ 'Humidifier2': HumiditySensorAccessory,
461
+ 'Thermostat': ThermostatAccessory,
462
+ 'Water Heater': ThermostatAccessory,
463
+ };
464
+ const Ctor = mapping[dev.deviceType ?? ''];
465
+ if (!Ctor) {
466
+ this.debugLog(`No Matter mapping for deviceType='${dev.deviceType}', deviceId=${dev.deviceId}`);
467
+ return undefined;
468
+ }
469
+ // Build opts and handlers tailored for basic capabilities
470
+ const uuid = baseOpts.uuid;
471
+ const handlers = {};
472
+ // Choose connection type for this device (BLE vs OpenAPI)
473
+ const connectionType = this.chooseConnectionType(dev);
474
+ // On/Off common
475
+ handlers.onOff = makeOnOffHandlers(uuid, connectionType).onOff;
476
+ // If this is a light, add brightness and color handlers
477
+ if (['Color Bulb', 'Ceiling Light', 'Ceiling Light Pro', 'Strip Light', 'Dimmable Light'].includes(dev.deviceType ?? '')) {
478
+ // levelControl
479
+ handlers.levelControl = {
480
+ moveToLevelWithOnOff: async (request) => {
481
+ try {
482
+ const level = request.level;
483
+ const percent = Math.round((level / 254) * 100);
484
+ if (connectionType === 'BLE' && this.switchBotBLE) {
485
+ await sendBLE('setBrightness', percent);
486
+ }
487
+ else {
488
+ await sendOpenAPI('setBrightness', `${percent}`);
489
+ }
490
+ await this.api.matter.updateAccessoryState(uuid, this.api.matter.clusterNames.LevelControl, { currentLevel: level });
491
+ }
492
+ catch (e) {
493
+ this.errorLog(`Failed to set brightness for ${dev.deviceId}: ${e?.message ?? e}`);
494
+ }
495
+ },
496
+ };
497
+ // colorControl
498
+ handlers.colorControl = {
499
+ moveToHueAndSaturationLogic: async (request) => {
500
+ try {
501
+ const hue = request.hue;
502
+ const saturation = request.saturation;
503
+ const [r, g, b] = hs2rgb(Math.round((hue / 254) * 360), Math.round((saturation / 254) * 100));
504
+ if (connectionType === 'BLE' && this.switchBotBLE) {
505
+ await sendBLE('setRGB', Number(request.level ?? 100), r, g, b);
506
+ }
507
+ else {
508
+ await sendOpenAPI('setColor', `${r}:${g}:${b}`);
509
+ }
510
+ await this.api.matter.updateAccessoryState(uuid, this.api.matter.clusterNames.ColorControl, { currentHue: hue, currentSaturation: saturation });
511
+ }
512
+ catch (e) {
513
+ this.errorLog(`Failed to set hue/sat for ${dev.deviceId}: ${e?.message ?? e}`);
514
+ }
515
+ },
516
+ moveToColorLogic: async (request) => {
517
+ try {
518
+ // MoveToColor gives colorX/colorY values; convert to approximate RGB by mapping to 0-255 scale
519
+ const colorX = request.colorX;
520
+ const colorY = request.colorY;
521
+ // Naive conversion: map X/Y into RGB via hue approximation (not colorimetrically accurate)
522
+ const hueApprox = Math.round((colorX / 65535) * 360);
523
+ const satApprox = Math.round((colorY / 65535) * 100);
524
+ const [r, g, b] = hs2rgb(hueApprox, satApprox);
525
+ if (connectionType === 'BLE' && this.switchBotBLE) {
526
+ await sendBLE('setRGB', Number(request.level ?? 100), r, g, b);
527
+ }
528
+ else {
529
+ await sendOpenAPI('setColor', `${r}:${g}:${b}`);
530
+ }
531
+ await this.api.matter.updateAccessoryState(uuid, this.api.matter.clusterNames.ColorControl, { currentX: colorX, currentY: colorY });
532
+ }
533
+ catch (e) {
534
+ this.errorLog(`Failed to set XY color for ${dev.deviceId}: ${e?.message ?? e}`);
535
+ }
536
+ },
537
+ };
538
+ // color temperature — map to kelvin and send setColorTemperature
539
+ handlers.colorTemperature = {
540
+ moveToColorTemperature: async (request) => {
541
+ try {
542
+ const kelvin = Math.round(1000000 / Number(request.colorTemperature));
543
+ if (connectionType === 'BLE' && this.switchBotBLE) {
544
+ await sendBLE('setColorTemperature', kelvin);
545
+ }
546
+ else {
547
+ await sendOpenAPI('setColorTemperature', `${kelvin}`);
548
+ }
549
+ await this.api.matter.updateAccessoryState(uuid, this.api.matter.clusterNames.ColorControl, { currentX: request.colorX ?? 0, currentY: request.colorY ?? 0 });
550
+ }
551
+ catch (e) {
552
+ this.errorLog(`Failed to set color temperature for ${dev.deviceId}: ${e?.message ?? e}`);
553
+ }
554
+ },
555
+ };
556
+ }
557
+ // Expose platform helpers to the accessory via context so accessory
558
+ // classes can call OpenAPI/BLE actions (sendOpenAPI/sendBLE) and know
559
+ // the effective connection type.
560
+ try {
561
+ /* Inject platform helpers (OpenAPI/BLE senders + logging helpers + connection type)
562
+ into the accessory context so Matter accessory classes can use them without
563
+ reaching into the platform implementation directly. */
564
+ ;
565
+ baseOpts.context = Object.assign({}, baseOpts.context, {
566
+ sendOpenAPI,
567
+ sendBLE,
568
+ connectionType,
569
+ // Expose platform logging helpers so accessories can use consistent logging
570
+ infoLog: this.infoLog,
571
+ debugLog: this.debugLog,
572
+ warnLog: this.warnLog,
573
+ errorLog: this.errorLog,
574
+ successLog: this.successLog,
575
+ });
576
+ }
577
+ catch (e) {
578
+ this.debugLog('Failed to attach platform helpers to baseOpts.context: %s', e?.message ?? e);
579
+ }
580
+ const opts = Object.assign({}, baseOpts, { handlers });
581
+ // Instantiate the device class and return its serialized accessory
582
+ const instance = new Ctor(this.api, this.log, opts);
583
+ // Save instance in registry so platform can call device-specific update methods if needed
584
+ try {
585
+ if (dev?.deviceId) {
586
+ this.accessoryInstances.set(this.normalizeDeviceId(dev.deviceId), instance);
587
+ }
588
+ }
589
+ catch (e) {
590
+ this.debugLog('Failed to register accessory instance: %s', e?.message ?? e);
591
+ }
592
+ try {
593
+ this.infoLog(`Initialized Matter accessory: ${displayName} (type=${dev.deviceType ?? 'Unknown'}) id=${dev.deviceId}`);
594
+ }
595
+ catch (e) {
596
+ this.debugLog('Failed to log initialized accessory:', e?.message ?? e);
597
+ }
598
+ // Register BLE->Matter push handler for this device's MAC (if BLE scanning is active)
599
+ try {
600
+ const mac = formatDeviceIdAsMac(dev.deviceId).toLowerCase();
601
+ // Handler receives advertisement/serviceData when BLE scan events arrive
602
+ this.bleEventHandler[mac] = async (serviceData) => {
603
+ const uuidLocal = baseOpts.uuid;
604
+ // First try model-specific / normalized parsing of BLE advertisement
605
+ try {
606
+ const parsed = this.parseAdvertisementForDevice(dev, serviceData);
607
+ if (parsed) {
608
+ // Power
609
+ if (parsed.power !== undefined) {
610
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.OnOff, { onOff: Boolean(parsed.power) });
611
+ }
612
+ // Brightness
613
+ if (parsed.brightness !== undefined) {
614
+ const level = Math.round((Number(parsed.brightness) / 100) * 254);
615
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.LevelControl, { currentLevel: level });
616
+ }
617
+ // Color
618
+ if (parsed.color !== undefined) {
619
+ const { r, g, b } = parsed.color;
620
+ const [h, s] = rgb2hs(r, g, b);
621
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.ColorControl, { currentHue: Math.round((h / 360) * 254), currentSaturation: Math.round((s / 100) * 254) });
622
+ }
623
+ // Battery -> powerSource cluster (common mapping)
624
+ if (parsed.battery !== undefined) {
625
+ try {
626
+ const percentage = Number(parsed.battery);
627
+ const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)));
628
+ let batChargeLevel = 0;
629
+ if (percentage < 20) {
630
+ batChargeLevel = 2;
631
+ }
632
+ else if (percentage < 40) {
633
+ batChargeLevel = 1;
634
+ }
635
+ await this.api.matter.updateAccessoryState(uuidLocal, 'powerSource', { batPercentRemaining, batChargeLevel });
636
+ }
637
+ catch (e) {
638
+ this.debugLog(`Failed to update battery state for ${dev.deviceId}: ${e?.message ?? e}`);
639
+ }
640
+ }
641
+ // Temperature -> temperatureMeasurement
642
+ if (parsed.temperature !== undefined) {
643
+ try {
644
+ const c = Number(parsed.temperature);
645
+ const measured = Math.round(c * 100);
646
+ await this.api.matter.updateAccessoryState(uuidLocal, 'temperatureMeasurement', { measuredValue: measured });
647
+ }
648
+ catch (e) {
649
+ this.debugLog(`Failed to update temperature for ${dev.deviceId}: ${e?.message ?? e}`);
650
+ }
651
+ }
652
+ // Humidity -> relativeHumidityMeasurement
653
+ if (parsed.humidity !== undefined) {
654
+ try {
655
+ const percent = Number(parsed.humidity);
656
+ const measured = Math.round(percent * 100);
657
+ await this.api.matter.updateAccessoryState(uuidLocal, 'relativeHumidityMeasurement', { measuredValue: measured });
658
+ }
659
+ catch (e) {
660
+ this.debugLog(`Failed to update humidity for ${dev.deviceId}: ${e?.message ?? e}`);
661
+ }
662
+ }
663
+ // Contact / Leak -> BooleanState
664
+ if (parsed.contact !== undefined || parsed.leak !== undefined) {
665
+ try {
666
+ // Some devices report contact as true=open; ContactSensor expects inverted value
667
+ const isContactOpen = parsed.contact === undefined ? undefined : Boolean(parsed.contact);
668
+ const leakDetected = parsed.leak === undefined ? undefined : Boolean(parsed.leak);
669
+ if (isContactOpen !== undefined) {
670
+ // If this is a contact sensor device type, invert; otherwise set conservatively
671
+ if ((dev.deviceType || '').includes('Contact')) {
672
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.BooleanState, { stateValue: !isContactOpen });
673
+ }
674
+ else {
675
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.BooleanState, { stateValue: isContactOpen });
676
+ }
677
+ }
678
+ if (leakDetected !== undefined) {
679
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.BooleanState, { stateValue: leakDetected });
680
+ }
681
+ }
682
+ catch (e) {
683
+ this.debugLog(`Failed to update contact/leak for ${dev.deviceId}: ${e?.message ?? e}`);
684
+ }
685
+ }
686
+ // Motion -> occupancy
687
+ if (parsed.motion !== undefined) {
688
+ try {
689
+ await this.api.matter.updateAccessoryState(uuidLocal, 'occupancySensing', { occupancy: { occupied: Boolean(parsed.motion) } });
690
+ }
691
+ catch (e) {
692
+ this.debugLog(`Failed to update occupancy for ${dev.deviceId}: ${e?.message ?? e}`);
693
+ }
694
+ }
695
+ // Lock state -> doorLock
696
+ if (parsed.lock !== undefined) {
697
+ try {
698
+ const s = String(parsed.lock).toLowerCase();
699
+ let lockState = 0;
700
+ if (s === 'locked' || s === '1' || s === 'true') {
701
+ lockState = 1;
702
+ }
703
+ else if (s === 'unlocked' || s === '0' || s === 'false') {
704
+ lockState = 2;
705
+ }
706
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.DoorLock, { lockState });
707
+ }
708
+ catch (e) {
709
+ this.debugLog(`Failed to update lock for ${dev.deviceId}: ${e?.message ?? e}`);
710
+ }
711
+ }
712
+ // Position / Cover -> WindowCovering (convert open percent to closed*100)
713
+ if (parsed.position !== undefined) {
714
+ try {
715
+ const openPercent = Number(parsed.position);
716
+ const closedPercent = 100 - Math.max(0, Math.min(100, openPercent));
717
+ const value = Math.round(closedPercent * 100);
718
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.WindowCovering, { currentPositionLiftPercent100ths: value, targetPositionLiftPercent100ths: value });
719
+ }
720
+ catch (e) {
721
+ this.debugLog(`Failed to update cover position for ${dev.deviceId}: ${e?.message ?? e}`);
722
+ }
723
+ }
724
+ // Fan speed -> FanControl
725
+ if (parsed.fanSpeed !== undefined) {
726
+ try {
727
+ const percent = Number(parsed.fanSpeed);
728
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.FanControl, { percentSetting: percent, percentCurrent: percent });
729
+ }
730
+ catch (e) {
731
+ this.debugLog(`Failed to update fan speed for ${dev.deviceId}: ${e?.message ?? e}`);
732
+ }
733
+ }
734
+ // Robot vacuum fields (battery already handled) - update run/operational/clean modes when present
735
+ if (parsed.rvcRunMode !== undefined) {
736
+ try {
737
+ await this.api.matter.updateAccessoryState(uuidLocal, 'rvcRunMode', { currentMode: Number(parsed.rvcRunMode) });
738
+ }
739
+ catch (e) {
740
+ this.debugLog(`Failed to update rvcRunMode for ${dev.deviceId}: ${e?.message ?? e}`);
741
+ }
742
+ }
743
+ if (parsed.rvcOperationalState !== undefined) {
744
+ try {
745
+ await this.api.matter.updateAccessoryState(uuidLocal, 'rvcOperationalState', { operationalState: Number(parsed.rvcOperationalState) });
746
+ }
747
+ catch (e) {
748
+ this.debugLog(`Failed to update rvcOperationalState for ${dev.deviceId}: ${e?.message ?? e}`);
749
+ }
750
+ }
751
+ // If we parsed something from serviceData prefer it and return early
752
+ if (serviceData) {
753
+ return;
754
+ }
755
+ }
756
+ }
757
+ catch (e) {
758
+ this.debugLog(`BLE advertisement parsing failed for ${dev.deviceId}: ${e?.message ?? e}`);
759
+ }
760
+ // Fallback to OpenAPI getDeviceStatus when serviceData is not present or parsing failed
761
+ if (!this.switchBotAPI) {
762
+ return;
763
+ }
764
+ try {
765
+ const { response, statusCode } = await this.switchBotAPI.getDeviceStatus(dev.deviceId, this.config.credentials?.token, this.config.credentials?.secret);
766
+ if (!(statusCode === 100 || statusCode === 200)) {
767
+ return;
768
+ }
769
+ const respAny = response;
770
+ const body = respAny?.body ?? respAny;
771
+ const status = body?.status ?? body;
772
+ // Use centralized mapper which prefers accessory instance update helpers
773
+ await this.applyStatusToAccessory(uuidLocal, dev, status);
774
+ }
775
+ catch (e) {
776
+ this.debugLog(`BLE push handler failed for ${dev.deviceId}: ${e?.message ?? e}`);
777
+ }
778
+ };
779
+ }
780
+ catch (e) {
781
+ this.debugLog(`Failed to register BLE handler for ${dev.deviceId}: ${e?.message ?? e}`);
782
+ }
783
+ // Schedule periodic OpenAPI refreshes for this device (if OpenAPI is configured)
784
+ try {
785
+ const nid = this.normalizeDeviceId(dev.deviceId);
786
+ // Clear any existing timer for this device
787
+ const existing = this.refreshTimers.get(nid);
788
+ if (existing) {
789
+ clearInterval(existing);
790
+ this.refreshTimers.delete(nid);
791
+ }
792
+ const refreshRateSec = dev.refreshRate ?? this.config.options?.refreshRate ?? 300;
793
+ if (this.switchBotAPI && refreshRateSec && Number(refreshRateSec) > 0) {
794
+ // Immediate one-shot to populate initial state
795
+ ;
796
+ (async () => {
797
+ try {
798
+ const { response, statusCode } = await this.switchBotAPI.getDeviceStatus(dev.deviceId, this.config.credentials?.token, this.config.credentials?.secret);
799
+ if (statusCode === 100 || statusCode === 200) {
800
+ const respAny = response;
801
+ const body = respAny?.body ?? respAny;
802
+ const status = body?.status ?? body;
803
+ await this.applyStatusToAccessory(uuid, dev, status);
804
+ }
805
+ }
806
+ catch (e) {
807
+ this.debugLog(`Initial OpenAPI refresh failed for ${dev.deviceId}: ${e?.message ?? e}`);
808
+ }
809
+ })();
810
+ const timer = setInterval(async () => {
811
+ try {
812
+ const { response, statusCode } = await this.switchBotAPI.getDeviceStatus(dev.deviceId, this.config.credentials?.token, this.config.credentials?.secret);
813
+ if (statusCode === 100 || statusCode === 200) {
814
+ const respAny = response;
815
+ const body = respAny?.body ?? respAny;
816
+ const status = body?.status ?? body;
817
+ await this.applyStatusToAccessory(uuid, dev, status);
818
+ }
819
+ }
820
+ catch (e) {
821
+ this.debugLog(`Periodic OpenAPI refresh failed for ${dev.deviceId}: ${e?.message ?? e}`);
822
+ }
823
+ }, Number(refreshRateSec) * 1000);
824
+ this.refreshTimers.set(nid, timer);
825
+ }
826
+ }
827
+ catch (e) {
828
+ this.debugLog(`Failed to schedule refresh for ${dev.deviceId}: ${e?.message ?? e}`);
829
+ }
830
+ return instance.toAccessory();
831
+ }
832
+ /**
833
+ * Discover devices via SwitchBot OpenAPI and cache them for later use
834
+ */
835
+ async discoverDevices() {
836
+ if (!this.switchBotAPI) {
837
+ this.debugLog('SwitchBot OpenAPI not configured; skipping discovery');
838
+ return;
839
+ }
840
+ try {
841
+ const { response, statusCode } = await this.switchBotAPI.getDevices();
842
+ this.debugLog(`SwitchBot getDevices response status: ${statusCode}`);
843
+ if (statusCode === 100 || statusCode === 200) {
844
+ const deviceList = Array.isArray(response?.body?.deviceList) ? response.body.deviceList : [];
845
+ this.discoveredDevices = deviceList;
846
+ this.infoLog(`Discovered ${deviceList.length} SwitchBot device(s) from OpenAPI`);
847
+ for (const d of deviceList) {
848
+ this.debugLog(` - ${d.deviceName} (${d.deviceType}) id=${d.deviceId}`);
849
+ }
850
+ }
851
+ else {
852
+ this.warnLog(`SwitchBot getDevices returned status ${statusCode}`);
853
+ }
854
+ }
855
+ catch (e) {
856
+ this.errorLog('Failed to discover SwitchBot devices:', e?.message ?? e);
857
+ }
858
+ }
859
+ /**
860
+ * Retry wrapper for control commands using SwitchBot OpenAPI
861
+ */
862
+ async retryCommand(deviceObj, bodyChange, maxRetries = 1, delayBetweenRetries = 1000) {
863
+ let retryCount = 0;
864
+ while (retryCount < maxRetries) {
865
+ try {
866
+ if (!this.switchBotAPI) {
867
+ throw new Error('SwitchBot OpenAPI not initialized');
868
+ }
869
+ const { response, statusCode } = await this.switchBotAPI.controlDevice(deviceObj.deviceId, bodyChange.command, bodyChange.parameter, bodyChange.commandType, this.config.credentials?.token, this.config.credentials?.secret);
870
+ return { response, statusCode };
871
+ }
872
+ catch (e) {
873
+ this.debugLog(`retryCommand error: ${e?.message ?? e}`);
874
+ }
875
+ retryCount++;
876
+ await sleep(delayBetweenRetries);
877
+ }
878
+ return { response: {}, statusCode: 500 };
879
+ }
880
+ /**
881
+ * Parse BLE advertisement/serviceData into normalized fields for a given device.
882
+ * Returns null when serviceData is falsy or parsing fails.
883
+ */
884
+ parseAdvertisementForDevice(dev, serviceData) {
885
+ if (!serviceData) {
886
+ return null;
887
+ }
888
+ try {
889
+ const sd = serviceData;
890
+ const result = {};
891
+ // Power/on state - supports multiple field names used by different models
892
+ const power = sd.power ?? sd.on ?? sd.p;
893
+ if (power !== undefined) {
894
+ result.power = (String(power).toLowerCase() === 'on' || Number(power) === 1);
895
+ }
896
+ // Brightness (0-100)
897
+ const brightness = sd.brightness ?? sd.b;
898
+ if (brightness !== undefined) {
899
+ result.brightness = Number(brightness);
900
+ }
901
+ // Color - could be 'r:g:b', '#rrggbb' or 'rrggbb'
902
+ const color = sd.color ?? sd.rgb ?? sd.c;
903
+ if (color !== undefined) {
904
+ let r = 0;
905
+ let g = 0;
906
+ let b = 0;
907
+ const c = String(color);
908
+ if (c.includes(':')) {
909
+ const parts = c.split(':').map(Number);
910
+ [r, g, b] = parts;
911
+ }
912
+ else if (c.startsWith('#')) {
913
+ const hex = c.replace('#', '');
914
+ r = Number.parseInt(hex.substring(0, 2), 16);
915
+ g = Number.parseInt(hex.substring(2, 4), 16);
916
+ b = Number.parseInt(hex.substring(4, 6), 16);
917
+ }
918
+ else if (/^[0-9a-f]{6}$/i.test(c)) {
919
+ r = Number.parseInt(c.substring(0, 2), 16);
920
+ g = Number.parseInt(c.substring(2, 4), 16);
921
+ b = Number.parseInt(c.substring(4, 6), 16);
922
+ }
923
+ result.color = { r, g, b };
924
+ }
925
+ // Battery (some devices use battery or batt)
926
+ const battery = sd.battery ?? sd.batt;
927
+ if (battery !== undefined) {
928
+ result.battery = Number(battery);
929
+ }
930
+ return result;
931
+ }
932
+ catch (e) {
933
+ this.debugLog(`parseAdvertisementForDevice failed for ${dev.deviceId}: ${e?.message ?? e}`);
934
+ return null;
935
+ }
936
+ }
937
+ /**
938
+ * Central helper to apply a SwitchBot status object to a Matter accessory.
939
+ * Tries to call accessory instance update helpers when available, otherwise
940
+ * falls back to calling api.matter.updateAccessoryState directly.
941
+ */
942
+ async applyStatusToAccessory(uuidLocal, dev, status) {
943
+ if (!status) {
944
+ return;
945
+ }
946
+ const instance = dev?.deviceId ? this.accessoryInstances.get(this.normalizeDeviceId(dev.deviceId)) : undefined;
947
+ // Helper to safely call instance methods or fallback to api.matter.updateAccessoryState
948
+ const safeUpdate = async (cluster, attributes, methodName) => {
949
+ try {
950
+ if (instance && methodName && typeof instance[methodName] === 'function') {
951
+ // prefer device-specific update helpers when available
952
+ await instance[methodName](...(Object.values(attributes)));
953
+ }
954
+ else if (instance && typeof instance.updateState === 'function') {
955
+ // some accessories expose updateState that accepts cluster and attributes
956
+ await instance.updateState(cluster, attributes);
957
+ }
958
+ else {
959
+ await this.api.matter.updateAccessoryState(uuidLocal, cluster, attributes);
960
+ }
961
+ }
962
+ catch (e) {
963
+ this.debugLog(`safeUpdate failed for ${dev.deviceId} cluster=${cluster}: ${e?.message ?? e}`);
964
+ }
965
+ };
966
+ try {
967
+ // On/Off
968
+ if (status?.power !== undefined) {
969
+ const on = (String(status.power).toLowerCase() === 'on' || Number(status.power) === 1 || Boolean(status.power) === true);
970
+ await safeUpdate(this.api.matter.clusterNames.OnOff, { onOff: on }, 'updateOnOffState');
971
+ }
972
+ // Brightness
973
+ if (status?.brightness !== undefined) {
974
+ const level = Math.round((Number(status.brightness) / 100) * 254);
975
+ await safeUpdate(this.api.matter.clusterNames.LevelControl, { currentLevel: level }, 'updateBrightness');
976
+ }
977
+ // Color
978
+ if (status?.color !== undefined) {
979
+ const color = String(status.color);
980
+ let r = 0;
981
+ let g = 0;
982
+ let b = 0;
983
+ if (color.includes(':')) {
984
+ const parts = color.split(':').map(Number);
985
+ [r, g, b] = parts;
986
+ }
987
+ else if (color.startsWith('#')) {
988
+ const hex = color.replace('#', '');
989
+ r = Number.parseInt(hex.substring(0, 2), 16);
990
+ g = Number.parseInt(hex.substring(2, 4), 16);
991
+ b = Number.parseInt(hex.substring(4, 6), 16);
992
+ }
993
+ const [h, s] = rgb2hs(r, g, b);
994
+ await safeUpdate(this.api.matter.clusterNames.ColorControl, { currentHue: Math.round((h / 360) * 254), currentSaturation: Math.round((s / 100) * 254) }, 'updateHueSaturation');
995
+ }
996
+ // Battery/powerSource
997
+ if (status?.battery !== undefined || status?.batt !== undefined) {
998
+ try {
999
+ const percentage = Number(status?.battery ?? status?.batt);
1000
+ const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)));
1001
+ let batChargeLevel = 0;
1002
+ if (percentage < 20) {
1003
+ batChargeLevel = 2;
1004
+ }
1005
+ else if (percentage < 40) {
1006
+ batChargeLevel = 1;
1007
+ }
1008
+ await safeUpdate('powerSource', { batPercentRemaining, batChargeLevel }, 'updateBatteryPercentage');
1009
+ }
1010
+ catch (e) {
1011
+ this.debugLog(`Failed to apply battery status for ${dev.deviceId}: ${e?.message ?? e}`);
1012
+ }
1013
+ }
1014
+ // Temperature + thermostat
1015
+ if (status?.temperature !== undefined || status?.temp !== undefined) {
1016
+ try {
1017
+ const c = Number(status?.temperature ?? status?.temp);
1018
+ const measured = Math.round(c * 100);
1019
+ await safeUpdate('temperatureMeasurement', { measuredValue: measured }, 'updateTemperature');
1020
+ // Thermostat-specific mapping
1021
+ if (status?.targetTemp !== undefined || status?.targetTemperature !== undefined || status?.heatingSetpoint !== undefined) {
1022
+ const target = Number(status?.targetTemp ?? status?.targetTemperature ?? status?.heatingSetpoint);
1023
+ const val = Math.round(target * 100);
1024
+ await safeUpdate('thermostat', { occupiedHeatingSetpoint: val }, 'updateHeatingSetpoint');
1025
+ }
1026
+ }
1027
+ catch (e) {
1028
+ this.debugLog(`Failed to apply temperature for ${dev.deviceId}: ${e?.message ?? e}`);
1029
+ }
1030
+ }
1031
+ // Humidity
1032
+ if (status?.humidity !== undefined || status?.h !== undefined) {
1033
+ try {
1034
+ const percent = Number(status?.humidity ?? status?.h);
1035
+ const measured = Math.round(percent * 100);
1036
+ await safeUpdate('relativeHumidityMeasurement', { measuredValue: measured }, 'updateHumidity');
1037
+ }
1038
+ catch (e) {
1039
+ this.debugLog(`Failed to apply humidity for ${dev.deviceId}: ${e?.message ?? e}`);
1040
+ }
1041
+ }
1042
+ // Contact / Leak -> BooleanState
1043
+ if (status?.contact !== undefined || status?.open !== undefined || status?.leak !== undefined || status?.water !== undefined) {
1044
+ try {
1045
+ const isContactOpen = status?.contact ?? status?.open;
1046
+ if (isContactOpen !== undefined) {
1047
+ if ((dev.deviceType || '').includes('Contact')) {
1048
+ await safeUpdate(this.api.matter.clusterNames.BooleanState, { stateValue: !(String(isContactOpen).toLowerCase() === 'true' || Number(isContactOpen) === 1) }, 'updateContactState');
1049
+ }
1050
+ else {
1051
+ await safeUpdate(this.api.matter.clusterNames.BooleanState, { stateValue: (String(isContactOpen).toLowerCase() === 'true' || Number(isContactOpen) === 1) }, 'updateContactState');
1052
+ }
1053
+ }
1054
+ const leakDetected = status?.leak ?? status?.water;
1055
+ if (leakDetected !== undefined) {
1056
+ await safeUpdate(this.api.matter.clusterNames.BooleanState, { stateValue: Boolean(leakDetected) }, 'updateLeakState');
1057
+ }
1058
+ }
1059
+ catch (e) {
1060
+ this.debugLog(`Failed to apply contact/leak for ${dev.deviceId}: ${e?.message ?? e}`);
1061
+ }
1062
+ }
1063
+ // Motion -> occupancy
1064
+ if (status?.motion !== undefined || status?.m !== undefined) {
1065
+ try {
1066
+ const detected = Boolean(status?.motion ?? status?.m);
1067
+ await safeUpdate('occupancySensing', { occupancy: { occupied: detected } }, 'updateOccupancy');
1068
+ }
1069
+ catch (e) {
1070
+ this.debugLog(`Failed to apply motion for ${dev.deviceId}: ${e?.message ?? e}`);
1071
+ }
1072
+ }
1073
+ // Lock state
1074
+ if (status?.lock !== undefined) {
1075
+ try {
1076
+ const s = String(status.lock).toLowerCase();
1077
+ let lockState = 0;
1078
+ if (s === 'locked' || s === '1' || s === 'true') {
1079
+ lockState = 1;
1080
+ }
1081
+ else if (s === 'unlocked' || s === '0' || s === 'false') {
1082
+ lockState = 2;
1083
+ }
1084
+ await safeUpdate(this.api.matter.clusterNames.DoorLock, { lockState }, 'updateLockState');
1085
+ }
1086
+ catch (e) {
1087
+ this.debugLog(`Failed to apply lock for ${dev.deviceId}: ${e?.message ?? e}`);
1088
+ }
1089
+ }
1090
+ // Cover position
1091
+ if (status?.position !== undefined || status?.percent !== undefined) {
1092
+ try {
1093
+ const openPercent = Number(status?.position ?? status?.percent);
1094
+ const closedPercent = 100 - Math.max(0, Math.min(100, openPercent));
1095
+ const value = Math.round(closedPercent * 100);
1096
+ await safeUpdate(this.api.matter.clusterNames.WindowCovering, { currentPositionLiftPercent100ths: value, targetPositionLiftPercent100ths: value }, 'updateLiftPosition');
1097
+ }
1098
+ catch (e) {
1099
+ this.debugLog(`Failed to apply cover position for ${dev.deviceId}: ${e?.message ?? e}`);
1100
+ }
1101
+ }
1102
+ // Fan
1103
+ if (status?.fanSpeed !== undefined || status?.speed !== undefined) {
1104
+ try {
1105
+ const percent = Number(status?.fanSpeed ?? status?.speed);
1106
+ await safeUpdate(this.api.matter.clusterNames.FanControl, { percentSetting: percent, percentCurrent: percent }, 'updateFanSpeed');
1107
+ }
1108
+ catch (e) {
1109
+ this.debugLog(`Failed to apply fan speed for ${dev.deviceId}: ${e?.message ?? e}`);
1110
+ }
1111
+ }
1112
+ // Robot vacuum: run/operational/clean modes
1113
+ if (status?.rvcRunMode !== undefined) {
1114
+ try {
1115
+ await safeUpdate('rvcRunMode', { currentMode: Number(status.rvcRunMode) }, 'updateRunMode');
1116
+ }
1117
+ catch (e) {
1118
+ this.debugLog(`Failed to apply rvcRunMode for ${dev.deviceId}: ${e?.message ?? e}`);
1119
+ }
1120
+ }
1121
+ if (status?.rvcOperationalState !== undefined) {
1122
+ try {
1123
+ await safeUpdate('rvcOperationalState', { operationalState: Number(status.rvcOperationalState) }, 'updateOperationalState');
1124
+ }
1125
+ catch (e) {
1126
+ this.debugLog(`Failed to apply rvcOperationalState for ${dev.deviceId}: ${e?.message ?? e}`);
1127
+ }
1128
+ }
1129
+ }
1130
+ catch (e) {
1131
+ this.debugLog(`applyStatusToAccessory top-level failure for ${dev.deviceId}: ${e?.message ?? e}`);
1132
+ }
1133
+ }
59
1134
  /**
60
1135
  * Required for DynamicPlatformPlugin
61
1136
  * Called when homebridge restores cached accessories from disk at startup
@@ -72,30 +1147,88 @@ export class SwitchBotMatterPlatform {
72
1147
  * any custom data you stored when the accessory was originally registered.
73
1148
  */
74
1149
  configureMatterAccessory(accessory) {
75
- this.log.debug('Loading cached Matter accessory:', accessory.displayName);
1150
+ this.debugLog('Loading cached Matter accessory:', accessory.displayName);
76
1151
  this.matterAccessories.set(accessory.uuid, accessory);
77
1152
  }
78
1153
  /**
79
1154
  * Register all Matter accessories
80
1155
  */
81
1156
  async registerMatterAccessories() {
82
- this.log.info('═'.repeat(80));
83
- this.log.info('Homebridge Matter Plugin');
84
- this.log.info('═'.repeat(80));
1157
+ this.debugLog('═'.repeat(80));
1158
+ this.infoLog('Homebridge Matter Plugin');
1159
+ this.debugLog('═'.repeat(80));
85
1160
  // Remove accessories that are disabled in config
86
1161
  await this.removeDisabledAccessories();
87
- // Register devices by Matter specification sections
88
- await this.registerSection4Lighting();
89
- await this.registerSection5SmartPlugs();
90
- await this.registerSection6Switches();
91
- await this.registerSection7Sensors();
92
- await this.registerSection8Closure();
93
- await this.registerSection9HVAC();
94
- await this.registerSection12Robotic();
95
- await this.registerCustomDevices();
96
- this.log.info('═'.repeat(80));
97
- this.log.info('Finished registering Matter accessories');
98
- this.log.info('═'.repeat(80));
1162
+ // If we discovered real SwitchBot devices via OpenAPI, map and register them
1163
+ if (this.discoveredDevices && this.discoveredDevices.length > 0) {
1164
+ this.infoLog(`Registering ${this.discoveredDevices.length} discovered SwitchBot device(s) as Matter accessories`);
1165
+ // Merge device config (deviceConfig per deviceType and per-device overrides) to match HAP behavior
1166
+ const devicesToProcess = await this.mergeDiscoveredDevices(this.discoveredDevices);
1167
+ // We'll separate discovered devices into two buckets:
1168
+ // - platformAccessories: accessories that will be hosted under the plugin's Matter bridge
1169
+ // - roboticAccessories: robot vacuum devices which require standalone commissioning behaviour
1170
+ const platformAccessories = [];
1171
+ const roboticAccessories = [];
1172
+ // Known robot vacuum deviceType names (matches mapping in createAccessoryFromDevice)
1173
+ const robotTypes = new Set([
1174
+ 'K10+',
1175
+ 'K10+ Pro',
1176
+ 'WoSweeper',
1177
+ 'WoSweeperMini',
1178
+ 'Robot Vacuum Cleaner S1',
1179
+ 'Robot Vacuum Cleaner S1 Plus',
1180
+ 'Robot Vacuum Cleaner S10',
1181
+ 'Robot Vacuum Cleaner S1 Pro',
1182
+ 'Robot Vacuum Cleaner S1 Mini',
1183
+ ]);
1184
+ for (const dev of devicesToProcess) {
1185
+ try {
1186
+ const acc = await this.createAccessoryFromDevice(dev);
1187
+ if (!acc) {
1188
+ continue;
1189
+ }
1190
+ if (robotTypes.has(dev.deviceType ?? '')) {
1191
+ roboticAccessories.push(acc);
1192
+ }
1193
+ else {
1194
+ platformAccessories.push(acc);
1195
+ }
1196
+ }
1197
+ catch (e) {
1198
+ this.errorLog(`Failed to create Matter accessory for ${dev.deviceId}: ${e?.message ?? e}`);
1199
+ }
1200
+ }
1201
+ // Register platform-hosted accessories (most devices)
1202
+ if (platformAccessories.length > 0) {
1203
+ this.infoLog(`✓ Registered ${platformAccessories.length} discovered platform-hosted device(s)`);
1204
+ for (const acc of platformAccessories) {
1205
+ this.infoLog(` - ${acc.displayName}`);
1206
+ }
1207
+ await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, platformAccessories);
1208
+ }
1209
+ // Register robotic accessories (robot vacuums) separately so they can be
1210
+ // commissioned in the way Apple Home expects (these devices often require
1211
+ // standalone commissioning flow). We still call registerPlatformAccessories
1212
+ // because the accessory implementations manage their commissioning behavior.
1213
+ if (roboticAccessories.length > 0) {
1214
+ this.infoLog(`✓ Registered ${roboticAccessories.length} discovered robot vacuum device(s)`);
1215
+ for (const acc of roboticAccessories) {
1216
+ this.infoLog(` - ${acc.displayName} (standalone for Apple Home compatibility)`);
1217
+ }
1218
+ await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, roboticAccessories);
1219
+ }
1220
+ // Debug/info: how many discovered vs example accessories were registered.
1221
+ // Example accessories are disabled — we intentionally do NOT register them.
1222
+ const discoveredRegistered = platformAccessories.length + roboticAccessories.length;
1223
+ const exampleRegistered = 0;
1224
+ this.debugLog(`Discovered accessories registered: ${discoveredRegistered}; Example accessories registered: ${exampleRegistered}`);
1225
+ return;
1226
+ }
1227
+ // If no discovered devices are available, do not register example/demo accessories.
1228
+ this.infoLog('No discovered SwitchBot devices found; not registering example Matter accessories by default.');
1229
+ this.debugLog('═'.repeat(80));
1230
+ this.debugLog('Finished registering Matter accessories');
1231
+ this.debugLog('═'.repeat(80));
99
1232
  }
100
1233
  /**
101
1234
  * Remove accessories that are disabled in config
@@ -128,7 +1261,17 @@ export class SwitchBotMatterPlatform {
128
1261
  if (enabled === false) {
129
1262
  const existingAccessory = this.matterAccessories.get(uuid);
130
1263
  if (existingAccessory) {
131
- this.log.info(`Removing accessory '${name}' (disabled in config)`);
1264
+ this.infoLog(`Removing accessory '${name}' (disabled in config)`);
1265
+ // Attempt to clear any per-device resources (timers, BLE handlers, instances)
1266
+ try {
1267
+ const deviceId = existingAccessory?.context?.deviceId;
1268
+ if (deviceId) {
1269
+ this.clearDeviceResources(deviceId);
1270
+ }
1271
+ }
1272
+ catch (e) {
1273
+ this.debugLog(`Failed to clear resources for disabled accessory ${name}: ${e?.message ?? e}`);
1274
+ }
132
1275
  await this.api.matter.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
133
1276
  this.matterAccessories.delete(uuid);
134
1277
  }
@@ -139,9 +1282,9 @@ export class SwitchBotMatterPlatform {
139
1282
  * Section 4: Lighting Devices (Matter Spec § 4)
140
1283
  */
141
1284
  async registerSection4Lighting() {
142
- this.log.info('═'.repeat(80));
143
- this.log.info('Section 4: Lighting Devices (Matter Spec § 4)');
144
- this.log.info('═'.repeat(80));
1285
+ this.debugLog('═'.repeat(80));
1286
+ this.infoLog('Section 4: Lighting Devices (Matter Spec § 4)');
1287
+ this.debugLog('═'.repeat(80));
145
1288
  const accessories = [];
146
1289
  // On/Off Light
147
1290
  if (this.config.enableOnOffLight !== false) {
@@ -169,9 +1312,9 @@ export class SwitchBotMatterPlatform {
169
1312
  accessories.push(device.toAccessory());
170
1313
  }
171
1314
  if (accessories.length > 0) {
172
- this.log.info(`✓ Registered ${accessories.length} lighting device(s)`);
1315
+ this.infoLog(`✓ Registered ${accessories.length} lighting device(s)`);
173
1316
  for (const acc of accessories) {
174
- this.log.info(` - ${acc.displayName}`);
1317
+ this.infoLog(` - ${acc.displayName}`);
175
1318
  }
176
1319
  await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
177
1320
  }
@@ -180,9 +1323,9 @@ export class SwitchBotMatterPlatform {
180
1323
  * Section 5: Smart Plugs/Actuators (Matter Spec § 5)
181
1324
  */
182
1325
  async registerSection5SmartPlugs() {
183
- this.log.info('═'.repeat(80));
184
- this.log.info('Section 5: Smart Plugs/Actuators (Matter Spec § 5)');
185
- this.log.info('═'.repeat(80));
1326
+ this.debugLog('═'.repeat(80));
1327
+ this.infoLog('Section 5: Smart Plugs/Actuators (Matter Spec § 5)');
1328
+ this.debugLog('═'.repeat(80));
186
1329
  const accessories = [];
187
1330
  // On/Off Outlet
188
1331
  if (this.config.enableOnOffOutlet !== false) {
@@ -190,9 +1333,9 @@ export class SwitchBotMatterPlatform {
190
1333
  accessories.push(device.toAccessory());
191
1334
  }
192
1335
  if (accessories.length > 0) {
193
- this.log.info(`✓ Registered ${accessories.length} smart plug/actuator device(s)`);
1336
+ this.infoLog(`✓ Registered ${accessories.length} smart plug/actuator device(s)`);
194
1337
  for (const acc of accessories) {
195
- this.log.info(` - ${acc.displayName}`);
1338
+ this.infoLog(` - ${acc.displayName}`);
196
1339
  }
197
1340
  await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
198
1341
  }
@@ -201,9 +1344,9 @@ export class SwitchBotMatterPlatform {
201
1344
  * Section 6: Switches & Controllers (Matter Spec § 6)
202
1345
  */
203
1346
  async registerSection6Switches() {
204
- this.log.info('═'.repeat(80));
205
- this.log.info('Section 6: Switches & Controllers (Matter Spec § 6)');
206
- this.log.info('═'.repeat(80));
1347
+ this.debugLog('═'.repeat(80));
1348
+ this.infoLog('Section 6: Switches & Controllers (Matter Spec § 6)');
1349
+ this.debugLog('═'.repeat(80));
207
1350
  const accessories = [];
208
1351
  // On/Off Switch
209
1352
  if (this.config.enableOnOffSwitch !== false) {
@@ -211,9 +1354,9 @@ export class SwitchBotMatterPlatform {
211
1354
  accessories.push(device.toAccessory());
212
1355
  }
213
1356
  if (accessories.length > 0) {
214
- this.log.info(`✓ Registered ${accessories.length} switch/controller device(s)`);
1357
+ this.infoLog(`✓ Registered ${accessories.length} switch/controller device(s)`);
215
1358
  for (const acc of accessories) {
216
- this.log.info(` - ${acc.displayName}`);
1359
+ this.infoLog(` - ${acc.displayName}`);
217
1360
  }
218
1361
  await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
219
1362
  }
@@ -222,9 +1365,9 @@ export class SwitchBotMatterPlatform {
222
1365
  * Section 7: Sensors (Matter Spec § 7)
223
1366
  */
224
1367
  async registerSection7Sensors() {
225
- this.log.info('═'.repeat(80));
226
- this.log.info('Section 7: Sensors (Matter Spec § 7)');
227
- this.log.info('═'.repeat(80));
1368
+ this.debugLog('═'.repeat(80));
1369
+ this.infoLog('Section 7: Sensors (Matter Spec § 7)');
1370
+ this.debugLog('═'.repeat(80));
228
1371
  const accessories = [];
229
1372
  // Contact Sensor
230
1373
  if (this.config.enableContactSensor !== false) {
@@ -262,9 +1405,9 @@ export class SwitchBotMatterPlatform {
262
1405
  accessories.push(device.toAccessory());
263
1406
  }
264
1407
  if (accessories.length > 0) {
265
- this.log.info(`✓ Registered ${accessories.length} sensor device(s)`);
1408
+ this.infoLog(`✓ Registered ${accessories.length} sensor device(s)`);
266
1409
  for (const acc of accessories) {
267
- this.log.info(` - ${acc.displayName}`);
1410
+ this.infoLog(` - ${acc.displayName}`);
268
1411
  }
269
1412
  await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
270
1413
  }
@@ -273,9 +1416,9 @@ export class SwitchBotMatterPlatform {
273
1416
  * Section 8: Closure Devices (Matter Spec § 8)
274
1417
  */
275
1418
  async registerSection8Closure() {
276
- this.log.info('═'.repeat(80));
277
- this.log.info('Section 8: Closure Devices (Matter Spec § 8)');
278
- this.log.info('═'.repeat(80));
1419
+ this.debugLog('═'.repeat(80));
1420
+ this.infoLog('Section 8: Closure Devices (Matter Spec § 8)');
1421
+ this.debugLog('═'.repeat(80));
279
1422
  const accessories = [];
280
1423
  // Door Lock
281
1424
  if (this.config.enableDoorLock !== false) {
@@ -293,9 +1436,9 @@ export class SwitchBotMatterPlatform {
293
1436
  accessories.push(device.toAccessory());
294
1437
  }
295
1438
  if (accessories.length > 0) {
296
- this.log.info(`✓ Registered ${accessories.length} closure device(s)`);
1439
+ this.infoLog(`✓ Registered ${accessories.length} closure device(s)`);
297
1440
  for (const acc of accessories) {
298
- this.log.info(` - ${acc.displayName}`);
1441
+ this.infoLog(` - ${acc.displayName}`);
299
1442
  }
300
1443
  await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
301
1444
  }
@@ -304,9 +1447,9 @@ export class SwitchBotMatterPlatform {
304
1447
  * Section 9: HVAC (Matter Spec § 9)
305
1448
  */
306
1449
  async registerSection9HVAC() {
307
- this.log.info('═'.repeat(80));
308
- this.log.info('Section 9: HVAC (Matter Spec § 9)');
309
- this.log.info('═'.repeat(80));
1450
+ this.debugLog('═'.repeat(80));
1451
+ this.infoLog('Section 9: HVAC (Matter Spec § 9)');
1452
+ this.debugLog('═'.repeat(80));
310
1453
  const accessories = [];
311
1454
  // Thermostat
312
1455
  if (this.config.enableThermostat !== false) {
@@ -319,9 +1462,9 @@ export class SwitchBotMatterPlatform {
319
1462
  accessories.push(device.toAccessory());
320
1463
  }
321
1464
  if (accessories.length > 0) {
322
- this.log.info(`✓ Registered ${accessories.length} HVAC device(s)`);
1465
+ this.infoLog(`✓ Registered ${accessories.length} HVAC device(s)`);
323
1466
  for (const acc of accessories) {
324
- this.log.info(` - ${acc.displayName}`);
1467
+ this.infoLog(` - ${acc.displayName}`);
325
1468
  }
326
1469
  await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
327
1470
  }
@@ -333,9 +1476,9 @@ export class SwitchBotMatterPlatform {
333
1476
  * Use those codes to pair the vacuum as a separate bridge in your Home app.
334
1477
  */
335
1478
  async registerSection12Robotic() {
336
- this.log.info('═'.repeat(80));
337
- this.log.info('Section 12: Robotic Devices (Matter Spec § 12)');
338
- this.log.info('═'.repeat(80));
1479
+ this.debugLog('═'.repeat(80));
1480
+ this.infoLog('Section 12: Robotic Devices (Matter Spec § 12)');
1481
+ this.debugLog('═'.repeat(80));
339
1482
  const accessories = [];
340
1483
  // Robot Vacuum
341
1484
  if (this.config.enableRobotVacuum !== false) {
@@ -343,9 +1486,9 @@ export class SwitchBotMatterPlatform {
343
1486
  accessories.push(device.toAccessory());
344
1487
  }
345
1488
  if (accessories.length > 0) {
346
- this.log.info(`✓ Registered ${accessories.length} robot vacuum device(s)`);
1489
+ this.infoLog(`✓ Registered ${accessories.length} robot vacuum device(s)`);
347
1490
  for (const acc of accessories) {
348
- this.log.info(` - ${acc.displayName} (standalone for Apple Home compatibility)`);
1491
+ this.infoLog(` - ${acc.displayName} (standalone for Apple Home compatibility)`);
349
1492
  }
350
1493
  await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
351
1494
  }
@@ -358,9 +1501,9 @@ export class SwitchBotMatterPlatform {
358
1501
  * like managing multiple logical components within a single device.
359
1502
  */
360
1503
  async registerCustomDevices() {
361
- this.log.info('═'.repeat(80));
362
- this.log.info('Custom Devices');
363
- this.log.info('═'.repeat(80));
1504
+ this.debugLog('═'.repeat(80));
1505
+ this.infoLog('Custom Devices');
1506
+ this.debugLog('═'.repeat(80));
364
1507
  const accessories = [];
365
1508
  // Power Strip (4 Outlets)
366
1509
  if (this.config.enablePowerStrip !== false) {
@@ -368,9 +1511,9 @@ export class SwitchBotMatterPlatform {
368
1511
  accessories.push(device.toAccessory());
369
1512
  }
370
1513
  if (accessories.length > 0) {
371
- this.log.info(`✓ Registered ${accessories.length} custom device(s)`);
1514
+ this.infoLog(`✓ Registered ${accessories.length} custom device(s)`);
372
1515
  for (const acc of accessories) {
373
- this.log.info(` - ${acc.displayName}`);
1516
+ this.infoLog(` - ${acc.displayName}`);
374
1517
  }
375
1518
  await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
376
1519
  }