@switchbot/homebridge-switchbot 5.1.0-beta.9 → 5.1.0
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/.github/copilot-instructions.md +0 -63
- package/.github/scripts/normalize-changelog-header.cjs +1 -1
- package/.github/scripts/npm-version-script-esm-auto.js +1 -1
- package/.github/scripts/resolve-version-bump-esm.js +1 -1
- package/CHANGELOG.md +10 -9
- package/MIGRATION.md +0 -5
- package/README.md +0 -25
- package/config.schema.json +0 -11
- package/dist/{Platform.HAP.d.ts → SwitchBotHAPPlatform.d.ts} +16 -2
- package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
- package/dist/{Platform.HAP.js → SwitchBotHAPPlatform.js} +111 -40
- package/dist/SwitchBotHAPPlatform.js.map +1 -0
- package/dist/{Platform.Matter.d.ts → SwitchBotMatterPlatform.d.ts} +2 -4
- package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
- package/dist/{Platform.Matter.js → SwitchBotMatterPlatform.js} +6 -62
- package/dist/SwitchBotMatterPlatform.js.map +1 -0
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +5 -15
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +63 -5
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +251 -11
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/config.js +2 -9
- package/dist/homebridge-ui/endpoints/config.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +2 -9
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +0 -8
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +1 -61
- package/dist/homebridge-ui/public/index.html +2 -61
- package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/advanced-settings.js +3 -4
- package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -1
- package/dist/homebridge-ui/public/js/advanced-settings.ts +3 -4
- package/dist/homebridge-ui/public/js/api.d.ts +0 -16
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +0 -48
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +1 -61
- package/dist/homebridge-ui/public/js/app.js +115 -267
- package/dist/homebridge-ui/public/js/app.js.map +4 -4
- package/dist/homebridge-ui/public/js/app.ts +4 -47
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.js +5 -11
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.ts +5 -11
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +0 -6
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +0 -6
- package/dist/homebridge-ui/public/js/modals.d.ts +0 -1
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +0 -53
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +0 -58
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +1 -2
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +1 -2
- package/dist/homebridge-ui/settings.js.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.d.ts +0 -8
- package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.js +0 -8
- package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/settings.d.ts +0 -11
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js.map +1 -1
- package/dist/utils.d.ts +0 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +0 -27
- package/dist/utils.js.map +1 -1
- package/docs/assets/highlight.css +15 -15
- package/docs/index.html +5 -22
- package/docs/variables/default.html +1 -1
- package/package.json +15 -9
- package/scripts/build-ui.js +0 -30
- package/src/{Platform.HAP.ts → SwitchBotHAPPlatform.ts} +112 -39
- package/src/{Platform.Matter.ts → SwitchBotMatterPlatform.ts} +8 -63
- package/src/deviceFactory.ts +5 -14
- package/src/devices/genericDevice.ts +271 -12
- package/src/homebridge-ui/endpoints/config.ts +2 -10
- package/src/homebridge-ui/public/css/styles.css +1 -61
- package/src/homebridge-ui/public/index.html +0 -59
- package/src/homebridge-ui/public/js/advanced-settings.ts +3 -4
- package/src/homebridge-ui/public/js/api.ts +1 -61
- package/src/homebridge-ui/public/js/app.ts +4 -47
- package/src/homebridge-ui/public/js/credentials.ts +5 -11
- package/src/homebridge-ui/public/js/devices.ts +0 -6
- package/src/homebridge-ui/public/js/modals.ts +0 -58
- package/src/homebridge-ui/public/js/render.ts +1 -2
- package/src/homebridge-ui/utils/config-parser.ts +0 -9
- package/src/index.ts +2 -2
- package/src/settings.ts +0 -12
- package/src/utils.ts +0 -31
- package/test/device/deviceFactory.spec.ts +9 -0
- package/test/device/getstate-cache.spec.ts +62 -0
- package/test/device/getstate-returns-status.spec.ts +89 -0
- package/test/device/water-detector-battery.spec.ts +81 -0
- package/test/device/water-detector-leak.spec.ts +43 -0
- package/test/hap-push-updates.spec.ts +150 -0
- package/test/platform/accessory-restore.spec.ts +2 -2
- package/test/platform/matter-childbridge.spec.ts +1 -1
- package/test/platform/matter-integration.spec.ts +1 -51
- package/test/platform/platform-edge.spec.ts +2 -2
- package/test/platform/platform.integration.spec.ts +1 -1
- package/test/utils/utils.spec.ts +0 -15
- package/.github/npm-version-script-esm.js +0 -97
- package/dist/Platform.HAP.d.ts.map +0 -1
- package/dist/Platform.HAP.js.map +0 -1
- package/dist/Platform.Matter.d.ts.map +0 -1
- package/dist/Platform.Matter.js.map +0 -1
- package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +0 -23
- package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +0 -1
- package/dist/homebridge-ui/public/js/legacy-view.d.ts +0 -3
- package/dist/homebridge-ui/public/js/legacy-view.d.ts.map +0 -1
- package/dist/homebridge-ui/public/js/legacy-view.js +0 -45
- package/dist/homebridge-ui/public/js/legacy-view.js.map +0 -1
- package/dist/homebridge-ui/public/js/legacy-view.ts +0 -107
- package/dist/homebridge-ui/utils/v4-detection.d.ts +0 -12
- package/dist/homebridge-ui/utils/v4-detection.d.ts.map +0 -1
- package/dist/homebridge-ui/utils/v4-detection.js +0 -23
- package/dist/homebridge-ui/utils/v4-detection.js.map +0 -1
- package/src/homebridge-ui/public/js/legacy-view.js +0 -45
- package/src/homebridge-ui/public/js/legacy-view.ts +0 -107
- package/src/homebridge-ui/utils/v4-detection.ts +0 -23
- package/test/homebridge-ui/config-parser.spec.ts +0 -61
package/docs/index.html
CHANGED
|
@@ -40,23 +40,6 @@
|
|
|
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">"deviceId"</span><span class="hl-0">: </span><span class="hl-2">"E7F8A1B2C3D4"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">"configDeviceName"</span><span class="hl-0">: </span><span class="hl-2">"Bedroom Bot"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">"configDeviceType"</span><span class="hl-0">: </span><span class="hl-2">"Bot"</span><span class="hl-0">,</span><br/><span class="hl-0"> </span><span class="hl-1">"externalPublishProtocol"</span><span class="hl-0">: </span><span class="hl-2">"hap"</span><br/><span class="hl-0">}</span>
|
|
58
|
-
</code><button type="button">Copy</button></pre>
|
|
59
|
-
|
|
60
43
|
<ul>
|
|
61
44
|
<li>
|
|
62
45
|
<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>
|
|
@@ -134,7 +117,7 @@
|
|
|
134
117
|
(This is what is intended in documentation for the noble bluetooth package <a href="https://github.com/abandonware/noble#prerequisites">prerequisites</a> by "Add terminal app", however for HomeBridge it is <code>node</code> that needs the permission granted, not <code>terminal</code>.
|
|
135
118
|
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>
|
|
136
119
|
</ol>
|
|
137
|
-
<pre><code><span class="hl-
|
|
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">.<</span><span class="hl-3">anonymous</span><span class="hl-1">> (</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>
|
|
138
121
|
</code><button>Copy</button></pre>
|
|
139
122
|
|
|
140
123
|
</li>
|
|
@@ -362,7 +345,7 @@ Without this step, then you will receive the following error when the swichbot p
|
|
|
362
345
|
<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>
|
|
363
346
|
<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>
|
|
364
347
|
<p><strong>Example device config excerpt:</strong></p>
|
|
365
|
-
<pre><code class="json"><span class="hl-
|
|
348
|
+
<pre><code class="json"><span class="hl-1">{</span><br/><span class="hl-1"> </span><span class="hl-6">"deviceId"</span><span class="hl-1">: </span><span class="hl-7">"E7F8A1B2C3D4"</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">"deviceName"</span><span class="hl-1">: </span><span class="hl-7">"SwitchBot Lock"</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">"enableBLE"</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">"encryptionKey"</span><span class="hl-1">: </span><span class="hl-7">"0123456789abcdef0123456789abcdef"</span><span class="hl-1">,</span><br/><span class="hl-1"> </span><span class="hl-6">"keyId"</span><span class="hl-1">: </span><span class="hl-7">"01"</span><br/><span class="hl-1">}</span>
|
|
366
349
|
</code><button type="button">Copy</button></pre>
|
|
367
350
|
|
|
368
351
|
<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>
|
|
@@ -511,7 +494,7 @@ Without this step, then you will receive the following error when the swichbot p
|
|
|
511
494
|
<li><code>dailyApiResetLocalMidnight</code> (boolean, default false): if true, resets the daily counter at local midnight; when false, resets at UTC midnight.</li>
|
|
512
495
|
</ul>
|
|
513
496
|
<p>Example (excerpt):</p>
|
|
514
|
-
<pre><code class="json"><span class="hl-
|
|
497
|
+
<pre><code class="json"><span class="hl-1">{</span><br/><span class="hl-1"> </span><span class="hl-6">"options"</span><span class="hl-1">: {</span><br/><span class="hl-1"> </span><span class="hl-6">"dailyApiLimit"</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">"dailyApiReserveForCommands"</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">"webhookOnlyOnReserve"</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">"dailyApiResetLocalMidnight"</span><span class="hl-1">: </span><span class="hl-8">true</span><br/><span class="hl-1"> }</span><br/><span class="hl-1">}</span>
|
|
515
498
|
</code><button type="button">Copy</button></pre>
|
|
516
499
|
|
|
517
500
|
<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>
|
|
@@ -527,7 +510,7 @@ Without this step, then you will receive the following error when the swichbot p
|
|
|
527
510
|
<ul>
|
|
528
511
|
<li>
|
|
529
512
|
<p>Run unit tests:</p>
|
|
530
|
-
<pre><code class="bash"><span class="hl-9">npm</span><span class="hl-
|
|
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>
|
|
531
514
|
</code><button type="button">Copy</button></pre>
|
|
532
515
|
|
|
533
516
|
</li>
|
|
@@ -544,4 +527,4 @@ Without this step, then you will receive the following error when the swichbot p
|
|
|
544
527
|
<li><a href="https://www.facebook.com/SwitchBotRobot/">Facebook @SwitchBotRobot</a></li>
|
|
545
528
|
<li><a href="https://twitter.com/switchbot">Twitter @SwitchBot</a></li>
|
|
546
529
|
</ul>
|
|
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="#
|
|
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'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">=></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/
|
|
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/a8567ae7471ac7354c5c3424818c54371239af9b/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.1.0
|
|
5
|
+
"version": "5.1.0",
|
|
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": [
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
34
34
|
"homebridge-plugin",
|
|
35
|
-
"homebridge-matter",
|
|
36
35
|
"switchbot",
|
|
37
36
|
"lights",
|
|
38
37
|
"fan",
|
|
@@ -93,21 +92,28 @@
|
|
|
93
92
|
},
|
|
94
93
|
"dependencies": {
|
|
95
94
|
"@homebridge/plugin-ui-utils": "^2.2.3",
|
|
96
|
-
"
|
|
95
|
+
"class-validator": "^0.15.1",
|
|
96
|
+
"node-switchbot": "^4.0.1"
|
|
97
97
|
},
|
|
98
98
|
"devDependencies": {
|
|
99
99
|
"@antfu/eslint-config": "^8.2.0",
|
|
100
|
+
"@types/debug": "^4.1.13",
|
|
101
|
+
"@types/fs-extra": "^11.0.4",
|
|
100
102
|
"@types/node": "^25.6.0",
|
|
101
|
-
"@
|
|
103
|
+
"@types/semver": "^7.7.1",
|
|
104
|
+
"@types/source-map-support": "^0.5.10",
|
|
105
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
102
106
|
"esbuild": "^0.28.0",
|
|
103
|
-
"eslint": "^10.
|
|
107
|
+
"eslint": "^10.2.0",
|
|
104
108
|
"eslint-plugin-format": "^2.0.1",
|
|
105
|
-
"eslint-plugin-
|
|
106
|
-
"
|
|
109
|
+
"eslint-plugin-html": "^8.1.4",
|
|
110
|
+
"eslint-plugin-perfectionist": "^5.8.0",
|
|
111
|
+
"homebridge": "^2.0.0-beta.89",
|
|
112
|
+
"homebridge-config-ui-x": "^5.21.0",
|
|
107
113
|
"nodemon": "^3.1.14",
|
|
108
114
|
"shx": "^0.4.0",
|
|
109
115
|
"typedoc": "^0.28.19",
|
|
110
|
-
"typescript": "^6.0.
|
|
111
|
-
"vitest": "^4.1.
|
|
116
|
+
"typescript": "^6.0.2",
|
|
117
|
+
"vitest": "^4.1.4"
|
|
112
118
|
}
|
|
113
119
|
}
|
package/scripts/build-ui.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import esbuild from 'esbuild'
|
|
4
|
-
import crypto from 'node:crypto'
|
|
5
4
|
import fs from 'node:fs'
|
|
6
5
|
import path from 'node:path'
|
|
7
6
|
import process from 'node:process'
|
|
@@ -11,38 +10,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
11
10
|
|
|
12
11
|
const srcDir = path.resolve(__dirname, '../src/homebridge-ui/public/js')
|
|
13
12
|
const outputDir = path.resolve(__dirname, '../dist/homebridge-ui/public/js')
|
|
14
|
-
const publicDir = path.resolve(__dirname, '../dist/homebridge-ui/public')
|
|
15
13
|
|
|
16
14
|
// Ensure output directory exists
|
|
17
15
|
fs.mkdirSync(outputDir, { recursive: true })
|
|
18
16
|
|
|
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
|
-
|
|
46
17
|
// Transpile TypeScript to JavaScript with esbuild
|
|
47
18
|
esbuild
|
|
48
19
|
.build({
|
|
@@ -58,7 +29,6 @@ esbuild
|
|
|
58
29
|
logLevel: 'info',
|
|
59
30
|
})
|
|
60
31
|
.then(() => {
|
|
61
|
-
cacheBustBuiltAssets()
|
|
62
32
|
console.log('✓ UI bundling complete')
|
|
63
33
|
})
|
|
64
34
|
.catch((error) => {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type { API, Logger, PlatformConfig } from 'homebridge'
|
|
2
|
-
|
|
3
1
|
import type { SwitchBotPluginConfig } from './settings.js'
|
|
2
|
+
import type { API, Logger, PlatformConfig } from 'homebridge'
|
|
4
3
|
|
|
5
4
|
import { createDevice } from './deviceFactory.js'
|
|
6
5
|
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'
|
|
7
6
|
import { SwitchBotClient } from './switchbotClient.js'
|
|
8
|
-
import { normalizeTypeForMatter
|
|
7
|
+
import { normalizeTypeForMatter } from './utils.js'
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Homebridge platform class for SwitchBot HAP (HomeKit Accessory Protocol) integration.
|
|
@@ -42,6 +41,9 @@ export class SwitchBotHAPPlatform {
|
|
|
42
41
|
private lastConfigHash: string = ''
|
|
43
42
|
/** Interval for periodic config reload */
|
|
44
43
|
private configReloadInterval: NodeJS.Timeout | null = null
|
|
44
|
+
/** Readable characteristics per device id, so a poll can push new values */
|
|
45
|
+
private hapPushTargets: Map<string, { device: any, entries: Array<{ service: any, characteristic: any, get: () => Promise<any> }> }> = new Map()
|
|
46
|
+
|
|
45
47
|
/** Timers for per-device OpenAPI polling */
|
|
46
48
|
private openApiPollTimers: Map<string, NodeJS.Timeout> = new Map()
|
|
47
49
|
/** Timer for batched OpenAPI polling */
|
|
@@ -141,7 +143,7 @@ export class SwitchBotHAPPlatform {
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
const devices = (this.config as any)?.devices ?? []
|
|
144
|
-
const createdDevices: { created: any, d: any, type: string
|
|
146
|
+
const createdDevices: { created: any, d: any, type: string }[] = []
|
|
145
147
|
for (const raw of devices) {
|
|
146
148
|
// Normalize config keys from UI schema to internal shape (for cross-platform consistency)
|
|
147
149
|
const d: any = {
|
|
@@ -153,19 +155,19 @@ export class SwitchBotHAPPlatform {
|
|
|
153
155
|
_raw: raw,
|
|
154
156
|
}
|
|
155
157
|
const type: string = normalizeTypeForMatter(d.type)
|
|
156
|
-
const externalPublishProtocol = resolveExternalPublishProtocol(raw)
|
|
157
158
|
const deviceOpts: any = { id: d.id, type, name: d.name, encryptionKey: d.encryptionKey, keyId: d.keyId, log: this.log }
|
|
158
159
|
this.log.debug(`[HAP/Debug] Device options for ${d.name ?? d.id}:`, JSON.stringify(deviceOpts, null, 2))
|
|
159
160
|
try {
|
|
160
161
|
const created = await createDevice(deviceOpts, this.config, false)
|
|
161
162
|
this.devices.push(created)
|
|
162
|
-
createdDevices.push({ created, d, type
|
|
163
|
+
createdDevices.push({ created, d, type })
|
|
163
164
|
} catch (e: any) {
|
|
164
165
|
this.log.error(`Failed to create HAP device ${d.id}:`, e instanceof Error ? e.stack || e.message : e)
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
// Register HAP accessories after all devices are created for symmetry with Matter platform
|
|
168
169
|
await this.registerHAPAccessories(createdDevices)
|
|
170
|
+
this.primeHapValues()
|
|
169
171
|
this.lastConfigHash = newHash
|
|
170
172
|
}
|
|
171
173
|
|
|
@@ -190,15 +192,14 @@ export class SwitchBotHAPPlatform {
|
|
|
190
192
|
*
|
|
191
193
|
* If the Homebridge HAP API is not available, registration is skipped and a log message is emitted.
|
|
192
194
|
*/
|
|
193
|
-
private async registerHAPAccessories(createdDevices: { created: any, d: any, type: string
|
|
194
|
-
if (!this.api || !(this.api as any).hap || typeof (this.api as any).
|
|
195
|
+
private async registerHAPAccessories(createdDevices: { created: any, d: any, type: string }[]): Promise<void> {
|
|
196
|
+
if (!this.api || !(this.api as any).hap || typeof (this.api as any).registerPlatformAccessories !== 'function') {
|
|
195
197
|
this.log.info('HAP API not available to register accessories')
|
|
196
198
|
return
|
|
197
199
|
}
|
|
198
200
|
const hap = (this.api as any).hap
|
|
199
201
|
const accessoriesToRegister: any[] = []
|
|
200
|
-
const
|
|
201
|
-
for (const { created, d, type, externalPublishProtocol } of createdDevices) {
|
|
202
|
+
for (const { created, d, type } of createdDevices) {
|
|
202
203
|
try {
|
|
203
204
|
const uuid = hap.uuid.generate(`${d.id}`)
|
|
204
205
|
let accessory: any = this.accessories.get(uuid)
|
|
@@ -215,9 +216,7 @@ export class SwitchBotHAPPlatform {
|
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
if (!accessory) {
|
|
218
|
-
|
|
219
|
-
const PlatformAccessory = (this.api as any).hap?.PlatformAccessory || (this.api as any).platformAccessory
|
|
220
|
-
accessory = new PlatformAccessory(d.name || type, uuid)
|
|
219
|
+
accessory = new (this.api as any).platformAccessory(d.name || type, uuid)
|
|
221
220
|
try {
|
|
222
221
|
accessory.context = accessory.context || {}
|
|
223
222
|
accessory.context.deviceId = d.id
|
|
@@ -225,11 +224,7 @@ export class SwitchBotHAPPlatform {
|
|
|
225
224
|
} catch (e) {
|
|
226
225
|
// ignore context failures
|
|
227
226
|
}
|
|
228
|
-
|
|
229
|
-
externalAccessoriesToPublish.push(accessory)
|
|
230
|
-
} else {
|
|
231
|
-
accessoriesToRegister.push(accessory)
|
|
232
|
-
}
|
|
227
|
+
accessoriesToRegister.push(accessory)
|
|
233
228
|
this.accessories.set(uuid, accessory)
|
|
234
229
|
} else {
|
|
235
230
|
try {
|
|
@@ -249,12 +244,38 @@ export class SwitchBotHAPPlatform {
|
|
|
249
244
|
// ignore
|
|
250
245
|
}
|
|
251
246
|
}
|
|
247
|
+
// Ensure AccessoryInformation service exists - required for cache deserialization.
|
|
248
|
+
// Without it, hap-nodejs _sideloadServices crashes on next start.
|
|
249
|
+
try {
|
|
250
|
+
const InfoService = hap.Service.AccessoryInformation
|
|
251
|
+
if (InfoService) {
|
|
252
|
+
const info = accessory.getService(InfoService) || accessory.addService(InfoService)
|
|
253
|
+
const accDescForInfo = await created.createAccessory?.(this.api).catch?.(() => undefined)
|
|
254
|
+
const manufacturer = (accDescForInfo && accDescForInfo.manufacturer) || 'SwitchBot'
|
|
255
|
+
const model = (accDescForInfo && accDescForInfo.model) || type || 'SwitchBot Device'
|
|
256
|
+
const serial = (accDescForInfo && accDescForInfo.serialNumber) || d.id
|
|
257
|
+
const firmware = (accDescForInfo && accDescForInfo.firmwareRevision) || '1.0.0'
|
|
258
|
+
try {
|
|
259
|
+
info.setCharacteristic(hap.Characteristic.Manufacturer, manufacturer)
|
|
260
|
+
info.setCharacteristic(hap.Characteristic.Model, model)
|
|
261
|
+
info.setCharacteristic(hap.Characteristic.SerialNumber, serial)
|
|
262
|
+
info.setCharacteristic(hap.Characteristic.FirmwareRevision, firmware)
|
|
263
|
+
} catch (e) {
|
|
264
|
+
// ignore
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (e) {
|
|
268
|
+
// ignore
|
|
269
|
+
}
|
|
252
270
|
// Add basic service descriptor from device (symmetrical to Matter: remove stale services/chars)
|
|
253
271
|
const accDesc = await created.createAccessory?.(this.api)
|
|
254
272
|
if (accDesc && accDesc.services) {
|
|
255
273
|
const serviceTypes = accDesc.services.map((s: any) => s.type)
|
|
256
274
|
for (const existingService of accessory.services.slice()) {
|
|
257
|
-
|
|
275
|
+
// Never remove AccessoryInformation - required for cache deserialization
|
|
276
|
+
if (existingService.constructor && existingService.constructor.name
|
|
277
|
+
&& existingService.constructor.name !== 'AccessoryInformation'
|
|
278
|
+
&& !serviceTypes.includes(existingService.constructor.name)) {
|
|
258
279
|
accessory.removeService(existingService)
|
|
259
280
|
}
|
|
260
281
|
}
|
|
@@ -285,9 +306,32 @@ export class SwitchBotHAPPlatform {
|
|
|
285
306
|
}
|
|
286
307
|
if (getterSetter && typeof getterSetter.get === 'function') {
|
|
287
308
|
service.getCharacteristic(Characteristic).onGet(getterSetter.get)
|
|
309
|
+
// Remember it so a poll can push the new value.
|
|
310
|
+
const target = this.hapPushTargets.get(d.id) ?? { device: created?.instance ?? created, entries: [] }
|
|
311
|
+
target.entries.push({ service, characteristic: Characteristic, get: getterSetter.get })
|
|
312
|
+
this.hapPushTargets.set(d.id, target)
|
|
288
313
|
}
|
|
289
314
|
if (getterSetter && typeof getterSetter.set === 'function') {
|
|
290
|
-
service.getCharacteristic(Characteristic).onSet(
|
|
315
|
+
service.getCharacteristic(Characteristic).onSet(async (value: any) => {
|
|
316
|
+
await getterSetter.set(value)
|
|
317
|
+
|
|
318
|
+
const refreshAfterSet = Array.isArray(getterSetter.refreshAfterSet) ? getterSetter.refreshAfterSet : []
|
|
319
|
+
for (const refreshCharName of refreshAfterSet) {
|
|
320
|
+
const refreshGetterSetter: any = (s.characteristics || {})[refreshCharName]
|
|
321
|
+
if (!refreshGetterSetter || typeof refreshGetterSetter.get !== 'function') {
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
324
|
+
const RefreshCharacteristic = (hap.Characteristic as any)[refreshCharName]
|
|
325
|
+
if (!RefreshCharacteristic) {
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
service.getCharacteristic(RefreshCharacteristic).updateValue(await refreshGetterSetter.get())
|
|
330
|
+
} catch (e) {
|
|
331
|
+
this.log.debug?.(`Failed to refresh ${refreshCharName} after setting ${charName}`, e)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
})
|
|
291
335
|
}
|
|
292
336
|
}
|
|
293
337
|
}
|
|
@@ -307,24 +351,6 @@ export class SwitchBotHAPPlatform {
|
|
|
307
351
|
} else {
|
|
308
352
|
this.log.info('No HAP accessories to register')
|
|
309
353
|
}
|
|
310
|
-
if (externalAccessoriesToPublish.length > 0) {
|
|
311
|
-
if (typeof (this.api as any).publishExternalAccessories === 'function') {
|
|
312
|
-
try {
|
|
313
|
-
(this.api as any).publishExternalAccessories(PLUGIN_NAME, externalAccessoriesToPublish)
|
|
314
|
-
this.log.info(`Published ${externalAccessoriesToPublish.length} HAP external accessory(ies) with Homebridge`)
|
|
315
|
-
} catch (e) {
|
|
316
|
-
this.log.warn('Failed to publish HAP external accessories', e)
|
|
317
|
-
}
|
|
318
|
-
} else {
|
|
319
|
-
this.log.warn('HAP external accessory publishing API not available; falling back to regular platform registration')
|
|
320
|
-
try {
|
|
321
|
-
(this.api as any).registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, externalAccessoriesToPublish)
|
|
322
|
-
this.log.info(`Registered ${externalAccessoriesToPublish.length} HAP accessory(ies) as fallback (external publishing unavailable)`)
|
|
323
|
-
} catch (e) {
|
|
324
|
-
this.log.warn('Failed to register fallback HAP accessories', e)
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
354
|
}
|
|
329
355
|
|
|
330
356
|
/**
|
|
@@ -359,7 +385,6 @@ export class SwitchBotHAPPlatform {
|
|
|
359
385
|
id: d.deviceId ?? d.id,
|
|
360
386
|
type: d.configDeviceType ?? d.type,
|
|
361
387
|
name: d.configDeviceName ?? d.name,
|
|
362
|
-
externalPublishProtocol: resolveExternalPublishProtocol(d),
|
|
363
388
|
})))
|
|
364
389
|
}
|
|
365
390
|
|
|
@@ -422,6 +447,52 @@ export class SwitchBotHAPPlatform {
|
|
|
422
447
|
*
|
|
423
448
|
* @returns {void}
|
|
424
449
|
*/
|
|
450
|
+
/**
|
|
451
|
+
* Read every registered characteristic for a device and notify HomeKit.
|
|
452
|
+
*
|
|
453
|
+
* The device state is cached and de-duplicated, so one call costs a single
|
|
454
|
+
* API request however many characteristics the accessory has.
|
|
455
|
+
*/
|
|
456
|
+
private async _pushDeviceState(id: string): Promise<void> {
|
|
457
|
+
const target = this.hapPushTargets.get(id)
|
|
458
|
+
if (!target?.entries.length) {
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
// Read once first. A device that could not be read must push nothing: the
|
|
462
|
+
// getters report a fallback for a missing field, and pushing that would
|
|
463
|
+
// replace a good value in HomeKit with one that looks like a measurement.
|
|
464
|
+
try {
|
|
465
|
+
const state = await target.device?.getState?.()
|
|
466
|
+
if (state?.unreadable) {
|
|
467
|
+
this.log.debug?.(`Skipped pushing updates for ${id}: device not readable`)
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {
|
|
471
|
+
this.log.debug?.(`Skipped pushing updates for ${id}:`, (e as Error)?.message)
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
for (const { service, characteristic, get } of target.entries) {
|
|
475
|
+
try {
|
|
476
|
+
const value = await get()
|
|
477
|
+
if (value !== undefined && value !== null) {
|
|
478
|
+
service.getCharacteristic(characteristic).updateValue(value)
|
|
479
|
+
}
|
|
480
|
+
} catch (e) {
|
|
481
|
+
this.log.debug?.(`Failed to push an update for ${id}:`, (e as Error)?.message)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Push once now, so the Home app shows a value without waiting for the first
|
|
488
|
+
* poll interval.
|
|
489
|
+
*/
|
|
490
|
+
private primeHapValues(): void {
|
|
491
|
+
for (const id of this.hapPushTargets.keys()) {
|
|
492
|
+
void this._pushDeviceState(id)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
425
496
|
private _setupOpenApiPolling(): void {
|
|
426
497
|
// Clear any existing timers
|
|
427
498
|
for (const t of this.openApiPollTimers.values()) clearInterval(t)
|
|
@@ -497,6 +568,7 @@ export class SwitchBotHAPPlatform {
|
|
|
497
568
|
if (client && typeof client.getDevice === 'function') {
|
|
498
569
|
await client.getDevice(id)
|
|
499
570
|
this.openApiRequestsToday++
|
|
571
|
+
await this._pushDeviceState(id)
|
|
500
572
|
this.log.debug(`[OpenAPI] Polled device ${id} (per-device interval ${perDeviceRate}s) [${this.openApiRequestsToday}/${dailyLimit}]`)
|
|
501
573
|
}
|
|
502
574
|
} catch (e) {
|
|
@@ -536,6 +608,7 @@ export class SwitchBotHAPPlatform {
|
|
|
536
608
|
try {
|
|
537
609
|
await client.getDevice(dev.deviceId ?? dev.id)
|
|
538
610
|
this.openApiRequestsToday++
|
|
611
|
+
await this._pushDeviceState(dev.deviceId ?? dev.id)
|
|
539
612
|
this.log.debug(`[OpenAPI] Batched poll device ${dev.deviceId ?? dev.id} [${this.openApiRequestsToday}/${dailyLimit}]`)
|
|
540
613
|
} catch (e) {
|
|
541
614
|
this.log.debug(`[OpenAPI] Batched polling failed for device ${dev.deviceId ?? dev.id}:`, (e as Error)?.message)
|