@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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file. This projec
8
8
  - Matter platform: register only devices discovered via the SwitchBot OpenAPI by default. Per-device config overrides (by deviceId) are correctly merged into discovered devices.
9
9
  - Add `options.allowConfigOnlyDevices` (boolean) to opt-in to registering devices that exist only in config (not discovered).
10
10
  - Add unit tests for per-device merging and config-only behavior.
11
+ - Matter (Matter platform): default to discovery-first registration and automatically remove previously-registered "stale" accessories that are no longer discovered or configured.
12
+ - Add `options.keepStaleAccessories` (Advanced Settings) — opt-in to preserve previously-registered accessories when set to true. Default: false (stale accessories are removed automatically).
13
+ - Implement per-device OpenAPI polling with tracked timers and proper lifecycle cleanup (timers and BLE handlers cleared on unregister/shutdown).
14
+ - Centralize OpenAPI/BLE -> Matter mapping and expand BLE/OpenAPI parsing to include PM2.5/PM10/VOC/CO2, temperature/humidity, improved color parsing, and additional sensor/robot-vacuum fields.
15
+ - Prefer accessory-specific update helpers where available; fall back to generic Matter updates otherwise.
16
+ - Add unit tests covering BLE parsing, stale-accessory behavior, mapping helpers, and lifecycle cleanup.
11
17
 
12
18
  ## [4.3.1](https://github.com/OpenWonderLabs/homebridge-switchbot/releases/tag/v4.3.1) (2025-03-04)
13
19
 
@@ -13082,6 +13082,11 @@
13082
13082
  "type": "boolean",
13083
13083
  "description": "If true, devices declared in options.devices that are not discovered by the SwitchBot OpenAPI will still be included (config-only devices). Default: false."
13084
13084
  },
13085
+ "keepStaleAccessories": {
13086
+ "title": "Keep Stale Accessories",
13087
+ "type": "boolean",
13088
+ "description": "If true, accessories that are no longer found via the SwitchBot OpenAPI will be kept in HomeKit. Default: false."
13089
+ },
13085
13090
  "allowInvalidCharacters": {
13086
13091
  "title": "Allow Invalid Characters",
13087
13092
  "type": "boolean",
@@ -14835,6 +14840,10 @@
14835
14840
  "key": "options.allowConfigOnlyDevices",
14836
14841
  "description": "<em class='primary-text'>If true, devices declared in options.devices that are not discovered by the SwitchBot OpenAPI will still be included (config-only devices). Default: false.</em>"
14837
14842
  },
14843
+ {
14844
+ "key": "options.keepStaleAccessories",
14845
+ "description": "<em class='primary-text'>If true, previously-registered accessories for devices that are no longer discovered or configured will be kept on the bridge. Default: false (stale accessories are removed automatically).</em>"
14846
+ },
14838
14847
  {
14839
14848
  "key": "options.allowInvalidCharacters",
14840
14849
  "description": "<em class='primary-text'>Allows device names with characters that are normally invalid in HomeKit. Use with caution, as this may lead to unexpected behavior.</em>"
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=platform-matter.cleanup.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-matter.cleanup.test.d.ts","sourceRoot":"","sources":["../src/platform-matter.cleanup.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { SwitchBotMatterPlatform } from './platform-matter.js';
3
+ import { formatDeviceIdAsMac } from './utils.js';
4
+ describe('platform-matter lifecycle cleanup', () => {
5
+ it('clearDeviceResources removes timers, instances and BLE handler entries', async () => {
6
+ // Setup stubbed API and logs
7
+ const handlers = {};
8
+ const api = {
9
+ matter: {
10
+ uuid: { generate: (s) => `uuid-${s}` },
11
+ registerPlatformAccessories: vi.fn(),
12
+ unregisterPlatformAccessories: vi.fn(),
13
+ clusterNames: { OnOff: 'OnOff' },
14
+ },
15
+ isMatterAvailable: () => true,
16
+ isMatterEnabled: () => true,
17
+ on: (ev, fn) => { handlers[ev] = fn; },
18
+ _handlers: handlers,
19
+ };
20
+ const log = { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() };
21
+ const platform = new SwitchBotMatterPlatform(log, {}, api);
22
+ // Insert a fake timer, accessory instance, and BLE handler
23
+ const deviceId = 'AA:BB:CC:11:22:33';
24
+ const nid = platform.normalizeDeviceId(deviceId);
25
+ const timer = setInterval(() => { }, 100000);
26
+ platform.refreshTimers.set(nid, timer);
27
+ platform.accessoryInstances.set(nid, { dummy: true });
28
+ platform.bleEventHandler[deviceId.toLowerCase()] = () => { };
29
+ // Ensure they exist prior
30
+ expect(platform.refreshTimers.get(nid)).toBeDefined();
31
+ expect(platform.accessoryInstances.get(nid)).toBeDefined();
32
+ expect(platform.bleEventHandler[deviceId.toLowerCase()]).toBeDefined();
33
+ platform.clearDeviceResources(deviceId);
34
+ // Now they should be removed
35
+ expect(platform.refreshTimers.get(nid)).toBeUndefined();
36
+ expect(platform.accessoryInstances.get(nid)).toBeUndefined();
37
+ expect(platform.bleEventHandler[deviceId.toLowerCase()]).toBeUndefined();
38
+ });
39
+ it('shutdown handler clears all timers and handlers when invoked', async () => {
40
+ // Setup stubbed API and logs
41
+ const handlers = {};
42
+ const api = {
43
+ matter: {
44
+ uuid: { generate: (s) => `uuid-${s}` },
45
+ registerPlatformAccessories: vi.fn(),
46
+ unregisterPlatformAccessories: vi.fn(),
47
+ clusterNames: { OnOff: 'OnOff' },
48
+ },
49
+ isMatterAvailable: () => true,
50
+ isMatterEnabled: () => true,
51
+ on: (ev, fn) => { handlers[ev] = fn; },
52
+ _handlers: handlers,
53
+ };
54
+ const log = { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() };
55
+ const platform = new SwitchBotMatterPlatform(log, {}, api);
56
+ // Add two timers to the platform (using normalized ids)
57
+ const ids = ['devA', 'devB'];
58
+ for (const id of ids) {
59
+ const nid = platform.normalizeDeviceId(id);
60
+ const t = setInterval(() => { }, 100000);
61
+ platform.refreshTimers.set(nid, t);
62
+ platform.accessoryInstances.set(nid, { dummy: true });
63
+ try {
64
+ const mac = formatDeviceIdAsMac(id).toLowerCase();
65
+ platform.bleEventHandler[mac] = () => { };
66
+ }
67
+ catch {
68
+ // ignore formatting errors in this test
69
+ }
70
+ }
71
+ // Invoke didFinishLaunching to ensure the platform registered its shutdown handler
72
+ await Promise.resolve(api._handlers.didFinishLaunching?.());
73
+ // Shutdown handler should now be registered on api._handlers.shutdown
74
+ expect(typeof api._handlers.shutdown).toBe('function');
75
+ // Call the shutdown handler
76
+ await Promise.resolve(api._handlers.shutdown());
77
+ // All refresh timers should be cleared
78
+ for (const id of ids) {
79
+ const nid = platform.normalizeDeviceId(id);
80
+ expect(platform.refreshTimers.get(nid)).toBeUndefined();
81
+ expect(platform.accessoryInstances.get(nid)).toBeUndefined();
82
+ }
83
+ });
84
+ });
85
+ //# sourceMappingURL=platform-matter.cleanup.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-matter.cleanup.test.js","sourceRoot":"","sources":["../src/platform-matter.cleanup.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAExE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAEhD,QAAQ,CAAC,mCAAmC,EAAE,GAAG,EAAE;IACjD,EAAE,CAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;QACtF,6BAA6B;QAC7B,MAAM,QAAQ,GAA4C,EAAE,CAAA;QAC5D,MAAM,GAAG,GAAQ;YACf,MAAM,EAAE;gBACN,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE;gBAC9C,2BAA2B,EAAE,EAAE,CAAC,EAAE,EAAE;gBACpC,6BAA6B,EAAE,EAAE,CAAC,EAAE,EAAE;gBACtC,YAAY,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE;aACjC;YACD,iBAAiB,EAAE,GAAG,EAAE,CAAC,IAAI;YAC7B,eAAe,EAAE,GAAG,EAAE,CAAC,IAAI;YAC3B,EAAE,EAAE,CAAC,EAAU,EAAE,EAA2B,EAAE,EAAE,GAAG,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,CAAA,CAAC,CAAC;YACtE,SAAS,EAAE,QAAQ;SACpB,CAAA;QAED,MAAM,GAAG,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAA;QAE9F,MAAM,QAAQ,GAAG,IAAI,uBAAuB,CAAC,GAAU,EAAE,EAAS,EAAE,GAAG,CAAC,CAAA;QAExE,2DAA2D;QAC3D,MAAM,QAAQ,GAAG,mBAAmB,CAAA;QACpC,MAAM,GAAG,GAAI,QAAgB,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAA;QAEzD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,GAAE,CAAC,EAAE,MAAM,CAAC,CAC1C;QAAC,QAAgB,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAC/C;QAAC,QAAgB,CAAC,kBAAkB,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAC9D;QAAC,QAAgB,CAAC,eAAe,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,GAAG,EAAE,GAAE,CAAC,CAAA;QAErE,0BAA0B;QAC1B,MAAM,CAAE,QAAgB,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;QAC9D,MAAM,CAAE,QAAgB,CAAC,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;QACnE,MAAM,CAAE,QAAgB,CAAC,eAAe,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAG9E;QAAC,QAAgB,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAA;QAEjD,6BAA6B;QAC7B,MAAM,CAAE,QAAgB,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;QAChE,MAAM,CAAE,QAAgB,CAAC,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;QACrE,MAAM,CAAE,QAAgB,CAAC,eAAe,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;IACnF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;QAC5E,6BAA6B;QAC7B,MAAM,QAAQ,GAA4C,EAAE,CAAA;QAC5D,MAAM,GAAG,GAAQ;YACf,MAAM,EAAE;gBACN,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE;gBAC9C,2BAA2B,EAAE,EAAE,CAAC,EAAE,EAAE;gBACpC,6BAA6B,EAAE,EAAE,CAAC,EAAE,EAAE;gBACtC,YAAY,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE;aACjC;YACD,iBAAiB,EAAE,GAAG,EAAE,CAAC,IAAI;YAC7B,eAAe,EAAE,GAAG,EAAE,CAAC,IAAI;YAC3B,EAAE,EAAE,CAAC,EAAU,EAAE,EAA2B,EAAE,EAAE,GAAG,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,CAAA,CAAC,CAAC;YACtE,SAAS,EAAE,QAAQ;SACpB,CAAA;QAED,MAAM,GAAG,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAA;QAE9F,MAAM,QAAQ,GAAG,IAAI,uBAAuB,CAAC,GAAU,EAAE,EAAS,EAAE,GAAG,CAAC,CAAA;QAExE,wDAAwD;QACxD,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAC5B,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;YACrB,MAAM,GAAG,GAAI,QAAgB,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAA;YACnD,MAAM,CAAC,GAAG,WAAW,CAAC,GAAG,EAAE,GAAE,CAAC,EAAE,MAAM,CAAC,CACtC;YAAC,QAAgB,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAC3C;YAAC,QAAgB,CAAC,kBAAkB,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;YAC/D,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,mBAAmB,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAChD;gBAAC,QAAgB,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,GAAE,CAAC,CAAA;YACpD,CAAC;YAAC,MAAM,CAAC;gBACP,wCAAwC;YAC1C,CAAC;QACH,CAAC;QAED,mFAAmF;QACnF,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;QAE3D,sEAAsE;QACtE,MAAM,CAAC,OAAO,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QAEtD,4BAA4B;QAC5B,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAA;QAE/C,uCAAuC;QACvC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;YACrB,MAAM,GAAG,GAAI,QAAgB,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAA;YACnD,MAAM,CAAE,QAAgB,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;YAChE,MAAM,CAAE,QAAgB,CAAC,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;QACvE,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"platform-matter.d.ts","sourceRoot":"","sources":["../src/platform-matter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,GAAG,EACH,qBAAqB,EACrB,OAAO,EAEP,yBAAyB,EAC1B,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAExD,OAAO,KAAK,EAAiB,uBAAuB,EAAE,MAAM,eAAe,CAAA;AA8B3E,qBAAa,uBAAwB,YAAW,qBAAqB;aAkCjD,GAAG,EAAE,OAAO;aACZ,MAAM,EAAE,uBAAuB;aAC/B,GAAG,EAAE,GAAG;IA9B1B,SAAgB,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAY;IAErF,OAAO,CAAC,YAAY,CAAC,CAAkB;IACvC,OAAO,CAAC,YAAY,CAAC,CAAc;IAEnC,OAAO,CAAC,iBAAiB,CAAe;IAExC,OAAO,CAAC,kBAAkB,CAA8B;IAExD,OAAO,CAAC,aAAa,CAAyC;IAE9D,OAAO,CAAC,eAAe,CAA8C;IAErE,OAAO,CAAC,eAAe,CAAC,CAAS;IAGjC,OAAO,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAClC,UAAU,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACrC,eAAe,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC1C,OAAO,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAClC,YAAY,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACvC,QAAQ,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACnC,aAAa,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACxC,QAAQ,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACnC,cAAc,EAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IACvC,uBAAuB,EAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;gBAG9B,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,uBAAuB,EAC/B,GAAG,EAAE,GAAG;IAoK1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAoC5B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAgBvB;;;OAGG;YACW,sBAAsB;IAuCpC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAW5B;;;OAGG;YACW,yBAAyB;IA2gBvC;;OAEG;YACW,eAAe;IAwB7B;;OAEG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,SAAI,EAAE,mBAAmB,SAAO,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,GAAG,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IA0BzJ;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IAwDnC;;;;OAIG;YACW,sBAAsB;IA6LpC;;;OAGG;IACH,kBAAkB;IAMlB;;;;;OAKG;IACH,wBAAwB,CAAC,SAAS,EAAE,yBAAyB;IAK7D;;OAEG;YACW,yBAAyB;IAwFvC;;OAEG;YACW,yBAAyB;IA8CvC;;OAEG;YACW,wBAAwB;IA8CtC;;OAEG;YACW,0BAA0B;IAsBxC;;OAEG;YACW,wBAAwB;IAsBtC;;OAEG;YACW,uBAAuB;IA0DrC;;OAEG;YACW,uBAAuB;IAkCrC;;OAEG;YACW,oBAAoB;IA4BlC;;;;;OAKG;YACW,wBAAwB;IAsBtC;;;;;;OAMG;YACW,qBAAqB;CAqBpC"}
1
+ {"version":3,"file":"platform-matter.d.ts","sourceRoot":"","sources":["../src/platform-matter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,GAAG,EACH,qBAAqB,EACrB,OAAO,EAEP,yBAAyB,EAC1B,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAExD,OAAO,KAAK,EAAiB,uBAAuB,EAAE,MAAM,eAAe,CAAA;AA8B3E,qBAAa,uBAAwB,YAAW,qBAAqB;aAkCjD,GAAG,EAAE,OAAO;aACZ,MAAM,EAAE,uBAAuB;aAC/B,GAAG,EAAE,GAAG;IA9B1B,SAAgB,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAY;IAErF,OAAO,CAAC,YAAY,CAAC,CAAkB;IACvC,OAAO,CAAC,YAAY,CAAC,CAAc;IAEnC,OAAO,CAAC,iBAAiB,CAAe;IAExC,OAAO,CAAC,kBAAkB,CAA8B;IAExD,OAAO,CAAC,aAAa,CAAyC;IAE9D,OAAO,CAAC,eAAe,CAA8C;IAErE,OAAO,CAAC,eAAe,CAAC,CAAS;IAGjC,OAAO,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAClC,UAAU,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACrC,eAAe,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC1C,OAAO,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAClC,YAAY,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACvC,QAAQ,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACnC,aAAa,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACxC,QAAQ,EAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IACnC,cAAc,EAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IACvC,uBAAuB,EAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;gBAG9B,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,uBAAuB,EAC/B,GAAG,EAAE,GAAG;IAoK1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAoC5B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAgBvB;;;OAGG;YACW,sBAAsB;IAuCpC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAW5B;;;OAGG;YACW,yBAAyB;IA2gBvC;;OAEG;YACW,eAAe;IAwB7B;;OAEG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,SAAI,EAAE,mBAAmB,SAAO,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,GAAG,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IA0BzJ;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IA6InC;;;;OAIG;YACW,sBAAsB;IA0OpC;;;OAGG;IACH,kBAAkB;IAMlB;;;;;OAKG;IACH,wBAAwB,CAAC,SAAS,EAAE,yBAAyB;IAK7D;;OAEG;YACW,yBAAyB;IAsIvC;;OAEG;YACW,yBAAyB;IA8CvC;;OAEG;YACW,wBAAwB;IA8CtC;;OAEG;YACW,0BAA0B;IAsBxC;;OAEG;YACW,wBAAwB;IAsBtC;;OAEG;YACW,uBAAuB;IA0DrC;;OAEG;YACW,uBAAuB;IAkCrC;;OAEG;YACW,oBAAoB;IA4BlC;;;;;OAKG;YACW,wBAAwB;IAsBtC;;;;;;OAMG;YACW,qBAAqB;CAqBpC"}
@@ -909,6 +909,14 @@ export class SwitchBotMatterPlatform {
909
909
  const parts = c.split(':').map(Number);
910
910
  [r, g, b] = parts;
911
911
  }
912
+ else if (c.includes(',')) {
913
+ const parts = c.split(',').map(s => Number(s.trim()));
914
+ [r, g, b] = parts;
915
+ }
916
+ else if (c.includes(' ')) {
917
+ const parts = c.split(' ').map(s => Number(s.trim()));
918
+ [r, g, b] = parts;
919
+ }
912
920
  else if (c.startsWith('#')) {
913
921
  const hex = c.replace('#', '');
914
922
  r = Number.parseInt(hex.substring(0, 2), 16);
@@ -927,6 +935,72 @@ export class SwitchBotMatterPlatform {
927
935
  if (battery !== undefined) {
928
936
  result.battery = Number(battery);
929
937
  }
938
+ // VOC / TVOC (some air quality devices report total volatile organic compounds)
939
+ const voc = sd.voc ?? sd.tvoc;
940
+ if (voc !== undefined) {
941
+ result.voc = Number(voc);
942
+ }
943
+ // PM10 (some devices report PM10 alongside PM2.5)
944
+ const pm10 = sd.pm10;
945
+ if (pm10 !== undefined) {
946
+ result.pm10 = Number(pm10);
947
+ }
948
+ // PM2.5 (some BLE adverts use pm25 / pm_2_5)
949
+ const pm25 = sd.pm2_5 ?? sd.pm25 ?? sd.pm_2_5;
950
+ if (pm25 !== undefined) {
951
+ result.pm25 = Number(pm25);
952
+ }
953
+ // CO2 (carbon dioxide ppm)
954
+ const co2 = sd.co2 ?? sd.co2ppm ?? sd.carbonDioxide;
955
+ if (co2 !== undefined) {
956
+ result.co2 = Number(co2);
957
+ }
958
+ // Temperature (C) and Humidity (%) — support common shorthand keys
959
+ const temperature = sd.temperature ?? sd.temp ?? sd.t;
960
+ if (temperature !== undefined) {
961
+ result.temperature = Number(temperature);
962
+ }
963
+ const humidity = sd.humidity ?? sd.h ?? sd.humid;
964
+ if (humidity !== undefined) {
965
+ result.humidity = Number(humidity);
966
+ }
967
+ // Motion, Contact, Leak
968
+ const motion = sd.motion ?? sd.m;
969
+ if (motion !== undefined) {
970
+ result.motion = Boolean(motion);
971
+ }
972
+ const contact = sd.contact ?? sd.open;
973
+ if (contact !== undefined) {
974
+ result.contact = contact;
975
+ }
976
+ const leak = sd.leak ?? sd.water;
977
+ if (leak !== undefined) {
978
+ result.leak = Boolean(leak);
979
+ }
980
+ // Position / Cover / Curtain synonyms
981
+ const position = sd.position ?? sd.percent ?? sd.slidePosition ?? sd.curtainPosition;
982
+ if (position !== undefined) {
983
+ result.position = Number(position);
984
+ }
985
+ // Fan speed/speed
986
+ const fanSpeed = sd.fanSpeed ?? sd.speed;
987
+ if (fanSpeed !== undefined) {
988
+ result.fanSpeed = Number(fanSpeed);
989
+ }
990
+ // Lock state
991
+ const lock = sd.lock;
992
+ if (lock !== undefined) {
993
+ result.lock = lock;
994
+ }
995
+ // Robot vacuum fields
996
+ const rvcRunMode = sd.rvcRunMode;
997
+ if (rvcRunMode !== undefined) {
998
+ result.rvcRunMode = rvcRunMode;
999
+ }
1000
+ const rvcOperationalState = sd.rvcOperationalState;
1001
+ if (rvcOperationalState !== undefined) {
1002
+ result.rvcOperationalState = rvcOperationalState;
1003
+ }
930
1004
  return result;
931
1005
  }
932
1006
  catch (e) {
@@ -984,6 +1058,14 @@ export class SwitchBotMatterPlatform {
984
1058
  const parts = color.split(':').map(Number);
985
1059
  [r, g, b] = parts;
986
1060
  }
1061
+ else if (color.includes(',')) {
1062
+ const parts = color.split(',').map(s => Number(s.trim()));
1063
+ [r, g, b] = parts;
1064
+ }
1065
+ else if (color.includes(' ')) {
1066
+ const parts = color.split(' ').map(s => Number(s.trim()));
1067
+ [r, g, b] = parts;
1068
+ }
987
1069
  else if (color.startsWith('#')) {
988
1070
  const hex = color.replace('#', '');
989
1071
  r = Number.parseInt(hex.substring(0, 2), 16);
@@ -993,10 +1075,10 @@ export class SwitchBotMatterPlatform {
993
1075
  const [h, s] = rgb2hs(r, g, b);
994
1076
  await safeUpdate(this.api.matter.clusterNames.ColorControl, { currentHue: Math.round((h / 360) * 254), currentSaturation: Math.round((s / 100) * 254) }, 'updateHueSaturation');
995
1077
  }
996
- // Battery/powerSource
997
- if (status?.battery !== undefined || status?.batt !== undefined) {
1078
+ // Battery/powerSource (support many possible field names)
1079
+ if (status?.battery !== undefined || status?.batt !== undefined || status?.batteryLevel !== undefined || status?.batteryPercentage !== undefined || status?.battery_level !== undefined) {
998
1080
  try {
999
- const percentage = Number(status?.battery ?? status?.batt);
1081
+ const percentage = Number(status?.battery ?? status?.batt ?? status?.batteryPercentage ?? status?.batteryLevel ?? status?.battery_level);
1000
1082
  const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)));
1001
1083
  let batChargeLevel = 0;
1002
1084
  if (percentage < 20) {
@@ -1005,7 +1087,8 @@ export class SwitchBotMatterPlatform {
1005
1087
  else if (percentage < 40) {
1006
1088
  batChargeLevel = 1;
1007
1089
  }
1008
- await safeUpdate('powerSource', { batPercentRemaining, batChargeLevel }, 'updateBatteryPercentage');
1090
+ const powerCluster = (this.api.matter?.clusterNames && this.api.matter.clusterNames.PowerSource) ? this.api.matter.clusterNames.PowerSource : 'powerSource';
1091
+ await safeUpdate(powerCluster, { batPercentRemaining, batChargeLevel }, 'updateBatteryPercentage');
1009
1092
  }
1010
1093
  catch (e) {
1011
1094
  this.debugLog(`Failed to apply battery status for ${dev.deviceId}: ${e?.message ?? e}`);
@@ -1028,10 +1111,10 @@ export class SwitchBotMatterPlatform {
1028
1111
  this.debugLog(`Failed to apply temperature for ${dev.deviceId}: ${e?.message ?? e}`);
1029
1112
  }
1030
1113
  }
1031
- // Humidity
1032
- if (status?.humidity !== undefined || status?.h !== undefined) {
1114
+ // Humidity (support different keys)
1115
+ if (status?.humidity !== undefined || status?.h !== undefined || status?.humid !== undefined) {
1033
1116
  try {
1034
- const percent = Number(status?.humidity ?? status?.h);
1117
+ const percent = Number(status?.humidity ?? status?.h ?? status?.humid);
1035
1118
  const measured = Math.round(percent * 100);
1036
1119
  await safeUpdate('relativeHumidityMeasurement', { measuredValue: measured }, 'updateHumidity');
1037
1120
  }
@@ -1118,6 +1201,46 @@ export class SwitchBotMatterPlatform {
1118
1201
  this.debugLog(`Failed to apply rvcRunMode for ${dev.deviceId}: ${e?.message ?? e}`);
1119
1202
  }
1120
1203
  }
1204
+ // CO2 (carbon dioxide) - support common synonyms
1205
+ if (status?.co2 !== undefined || status?.co2ppm !== undefined || status?.carbonDioxide !== undefined) {
1206
+ try {
1207
+ const val = Number(status?.co2 ?? status?.co2ppm ?? status?.carbonDioxide);
1208
+ await safeUpdate('carbonDioxide', { carbonDioxideLevel: val }, 'updateCO2');
1209
+ }
1210
+ catch (e) {
1211
+ this.debugLog(`Failed to apply CO2 for ${dev.deviceId}: ${e?.message ?? e}`);
1212
+ }
1213
+ }
1214
+ // PM2.5 / particulate matter
1215
+ if (status?.pm2_5 !== undefined || status?.pm25 !== undefined || status?.pm_2_5 !== undefined) {
1216
+ try {
1217
+ const pm = Number(status?.pm2_5 ?? status?.pm25 ?? status?.pm_2_5);
1218
+ await safeUpdate('pm2_5', { pm25: pm }, 'updatePM25');
1219
+ }
1220
+ catch (e) {
1221
+ this.debugLog(`Failed to apply PM2.5 for ${dev.deviceId}: ${e?.message ?? e}`);
1222
+ }
1223
+ }
1224
+ // PM10 (some devices report pm10)
1225
+ if (status?.pm10 !== undefined) {
1226
+ try {
1227
+ const pm10 = Number(status?.pm10);
1228
+ await safeUpdate('pm10', { pm10 }, 'updatePM10');
1229
+ }
1230
+ catch (e) {
1231
+ this.debugLog(`Failed to apply PM10 for ${dev.deviceId}: ${e?.message ?? e}`);
1232
+ }
1233
+ }
1234
+ // VOC / TVOC - volatile organic compounds
1235
+ if (status?.voc !== undefined || status?.tvoc !== undefined) {
1236
+ try {
1237
+ const val = Number(status?.voc ?? status?.tvoc);
1238
+ await safeUpdate('voc', { voc: val }, 'updateVOC');
1239
+ }
1240
+ catch (e) {
1241
+ this.debugLog(`Failed to apply VOC for ${dev.deviceId}: ${e?.message ?? e}`);
1242
+ }
1243
+ }
1121
1244
  if (status?.rvcOperationalState !== undefined) {
1122
1245
  try {
1123
1246
  await safeUpdate('rvcOperationalState', { operationalState: Number(status.rvcOperationalState) }, 'updateOperationalState');
@@ -1164,6 +1287,55 @@ export class SwitchBotMatterPlatform {
1164
1287
  this.infoLog(`Registering ${this.discoveredDevices.length} discovered SwitchBot device(s) as Matter accessories`);
1165
1288
  // Merge device config (deviceConfig per deviceType and per-device overrides) to match HAP behavior
1166
1289
  const devicesToProcess = await this.mergeDiscoveredDevices(this.discoveredDevices);
1290
+ // By default, automatically remove previously-registered Matter
1291
+ // accessories whose deviceId is not present in the merged discovered
1292
+ // list. If the user explicitly sets `options.keepStaleAccessories` to
1293
+ // true, then we will keep previously-registered accessories (legacy
1294
+ // behavior).
1295
+ if (this.config.options?.keepStaleAccessories) {
1296
+ this.debugLog('Keeping previously-registered stale accessories because options.keepStaleAccessories=true');
1297
+ }
1298
+ else {
1299
+ try {
1300
+ const desiredIds = new Set((devicesToProcess || []).map((d) => this.normalizeDeviceId(d.deviceId)));
1301
+ const toUnregister = [];
1302
+ for (const [uuid, acc] of Array.from(this.matterAccessories.entries())) {
1303
+ try {
1304
+ const deviceId = acc?.context?.deviceId;
1305
+ if (!deviceId) {
1306
+ continue;
1307
+ }
1308
+ const nid = this.normalizeDeviceId(deviceId);
1309
+ if (!desiredIds.has(nid)) {
1310
+ // Accessory exists but is no longer desired -> schedule for removal
1311
+ this.infoLog(`Removing previously-registered accessory for deviceId=${deviceId} (no longer discovered or configured)`);
1312
+ try {
1313
+ this.clearDeviceResources(deviceId);
1314
+ }
1315
+ catch (e) {
1316
+ this.debugLog(`Failed to clear resources for ${deviceId} before unregister: ${e?.message ?? e}`);
1317
+ }
1318
+ toUnregister.push(acc);
1319
+ this.matterAccessories.delete(uuid);
1320
+ }
1321
+ }
1322
+ catch (e) {
1323
+ this.debugLog(`Error while checking existing accessory ${uuid}: ${e?.message ?? e}`);
1324
+ }
1325
+ }
1326
+ if (toUnregister.length > 0) {
1327
+ try {
1328
+ await this.api.matter.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, toUnregister);
1329
+ }
1330
+ catch (e) {
1331
+ this.debugLog(`Failed to unregister accessories: ${e?.message ?? e}`);
1332
+ }
1333
+ }
1334
+ }
1335
+ catch (e) {
1336
+ this.debugLog(`Failed to remove stale accessories: ${e?.message ?? e}`);
1337
+ }
1338
+ }
1167
1339
  // We'll separate discovered devices into two buckets:
1168
1340
  // - platformAccessories: accessories that will be hosted under the plugin's Matter bridge
1169
1341
  // - roboticAccessories: robot vacuum devices which require standalone commissioning behaviour