@switchbot/homebridge-switchbot 5.1.0-beta.9 → 5.1.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 (143) hide show
  1. package/.github/copilot-instructions.md +0 -63
  2. package/.github/scripts/compute-version-override.cjs +51 -0
  3. package/.github/scripts/normalize-changelog-header.cjs +1 -1
  4. package/.github/scripts/npm-version-script-esm-auto.js +1 -1
  5. package/.github/scripts/resolve-version-bump-esm.js +1 -1
  6. package/CHANGELOG.md +32 -9
  7. package/MIGRATION.md +0 -5
  8. package/README.md +0 -25
  9. package/config.schema.json +0 -11
  10. package/dist/{Platform.HAP.d.ts → SwitchBotHAPPlatform.d.ts} +16 -2
  11. package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
  12. package/dist/{Platform.HAP.js → SwitchBotHAPPlatform.js} +111 -40
  13. package/dist/SwitchBotHAPPlatform.js.map +1 -0
  14. package/dist/{Platform.Matter.d.ts → SwitchBotMatterPlatform.d.ts} +2 -4
  15. package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
  16. package/dist/{Platform.Matter.js → SwitchBotMatterPlatform.js} +6 -62
  17. package/dist/SwitchBotMatterPlatform.js.map +1 -0
  18. package/dist/device-types.d.ts.map +1 -1
  19. package/dist/device-types.js +10 -0
  20. package/dist/device-types.js.map +1 -1
  21. package/dist/deviceFactory.d.ts.map +1 -1
  22. package/dist/deviceFactory.js +10 -16
  23. package/dist/deviceFactory.js.map +1 -1
  24. package/dist/devices/genericDevice.d.ts +63 -5
  25. package/dist/devices/genericDevice.d.ts.map +1 -1
  26. package/dist/devices/genericDevice.js +251 -11
  27. package/dist/devices/genericDevice.js.map +1 -1
  28. package/dist/homebridge-ui/device-types.js +10 -0
  29. package/dist/homebridge-ui/device-types.js.map +1 -1
  30. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
  31. package/dist/homebridge-ui/endpoints/config.js +2 -9
  32. package/dist/homebridge-ui/endpoints/config.js.map +1 -1
  33. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +2 -9
  34. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
  35. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +0 -8
  36. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
  37. package/dist/homebridge-ui/public/css/styles.css +1 -61
  38. package/dist/homebridge-ui/public/index.html +2 -61
  39. package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -1
  40. package/dist/homebridge-ui/public/js/advanced-settings.js +3 -4
  41. package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -1
  42. package/dist/homebridge-ui/public/js/advanced-settings.ts +3 -4
  43. package/dist/homebridge-ui/public/js/api.d.ts +0 -16
  44. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  45. package/dist/homebridge-ui/public/js/api.js +0 -48
  46. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  47. package/dist/homebridge-ui/public/js/api.ts +1 -61
  48. package/dist/homebridge-ui/public/js/app.js +135 -272
  49. package/dist/homebridge-ui/public/js/app.js.map +4 -4
  50. package/dist/homebridge-ui/public/js/app.ts +4 -47
  51. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
  52. package/dist/homebridge-ui/public/js/credentials.js +5 -11
  53. package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
  54. package/dist/homebridge-ui/public/js/credentials.ts +5 -11
  55. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  56. package/dist/homebridge-ui/public/js/devices.js +0 -6
  57. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  58. package/dist/homebridge-ui/public/js/devices.ts +0 -6
  59. package/dist/homebridge-ui/public/js/modals.d.ts +0 -1
  60. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  61. package/dist/homebridge-ui/public/js/modals.js +0 -53
  62. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  63. package/dist/homebridge-ui/public/js/modals.ts +0 -58
  64. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  65. package/dist/homebridge-ui/public/js/render.js +1 -2
  66. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  67. package/dist/homebridge-ui/public/js/render.ts +1 -2
  68. package/dist/homebridge-ui/settings.js.map +1 -1
  69. package/dist/homebridge-ui/utils/config-parser.d.ts +0 -8
  70. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
  71. package/dist/homebridge-ui/utils/config-parser.js +0 -8
  72. package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
  73. package/dist/index.js +2 -2
  74. package/dist/index.js.map +1 -1
  75. package/dist/settings.d.ts +0 -11
  76. package/dist/settings.d.ts.map +1 -1
  77. package/dist/settings.js.map +1 -1
  78. package/dist/utils.d.ts +0 -5
  79. package/dist/utils.d.ts.map +1 -1
  80. package/dist/utils.js +0 -27
  81. package/dist/utils.js.map +1 -1
  82. package/docs/assets/highlight.css +16 -16
  83. package/docs/assets/main.js +1 -1
  84. package/docs/assets/style.css +18 -3
  85. package/docs/hierarchy.html +26 -1
  86. package/docs/index.html +70 -23
  87. package/docs/modules.html +33 -1
  88. package/docs/variables/default.html +46 -3
  89. package/package.json +22 -15
  90. package/scripts/build-ui.js +0 -30
  91. package/src/{Platform.HAP.ts → SwitchBotHAPPlatform.ts} +112 -39
  92. package/src/{Platform.Matter.ts → SwitchBotMatterPlatform.ts} +8 -63
  93. package/src/device-types.ts +11 -0
  94. package/src/deviceFactory.ts +10 -15
  95. package/src/devices/genericDevice.ts +271 -12
  96. package/src/homebridge-ui/endpoints/config.ts +2 -10
  97. package/src/homebridge-ui/public/css/styles.css +1 -61
  98. package/src/homebridge-ui/public/index.html +0 -59
  99. package/src/homebridge-ui/public/js/advanced-settings.ts +3 -4
  100. package/src/homebridge-ui/public/js/api.ts +1 -61
  101. package/src/homebridge-ui/public/js/app.ts +4 -47
  102. package/src/homebridge-ui/public/js/credentials.ts +5 -11
  103. package/src/homebridge-ui/public/js/devices.ts +0 -6
  104. package/src/homebridge-ui/public/js/modals.ts +0 -58
  105. package/src/homebridge-ui/public/js/render.ts +1 -2
  106. package/src/homebridge-ui/utils/config-parser.ts +0 -9
  107. package/src/index.ts +2 -2
  108. package/src/settings.ts +0 -12
  109. package/src/utils.ts +0 -31
  110. package/test/device/deviceFactory.spec.ts +9 -0
  111. package/test/device/getstate-cache.spec.ts +62 -0
  112. package/test/device/getstate-returns-status.spec.ts +89 -0
  113. package/test/device/model-identities.spec.ts +45 -0
  114. package/test/device/water-detector-battery.spec.ts +81 -0
  115. package/test/device/water-detector-leak.spec.ts +43 -0
  116. package/test/hap-push-updates.spec.ts +150 -0
  117. package/test/platform/accessory-restore.spec.ts +2 -2
  118. package/test/platform/matter-childbridge.spec.ts +1 -1
  119. package/test/platform/matter-integration.spec.ts +1 -51
  120. package/test/platform/platform-edge.spec.ts +2 -2
  121. package/test/platform/platform.integration.spec.ts +1 -1
  122. package/test/utils/utils.spec.ts +0 -15
  123. package/.github/npm-version-script-esm.js +0 -97
  124. package/dist/Platform.HAP.d.ts.map +0 -1
  125. package/dist/Platform.HAP.js.map +0 -1
  126. package/dist/Platform.Matter.d.ts.map +0 -1
  127. package/dist/Platform.Matter.js.map +0 -1
  128. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +0 -23
  129. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +0 -1
  130. package/dist/homebridge-ui/public/js/legacy-view.d.ts +0 -3
  131. package/dist/homebridge-ui/public/js/legacy-view.d.ts.map +0 -1
  132. package/dist/homebridge-ui/public/js/legacy-view.js +0 -45
  133. package/dist/homebridge-ui/public/js/legacy-view.js.map +0 -1
  134. package/dist/homebridge-ui/public/js/legacy-view.ts +0 -107
  135. package/dist/homebridge-ui/utils/v4-detection.d.ts +0 -12
  136. package/dist/homebridge-ui/utils/v4-detection.d.ts.map +0 -1
  137. package/dist/homebridge-ui/utils/v4-detection.js +0 -23
  138. package/dist/homebridge-ui/utils/v4-detection.js.map +0 -1
  139. package/src/device-types.js +0 -246
  140. package/src/homebridge-ui/public/js/legacy-view.js +0 -45
  141. package/src/homebridge-ui/public/js/legacy-view.ts +0 -107
  142. package/src/homebridge-ui/utils/v4-detection.ts +0 -23
  143. package/test/homebridge-ui/config-parser.spec.ts +0 -61
@@ -1,3 +1,46 @@
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
- </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/324ff500a95bfe7d722fc56e5a653b3028ebd446/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>
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">
2
+ <div class="tsd-toolbar-contents container"><a href="../index.html" class="title">@switchbot/homebridge-switchbot</a>
3
+ <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"/>
4
+ <ul role="listbox" id="tsd-search-results"></ul>
5
+ <div id="tsd-search-status" aria-live="polite" aria-atomic="true">
6
+ <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>
7
+ <div class="container container-main">
8
+ <div class="col-content">
9
+ <div class="tsd-page-title">
10
+ <ul class="tsd-breadcrumb" aria-label="Breadcrumb">
11
+ <li><a href="" aria-current="page">default</a></li></ul>
12
+ <h1>Variable default</h1></div>
13
+ <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>
14
+ <div class="tsd-comment tsd-typography"><p>Registers the SwitchBot platform with Homebridge.</p>
15
+ </div>
16
+ <div class="tsd-type-declaration">
17
+ <h4>Type Declaration</h4>
18
+ <ul class="tsd-parameters">
19
+ <li class="tsd-parameter-signature">
20
+ <ul class="tsd-signatures">
21
+ <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>
22
+ <li class="tsd-description">
23
+ <div class="tsd-parameters">
24
+ <h4 class="tsd-parameters-title">Parameters</h4>
25
+ <ul class="tsd-parameter-list">
26
+ <li><span><span class="tsd-kind-parameter">api</span>: <span class="tsd-signature-type">API</span></span>
27
+ <div class="tsd-comment tsd-typography"><p>The Homebridge API instance.</p>
28
+ </div></li></ul></div>
29
+ <h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">void</span></h4></li></ul></li></ul></div><aside class="tsd-sources">
30
+ <ul>
31
+ <li>Defined in <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/64fac398b3b1f3ecf7b59ce9e465622014310354/src/index.ts#L22">index.ts:22</a></li></ul></aside></div>
32
+ <div class="col-sidebar">
33
+ <div class="page-menu">
34
+ <div class="tsd-navigation settings">
35
+ <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>
36
+ <h3>Settings</h3></summary>
37
+ <div class="tsd-accordion-details">
38
+ <div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span>
39
+ <ul id="tsd-filter-options">
40
+ <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>
41
+ <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>
42
+ <div class="site-menu">
43
+ <nav class="tsd-navigation"><a href="../modules.html">@switchbot/homebridge-switchbot</a>
44
+ <ul class="tsd-small-nested-navigation" id="tsd-nav-container">
45
+ <li>Loading...</li></ul></nav></div></div></div><footer></footer>
46
+ <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-beta.9",
5
+ "version": "5.1.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": [
@@ -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",
@@ -57,7 +56,7 @@
57
56
  "icon": "https://raw.githubusercontent.com/OpenWonderLabs/homebridge-switchbot/latest/branding/icon.png",
58
57
  "engineStrict": true,
59
58
  "engines": {
60
- "homebridge": "^2.0.0-beta.89",
59
+ "homebridge": "^2.0.0",
61
60
  "node": "^22 || ^24"
62
61
  },
63
62
  "directories": {
@@ -92,22 +91,30 @@
92
91
  "access": "public"
93
92
  },
94
93
  "dependencies": {
95
- "@homebridge/plugin-ui-utils": "^2.2.3",
96
- "node-switchbot": "^4.0.2"
94
+ "@homebridge/plugin-ui-utils": "^2.2.6",
95
+ "class-validator": "^0.15.1",
96
+ "node-switchbot": "^5.0.0"
97
97
  },
98
98
  "devDependencies": {
99
- "@antfu/eslint-config": "^8.2.0",
100
- "@types/node": "^25.6.0",
101
- "@vitest/coverage-v8": "^4.1.5",
102
- "esbuild": "^0.28.0",
103
- "eslint": "^10.3.0",
99
+ "@antfu/eslint-config": "^9.5.1",
100
+ "@types/debug": "^4.1.13",
101
+ "@types/fs-extra": "^11.0.4",
102
+ "@types/node": "^26.6.4",
103
+ "@types/semver": "^7.8.0",
104
+ "@types/source-map-support": "^0.5.10",
105
+ "@vitest/coverage-v8": "^5.0.3",
106
+ "esbuild": "^0.28.2",
107
+ "eslint": "^10.12.0",
104
108
  "eslint-plugin-format": "^2.0.1",
105
- "eslint-plugin-perfectionist": "^5.9.0",
106
- "homebridge": "^2.0.1",
109
+ "eslint-plugin-html": "^8.2.1",
110
+ "eslint-plugin-perfectionist": "^5.12.1",
111
+ "homebridge": "^2.4.0",
112
+ "homebridge-config-ui-x": "^5.29.0",
107
113
  "nodemon": "^3.1.14",
108
114
  "shx": "^0.4.0",
109
- "typedoc": "^0.28.19",
110
- "typescript": "^6.0.3",
111
- "vitest": "^4.1.5"
115
+ "typedoc": "^0.28.20",
116
+ "typescript": "^6.0.2",
117
+ "vite": "^7.0.0",
118
+ "vitest": "^5.0.3"
112
119
  }
113
120
  }
@@ -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, resolveExternalPublishProtocol } from './utils.js'
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, externalPublishProtocol: 'none' | 'hap' | 'matter' }[] = []
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, externalPublishProtocol })
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, externalPublishProtocol: 'none' | 'hap' | 'matter' }[]): Promise<void> {
194
- if (!this.api || !(this.api as any).hap || typeof (this.api as any).hap.registerPlatformAccessories !== 'function') {
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 externalAccessoriesToPublish: any[] = []
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
- // Use the PlatformAccessory constructor from the HAP API
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
- if (externalPublishProtocol === 'hap') {
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
- if (existingService.constructor && existingService.constructor.name && !serviceTypes.includes(existingService.constructor.name)) {
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(getterSetter.set)
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)
@@ -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 { createMatterHandlers, DEVICE_MATTER_CLUSTERS, DEVICE_MATTER_SUPPORTED, normalizeTypeForMatter, resolveExternalPublishProtocol, resolveMatterDeviceType } from './utils.js'
7
+ import { createMatterHandlers, DEVICE_MATTER_CLUSTERS, DEVICE_MATTER_SUPPORTED, normalizeTypeForMatter, resolveMatterDeviceType } from './utils.js'
9
8
 
10
9
  /**
11
10
  * Homebridge platform class for SwitchBot Matter integration.
@@ -114,7 +113,7 @@ export class SwitchBotMatterPlatform {
114
113
  }
115
114
 
116
115
  const devices = (this.config as any)?.devices ?? []
117
- const createdDevices: { created: any, d: any, type: string, useMatter: boolean, matterAvailable: boolean, externalPublishProtocol: 'none' | 'hap' | 'matter' }[] = []
116
+ const createdDevices: { created: any, d: any, type: string, useMatter: boolean, matterAvailable: boolean }[] = []
118
117
  for (const raw of devices) {
119
118
  // Normalize config keys from UI schema to internal shape (for cross-platform consistency)
120
119
  const d: any = {
@@ -126,7 +125,6 @@ export class SwitchBotMatterPlatform {
126
125
  _raw: raw,
127
126
  }
128
127
  const type: string = normalizeTypeForMatter(d.type)
129
- const externalPublishProtocol = resolveExternalPublishProtocol(raw)
130
128
  const deviceOpts: any = { id: d.id, type, name: d.name, encryptionKey: d.encryptionKey, keyId: d.keyId, log: this.log }
131
129
  this.log.debug(`[Matter/Debug] Device options for ${d.name ?? d.id}:`, JSON.stringify(deviceOpts, null, 2))
132
130
  const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
@@ -136,7 +134,7 @@ export class SwitchBotMatterPlatform {
136
134
  try {
137
135
  const created = await createDevice(deviceOpts, this.config, useMatter)
138
136
  this.devices.push(created)
139
- createdDevices.push({ created, d, type, useMatter, matterAvailable, externalPublishProtocol })
137
+ createdDevices.push({ created, d, type, useMatter, matterAvailable })
140
138
  if (useMatter) {
141
139
  this.log.info(`Prepared Matter accessory for ${d.id} (${type})${matterAvailable ? ' (auto-detected)' : ' (manually enabled)'}`)
142
140
  } else {
@@ -181,15 +179,14 @@ export class SwitchBotMatterPlatform {
181
179
  * If the Homebridge Matter API is not available, registration is skipped and a log message is emitted.
182
180
  * Accessories that are not enabled for Matter are ignored.
183
181
  */
184
- async registerMatterAccessories(createdDevices: { created: any, d: any, type: string, useMatter: boolean, matterAvailable: boolean, externalPublishProtocol: 'none' | 'hap' | 'matter' }[]): Promise<void> {
182
+ async registerMatterAccessories(createdDevices: { created: any, d: any, type: string, useMatter: boolean, matterAvailable: boolean }[]): Promise<void> {
185
183
  if (!this.api || !(this.api as any).matter || typeof (this.api as any).matter.registerPlatformAccessories !== 'function') {
186
184
  this.log.info('Homebridge Matter API not available; skipping Matter accessory registration')
187
185
  return
188
186
  }
189
187
  const matterApi = (this.api as any).matter
190
188
  const accessoriesToRegister: any[] = []
191
- const externalAccessoriesToPublish: any[] = []
192
- for (const { created, d, type, useMatter, matterAvailable, externalPublishProtocol } of createdDevices) {
189
+ for (const { created, d, type, useMatter, matterAvailable } of createdDevices) {
193
190
  // Only register accessories where Matter is enabled and supported
194
191
  if (!useMatter) {
195
192
  // Log reason for skipping registration
@@ -240,11 +237,7 @@ export class SwitchBotMatterPlatform {
240
237
  handlers: createdDesc.handlers || createMatterHandlers(this.log, d.id, type, (this.config as any)?._client) || undefined,
241
238
  context: { deviceId: d.id, type, created: true },
242
239
  }
243
- if (externalPublishProtocol === 'matter') {
244
- externalAccessoriesToPublish.push(accessory)
245
- } else {
246
- accessoriesToRegister.push(accessory)
247
- }
240
+ accessoriesToRegister.push(accessory)
248
241
  this.accessories.set(uuid, accessory)
249
242
  } else {
250
243
  // Ensure context and update properties for restored accessory
@@ -267,11 +260,7 @@ export class SwitchBotMatterPlatform {
267
260
  accessory.handlers = createdDesc.handlers || createMatterHandlers(this.log, d.id, type, (this.config as any)?._client) || undefined
268
261
  accessory.displayName = createdDesc.name || d.name || type
269
262
  accessory.UUID = accessory.UUID || accessory.uuid || uuid
270
- if (externalPublishProtocol === 'matter') {
271
- externalAccessoriesToPublish.push(accessory)
272
- } else {
273
- accessoriesToRegister.push(accessory)
274
- }
263
+ accessoriesToRegister.push(accessory)
275
264
  this.accessories.set(accessory.UUID || uuid, accessory)
276
265
  }
277
266
  this.log.info(`Created/updated Matter accessory ${d.id} (${type})`)
@@ -289,49 +278,6 @@ export class SwitchBotMatterPlatform {
289
278
  } else {
290
279
  this.log.info('No Matter accessories to register')
291
280
  }
292
- if (externalAccessoriesToPublish.length > 0) {
293
- try {
294
- if (typeof matterApi.publishExternalAccessories === 'function') {
295
- await matterApi.publishExternalAccessories(PLUGIN_NAME, externalAccessoriesToPublish)
296
- this.log.info(`Published ${externalAccessoriesToPublish.length} Matter external accessory(ies) with Homebridge`)
297
- } else if (typeof matterApi.registerExternalAccessories === 'function') {
298
- await matterApi.registerExternalAccessories(PLUGIN_NAME, PLATFORM_NAME, externalAccessoriesToPublish)
299
- this.log.info(`Registered ${externalAccessoriesToPublish.length} Matter external accessory(ies) with Homebridge`)
300
- } else if (await this.publishMatterExternalAccessories(externalAccessoriesToPublish)) {
301
- this.log.info(`Published ${externalAccessoriesToPublish.length} Matter external accessory(ies) with Homebridge`)
302
- } else {
303
- this.log.warn('Matter external accessory publishing API not available; falling back to regular platform registration')
304
- await matterApi.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, externalAccessoriesToPublish)
305
- this.log.info(`Registered ${externalAccessoriesToPublish.length} Matter accessory(ies) as fallback (external publishing unavailable)`)
306
- }
307
- } catch (e) {
308
- this.log.warn('Failed to publish/register Matter external accessories', e)
309
- }
310
- }
311
- }
312
-
313
- private async publishMatterExternalAccessories(accessories: any[]): Promise<boolean> {
314
- const api = this.api as any
315
- if (!api || typeof api.emit !== 'function') {
316
- return false
317
- }
318
-
319
- const pendingRegistrations = api._pendingExternalRegistrations
320
- if (!(pendingRegistrations instanceof Map) && pendingRegistrations !== undefined) {
321
- return false
322
- }
323
-
324
- const registrationId = `${PLUGIN_NAME}-${Date.now()}-${Math.random()}`
325
- const registrationPromise = new Promise<void>((resolve) => {
326
- if (!(api._pendingExternalRegistrations instanceof Map)) {
327
- api._pendingExternalRegistrations = new Map()
328
- }
329
- api._pendingExternalRegistrations.set(registrationId, resolve)
330
- })
331
-
332
- api.emit('publishExternalMatterAccessories', accessories, registrationId)
333
- await registrationPromise
334
- return true
335
281
  }
336
282
 
337
283
  /**
@@ -366,7 +312,6 @@ export class SwitchBotMatterPlatform {
366
312
  id: d.deviceId ?? d.id,
367
313
  type: d.configDeviceType ?? d.type,
368
314
  name: d.configDeviceName ?? d.name,
369
- externalPublishProtocol: resolveExternalPublishProtocol(d),
370
315
  })))
371
316
  }
372
317
 
@@ -191,6 +191,17 @@ export const DEVICE_TYPE_NORMALIZATION_MAP: Record<string, string> = {
191
191
 
192
192
  // Additional normalization for new/unknown types from logs
193
193
  'woplugus': 'Plug Mini (US)',
194
+
195
+ // Product-specific identities reported by node-switchbot 5 (pySwitchbot 3.0.0 parity)
196
+ 'woplugjp': 'Plug Mini (JP)',
197
+ 'wocurtain3': 'Curtain3',
198
+ 'wometerplus': 'Meter Plus',
199
+ 'wothpc': 'Meter Pro (CO2)',
200
+ 'woceilingpro': 'Ceiling Light Pro',
201
+ 'wolinkmini': 'Hub Mini',
202
+ 'woiosensorth': 'WoIOSensor',
203
+ 'outdoor meter': 'WoIOSensor',
204
+ 'indoor/outdoor thermo-hygrometer': 'WoIOSensor',
194
205
  // Removed duplicate keys below, only last occurrence kept
195
206
  // 'plug mini us': 'plug mini (us)', // duplicate, removed
196
207
  // 'plug us': 'plug mini (us)', // duplicate, removed