@switchbot/homebridge-switchbot 5.0.0 → 5.1.0-beta.1

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 (85) hide show
  1. package/.github/npm-version-script-esm.js +97 -0
  2. package/CHANGELOG.md +21 -8
  3. package/MIGRATION.md +5 -0
  4. package/README.md +25 -0
  5. package/config.schema.json +11 -0
  6. package/dist/SwitchBotHAPPlatform.d.ts.map +1 -1
  7. package/dist/SwitchBotHAPPlatform.js +33 -4
  8. package/dist/SwitchBotHAPPlatform.js.map +1 -1
  9. package/dist/SwitchBotMatterPlatform.d.ts +2 -0
  10. package/dist/SwitchBotMatterPlatform.d.ts.map +1 -1
  11. package/dist/SwitchBotMatterPlatform.js +61 -5
  12. package/dist/SwitchBotMatterPlatform.js.map +1 -1
  13. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
  14. package/dist/homebridge-ui/endpoints/config.js +9 -2
  15. package/dist/homebridge-ui/endpoints/config.js.map +1 -1
  16. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +9 -2
  17. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
  18. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +8 -0
  19. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
  20. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +23 -0
  21. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +1 -0
  22. package/dist/homebridge-ui/public/css/styles.css +61 -1
  23. package/dist/homebridge-ui/public/index.html +25 -2
  24. package/dist/homebridge-ui/public/js/api.d.ts +16 -0
  25. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  26. package/dist/homebridge-ui/public/js/api.js +48 -0
  27. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  28. package/dist/homebridge-ui/public/js/api.ts +61 -1
  29. package/dist/homebridge-ui/public/js/app.js +237 -111
  30. package/dist/homebridge-ui/public/js/app.js.map +4 -4
  31. package/dist/homebridge-ui/public/js/app.ts +22 -4
  32. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  33. package/dist/homebridge-ui/public/js/devices.js +6 -0
  34. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  35. package/dist/homebridge-ui/public/js/devices.ts +6 -0
  36. package/dist/homebridge-ui/public/js/modals.d.ts +1 -0
  37. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  38. package/dist/homebridge-ui/public/js/modals.js +53 -0
  39. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  40. package/dist/homebridge-ui/public/js/modals.ts +58 -0
  41. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  42. package/dist/homebridge-ui/public/js/render.js +2 -1
  43. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  44. package/dist/homebridge-ui/public/js/render.ts +2 -1
  45. package/dist/homebridge-ui/settings.js.map +1 -1
  46. package/dist/homebridge-ui/utils/config-parser.d.ts +8 -0
  47. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
  48. package/dist/homebridge-ui/utils/config-parser.js +8 -0
  49. package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
  50. package/dist/homebridge-ui/utils/v4-detection.d.ts +12 -0
  51. package/dist/homebridge-ui/utils/v4-detection.d.ts.map +1 -0
  52. package/dist/homebridge-ui/utils/v4-detection.js +23 -0
  53. package/dist/homebridge-ui/utils/v4-detection.js.map +1 -0
  54. package/dist/settings.d.ts +11 -0
  55. package/dist/settings.d.ts.map +1 -1
  56. package/dist/settings.js.map +1 -1
  57. package/dist/utils.d.ts +5 -0
  58. package/dist/utils.d.ts.map +1 -1
  59. package/dist/utils.js +27 -0
  60. package/dist/utils.js.map +1 -1
  61. package/docs/assets/highlight.css +15 -15
  62. package/docs/assets/icons.js +1 -1
  63. package/docs/assets/icons.svg +1 -1
  64. package/docs/assets/main.js +3 -3
  65. package/docs/index.html +22 -5
  66. package/docs/variables/default.html +1 -1
  67. package/package.json +6 -6
  68. package/scripts/build-ui.js +30 -0
  69. package/src/SwitchBotHAPPlatform.ts +31 -6
  70. package/src/SwitchBotMatterPlatform.ts +61 -7
  71. package/src/homebridge-ui/endpoints/config.ts +10 -2
  72. package/src/homebridge-ui/public/css/styles.css +61 -1
  73. package/src/homebridge-ui/public/index.html +23 -0
  74. package/src/homebridge-ui/public/js/api.ts +61 -1
  75. package/src/homebridge-ui/public/js/app.ts +22 -4
  76. package/src/homebridge-ui/public/js/devices.ts +6 -0
  77. package/src/homebridge-ui/public/js/modals.ts +58 -0
  78. package/src/homebridge-ui/public/js/render.ts +2 -1
  79. package/src/homebridge-ui/utils/config-parser.ts +9 -0
  80. package/src/homebridge-ui/utils/v4-detection.ts +23 -0
  81. package/src/settings.ts +12 -0
  82. package/src/utils.ts +31 -0
  83. package/test/homebridge-ui/config-parser.spec.ts +61 -0
  84. package/test/platform/matter-integration.spec.ts +50 -0
  85. package/test/utils/utils.spec.ts +15 -0
package/docs/index.html CHANGED
@@ -40,6 +40,23 @@
40
40
  </ul>
41
41
  <p>Click <strong>Save Advanced Settings</strong> to apply changes. These settings match the options available in <code>config.schema.json</code> and can be overridden per device.</p>
42
42
  <!-- Optionally add a screenshot here -->
43
+ <h3 id="per-device-external-publishing-hap-or-matter" class="tsd-anchor-link">Per-Device External Publishing (HAP or Matter)<a href="#per-device-external-publishing-hap-or-matter" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
44
+ <p>Each device now supports an <code>externalPublishProtocol</code> option to control whether it is published as an external accessory.</p>
45
+ <ul>
46
+ <li><code>none</code> (default): normal platform registration behavior.</li>
47
+ <li><code>hap</code>: publish this device as a HAP external accessory.</li>
48
+ <li><code>matter</code>: publish this device as a Matter external accessory.</li>
49
+ </ul>
50
+ <p>Behavior notes:</p>
51
+ <ul>
52
+ <li>This is configured per device in the Homebridge UI under <strong>Publish as External Device</strong> when adding or editing a device.</li>
53
+ <li>If the selected external publish API is not available on your Homebridge runtime, the plugin falls back to normal platform registration for that device.</li>
54
+ <li>Use <code>hap</code> for HAP-focused setups and <code>matter</code> for Matter-focused setups; leave as <code>none</code> unless you specifically want external accessory publishing.</li>
55
+ </ul>
56
+ <p>Example device config excerpt:</p>
57
+ <pre><code class="json"><span class="hl-0">{</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;deviceId&quot;</span><span class="hl-0">: </span><span class="hl-2">&quot;E7F8A1B2C3D4&quot;</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;configDeviceName&quot;</span><span class="hl-0">: </span><span class="hl-2">&quot;Bedroom Bot&quot;</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;configDeviceType&quot;</span><span class="hl-0">: </span><span class="hl-2">&quot;Bot&quot;</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;externalPublishProtocol&quot;</span><span class="hl-0">: </span><span class="hl-2">&quot;hap&quot;</span><br/><span class="hl-0">}</span>
58
+ </code><button type="button">Copy</button></pre>
59
+
43
60
  <ul>
44
61
  <li>
45
62
  <h3 id="if-using-openapi-connection" class="tsd-anchor-link">If using OpenAPI Connection<a href="#if-using-openapi-connection" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
@@ -117,7 +134,7 @@
117
134
  (This is what is intended in documentation for the noble bluetooth package <a href="https://github.com/abandonware/noble#prerequisites">prerequisites</a> by &quot;Add terminal app&quot;, however for HomeBridge it is <code>node</code> that needs the permission granted, not <code>terminal</code>.
118
135
  Without this step, then you will receive the following error when the swichbot plugin launches, which will cause Homebridge or the child bridge process to restart:</li>
119
136
  </ol>
120
- <pre><code><span class="hl-0">Error</span><span class="hl-1">: </span><span class="hl-2">Failed</span><span class="hl-1"> </span><span class="hl-2">to</span><span class="hl-1"> </span><span class="hl-2">initialize</span><span class="hl-1"> </span><span class="hl-2">the</span><span class="hl-1"> </span><span class="hl-2">Noble</span><span class="hl-1"> </span><span class="hl-0">object</span><span class="hl-1">: </span><span class="hl-2">unauthorized</span><br/><span class="hl-1"> </span><span class="hl-2">at</span><span class="hl-1"> </span><span class="hl-2">Noble</span><span class="hl-1">.&lt;</span><span class="hl-3">anonymous</span><span class="hl-1">&gt; (</span><span class="hl-2">file</span><span class="hl-1">:</span><span class="hl-4">///usr/local/lib/node_modules/@switchbot/homebridge-switchbot/node_modules/node-switchbot/src/switchbot.ts:244:19)</span><br/><span class="hl-1"> </span><span class="hl-3">at</span><span class="hl-1"> </span><span class="hl-3">Object</span><span class="hl-1">.</span><span class="hl-3">onceWrapper</span><span class="hl-1"> (</span><span class="hl-2">node</span><span class="hl-1">:</span><span class="hl-3">events</span><span class="hl-1">:</span><span class="hl-5">629</span><span class="hl-1">:</span><span class="hl-5">26</span><span class="hl-1">)</span><br/><span class="hl-1"> </span><span class="hl-3">at</span><span class="hl-1"> </span><span class="hl-3">Noble</span><span class="hl-1">.</span><span class="hl-3">emit</span><span class="hl-1"> (</span><span class="hl-2">node</span><span class="hl-1">:</span><span class="hl-3">events</span><span class="hl-1">:</span><span class="hl-5">514</span><span class="hl-1">:</span><span class="hl-5">28</span><span class="hl-1">)</span><br/><span class="hl-1"> </span><span class="hl-3">at</span><span class="hl-1"> </span><span class="hl-3">Noble</span><span class="hl-1">.</span><span class="hl-3">onStateChange</span><span class="hl-1"> (/</span><span class="hl-3">usr</span><span class="hl-1">/</span><span class="hl-3">local</span><span class="hl-1">/</span><span class="hl-3">lib</span><span class="hl-1">/</span><span class="hl-3">node_modules</span><span class="hl-1">/@</span><span class="hl-3">switchbot</span><span class="hl-1">/</span><span class="hl-3">homebridge</span><span class="hl-1">-</span><span class="hl-3">switchbot</span><span class="hl-1">/</span><span class="hl-3">node_modules</span><span class="hl-1">/@</span><span class="hl-3">stoprocent</span><span class="hl-1">/</span><span class="hl-3">noble</span><span class="hl-1">/</span><span class="hl-3">lib</span><span class="hl-1">/</span><span class="hl-3">noble</span><span class="hl-1">.</span><span class="hl-3">js</span><span class="hl-1">:</span><span class="hl-5">92</span><span class="hl-1">:</span><span class="hl-5">8</span><span class="hl-1">)</span><br/><span class="hl-1"> </span><span class="hl-3">at</span><span class="hl-1"> </span><span class="hl-3">NobleMac</span><span class="hl-1">.</span><span class="hl-3">emit</span><span class="hl-1"> (</span><span class="hl-2">node</span><span class="hl-1">:</span><span class="hl-3">events</span><span class="hl-1">:</span><span class="hl-5">514</span><span class="hl-1">:</span><span class="hl-5">28</span><span class="hl-1">)</span>
137
+ <pre><code><span class="hl-3">Error</span><span class="hl-0">: </span><span class="hl-4">Failed</span><span class="hl-0"> </span><span class="hl-4">to</span><span class="hl-0"> </span><span class="hl-4">initialize</span><span class="hl-0"> </span><span class="hl-4">the</span><span class="hl-0"> </span><span class="hl-4">Noble</span><span class="hl-0"> </span><span class="hl-3">object</span><span class="hl-0">: </span><span class="hl-4">unauthorized</span><br/><span class="hl-0"> </span><span class="hl-4">at</span><span class="hl-0"> </span><span class="hl-4">Noble</span><span class="hl-0">.&lt;</span><span class="hl-5">anonymous</span><span class="hl-0">&gt; (</span><span class="hl-4">file</span><span class="hl-0">:</span><span class="hl-6">///usr/local/lib/node_modules/@switchbot/homebridge-switchbot/node_modules/node-switchbot/src/switchbot.ts:244:19)</span><br/><span class="hl-0"> </span><span class="hl-5">at</span><span class="hl-0"> </span><span class="hl-5">Object</span><span class="hl-0">.</span><span class="hl-5">onceWrapper</span><span class="hl-0"> (</span><span class="hl-4">node</span><span class="hl-0">:</span><span class="hl-5">events</span><span class="hl-0">:</span><span class="hl-7">629</span><span class="hl-0">:</span><span class="hl-7">26</span><span class="hl-0">)</span><br/><span class="hl-0"> </span><span class="hl-5">at</span><span class="hl-0"> </span><span class="hl-5">Noble</span><span class="hl-0">.</span><span class="hl-5">emit</span><span class="hl-0"> (</span><span class="hl-4">node</span><span class="hl-0">:</span><span class="hl-5">events</span><span class="hl-0">:</span><span class="hl-7">514</span><span class="hl-0">:</span><span class="hl-7">28</span><span class="hl-0">)</span><br/><span class="hl-0"> </span><span class="hl-5">at</span><span class="hl-0"> </span><span class="hl-5">Noble</span><span class="hl-0">.</span><span class="hl-5">onStateChange</span><span class="hl-0"> (/</span><span class="hl-5">usr</span><span class="hl-0">/</span><span class="hl-5">local</span><span class="hl-0">/</span><span class="hl-5">lib</span><span class="hl-0">/</span><span class="hl-5">node_modules</span><span class="hl-0">/@</span><span class="hl-5">switchbot</span><span class="hl-0">/</span><span class="hl-5">homebridge</span><span class="hl-0">-</span><span class="hl-5">switchbot</span><span class="hl-0">/</span><span class="hl-5">node_modules</span><span class="hl-0">/@</span><span class="hl-5">stoprocent</span><span class="hl-0">/</span><span class="hl-5">noble</span><span class="hl-0">/</span><span class="hl-5">lib</span><span class="hl-0">/</span><span class="hl-5">noble</span><span class="hl-0">.</span><span class="hl-5">js</span><span class="hl-0">:</span><span class="hl-7">92</span><span class="hl-0">:</span><span class="hl-7">8</span><span class="hl-0">)</span><br/><span class="hl-0"> </span><span class="hl-5">at</span><span class="hl-0"> </span><span class="hl-5">NobleMac</span><span class="hl-0">.</span><span class="hl-5">emit</span><span class="hl-0"> (</span><span class="hl-4">node</span><span class="hl-0">:</span><span class="hl-5">events</span><span class="hl-0">:</span><span class="hl-7">514</span><span class="hl-0">:</span><span class="hl-7">28</span><span class="hl-0">)</span>
121
138
  </code><button>Copy</button></pre>
122
139
 
123
140
  </li>
@@ -345,7 +362,7 @@ Without this step, then you will receive the following error when the swichbot p
345
362
  <h4 id="how-to-configure-in-homebridge" class="tsd-anchor-link">How to Configure in Homebridge<a href="#how-to-configure-in-homebridge" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h4>
346
363
  <p>In the Homebridge UI, when adding or editing a SwitchBot device, enter the <strong>Encryption Key</strong> and <strong>Key ID</strong> in the provided fields. These values will be securely used for BLE communication with your device.</p>
347
364
  <p><strong>Example device config excerpt:</strong></p>
348
- <pre><code class="json"><span class="hl-1">{</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;deviceId&quot;</span><span class="hl-1">: </span><span class="hl-7">&quot;E7F8A1B2C3D4&quot;</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;deviceName&quot;</span><span class="hl-1">: </span><span class="hl-7">&quot;SwitchBot Lock&quot;</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;enableBLE&quot;</span><span class="hl-1">: </span><span class="hl-8">true</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;encryptionKey&quot;</span><span class="hl-1">: </span><span class="hl-7">&quot;0123456789abcdef0123456789abcdef&quot;</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;keyId&quot;</span><span class="hl-1">: </span><span class="hl-7">&quot;01&quot;</span><br/><span class="hl-1">}</span>
365
+ <pre><code class="json"><span class="hl-0">{</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;deviceId&quot;</span><span class="hl-0">: </span><span class="hl-2">&quot;E7F8A1B2C3D4&quot;</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;deviceName&quot;</span><span class="hl-0">: </span><span class="hl-2">&quot;SwitchBot Lock&quot;</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;enableBLE&quot;</span><span class="hl-0">: </span><span class="hl-8">true</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;encryptionKey&quot;</span><span class="hl-0">: </span><span class="hl-2">&quot;0123456789abcdef0123456789abcdef&quot;</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;keyId&quot;</span><span class="hl-0">: </span><span class="hl-2">&quot;01&quot;</span><br/><span class="hl-0">}</span>
349
366
  </code><button type="button">Copy</button></pre>
350
367
 
351
368
  <h4 id="which-devices-require-ble-encryption" class="tsd-anchor-link">Which Devices Require BLE Encryption?<a href="#which-devices-require-ble-encryption" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h4>
@@ -494,7 +511,7 @@ Without this step, then you will receive the following error when the swichbot p
494
511
  <li><code>dailyApiResetLocalMidnight</code> (boolean, default false): if true, resets the daily counter at local midnight; when false, resets at UTC midnight.</li>
495
512
  </ul>
496
513
  <p>Example (excerpt):</p>
497
- <pre><code class="json"><span class="hl-1">{</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;options&quot;</span><span class="hl-1">: {</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;dailyApiLimit&quot;</span><span class="hl-1">: </span><span class="hl-5">10000</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;dailyApiReserveForCommands&quot;</span><span class="hl-1">: </span><span class="hl-5">1000</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;webhookOnlyOnReserve&quot;</span><span class="hl-1">: </span><span class="hl-8">false</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">&quot;dailyApiResetLocalMidnight&quot;</span><span class="hl-1">: </span><span class="hl-8">true</span><br/><span class="hl-1"> }</span><br/><span class="hl-1">}</span>
514
+ <pre><code class="json"><span class="hl-0">{</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;options&quot;</span><span class="hl-0">: {</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;dailyApiLimit&quot;</span><span class="hl-0">: </span><span class="hl-7">10000</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;dailyApiReserveForCommands&quot;</span><span class="hl-0">: </span><span class="hl-7">1000</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;webhookOnlyOnReserve&quot;</span><span class="hl-0">: </span><span class="hl-8">false</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">&quot;dailyApiResetLocalMidnight&quot;</span><span class="hl-0">: </span><span class="hl-8">true</span><br/><span class="hl-0"> }</span><br/><span class="hl-0">}</span>
498
515
  </code><button type="button">Copy</button></pre>
499
516
 
500
517
  <h2 id="switchbot-apis" class="tsd-anchor-link">SwitchBot APIs<a href="#switchbot-apis" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2>
@@ -510,7 +527,7 @@ Without this step, then you will receive the following error when the swichbot p
510
527
  <ul>
511
528
  <li>
512
529
  <p>Run unit tests:</p>
513
- <pre><code class="bash"><span class="hl-9">npm</span><span class="hl-1"> </span><span class="hl-7">run</span><span class="hl-1"> </span><span class="hl-7">test</span>
530
+ <pre><code class="bash"><span class="hl-9">npm</span><span class="hl-0"> </span><span class="hl-2">run</span><span class="hl-0"> </span><span class="hl-2">test</span>
514
531
  </code><button type="button">Copy</button></pre>
515
532
 
516
533
  </li>
@@ -527,4 +544,4 @@ Without this step, then you will receive the following error when the swichbot p
527
544
  <li><a href="https://www.facebook.com/SwitchBotRobot/">Facebook @SwitchBotRobot</a></li>
528
545
  <li><a href="https://twitter.com/switchbot">Twitter @SwitchBot</a></li>
529
546
  </ul>
530
- </div></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div><details open class="tsd-accordion tsd-page-navigation"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>On This Page</h3></summary><div class="tsd-accordion-details"><a href="#switchbothomebridge-switchbot"><span>@switchbot/homebridge-<wbr/>switchbot</span></a><ul><li><a href="#installation"><span>Installation</span></a></li><li><a href="#configuration"><span>Configuration</span></a></li><li><ul><li><a href="#openapi-pollingrate-advanced-settings-ui"><span>Open<wbr/>API <wbr/>Polling/<wbr/>Rate <wbr/>Advanced <wbr/>Settings (<wbr/>UI)</span></a></li><li><a href="#if-using-openapi-connection"><span>If using <wbr/>Open<wbr/>API <wbr/>Connection</span></a></li><li><a href="#if-using-ble-connection"><span>If using <wbr/>BLE <wbr/>Connection</span></a></li></ul></li><li><a href="#troubleshooting"><span>Troubleshooting</span></a></li><li><ul><li><a href="#if-using-linux-raspberry-pi-os"><span>If using <wbr/>Linux / <wbr/>Raspberry <wbr/>Pi <wbr/>OS</span></a></li><li><a href="#if-using-macos"><span>If using <wbr/>Mac<wbr/>OS</span></a></li></ul></li><li><a href="#supported-switchbot-devices"><span>Supported <wbr/>Switch<wbr/>Bot <wbr/>Devices</span></a></li><li><a href="#ble-encryption-support"><span>BLE <wbr/>Encryption <wbr/>Support</span></a></li><li><ul><li><a href="#ble-encryption-key-and-key-id"><span>BLE <wbr/>Encryption <wbr/>Key and <wbr/>Key <wbr/>ID</span></a></li><li><ul><li><a href="#how-to-obtain-ble-encryption-key-and-key-id"><span>How to <wbr/>Obtain <wbr/>BLE <wbr/>Encryption <wbr/>Key and <wbr/>Key <wbr/>ID</span></a></li><li><a href="#how-to-configure-in-homebridge"><span>How to <wbr/>Configure in <wbr/>Homebridge</span></a></li><li><a href="#which-devices-require-ble-encryption"><span>Which <wbr/>Devices <wbr/>Require <wbr/>BLE <wbr/>Encryption?</span></a></li></ul></li></ul></li><li><a href="#supported-ir-devices"><span>Supported <wbr/>IR <wbr/>Devices</span></a></li><li><ul><li><a href="#all-ir-devices-require-switchbot-hub-2-switchbot-hub-mini-2-switchbot-hub-3-or-hub-mini"><span>(<wbr/>All <wbr/>IR <wbr/>Devices require <wbr/>Switch<wbr/>Bot <wbr/>Hub 2, <wbr/>Switch<wbr/>Bot <wbr/>Hub <wbr/>Mini 2, <wbr/>Switch<wbr/>Bot <wbr/>Hub 3, or <wbr/>Hub <wbr/>Mini)</span></a></li></ul></li><li><a href="#matter-platform"><span>Matter <wbr/>Platform</span></a></li><li><ul><li><a href="#batched-refresh-and-api-load-control"><span>Batched refresh and <wbr/>API load control</span></a></li></ul></li><li><a href="#whats-new-with-node-switchbot-v400"><span>What&#39;s new with node-<wbr/>switchbot v4.0.0</span></a></li><li><a href="#openapi-rate-limiting-and-daily-budget"><span>Open<wbr/>API rate limiting and daily budget</span></a></li><li><a href="#switchbot-apis"><span>Switch<wbr/>Bot <wbr/>AP<wbr/>Is</span></a></li><li><a href="#development-tests"><span>Development / <wbr/>Tests</span></a></li><li><a href="#community"><span>Community</span></a></li></ul></div></details></div><div class="site-menu"><nav class="tsd-navigation"><a href="modules.html">@switchbot/homebridge-switchbot</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer></footer><div class="overlay"></div></body></html>
547
+ </div></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div><details open class="tsd-accordion tsd-page-navigation"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>On This Page</h3></summary><div class="tsd-accordion-details"><a href="#switchbothomebridge-switchbot"><span>@switchbot/homebridge-<wbr/>switchbot</span></a><ul><li><a href="#installation"><span>Installation</span></a></li><li><a href="#configuration"><span>Configuration</span></a></li><li><ul><li><a href="#openapi-pollingrate-advanced-settings-ui"><span>Open<wbr/>API <wbr/>Polling/<wbr/>Rate <wbr/>Advanced <wbr/>Settings (<wbr/>UI)</span></a></li><li><a href="#per-device-external-publishing-hap-or-matter"><span>Per-<wbr/>Device <wbr/>External <wbr/>Publishing (<wbr/>HAP or <wbr/>Matter)</span></a></li><li><a href="#if-using-openapi-connection"><span>If using <wbr/>Open<wbr/>API <wbr/>Connection</span></a></li><li><a href="#if-using-ble-connection"><span>If using <wbr/>BLE <wbr/>Connection</span></a></li></ul></li><li><a href="#troubleshooting"><span>Troubleshooting</span></a></li><li><ul><li><a href="#if-using-linux-raspberry-pi-os"><span>If using <wbr/>Linux / <wbr/>Raspberry <wbr/>Pi <wbr/>OS</span></a></li><li><a href="#if-using-macos"><span>If using <wbr/>Mac<wbr/>OS</span></a></li></ul></li><li><a href="#supported-switchbot-devices"><span>Supported <wbr/>Switch<wbr/>Bot <wbr/>Devices</span></a></li><li><a href="#ble-encryption-support"><span>BLE <wbr/>Encryption <wbr/>Support</span></a></li><li><ul><li><a href="#ble-encryption-key-and-key-id"><span>BLE <wbr/>Encryption <wbr/>Key and <wbr/>Key <wbr/>ID</span></a></li><li><ul><li><a href="#how-to-obtain-ble-encryption-key-and-key-id"><span>How to <wbr/>Obtain <wbr/>BLE <wbr/>Encryption <wbr/>Key and <wbr/>Key <wbr/>ID</span></a></li><li><a href="#how-to-configure-in-homebridge"><span>How to <wbr/>Configure in <wbr/>Homebridge</span></a></li><li><a href="#which-devices-require-ble-encryption"><span>Which <wbr/>Devices <wbr/>Require <wbr/>BLE <wbr/>Encryption?</span></a></li></ul></li></ul></li><li><a href="#supported-ir-devices"><span>Supported <wbr/>IR <wbr/>Devices</span></a></li><li><ul><li><a href="#all-ir-devices-require-switchbot-hub-2-switchbot-hub-mini-2-switchbot-hub-3-or-hub-mini"><span>(<wbr/>All <wbr/>IR <wbr/>Devices require <wbr/>Switch<wbr/>Bot <wbr/>Hub 2, <wbr/>Switch<wbr/>Bot <wbr/>Hub <wbr/>Mini 2, <wbr/>Switch<wbr/>Bot <wbr/>Hub 3, or <wbr/>Hub <wbr/>Mini)</span></a></li></ul></li><li><a href="#matter-platform"><span>Matter <wbr/>Platform</span></a></li><li><ul><li><a href="#batched-refresh-and-api-load-control"><span>Batched refresh and <wbr/>API load control</span></a></li></ul></li><li><a href="#whats-new-with-node-switchbot-v400"><span>What&#39;s new with node-<wbr/>switchbot v4.0.0</span></a></li><li><a href="#openapi-rate-limiting-and-daily-budget"><span>Open<wbr/>API rate limiting and daily budget</span></a></li><li><a href="#switchbot-apis"><span>Switch<wbr/>Bot <wbr/>AP<wbr/>Is</span></a></li><li><a href="#development-tests"><span>Development / <wbr/>Tests</span></a></li><li><a href="#community"><span>Community</span></a></li></ul></div></details></div><div class="site-menu"><nav class="tsd-navigation"><a href="modules.html">@switchbot/homebridge-switchbot</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer></footer><div class="overlay"></div></body></html>
@@ -1,3 +1,3 @@
1
1
  <!DOCTYPE html><html class="default" lang="en" data-base="../"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>default | @switchbot/homebridge-switchbot</title><meta name="description" content="Documentation for @switchbot/homebridge-switchbot"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="../index.html" class="title">@switchbot/homebridge-switchbot</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">default</a></li></ul><h1>Variable default</h1></div><div class="tsd-signature"><span class="tsd-kind-variable">default</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span> <span class="tsd-signature-symbol">=&gt;</span> <span class="tsd-signature-type">void</span></div><div class="tsd-comment tsd-typography"><p>Registers the SwitchBot platform with Homebridge.</p>
2
2
  </div><div class="tsd-type-declaration"><h4>Type Declaration</h4><ul class="tsd-parameters"><li class="tsd-parameter-signature"><ul class="tsd-signatures"><li class="tsd-signature" id="__type"><span class="tsd-signature-symbol">(</span><span class="tsd-kind-parameter">api</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">API</span><span class="tsd-signature-symbol">)</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">void</span></li><li class="tsd-description"><div class="tsd-parameters"><h4 class="tsd-parameters-title">Parameters</h4><ul class="tsd-parameter-list"><li><span><span class="tsd-kind-parameter">api</span>: <span class="tsd-signature-type">API</span></span><div class="tsd-comment tsd-typography"><p>The Homebridge API instance.</p>
3
- </div></li></ul></div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4></li></ul></li></ul></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/50cea4b3bf59e48befc35e5599a830888265844b/src/index.ts#L22">index.ts:22</a></li></ul></aside></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@switchbot/homebridge-switchbot</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer></footer><div class="overlay"></div></body></html>
3
+ </div></li></ul></div><h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4></li></ul></li></ul></div><aside class="tsd-sources"><ul><li>Defined in <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/c12da554d58005474bfa08e2e8abd1ed5882b64b/src/index.ts#L22">index.ts:22</a></li></ul></aside></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">@switchbot/homebridge-switchbot</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer></footer><div class="overlay"></div></body></html>
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@switchbot/homebridge-switchbot",
3
3
  "displayName": "SwitchBot",
4
4
  "type": "module",
5
- "version": "5.0.0",
5
+ "version": "5.1.0-beta.1",
6
6
  "description": "The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.",
7
7
  "author": "SwitchBot <support@wondertechlabs.com> (https://github.com/SwitchBot)",
8
8
  "contributors": [
@@ -56,7 +56,7 @@
56
56
  "icon": "https://raw.githubusercontent.com/OpenWonderLabs/homebridge-switchbot/latest/branding/icon.png",
57
57
  "engineStrict": true,
58
58
  "engines": {
59
- "homebridge": "^2.0.0-beta.86",
59
+ "homebridge": "^2.0.0-beta.89",
60
60
  "node": "^22 || ^24"
61
61
  },
62
62
  "directories": {
@@ -93,10 +93,10 @@
93
93
  "dependencies": {
94
94
  "@homebridge/plugin-ui-utils": "^2.2.3",
95
95
  "class-validator": "^0.15.1",
96
- "node-switchbot": "^4.0.0"
96
+ "node-switchbot": "^4.0.1"
97
97
  },
98
98
  "devDependencies": {
99
- "@antfu/eslint-config": "^8.1.1",
99
+ "@antfu/eslint-config": "^8.2.0",
100
100
  "@types/debug": "^4.1.13",
101
101
  "@types/fs-extra": "^11.0.4",
102
102
  "@types/node": "^25.6.0",
@@ -108,11 +108,11 @@
108
108
  "eslint-plugin-format": "^2.0.1",
109
109
  "eslint-plugin-html": "^8.1.4",
110
110
  "eslint-plugin-perfectionist": "^5.8.0",
111
- "homebridge": "^2.0.0-beta.86",
111
+ "homebridge": "^2.0.0-beta.89",
112
112
  "homebridge-config-ui-x": "^5.21.0",
113
113
  "nodemon": "^3.1.14",
114
114
  "shx": "^0.4.0",
115
- "typedoc": "^0.28.18",
115
+ "typedoc": "^0.28.19",
116
116
  "typescript": "^6.0.2",
117
117
  "vitest": "^4.1.4"
118
118
  }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import esbuild from 'esbuild'
4
+ import crypto from 'node:crypto'
4
5
  import fs from 'node:fs'
5
6
  import path from 'node:path'
6
7
  import process from 'node:process'
@@ -10,10 +11,38 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
11
 
11
12
  const srcDir = path.resolve(__dirname, '../src/homebridge-ui/public/js')
12
13
  const outputDir = path.resolve(__dirname, '../dist/homebridge-ui/public/js')
14
+ const publicDir = path.resolve(__dirname, '../dist/homebridge-ui/public')
13
15
 
14
16
  // Ensure output directory exists
15
17
  fs.mkdirSync(outputDir, { recursive: true })
16
18
 
19
+ function assetHash(filePath) {
20
+ const contents = fs.readFileSync(filePath)
21
+ return crypto.createHash('sha256').update(contents).digest('hex').slice(0, 8)
22
+ }
23
+
24
+ function cacheBustBuiltAssets() {
25
+ const indexPath = path.join(publicDir, 'index.html')
26
+ if (!fs.existsSync(indexPath)) {
27
+ return
28
+ }
29
+
30
+ let html = fs.readFileSync(indexPath, 'utf8')
31
+ const assets = ['app.js', 'advanced-settings.js']
32
+
33
+ for (const assetName of assets) {
34
+ const assetPath = path.join(outputDir, assetName)
35
+ if (!fs.existsSync(assetPath)) {
36
+ continue
37
+ }
38
+
39
+ const hashedSrc = `js/${assetName}?v=${assetHash(assetPath)}`
40
+ html = html.replace(new RegExp(`js/${assetName}(\\?v=[^\"']+)?`, 'g'), hashedSrc)
41
+ }
42
+
43
+ fs.writeFileSync(indexPath, html)
44
+ }
45
+
17
46
  // Transpile TypeScript to JavaScript with esbuild
18
47
  esbuild
19
48
  .build({
@@ -29,6 +58,7 @@ esbuild
29
58
  logLevel: 'info',
30
59
  })
31
60
  .then(() => {
61
+ cacheBustBuiltAssets()
32
62
  console.log('✓ UI bundling complete')
33
63
  })
34
64
  .catch((error) => {
@@ -4,7 +4,7 @@ import type { API, Logger, PlatformConfig } from 'homebridge'
4
4
  import { createDevice } from './deviceFactory.js'
5
5
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'
6
6
  import { SwitchBotClient } from './switchbotClient.js'
7
- import { normalizeTypeForMatter } from './utils.js'
7
+ import { normalizeTypeForMatter, resolveExternalPublishProtocol } from './utils.js'
8
8
 
9
9
  /**
10
10
  * Homebridge platform class for SwitchBot HAP (HomeKit Accessory Protocol) integration.
@@ -140,7 +140,7 @@ export class SwitchBotHAPPlatform {
140
140
  }
141
141
 
142
142
  const devices = (this.config as any)?.devices ?? []
143
- const createdDevices: { created: any, d: any, type: string }[] = []
143
+ const createdDevices: { created: any, d: any, type: string, externalPublishProtocol: 'none' | 'hap' | 'matter' }[] = []
144
144
  for (const raw of devices) {
145
145
  // Normalize config keys from UI schema to internal shape (for cross-platform consistency)
146
146
  const d: any = {
@@ -152,12 +152,13 @@ export class SwitchBotHAPPlatform {
152
152
  _raw: raw,
153
153
  }
154
154
  const type: string = normalizeTypeForMatter(d.type)
155
+ const externalPublishProtocol = resolveExternalPublishProtocol(raw)
155
156
  const deviceOpts: any = { id: d.id, type, name: d.name, encryptionKey: d.encryptionKey, keyId: d.keyId, log: this.log }
156
157
  this.log.debug(`[HAP/Debug] Device options for ${d.name ?? d.id}:`, JSON.stringify(deviceOpts, null, 2))
157
158
  try {
158
159
  const created = await createDevice(deviceOpts, this.config, false)
159
160
  this.devices.push(created)
160
- createdDevices.push({ created, d, type })
161
+ createdDevices.push({ created, d, type, externalPublishProtocol })
161
162
  } catch (e: any) {
162
163
  this.log.error(`Failed to create HAP device ${d.id}:`, e instanceof Error ? e.stack || e.message : e)
163
164
  }
@@ -188,14 +189,15 @@ export class SwitchBotHAPPlatform {
188
189
  *
189
190
  * If the Homebridge HAP API is not available, registration is skipped and a log message is emitted.
190
191
  */
191
- private async registerHAPAccessories(createdDevices: { created: any, d: any, type: string }[]): Promise<void> {
192
+ private async registerHAPAccessories(createdDevices: { created: any, d: any, type: string, externalPublishProtocol: 'none' | 'hap' | 'matter' }[]): Promise<void> {
192
193
  if (!this.api || !(this.api as any).hap || typeof (this.api as any).hap.registerPlatformAccessories !== 'function') {
193
194
  this.log.info('HAP API not available to register accessories')
194
195
  return
195
196
  }
196
197
  const hap = (this.api as any).hap
197
198
  const accessoriesToRegister: any[] = []
198
- for (const { created, d, type } of createdDevices) {
199
+ const externalAccessoriesToPublish: any[] = []
200
+ for (const { created, d, type, externalPublishProtocol } of createdDevices) {
199
201
  try {
200
202
  const uuid = hap.uuid.generate(`${d.id}`)
201
203
  let accessory: any = this.accessories.get(uuid)
@@ -220,7 +222,11 @@ export class SwitchBotHAPPlatform {
220
222
  } catch (e) {
221
223
  // ignore context failures
222
224
  }
223
- accessoriesToRegister.push(accessory)
225
+ if (externalPublishProtocol === 'hap') {
226
+ externalAccessoriesToPublish.push(accessory)
227
+ } else {
228
+ accessoriesToRegister.push(accessory)
229
+ }
224
230
  this.accessories.set(uuid, accessory)
225
231
  } else {
226
232
  try {
@@ -298,6 +304,24 @@ export class SwitchBotHAPPlatform {
298
304
  } else {
299
305
  this.log.info('No HAP accessories to register')
300
306
  }
307
+ if (externalAccessoriesToPublish.length > 0) {
308
+ if (typeof (this.api as any).publishExternalAccessories === 'function') {
309
+ try {
310
+ (this.api as any).publishExternalAccessories(PLUGIN_NAME, externalAccessoriesToPublish)
311
+ this.log.info(`Published ${externalAccessoriesToPublish.length} HAP external accessory(ies) with Homebridge`)
312
+ } catch (e) {
313
+ this.log.warn('Failed to publish HAP external accessories', e)
314
+ }
315
+ } else {
316
+ this.log.warn('HAP external accessory publishing API not available; falling back to regular platform registration')
317
+ try {
318
+ (this.api as any).registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, externalAccessoriesToPublish)
319
+ this.log.info(`Registered ${externalAccessoriesToPublish.length} HAP accessory(ies) as fallback (external publishing unavailable)`)
320
+ } catch (e) {
321
+ this.log.warn('Failed to register fallback HAP accessories', e)
322
+ }
323
+ }
324
+ }
301
325
  }
302
326
 
303
327
  /**
@@ -332,6 +356,7 @@ export class SwitchBotHAPPlatform {
332
356
  id: d.deviceId ?? d.id,
333
357
  type: d.configDeviceType ?? d.type,
334
358
  name: d.configDeviceName ?? d.name,
359
+ externalPublishProtocol: resolveExternalPublishProtocol(d),
335
360
  })))
336
361
  }
337
362
 
@@ -4,7 +4,7 @@ import type { API, Logger, PlatformConfig } from 'homebridge'
4
4
  import { createDevice } from './deviceFactory.js'
5
5
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'
6
6
  import { SwitchBotClient } from './switchbotClient.js'
7
- import { createMatterHandlers, DEVICE_MATTER_CLUSTERS, DEVICE_MATTER_SUPPORTED, normalizeTypeForMatter, resolveMatterDeviceType } from './utils.js'
7
+ import { createMatterHandlers, DEVICE_MATTER_CLUSTERS, DEVICE_MATTER_SUPPORTED, normalizeTypeForMatter, resolveExternalPublishProtocol, resolveMatterDeviceType } from './utils.js'
8
8
 
9
9
  /**
10
10
  * Homebridge platform class for SwitchBot Matter integration.
@@ -113,7 +113,7 @@ export class SwitchBotMatterPlatform {
113
113
  }
114
114
 
115
115
  const devices = (this.config as any)?.devices ?? []
116
- const createdDevices: { created: any, d: any, type: string, useMatter: boolean, matterAvailable: boolean }[] = []
116
+ const createdDevices: { created: any, d: any, type: string, useMatter: boolean, matterAvailable: boolean, externalPublishProtocol: 'none' | 'hap' | 'matter' }[] = []
117
117
  for (const raw of devices) {
118
118
  // Normalize config keys from UI schema to internal shape (for cross-platform consistency)
119
119
  const d: any = {
@@ -125,6 +125,7 @@ export class SwitchBotMatterPlatform {
125
125
  _raw: raw,
126
126
  }
127
127
  const type: string = normalizeTypeForMatter(d.type)
128
+ const externalPublishProtocol = resolveExternalPublishProtocol(raw)
128
129
  const deviceOpts: any = { id: d.id, type, name: d.name, encryptionKey: d.encryptionKey, keyId: d.keyId, log: this.log }
129
130
  this.log.debug(`[Matter/Debug] Device options for ${d.name ?? d.id}:`, JSON.stringify(deviceOpts, null, 2))
130
131
  const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
@@ -134,7 +135,7 @@ export class SwitchBotMatterPlatform {
134
135
  try {
135
136
  const created = await createDevice(deviceOpts, this.config, useMatter)
136
137
  this.devices.push(created)
137
- createdDevices.push({ created, d, type, useMatter, matterAvailable })
138
+ createdDevices.push({ created, d, type, useMatter, matterAvailable, externalPublishProtocol })
138
139
  if (useMatter) {
139
140
  this.log.info(`Prepared Matter accessory for ${d.id} (${type})${matterAvailable ? ' (auto-detected)' : ' (manually enabled)'}`)
140
141
  } else {
@@ -179,14 +180,15 @@ export class SwitchBotMatterPlatform {
179
180
  * If the Homebridge Matter API is not available, registration is skipped and a log message is emitted.
180
181
  * Accessories that are not enabled for Matter are ignored.
181
182
  */
182
- async registerMatterAccessories(createdDevices: { created: any, d: any, type: string, useMatter: boolean, matterAvailable: boolean }[]): Promise<void> {
183
+ async registerMatterAccessories(createdDevices: { created: any, d: any, type: string, useMatter: boolean, matterAvailable: boolean, externalPublishProtocol: 'none' | 'hap' | 'matter' }[]): Promise<void> {
183
184
  if (!this.api || !(this.api as any).matter || typeof (this.api as any).matter.registerPlatformAccessories !== 'function') {
184
185
  this.log.info('Homebridge Matter API not available; skipping Matter accessory registration')
185
186
  return
186
187
  }
187
188
  const matterApi = (this.api as any).matter
188
189
  const accessoriesToRegister: any[] = []
189
- for (const { created, d, type, useMatter, matterAvailable } of createdDevices) {
190
+ const externalAccessoriesToPublish: any[] = []
191
+ for (const { created, d, type, useMatter, matterAvailable, externalPublishProtocol } of createdDevices) {
190
192
  // Only register accessories where Matter is enabled and supported
191
193
  if (!useMatter) {
192
194
  // Log reason for skipping registration
@@ -237,7 +239,11 @@ export class SwitchBotMatterPlatform {
237
239
  handlers: createdDesc.handlers || createMatterHandlers(this.log, d.id, type, (this.config as any)?._client) || undefined,
238
240
  context: { deviceId: d.id, type, created: true },
239
241
  }
240
- accessoriesToRegister.push(accessory)
242
+ if (externalPublishProtocol === 'matter') {
243
+ externalAccessoriesToPublish.push(accessory)
244
+ } else {
245
+ accessoriesToRegister.push(accessory)
246
+ }
241
247
  this.accessories.set(uuid, accessory)
242
248
  } else {
243
249
  // Ensure context and update properties for restored accessory
@@ -260,7 +266,11 @@ export class SwitchBotMatterPlatform {
260
266
  accessory.handlers = createdDesc.handlers || createMatterHandlers(this.log, d.id, type, (this.config as any)?._client) || undefined
261
267
  accessory.displayName = createdDesc.name || d.name || type
262
268
  accessory.UUID = accessory.UUID || accessory.uuid || uuid
263
- accessoriesToRegister.push(accessory)
269
+ if (externalPublishProtocol === 'matter') {
270
+ externalAccessoriesToPublish.push(accessory)
271
+ } else {
272
+ accessoriesToRegister.push(accessory)
273
+ }
264
274
  this.accessories.set(accessory.UUID || uuid, accessory)
265
275
  }
266
276
  this.log.info(`Created/updated Matter accessory ${d.id} (${type})`)
@@ -278,6 +288,49 @@ export class SwitchBotMatterPlatform {
278
288
  } else {
279
289
  this.log.info('No Matter accessories to register')
280
290
  }
291
+ if (externalAccessoriesToPublish.length > 0) {
292
+ try {
293
+ if (typeof matterApi.publishExternalAccessories === 'function') {
294
+ await matterApi.publishExternalAccessories(PLUGIN_NAME, externalAccessoriesToPublish)
295
+ this.log.info(`Published ${externalAccessoriesToPublish.length} Matter external accessory(ies) with Homebridge`)
296
+ } else if (typeof matterApi.registerExternalAccessories === 'function') {
297
+ await matterApi.registerExternalAccessories(PLUGIN_NAME, PLATFORM_NAME, externalAccessoriesToPublish)
298
+ this.log.info(`Registered ${externalAccessoriesToPublish.length} Matter external accessory(ies) with Homebridge`)
299
+ } else if (await this.publishMatterExternalAccessories(externalAccessoriesToPublish)) {
300
+ this.log.info(`Published ${externalAccessoriesToPublish.length} Matter external accessory(ies) with Homebridge`)
301
+ } else {
302
+ this.log.warn('Matter external accessory publishing API not available; falling back to regular platform registration')
303
+ await matterApi.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, externalAccessoriesToPublish)
304
+ this.log.info(`Registered ${externalAccessoriesToPublish.length} Matter accessory(ies) as fallback (external publishing unavailable)`)
305
+ }
306
+ } catch (e) {
307
+ this.log.warn('Failed to publish/register Matter external accessories', e)
308
+ }
309
+ }
310
+ }
311
+
312
+ private async publishMatterExternalAccessories(accessories: any[]): Promise<boolean> {
313
+ const api = this.api as any
314
+ if (!api || typeof api.emit !== 'function') {
315
+ return false
316
+ }
317
+
318
+ const pendingRegistrations = api._pendingExternalRegistrations
319
+ if (!(pendingRegistrations instanceof Map) && pendingRegistrations !== undefined) {
320
+ return false
321
+ }
322
+
323
+ const registrationId = `${PLUGIN_NAME}-${Date.now()}-${Math.random()}`
324
+ const registrationPromise = new Promise<void>((resolve) => {
325
+ if (!(api._pendingExternalRegistrations instanceof Map)) {
326
+ api._pendingExternalRegistrations = new Map()
327
+ }
328
+ api._pendingExternalRegistrations.set(registrationId, resolve)
329
+ })
330
+
331
+ api.emit('publishExternalMatterAccessories', accessories, registrationId)
332
+ await registrationPromise
333
+ return true
281
334
  }
282
335
 
283
336
  /**
@@ -312,6 +365,7 @@ export class SwitchBotMatterPlatform {
312
365
  id: d.deviceId ?? d.id,
313
366
  type: d.configDeviceType ?? d.type,
314
367
  name: d.configDeviceName ?? d.name,
368
+ externalPublishProtocol: resolveExternalPublishProtocol(d),
315
369
  })))
316
370
  }
317
371
 
@@ -4,7 +4,7 @@ import { RequestError } from '@homebridge/plugin-ui-utils'
4
4
  import fs from 'node:fs/promises'
5
5
 
6
6
  import { isValidDeviceType } from '../../device-types.js'
7
- import { getAllDevices, SWITCHBOT_PLATFORM_REGEX } from '../utils/config-parser.js'
7
+ import { getAllDevices, isV4Config, SWITCHBOT_PLATFORM_REGEX } from '../utils/config-parser.js'
8
8
  import { validateAndMigrateDeviceType } from '../utils/device-migration.js'
9
9
  import { uiLog } from '../utils/logger.js'
10
10
 
@@ -31,6 +31,7 @@ export function registerConfigEndpoints(server: HomebridgePluginUiServer) {
31
31
  typeValidationWarning?: string
32
32
  }> = []
33
33
  const invalidTypeDevices: Array<{ name: string, type: string, suggestion?: string }> = []
34
+ let v4ConfigDetected = false
34
35
 
35
36
  const platforms = Array.isArray(cfg.platforms) ? cfg.platforms : []
36
37
  for (const p of platforms) {
@@ -41,6 +42,12 @@ export function registerConfigEndpoints(server: HomebridgePluginUiServer) {
41
42
  continue
42
43
  }
43
44
 
45
+ // Detect legacy v4 config format
46
+ if (isV4Config(p)) {
47
+ v4ConfigDetected = true
48
+ uiLog.warn('Detected legacy v4 config format (credentials/options.devices). This config is not compatible with v5+. Please migrate to the v5 config format.')
49
+ }
50
+
44
51
  const devices = getAllDevices(p)
45
52
  for (const d of devices) {
46
53
  const id = d.deviceId ?? d.id
@@ -94,11 +101,12 @@ export function registerConfigEndpoints(server: HomebridgePluginUiServer) {
94
101
  )
95
102
  }
96
103
 
97
- uiLog.info(`GET /devices - Found ${found.length} devices in ${cfgPath}${invalidTypeDevices.length > 0 ? ` (${invalidTypeDevices.length} with validation warnings)` : ''}`)
104
+ uiLog.info(`GET /devices - Found ${found.length} devices in ${cfgPath}${invalidTypeDevices.length > 0 ? ` (${invalidTypeDevices.length} with validation warnings)` : ''}${v4ConfigDetected ? ' [v4 config detected]' : ''}`)
98
105
  return {
99
106
  success: true,
100
107
  data: found,
101
108
  ...(invalidTypeDevices.length > 0 && { validationWarnings: invalidTypeDevices }),
109
+ ...(v4ConfigDetected && { v4ConfigDetected: true }),
102
110
  }
103
111
  } catch (e) {
104
112
  const msg = e instanceof Error ? e.message : String(e)
@@ -480,4 +480,64 @@ ul.device-grid {
480
480
 
481
481
  .discovery-scanning-pulse {
482
482
  animation: pulse 1.5s ease-in-out infinite;
483
- }
483
+ }
484
+ /* v4 config incompatibility warning banner */
485
+ .v4-config-warning {
486
+ background: #7c3015;
487
+ color: #fff;
488
+ border: 1px solid #a04020;
489
+ border-radius: 8px;
490
+ padding: 14px 18px;
491
+ margin-bottom: 18px;
492
+ font-size: 13px;
493
+ line-height: 1.5;
494
+ }
495
+
496
+ .v4-config-warning strong {
497
+ display: block;
498
+ font-size: 15px;
499
+ margin-bottom: 8px;
500
+ }
501
+
502
+ .v4-config-warning p {
503
+ margin: 6px 0;
504
+ }
505
+
506
+ .v4-config-warning ul {
507
+ margin: 6px 0 6px 18px;
508
+ padding: 0;
509
+ }
510
+
511
+ .v4-config-warning li {
512
+ margin-bottom: 3px;
513
+ }
514
+
515
+ .v4-config-warning code {
516
+ background: rgba(255, 255, 255, 0.15);
517
+ border-radius: 3px;
518
+ padding: 1px 4px;
519
+ font-size: 12px;
520
+ color: #ffd;
521
+ }
522
+
523
+ .v4-config-warning a {
524
+ color: #ffd;
525
+ text-decoration: underline;
526
+ }
527
+
528
+ @media (prefers-color-scheme: light) {
529
+ .v4-config-warning {
530
+ background: #fff3cd;
531
+ color: #7c3015;
532
+ border-color: #f0ad4e;
533
+ }
534
+
535
+ .v4-config-warning code {
536
+ background: rgba(124, 48, 21, 0.1);
537
+ color: #7c3015;
538
+ }
539
+
540
+ .v4-config-warning a {
541
+ color: #7c3015;
542
+ }
543
+ }