@smarthome.exchange/integrations 2.0.0 → 4.0.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.
Files changed (251) hide show
  1. package/changelog.md +46 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/core/classes.nativeintegrationowner.d.ts +2 -0
  4. package/dist_ts/core/classes.nativeintegrationowner.js +5 -1
  5. package/dist_ts/core/classes.runtimemanager.d.ts +2 -0
  6. package/dist_ts/core/classes.runtimemanager.js +11 -3
  7. package/dist_ts/core/types.d.ts +2 -0
  8. package/dist_ts/integrations/deconz/deconz.classes.client.d.ts +37 -29
  9. package/dist_ts/integrations/deconz/deconz.classes.client.js +521 -294
  10. package/dist_ts/integrations/deconz/deconz.classes.configflow.d.ts +6 -1
  11. package/dist_ts/integrations/deconz/deconz.classes.configflow.js +63 -33
  12. package/dist_ts/integrations/deconz/deconz.classes.integration.d.ts +4 -3
  13. package/dist_ts/integrations/deconz/deconz.classes.integration.js +85 -40
  14. package/dist_ts/integrations/deconz/deconz.mapper.d.ts +1 -0
  15. package/dist_ts/integrations/deconz/deconz.mapper.js +24 -6
  16. package/dist_ts/integrations/deconz/deconz.types.d.ts +43 -6
  17. package/dist_ts/integrations/deconz/deconz.validation.d.ts +7 -1
  18. package/dist_ts/integrations/deconz/deconz.validation.js +215 -4
  19. package/dist_ts/integrations/esphome/esphome.classes.client.d.ts +19 -7
  20. package/dist_ts/integrations/esphome/esphome.classes.client.js +183 -89
  21. package/dist_ts/integrations/esphome/esphome.classes.configflow.d.ts +5 -2
  22. package/dist_ts/integrations/esphome/esphome.classes.configflow.js +43 -33
  23. package/dist_ts/integrations/esphome/esphome.classes.integration.d.ts +8 -6
  24. package/dist_ts/integrations/esphome/esphome.classes.integration.js +71 -43
  25. package/dist_ts/integrations/esphome/esphome.discovery.js +4 -12
  26. package/dist_ts/integrations/esphome/esphome.mapper.d.ts +2 -2
  27. package/dist_ts/integrations/esphome/esphome.mapper.js +6 -4
  28. package/dist_ts/integrations/esphome/esphome.types.d.ts +31 -6
  29. package/dist_ts/integrations/esphome/esphome.types.js +1 -1
  30. package/dist_ts/integrations/esphome/esphome.validation.d.ts +7 -0
  31. package/dist_ts/integrations/esphome/esphome.validation.js +66 -0
  32. package/dist_ts/integrations/homeassistant/homeassistant.classes.client.d.ts +32 -4
  33. package/dist_ts/integrations/homeassistant/homeassistant.classes.client.js +345 -116
  34. package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.d.ts +8 -5
  35. package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.js +34 -8
  36. package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.d.ts +7 -4
  37. package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.js +44 -56
  38. package/dist_ts/integrations/homeassistant/homeassistant.types.d.ts +12 -1
  39. package/dist_ts/integrations/hue/hue.classes.client.d.ts +32 -4
  40. package/dist_ts/integrations/hue/hue.classes.client.js +331 -57
  41. package/dist_ts/integrations/hue/hue.classes.configflow.d.ts +6 -1
  42. package/dist_ts/integrations/hue/hue.classes.configflow.js +79 -27
  43. package/dist_ts/integrations/hue/hue.classes.integration.d.ts +3 -1
  44. package/dist_ts/integrations/hue/hue.classes.integration.js +83 -62
  45. package/dist_ts/integrations/hue/hue.mapper.d.ts +2 -0
  46. package/dist_ts/integrations/hue/hue.mapper.js +8 -6
  47. package/dist_ts/integrations/hue/hue.types.d.ts +32 -1
  48. package/dist_ts/integrations/hue/hue.validation.d.ts +9 -3
  49. package/dist_ts/integrations/hue/hue.validation.js +112 -11
  50. package/dist_ts/integrations/mqtt/mqtt.classes.client.d.ts +15 -2
  51. package/dist_ts/integrations/mqtt/mqtt.classes.client.js +173 -23
  52. package/dist_ts/integrations/mqtt/mqtt.classes.configflow.d.ts +5 -1
  53. package/dist_ts/integrations/mqtt/mqtt.classes.configflow.js +53 -33
  54. package/dist_ts/integrations/mqtt/mqtt.classes.integration.d.ts +4 -1
  55. package/dist_ts/integrations/mqtt/mqtt.classes.integration.js +127 -56
  56. package/dist_ts/integrations/mqtt/mqtt.classes.session.d.ts +13 -0
  57. package/dist_ts/integrations/mqtt/mqtt.classes.session.js +130 -0
  58. package/dist_ts/integrations/mqtt/mqtt.mapper.d.ts +3 -0
  59. package/dist_ts/integrations/mqtt/mqtt.mapper.js +44 -1
  60. package/dist_ts/integrations/mqtt/mqtt.types.d.ts +46 -0
  61. package/dist_ts/integrations/mqtt/mqtt.validation.d.ts +7 -0
  62. package/dist_ts/integrations/mqtt/mqtt.validation.js +180 -0
  63. package/dist_ts/integrations/samsungtv/samsungtv.classes.client.d.ts +4 -2
  64. package/dist_ts/integrations/samsungtv/samsungtv.classes.client.js +41 -16
  65. package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.d.ts +5 -0
  66. package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.js +20 -7
  67. package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.d.ts +1 -0
  68. package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.js +53 -50
  69. package/dist_ts/integrations/samsungtv/samsungtv.types.d.ts +4 -1
  70. package/dist_ts/integrations/shelly/shelly.classes.client.d.ts +26 -30
  71. package/dist_ts/integrations/shelly/shelly.classes.client.js +504 -276
  72. package/dist_ts/integrations/shelly/shelly.classes.configflow.d.ts +8 -3
  73. package/dist_ts/integrations/shelly/shelly.classes.configflow.js +43 -38
  74. package/dist_ts/integrations/shelly/shelly.classes.integration.d.ts +6 -6
  75. package/dist_ts/integrations/shelly/shelly.classes.integration.js +103 -101
  76. package/dist_ts/integrations/shelly/shelly.classes.subscription.d.ts +4 -0
  77. package/dist_ts/integrations/shelly/shelly.classes.subscription.js +24 -2
  78. package/dist_ts/integrations/shelly/shelly.mapper.d.ts +1 -2
  79. package/dist_ts/integrations/shelly/shelly.mapper.js +21 -20
  80. package/dist_ts/integrations/shelly/shelly.rpc.js +4 -1
  81. package/dist_ts/integrations/shelly/shelly.types.d.ts +42 -2
  82. package/dist_ts/integrations/shelly/shelly.types.js +11 -2
  83. package/dist_ts/integrations/shelly/shelly.validation.d.ts +7 -0
  84. package/dist_ts/integrations/shelly/shelly.validation.js +79 -0
  85. package/dist_ts/integrations/sonos/sonos.classes.client.d.ts +79 -22
  86. package/dist_ts/integrations/sonos/sonos.classes.client.js +427 -139
  87. package/dist_ts/integrations/sonos/sonos.classes.configflow.d.ts +9 -2
  88. package/dist_ts/integrations/sonos/sonos.classes.configflow.js +26 -28
  89. package/dist_ts/integrations/sonos/sonos.classes.integration.d.ts +7 -6
  90. package/dist_ts/integrations/sonos/sonos.classes.integration.js +289 -224
  91. package/dist_ts/integrations/sonos/sonos.config.d.ts +6 -1
  92. package/dist_ts/integrations/sonos/sonos.config.js +23 -2
  93. package/dist_ts/integrations/sonos/sonos.mapper.d.ts +1 -1
  94. package/dist_ts/integrations/sonos/sonos.mapper.js +14 -9
  95. package/dist_ts/integrations/sonos/sonos.matchers.js +3 -3
  96. package/dist_ts/integrations/sonos/sonos.types.d.ts +26 -1
  97. package/dist_ts/integrations/tasmota/tasmota.classes.integration.d.ts +5 -0
  98. package/dist_ts/integrations/tasmota/tasmota.classes.integration.js +23 -7
  99. package/dist_ts/integrations/tasmota/tasmota.classes.runtime.d.ts +19 -5
  100. package/dist_ts/integrations/tasmota/tasmota.classes.runtime.js +115 -41
  101. package/dist_ts/integrations/tasmota/tasmota.config.js +23 -1
  102. package/dist_ts/integrations/tasmota/tasmota.credentials.d.ts +11 -0
  103. package/dist_ts/integrations/tasmota/tasmota.credentials.js +97 -0
  104. package/dist_ts/integrations/tasmota/tasmota.discovery.d.ts +7 -1
  105. package/dist_ts/integrations/tasmota/tasmota.discovery.js +108 -30
  106. package/dist_ts/integrations/tasmota/tasmota.mapper.d.ts +2 -2
  107. package/dist_ts/integrations/tasmota/tasmota.mapper.js +7 -7
  108. package/dist_ts/integrations/tasmota/tasmota.types.d.ts +41 -2
  109. package/dist_ts/integrations/tuya/tuya.classes.integration.d.ts +4 -1
  110. package/dist_ts/integrations/tuya/tuya.classes.integration.js +79 -110
  111. package/dist_ts/integrations/tuya/tuya.classes.runtime.d.ts +5 -2
  112. package/dist_ts/integrations/tuya/tuya.classes.runtime.js +73 -26
  113. package/dist_ts/integrations/tuya/tuya.mapper.js +2 -2
  114. package/dist_ts/integrations/tuya/tuya.types.d.ts +3 -0
  115. package/dist_ts/integrations/tuya/tuya.validation.d.ts +2 -2
  116. package/dist_ts/integrations/tuya/tuya.validation.js +6 -11
  117. package/dist_ts/integrations/webostv/webostv.classes.client.d.ts +6 -5
  118. package/dist_ts/integrations/webostv/webostv.classes.client.js +147 -76
  119. package/dist_ts/integrations/webostv/webostv.classes.configflow.d.ts +7 -2
  120. package/dist_ts/integrations/webostv/webostv.classes.configflow.js +36 -20
  121. package/dist_ts/integrations/webostv/webostv.classes.integration.d.ts +4 -4
  122. package/dist_ts/integrations/webostv/webostv.classes.integration.js +46 -54
  123. package/dist_ts/integrations/webostv/webostv.mapper.js +5 -5
  124. package/dist_ts/integrations/webostv/webostv.types.d.ts +16 -3
  125. package/dist_ts/integrations/webostv/webostv.types.js +1 -1
  126. package/dist_ts/integrations/webostv/webostv.validation.d.ts +3 -1
  127. package/dist_ts/integrations/webostv/webostv.validation.js +23 -1
  128. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.d.ts +6 -1
  129. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.js +18 -8
  130. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.d.ts +4 -1
  131. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.js +54 -6
  132. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.d.ts +12 -6
  133. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.js +73 -59
  134. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.d.ts +3 -2
  135. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.js +17 -12
  136. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.types.d.ts +31 -2
  137. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.d.ts +9 -1
  138. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.js +57 -7
  139. package/dist_ts/integrations/zwave_js/zwave_js.classes.client.d.ts +10 -4
  140. package/dist_ts/integrations/zwave_js/zwave_js.classes.client.js +167 -31
  141. package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.d.ts +6 -1
  142. package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.js +57 -35
  143. package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.d.ts +3 -1
  144. package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.js +59 -37
  145. package/dist_ts/integrations/zwave_js/zwave_js.discovery.js +3 -3
  146. package/dist_ts/integrations/zwave_js/zwave_js.mapper.js +41 -24
  147. package/dist_ts/integrations/zwave_js/zwave_js.types.d.ts +25 -3
  148. package/dist_ts/integrations/zwave_js/zwave_js.validation.d.ts +6 -2
  149. package/dist_ts/integrations/zwave_js/zwave_js.validation.js +52 -17
  150. package/dist_ts/plugins.d.ts +2 -1
  151. package/dist_ts/plugins.js +3 -2
  152. package/package.json +10 -10
  153. package/readme.md +25 -6
  154. package/ts/00_commitinfo_data.ts +1 -1
  155. package/ts/core/classes.nativeintegrationowner.ts +5 -0
  156. package/ts/core/classes.runtimemanager.ts +11 -2
  157. package/ts/core/types.ts +2 -0
  158. package/ts/integrations/deconz/deconz.classes.client.ts +348 -252
  159. package/ts/integrations/deconz/deconz.classes.configflow.ts +57 -29
  160. package/ts/integrations/deconz/deconz.classes.integration.ts +69 -37
  161. package/ts/integrations/deconz/deconz.mapper.ts +22 -5
  162. package/ts/integrations/deconz/deconz.types.ts +49 -6
  163. package/ts/integrations/deconz/deconz.validation.ts +177 -4
  164. package/ts/integrations/deconz/readme.md +15 -1
  165. package/ts/integrations/esphome/esphome.classes.client.ts +179 -94
  166. package/ts/integrations/esphome/esphome.classes.configflow.ts +40 -33
  167. package/ts/integrations/esphome/esphome.classes.integration.ts +63 -36
  168. package/ts/integrations/esphome/esphome.discovery.ts +3 -12
  169. package/ts/integrations/esphome/esphome.mapper.ts +9 -7
  170. package/ts/integrations/esphome/esphome.types.ts +35 -6
  171. package/ts/integrations/esphome/esphome.validation.ts +66 -0
  172. package/ts/integrations/esphome/readme.md +20 -6
  173. package/ts/integrations/homeassistant/homeassistant.classes.client.ts +353 -125
  174. package/ts/integrations/homeassistant/homeassistant.classes.integration.ts +35 -18
  175. package/ts/integrations/homeassistant/homeassistant.classes.runtime.ts +50 -63
  176. package/ts/integrations/homeassistant/homeassistant.types.ts +17 -1
  177. package/ts/integrations/homeassistant/readme.md +22 -6
  178. package/ts/integrations/hue/hue.classes.client.ts +288 -55
  179. package/ts/integrations/hue/hue.classes.configflow.ts +68 -25
  180. package/ts/integrations/hue/hue.classes.integration.ts +75 -54
  181. package/ts/integrations/hue/hue.mapper.ts +8 -5
  182. package/ts/integrations/hue/hue.types.ts +37 -1
  183. package/ts/integrations/hue/hue.validation.ts +102 -13
  184. package/ts/integrations/hue/readme.md +49 -12
  185. package/ts/integrations/mqtt/mqtt.classes.client.ts +136 -19
  186. package/ts/integrations/mqtt/mqtt.classes.configflow.ts +45 -32
  187. package/ts/integrations/mqtt/mqtt.classes.integration.ts +99 -38
  188. package/ts/integrations/mqtt/mqtt.classes.session.ts +118 -0
  189. package/ts/integrations/mqtt/mqtt.mapper.ts +43 -1
  190. package/ts/integrations/mqtt/mqtt.types.ts +52 -0
  191. package/ts/integrations/mqtt/mqtt.validation.ts +157 -0
  192. package/ts/integrations/mqtt/readme.md +12 -3
  193. package/ts/integrations/samsungtv/readme.md +11 -5
  194. package/ts/integrations/samsungtv/samsungtv.classes.client.ts +40 -15
  195. package/ts/integrations/samsungtv/samsungtv.classes.configflow.ts +21 -6
  196. package/ts/integrations/samsungtv/samsungtv.classes.integration.ts +53 -38
  197. package/ts/integrations/samsungtv/samsungtv.types.ts +4 -1
  198. package/ts/integrations/shelly/readme.md +22 -7
  199. package/ts/integrations/shelly/shelly.classes.client.ts +386 -269
  200. package/ts/integrations/shelly/shelly.classes.configflow.ts +43 -38
  201. package/ts/integrations/shelly/shelly.classes.integration.ts +92 -111
  202. package/ts/integrations/shelly/shelly.classes.subscription.ts +24 -1
  203. package/ts/integrations/shelly/shelly.mapper.ts +19 -20
  204. package/ts/integrations/shelly/shelly.rpc.ts +2 -0
  205. package/ts/integrations/shelly/shelly.types.ts +50 -2
  206. package/ts/integrations/shelly/shelly.validation.ts +76 -0
  207. package/ts/integrations/sonos/readme.md +117 -38
  208. package/ts/integrations/sonos/sonos.classes.client.ts +392 -123
  209. package/ts/integrations/sonos/sonos.classes.configflow.ts +25 -28
  210. package/ts/integrations/sonos/sonos.classes.integration.ts +211 -186
  211. package/ts/integrations/sonos/sonos.config.ts +24 -2
  212. package/ts/integrations/sonos/sonos.mapper.ts +13 -8
  213. package/ts/integrations/sonos/sonos.matchers.ts +2 -2
  214. package/ts/integrations/sonos/sonos.types.ts +30 -1
  215. package/ts/integrations/tasmota/readme.md +16 -5
  216. package/ts/integrations/tasmota/tasmota.classes.integration.ts +24 -6
  217. package/ts/integrations/tasmota/tasmota.classes.runtime.ts +112 -43
  218. package/ts/integrations/tasmota/tasmota.config.ts +17 -0
  219. package/ts/integrations/tasmota/tasmota.credentials.ts +92 -0
  220. package/ts/integrations/tasmota/tasmota.discovery.ts +87 -24
  221. package/ts/integrations/tasmota/tasmota.mapper.ts +6 -6
  222. package/ts/integrations/tasmota/tasmota.types.ts +43 -2
  223. package/ts/integrations/tuya/readme.md +52 -10
  224. package/ts/integrations/tuya/tuya.classes.integration.ts +69 -98
  225. package/ts/integrations/tuya/tuya.classes.runtime.ts +70 -24
  226. package/ts/integrations/tuya/tuya.mapper.ts +1 -1
  227. package/ts/integrations/tuya/tuya.types.ts +4 -0
  228. package/ts/integrations/tuya/tuya.validation.ts +5 -8
  229. package/ts/integrations/webostv/readme.md +115 -22
  230. package/ts/integrations/webostv/webostv.classes.client.ts +123 -50
  231. package/ts/integrations/webostv/webostv.classes.configflow.ts +35 -15
  232. package/ts/integrations/webostv/webostv.classes.integration.ts +44 -37
  233. package/ts/integrations/webostv/webostv.mapper.ts +4 -4
  234. package/ts/integrations/webostv/webostv.types.ts +18 -3
  235. package/ts/integrations/webostv/webostv.validation.ts +22 -1
  236. package/ts/integrations/zigbee2mqtt/readme.md +22 -27
  237. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.ts +16 -7
  238. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.ts +53 -6
  239. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.ts +62 -49
  240. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.ts +17 -11
  241. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.types.ts +35 -2
  242. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.ts +53 -7
  243. package/ts/integrations/zwave_js/readme.md +42 -9
  244. package/ts/integrations/zwave_js/zwave_js.classes.client.ts +155 -31
  245. package/ts/integrations/zwave_js/zwave_js.classes.configflow.ts +55 -31
  246. package/ts/integrations/zwave_js/zwave_js.classes.integration.ts +57 -35
  247. package/ts/integrations/zwave_js/zwave_js.discovery.ts +2 -2
  248. package/ts/integrations/zwave_js/zwave_js.mapper.ts +37 -23
  249. package/ts/integrations/zwave_js/zwave_js.types.ts +26 -3
  250. package/ts/integrations/zwave_js/zwave_js.validation.ts +52 -18
  251. package/ts/plugins.ts +2 -1
@@ -1,40 +1,35 @@
1
- # Zigbee2MQTT
1
+ # Native Zigbee2MQTT
2
2
 
3
- `Zigbee2MqttIntegration` connects directly to a configured MQTT broker and Zigbee2MQTT base topic. It reuses the package's native `MqttClient`; it does not require Home Assistant or connect to the Zigbee coordinator itself.
3
+ The adapter connects to an explicitly selected MQTT broker and Zigbee2MQTT base topic through the existing native MQTT.js transport. SHX home selection is separate from the Zigbee coordinator, network, IEEE address and friendly name. Broker TLS authenticates the selected broker name; Zigbee identity/inventory observations do not prove a unique or cryptographically authenticated physical controller.
4
4
 
5
5
  ```typescript
6
- const integration = new Zigbee2MqttIntegration();
6
+ const integration = new Zigbee2MqttIntegration({
7
+ homeId: hostSelectedHomeId,
8
+ controls: [{ ieeeAddress: selectedLowercaseIeeeAddress, property: 'state' }],
9
+ resolveCredentials: async (request) => {
10
+ // The host resolver owns storage. It validates the admitted home and exact
11
+ // request.host/port/protocol/baseTopic before returning runtime-only material.
12
+ return retrieveSelectedBrokerCredentials(request);
13
+ },
14
+ });
7
15
  const runtime = await integration.setup({
8
- host: 'broker.example.internal',
9
- protocol: 'mqtts',
10
- port: 8883,
16
+ homeId: hostSelectedHomeId,
17
+ host: 'broker.internal', protocol: 'mqtts', port: 8883,
11
18
  baseTopic: 'home/zigbee2mqtt',
12
- username: brokerUsername,
13
- password: brokerPassword,
14
- }, { abortSignal: lifetimeController.signal });
15
-
16
- const [device] = await runtime.devices();
17
- const power = device?.features.find((feature) => feature.id === 'state');
18
- if (device?.online && power?.writable) {
19
- await runtime.writeFeature?.({ device, feature: power, value: true });
20
- }
21
- await runtime.destroy();
19
+ authentication: 'provider', credentialKey: 'broker-reference',
20
+ }, { homeId: hostSelectedHomeId, abortSignal: hostLifetime.signal });
22
21
  ```
23
22
 
24
- The broker host and base topic are required explicitly. There is no automatic broker selection, injected inventory or fixture configuration in production. `mqtts` validates the broker certificate with Node's trusted certificate authorities; disabling certificate verification is not a supported option. The runtime holds supplied credentials in memory and does not persist them. The application must store and retrieve integration configuration through the public `NoSqlConfigStore` backed by `@lossless.org/client/nosqldb`, using its home scope and external encryption-key provider.
23
+ Every setup and config-flow context requires an independently selected `homeId`; an optional constructor binding does not replace it. Config, form fields, discovery, store scope and network IDs cannot choose home. One instance binds once even if setup later fails. Canonical device IDs are `encodeURIComponent(homeId) + ':zigbee2mqtt.' + lowercaseIeeeAddress`; entity IDs append the exact exposed property. Close is terminal.
25
24
 
26
- Setup waits for received `BASE/bridge/devices` inventory and `BASE/bridge/state`. The inventory contains each device's actual `definition.exposes`; unsupported devices and coordinators cannot be operated. Device IDs use `zigbee2mqtt.0xIEEEADDRESS`, and feature IDs use the exact exposed property, including endpoint suffixes. Entity IDs combine those IDs. A friendly-name change keeps identity stable and discards state and availability learned through the former name.
25
+ `authentication` is required and explicitly selects `anonymous`, `provider` or `config-store`. Anonymous forbids a credential reference. Provider requires both an existing `credentialKey` and constructor-injected `resolveCredentials`; missing provider does not fall back to another source. Config-store requires a borrowed actual scoped `NoSqlConfigStore` and the reference. It reads an encrypted value shaped as `{ version: 1, host, port, protocol, baseTopic, credentials: { username?, password?, ca?, cert?, key?, servername? } }` and verifies exact selected broker/topic binding before use. Configuration and flow output contain references only; passwords, CA/client certificates and private keys are runtime-only strings. No filesystem credential paths are supported. Client cert/key must be supplied together, TLS material requires mqtts, and certificate/hostname verification remains enabled. Plain MQTT requires `allowUnencryptedLan: true`. No-store anonymous and provider paths are legitimate.
27
26
 
28
- Binary, numeric, enum and text exposes are mapped, including primitive features inside advertised light, switch, fan, cover, lock and climate containers. Access bit 1 grants reported state and bit 2 grants writes. Binary features use normalized boolean values, translated into the advertised `value_on` and `value_off`. Numeric writes respect advertised minimum, maximum and step, enum writes accept advertised values only, and text values are limited to 1024 characters. Composite/list values, colors, schedules, numeric presets outside the normal range and binary toggles are unsupported. Their count is included in device metadata. Raw ZCL read/write/command fields are never exposed as controls.
27
+ The resolver request includes captured home, reference, exact broker/topic, an AbortSignal and an absolute performance.now() setup deadline. Provider work is owned before invocation. Deadline/abort prevents late transport allocation; destruction joins the actual foreign promise even if it ignores cancellation. Providers and borrowed store/database/key-provider resources remain caller-owned. Cleanup retry never reruns a provider, setup, subscription or physical command.
29
28
 
30
- Device availability must be enabled in Zigbee2MQTT. An explicit received `{"state":"online"}` on a device's availability topic, current inventory, an online bridge and a ready MQTT connection are all required for control. Missing availability remains `unknown`; neither a sensor report nor a device announcement implies online status. Bridge shutdown and MQTT disconnect revoke admission and clear cached availability. Reconnect restores the owned subscription and requires freshly received inventory and bridge state before control resumes. Per-device state and availability reports received before the inventory are buffered within the configured limits and applied only to validated inventory members.
29
+ Primitive binary, numeric, enum and text controls are preserved, including advertised primitive children of light/switch/fan/cover/lock/climate. Constructor `controls` is an immutable finite exact lowercase IEEE/property allowlist. Default is no control permission. Expose write access alone does not grant owner permission; editable config cannot grant it. At dispatch, current canonical home/target/capability, expose type/access/range/step/enum, supported/non-disabled membership and authoritative availability must all agree. Complex colors, lists, schedules, arbitrary ZCL, groups, joining, pairing, binding, OTA, bridge configuration and /get are excluded.
31
30
 
32
- `zigbee2mqtt.set` accepts exactly one target, either `deviceId` or `entityId`, and exactly `data: { property, value }`. The entity target must correspond to that property. Feature writes check the target's domain and resolve its current canonical feature, so caller-provided writable flags do not grant capabilities. Every command publishes one JSON property to `BASE/0xIEEEADDRESS/set`, using QoS 0 and `retain: false`. Commands are not retried. Success means the MQTT transport accepted publication and returns `physicalOutcome: 'unconfirmed'`; only subsequent device reports change state. MQTT publication does not prove physical execution.
31
+ Each admitted write publishes exactly one property to BASE/IEEE/set with QoS0 and retain:false. Commands are never retried or optimistically applied. Success preserves `{ confirmation: 'mqtt-publish', physicalOutcome: 'unconfirmed' }`; publication callback loss reports uncertainty. Device state changes only from subsequent observations. Retained and pre-inventory action reports never replay physical events. Current-generation cache seeding and callbacks preserve fresh inventory/availability across reconnects; a stale historical snapshot cannot admit control.
33
32
 
34
- Subscriptions emit added/removed entities and devices, state changes and availability changes. Accepted live action reports are emitted once per report, including repeated identical actions; retained or pre-inventory action state does not replay physical events. Authoritative `device_leave` events remove a device immediately. Subscriber cancellation removes only that listener; a throwing listener is removed and produces an error for remaining subscribers. Runtime destruction and the setup context's lifetime abort cancel pending I/O, stop reconnect, remove callbacks and await socket cleanup.
33
+ The owner shares a default capacity of 16 across pending setup/flow work, live runtimes and retained cleanup failures. Existing device/payload/buffer/deadline/write/subscriber bounds remain. Async foreign subscriber work is additionally bounded at 256 pending callbacks and drained during closure. A failed subscriber is removed and other subscribers receive a sanitized error, preserving established isolation behavior. Integration `destroy()` drains flow/setup/runtime, actual providers, subscriber work, commands and transport; repeated/reentrant calls share a stable promise. `retryCleanup()` retries retained failed cleanup only. Runtime destruction closes its lease. Config-flow destruction is terminal for its scope.
35
34
 
36
- Connection, publication and inventory deadlines default to 7 seconds and are capped at 60 seconds. Inventory defaults to at most 2048 devices, 256 expose descriptors per device, 8 MiB per MQTT payload and 32 MiB total buffered MQTT data. Commands are limited to 64 in flight and listeners to 256. Unsupported bridge topics, including bridge configuration and logging, are never forwarded as entity attributes or errors.
37
-
38
- Groups, pairing, joining, binding, OTA, bridge configuration, network management, `/get` polling and Home Assistant discovery are outside this adapter. Tests use the real MQTT.js TCP parser against a local test broker; no hardware access is performed.
39
-
40
- Protocol references: [MQTT topics and messages](https://www.zigbee2mqtt.io/guide/usage/mqtt_topics_and_messages.html), [exposes and access masks](https://www.zigbee2mqtt.io/guide/usage/exposes.html), and [device availability](https://www.zigbee2mqtt.io/guide/configuration/device-availability.html).
35
+ Verification uses real local MQTT.js TCP/TLS fixtures, including private CA/mutual client authentication and wrong broker-name denial. No hardware/private production broker acceptance has been established.
@@ -1,9 +1,13 @@
1
1
  import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
2
2
  import type { IZigbee2MqttConfig } from './zigbee2mqtt.types.js';
3
- import { zigbee2MqttConfig } from './zigbee2mqtt.validation.js';
3
+ import { zigbee2MqttConfig, zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
4
+ import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
4
5
 
5
6
  export class Zigbee2MqttConfigFlow implements IConfigFlow<IZigbee2MqttConfig> {
6
- public async start(_candidateArg: IDiscoveryCandidate, _contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IZigbee2MqttConfig>> {
7
+ readonly #scope: NativeIntegrationScope;
8
+ constructor(ownerArg = new NativeIntegrationOwner('zigbee2mqtt')) { this.#scope = ownerArg.createFlowScope(); }
9
+ public async start(_candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IZigbee2MqttConfig>> {
10
+ const context = this.#scope.admit(contextArg);
7
11
  return {
8
12
  kind: 'form', title: 'Connect Zigbee2MQTT',
9
13
  description: 'Enter the broker and exact Zigbee2MQTT base topic. Enable device availability in Zigbee2MQTT before operating devices.',
@@ -12,15 +16,20 @@ export class Zigbee2MqttConfigFlow implements IConfigFlow<IZigbee2MqttConfig> {
12
16
  { name: 'baseTopic', label: 'Zigbee2MQTT base topic', type: 'text', required: true },
13
17
  { name: 'port', label: 'Broker port', type: 'number' },
14
18
  { name: 'protocol', label: 'Protocol', type: 'select', options: [{ label: 'MQTT', value: 'mqtt' }, { label: 'MQTTS', value: 'mqtts' }] },
15
- { name: 'username', label: 'Username', type: 'text' },
16
- { name: 'password', label: 'Password', type: 'password' },
19
+ { name: 'authentication', label: 'Credential source', type: 'select', required: true, options: [{ label: 'Anonymous', value: 'anonymous' }, { label: 'Caller provider', value: 'provider' }, { label: 'Encrypted config store', value: 'config-store' }] },
20
+ { name: 'credentialKey', label: 'Existing credential reference', type: 'text' },
21
+ { name: 'allowUnencryptedLan', label: 'Allow plaintext LAN MQTT', type: 'boolean' },
17
22
  ],
18
- submit: async (valuesArg) => {
23
+ submit: (valuesArg) => this.#scope.run(context, undefined, async () => {
19
24
  try {
20
- const config = zigbee2MqttConfig(valuesArg as unknown as IZigbee2MqttConfig);
25
+ const values = zigbee2MqttRecord(valuesArg);
26
+ if (!values || values.homeId !== undefined && values.homeId !== context.homeId) throw new Error('Conflicting home or invalid form data.');
27
+ const config = zigbee2MqttConfig({ ...values, homeId: context.homeId } as unknown as IZigbee2MqttConfig);
21
28
  return { kind: 'done', title: 'Zigbee2MQTT configured', config };
22
29
  } catch { return { kind: 'error', error: 'Enter a valid broker, base topic and supported explicit configuration fields.' }; }
23
- },
30
+ }),
24
31
  };
25
32
  }
33
+ public destroy(): Promise<void> { return this.#scope.close(); }
34
+ public retryCleanup(): Promise<void> { return this.#scope.retryCleanup(); }
26
35
  }
@@ -1,23 +1,70 @@
1
1
  import { BaseIntegration } from '../../core/classes.baseintegration.js';
2
2
  import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
3
+ import { NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
3
4
  import type { IIntegrationRuntime, IIntegrationSetupContext } from '../../core/types.js';
4
5
  import { Zigbee2MqttConfigFlow } from './zigbee2mqtt.classes.configflow.js';
5
6
  import { Zigbee2MqttRuntime } from './zigbee2mqtt.classes.runtime.js';
6
- import type { IZigbee2MqttConfig } from './zigbee2mqtt.types.js';
7
+ import { MqttSession } from '../mqtt/mqtt.classes.session.js';
8
+ import type { TMqttCredentialResolver } from '../mqtt/mqtt.types.js';
9
+ import type { IZigbee2MqttConfig, IZigbee2MqttIntegrationOptions } from './zigbee2mqtt.types.js';
10
+ import { zigbee2MqttConfig, zigbee2MqttCredentials, zigbee2MqttOptions, zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
7
11
 
8
12
  export class Zigbee2MqttIntegration extends BaseIntegration<IZigbee2MqttConfig> {
9
13
  public readonly domain = 'zigbee2mqtt';
10
14
  public readonly displayName = 'Zigbee2MQTT';
11
15
  public readonly status = 'control-runtime' as const;
12
16
  public readonly discoveryDescriptor = new DiscoveryDescriptor({ integrationDomain: this.domain, displayName: this.displayName });
13
- public readonly configFlow = new Zigbee2MqttConfigFlow();
17
+ public readonly configFlow: Zigbee2MqttConfigFlow;
18
+ readonly #owner: NativeIntegrationOwner;
19
+ readonly #options: IZigbee2MqttIntegrationOptions;
20
+ readonly #controls: ReadonlySet<string>;
14
21
  public readonly metadata = { source: 'zigbee2mqtt', upstreamPath: 'Koenkk/zigbee2mqtt', iotClass: 'local_push' };
15
22
 
23
+ constructor(optionsArg: IZigbee2MqttIntegrationOptions = {}) {
24
+ super();
25
+ const { options, controls } = zigbee2MqttOptions(optionsArg);
26
+ this.#options = options;
27
+ this.#controls = controls;
28
+ this.#owner = new NativeIntegrationOwner(this.domain, { homeId: options.homeId, capacity: options.capacity });
29
+ this.configFlow = new Zigbee2MqttConfigFlow(this.#owner);
30
+ }
31
+
16
32
  public async setup(configArg: IZigbee2MqttConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
17
- const runtime = new Zigbee2MqttRuntime(configArg);
18
- await runtime.start(contextArg.abortSignal);
19
- return runtime;
33
+ const config = zigbee2MqttConfig(configArg);
34
+ return this.#owner.runSetup(contextArg, config.homeId, async (lease) => {
35
+ let resolver: TMqttCredentialResolver | undefined;
36
+ if (config.authentication === 'provider') {
37
+ const provider = this.#options.resolveCredentials;
38
+ if (!provider) throw new Error('Zigbee2MQTT provider mode requires an explicit caller resolver.');
39
+ resolver = async (request) => zigbee2MqttCredentials(config, await provider(Object.freeze({ ...request, host: config.host, port: config.port!, protocol: config.protocol!, baseTopic: config.baseTopic })));
40
+ } else if (config.authentication === 'config-store') {
41
+ const store = lease.context.configStore;
42
+ if (!store) throw new Error('Zigbee2MQTT config-store mode requires an admitted encrypted NoSqlConfigStore.');
43
+ resolver = async (request) => {
44
+ const value = zigbee2MqttRecord(await store.get<unknown>(request.credentialKey));
45
+ if (!value || Object.keys(value).some((key) => !['version', 'host', 'port', 'protocol', 'baseTopic', 'credentials'].includes(key)) ||
46
+ value.version !== 1 || value.host !== config.host || value.port !== config.port || value.protocol !== config.protocol || value.baseTopic !== config.baseTopic) {
47
+ throw new Error('Zigbee2MQTT encrypted credentials do not match the selected broker and base topic.');
48
+ }
49
+ return zigbee2MqttCredentials(config, value.credentials);
50
+ };
51
+ }
52
+ const session = new MqttSession({ ...config, discovery: false, birthMessage: false, willMessage: false, clean: true,
53
+ subscriptions: [config.baseTopic + '/#'], maxMessageTopics: (config.maxDevices ?? 2048) * 4 + 16 }, lease, resolver);
54
+ let runtime: Zigbee2MqttRuntime | undefined;
55
+ let attempts = 0;
56
+ const closeSession = (): Promise<void> => attempts++ === 0 ? session.close() : session.retryCleanup();
57
+ lease.own(() => runtime ? runtime.cleanup() : closeSession());
58
+ const client = await session.start();
59
+ lease.assertOpen();
60
+ runtime = new Zigbee2MqttRuntime(config, client, lease, closeSession, this.#controls);
61
+ await runtime.start();
62
+ lease.assertOpen();
63
+ lease.promoteToRuntime();
64
+ return runtime;
65
+ });
20
66
  }
21
67
 
22
- public async destroy(): Promise<void> {}
68
+ public destroy(): Promise<void> { return this.#owner.close(); }
69
+ public retryCleanup(): Promise<void> { return this.#owner.retryCleanup(); }
23
70
  }
@@ -1,10 +1,11 @@
1
1
  import type * as plugins from '../../plugins.js';
2
2
  import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
3
- import { MqttClient } from '../mqtt/mqtt.classes.client.js';
3
+ import type { MqttClient } from '../mqtt/mqtt.classes.client.js';
4
4
  import type { IMqttMessage } from '../mqtt/mqtt.types.js';
5
5
  import { Zigbee2MqttMapper } from './zigbee2mqtt.mapper.js';
6
6
  import type { IZigbee2MqttConfig, IZigbee2MqttDevice, IZigbee2MqttReportedState, IZigbee2MqttSnapshot } from './zigbee2mqtt.types.js';
7
- import { zigbee2MqttConfig, zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
7
+ import { zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
8
+ import type { INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
8
9
 
9
10
  interface IZigbee2MqttListener {
10
11
  handler: (eventArg: IIntegrationEvent) => void;
@@ -14,7 +15,7 @@ interface IZigbee2MqttListener {
14
15
  export class Zigbee2MqttRuntime implements IIntegrationRuntime {
15
16
  public readonly domain = 'zigbee2mqtt';
16
17
  private readonly config: IZigbee2MqttConfig;
17
- private readonly client: MqttClient;
18
+ readonly #controls: ReadonlySet<string>;
18
19
  private readonly removeMessageHandler: () => void;
19
20
  private readonly removeConnectionHandler: () => void;
20
21
  private readonly handlers = new Set<IZigbee2MqttListener>();
@@ -34,17 +35,18 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
34
35
  private inventoryCurrent = false;
35
36
  private inventoryError?: Error;
36
37
  private destroyed = false;
37
- private destroyPromise?: Promise<void>;
38
38
  private activeWrites = 0;
39
- private removeSetupAbort?: () => void;
39
+ private readonly pendingHandlers = new Set<Promise<void>>();
40
+ private readonly callbackFailures = new Set<Error>();
41
+ private readonly ownerAbort = (): void => { this.destroyed = true; };
40
42
 
41
- constructor(configArg: IZigbee2MqttConfig) {
42
- this.config = zigbee2MqttConfig(configArg);
43
- this.client = new MqttClient({
44
- ...this.config, discovery: false, birthMessage: false, willMessage: false, clean: true,
45
- subscriptions: [this.config.baseTopic + '/#'],
46
- maxMessageTopics: (this.config.maxDevices ?? 2048) * 4 + 16,
47
- });
43
+ constructor(configArg: IZigbee2MqttConfig, private readonly client: MqttClient,
44
+ private readonly lease: INativeSetupLease, private readonly closeSession: () => Promise<void>,
45
+ controlsArg: ReadonlySet<string>) {
46
+ this.config = configArg;
47
+ this.#controls = new Set(controlsArg);
48
+ this.connected = client.connected;
49
+ lease.signal.addEventListener('abort', this.ownerAbort, { once: true });
48
50
  this.removeMessageHandler = this.client.onMessage((messageArg) => this.receive(messageArg));
49
51
  this.removeConnectionHandler = this.client.onConnectionState((stateArg) => {
50
52
  const previous = this.snapshot();
@@ -64,24 +66,16 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
64
66
  });
65
67
  }
66
68
 
67
- public async start(abortSignalArg?: AbortSignal): Promise<void> {
68
- this.assertOpen(abortSignalArg);
69
+ public async start(): Promise<void> {
70
+ this.assertOpen();
69
71
  if (this.started) throw new Error('Zigbee2MQTT runtime has already started.');
70
72
  this.started = true;
71
- if (abortSignalArg) {
72
- const abort = (): void => { void this.destroy().catch(() => this.emitError('Zigbee2MQTT cancellation cleanup failed.')); };
73
- abortSignalArg.addEventListener('abort', abort, { once: true });
74
- this.removeSetupAbort = () => abortSignalArg.removeEventListener('abort', abort);
75
- }
76
- try {
77
- await this.client.start(abortSignalArg);
78
- this.assertOpen(abortSignalArg);
79
- await this.waitForInventory(abortSignalArg);
80
- } catch (error) {
81
- try { await this.destroy(); }
82
- catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Zigbee2MQTT setup and cleanup failed.'); }
83
- throw error;
73
+ // Handlers are installed before this synchronous current-generation snapshot. No await
74
+ // can let older cached observations overwrite a later callback or a reconnect.
75
+ for (const message of this.client.getCurrentSnapshot()) {
76
+ if (message.topic !== this.config.baseTopic + '/bridge/event') this.receive(message, true);
84
77
  }
78
+ await this.waitForInventory(this.lease.signal);
85
79
  }
86
80
 
87
81
  public isHealthy(): boolean {
@@ -128,6 +122,9 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
128
122
  !device || device.integrationDomain !== this.domain || typeof device.id !== 'string' || !feature || typeof feature.id !== 'string') {
129
123
  throw new Error('Zigbee2MQTT feature request requires a device owned by this domain.');
130
124
  }
125
+ const selected = this.findDevice(device.id);
126
+ const current = selected?.exposes.find((expose) => expose.property === feature.id);
127
+ if (!current || feature.capability !== current.capability) throw new Error('Zigbee2MQTT feature capability does not match the current target.');
131
128
  return await this.write(device.id, feature.id, request.value, abortSignalArg);
132
129
  } catch (error) { return this.failure(error); }
133
130
  }
@@ -149,26 +146,23 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
149
146
  selected = this.findDevice(target.entityId.slice(0, -(data.property.length + 1)));
150
147
  }
151
148
  if (!selected) throw new Error('Zigbee2MQTT target is not in the current inventory.');
152
- return await this.write(Zigbee2MqttMapper.deviceId(selected.ieeeAddress), data.property, data.value);
149
+ return await this.write(Zigbee2MqttMapper.deviceId(this.config.homeId, selected.ieeeAddress), data.property, data.value);
153
150
  } catch (error) { return this.failure(error); }
154
151
  }
155
152
 
156
- public destroy(): Promise<void> {
157
- if (!this.destroyPromise) this.destroyPromise = this.destroyConnection();
158
- return this.destroyPromise;
159
- }
153
+ public destroy(): Promise<void> { return this.lease.close(); }
160
154
 
161
155
  private async write(deviceIdArg: string, propertyArg: string, valueArg: unknown, abortSignalArg?: AbortSignal): Promise<IServiceCallResult> {
162
156
  this.assertOpen(abortSignalArg);
163
157
  const device = this.findDevice(deviceIdArg);
164
158
  const expose = device?.exposes.find((exposeArg) => exposeArg.property === propertyArg);
165
- if (!device || !expose || !Zigbee2MqttMapper.available(this.snapshot([device]), device)) throw new Error('Zigbee2MQTT target is unavailable or not exposed.');
159
+ if (!device || !expose || !Zigbee2MqttMapper.writable(this.snapshot([device]), device, expose) || !Zigbee2MqttMapper.available(this.snapshot([device]), device)) throw new Error('Zigbee2MQTT target is unavailable or not exposed.');
166
160
  const value = Zigbee2MqttMapper.commandValue(expose, valueArg);
167
161
  if (value === undefined) throw new Error('Zigbee2MQTT value is not permitted by the advertised expose.');
168
162
  if (this.activeWrites >= 64) throw new Error('Zigbee2MQTT command admission limit exceeded.');
169
163
  this.activeWrites++;
170
164
  try {
171
- const publication = this.client.publish({ topic: `${this.config.baseTopic}/${device.ieeeAddress}/set`, payload: { [expose.property]: value }, qos: 0, retain: false }, abortSignalArg);
165
+ const publication = this.client.publish({ topic: `${this.config.baseTopic}/${device.ieeeAddress}/set`, payload: { [expose.property]: value }, qos: 0, retain: false }, abortSignalArg ? AbortSignal.any([this.lease.signal, abortSignalArg]) : this.lease.signal);
172
166
  this.pendingWrites.add(publication);
173
167
  try { await publication; }
174
168
  catch { throw new Error('Zigbee2MQTT command publication failed; its physical outcome is unconfirmed.'); }
@@ -297,8 +291,8 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
297
291
  if (value === undefined) { states.delete(expose.property); this.emitError('Zigbee2MQTT reported a value outside its expose type.'); continue; }
298
292
  states.set(expose.property, { value, updatedAt: new Date(timestampArg).toISOString() });
299
293
  if (liveEventArg && (expose.property === 'action' || expose.property.startsWith('action_'))) {
300
- this.emit({ type: 'state_changed', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(deviceArg.ieeeAddress),
301
- entityId: Zigbee2MqttMapper.entityId(deviceArg.ieeeAddress, expose.property), data: { value }, timestamp: timestampArg });
294
+ this.emit({ type: 'state_changed', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(this.config.homeId, deviceArg.ieeeAddress),
295
+ entityId: Zigbee2MqttMapper.entityId(this.config.homeId, deviceArg.ieeeAddress, expose.property), data: { value }, timestamp: timestampArg });
302
296
  }
303
297
  }
304
298
  }
@@ -307,10 +301,10 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
307
301
  const next = this.snapshot(scopedDevicesArg);
308
302
  const previousDevices = new Map(previousArg.devices.map((deviceArg) => [deviceArg.ieeeAddress, deviceArg]));
309
303
  for (const device of next.devices) {
310
- if (!previousDevices.has(device.ieeeAddress)) this.emit({ type: 'device_added', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(device.ieeeAddress), timestamp: Date.now() });
304
+ if (!previousDevices.has(device.ieeeAddress)) this.emit({ type: 'device_added', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(this.config.homeId, device.ieeeAddress), timestamp: Date.now() });
311
305
  }
312
306
  for (const device of previousArg.devices) {
313
- if (!this.devicesByIeee.has(device.ieeeAddress)) this.emit({ type: 'device_removed', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(device.ieeeAddress), timestamp: Date.now() });
307
+ if (!this.devicesByIeee.has(device.ieeeAddress)) this.emit({ type: 'device_removed', integrationDomain: this.domain, deviceId: Zigbee2MqttMapper.deviceId(this.config.homeId, device.ieeeAddress), timestamp: Date.now() });
314
308
  }
315
309
  const previous = new Map(Zigbee2MqttMapper.toEntities(previousArg).map((entityArg) => [entityArg.id, entityArg]));
316
310
  const current = new Map(Zigbee2MqttMapper.toEntities(next).map((entityArg) => [entityArg.id, entityArg]));
@@ -331,7 +325,7 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
331
325
  private snapshot(scopedDevicesArg?: IZigbee2MqttDevice[]): IZigbee2MqttSnapshot {
332
326
  const devices = scopedDevicesArg ?? [...this.devicesByIeee.values()];
333
327
  return {
334
- devices, connected: this.connected && !this.destroyed,
328
+ homeId: this.config.homeId, controls: this.#controls, devices, connected: this.connected && !this.destroyed,
335
329
  bridgeOnline: this.bridgeOnline, inventoryCurrent: this.inventoryCurrent,
336
330
  availability: new Map(devices.filter((deviceArg) => this.availability.has(deviceArg.ieeeAddress)).map((deviceArg) =>
337
331
  [deviceArg.ieeeAddress, this.availability.get(deviceArg.ieeeAddress) === true])),
@@ -340,7 +334,8 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
340
334
  }
341
335
 
342
336
  private findDevice(idArg: string): IZigbee2MqttDevice | undefined {
343
- return idArg.startsWith('zigbee2mqtt.') ? this.devicesByIeee.get(idArg.slice('zigbee2mqtt.'.length)) : undefined;
337
+ const prefix = `${encodeURIComponent(this.config.homeId)}:zigbee2mqtt.`;
338
+ return idArg.startsWith(prefix) ? this.devicesByIeee.get(idArg.slice(prefix.length)) : undefined;
344
339
  }
345
340
 
346
341
  private deviceForTopic(topicArg: string): IZigbee2MqttDevice | undefined {
@@ -370,37 +365,55 @@ export class Zigbee2MqttRuntime implements IIntegrationRuntime {
370
365
  if (this.inventoryReceived && this.bridgeReceived && this.client.connected) for (const waiter of [...this.readyWaiters]) waiter.resolve();
371
366
  }
372
367
 
373
- private async destroyConnection(): Promise<void> {
368
+ public async cleanup(): Promise<void> {
374
369
  this.destroyed = true;
375
- this.removeSetupAbort?.();
370
+ this.lease.signal.removeEventListener('abort', this.ownerAbort);
376
371
  this.removeMessageHandler();
377
372
  this.removeConnectionHandler();
378
373
  for (const waiter of [...this.readyWaiters]) waiter.reject(new Error('Zigbee2MQTT runtime closed.'));
379
- for (const cleanup of [...this.subscriptionCleanups]) await cleanup();
374
+ const cleanups = [...this.subscriptionCleanups];
380
375
  this.handlers.clear();
381
- await this.client.destroy();
382
- await Promise.allSettled([...this.pendingWrites]);
376
+ const results = await Promise.allSettled([
377
+ ...cleanups.map((cleanup) => Promise.resolve().then(cleanup)),
378
+ Promise.resolve().then(this.closeSession),
379
+ ...[...this.pendingWrites].map((write) => write.then(() => undefined, () => undefined)),
380
+ ...this.pendingHandlers,
381
+ ]);
383
382
  this.devicesByIeee.clear();
384
383
  this.devicesByName.clear();
385
384
  this.availability.clear();
386
385
  this.state.clear();
387
386
  this.pendingMessages.clear();
387
+ const failures = [...this.callbackFailures, ...results.flatMap((result) => result.status === 'rejected' ? [result.reason as unknown] : [])];
388
+ this.callbackFailures.clear();
389
+ if (failures.length) throw new AggregateError(failures, 'Zigbee2MQTT runtime cleanup failed; retry cleanup explicitly.');
388
390
  }
389
391
 
390
392
  private assertOpen(abortSignalArg?: AbortSignal): void {
391
393
  if (abortSignalArg?.aborted) throw new DOMException('Zigbee2MQTT operation aborted.', 'AbortError');
392
394
  if (this.destroyed) throw new Error('Zigbee2MQTT runtime closed.');
395
+ this.lease.assertOpen();
393
396
  }
394
397
 
395
398
  private emit(eventArg: IIntegrationEvent): void {
396
399
  for (const listener of [...this.handlers]) {
397
400
  if (!this.handlers.has(listener)) continue;
398
- try { listener.handler(structuredClone(eventArg)); }
399
- catch {
400
- // A consumer callback must not disconnect the transport shared by other subscribers.
401
+ if (this.pendingHandlers.size >= 256) {
401
402
  void listener.cleanup();
402
- this.emitError('Zigbee2MQTT subscriber callback failed.');
403
+ this.emitError('Zigbee2MQTT subscriber work limit exceeded.');
404
+ continue;
403
405
  }
406
+ const work = Promise.resolve().then(async () => {
407
+ if (!this.handlers.has(listener) || this.destroyed) return;
408
+ try { await listener.handler(structuredClone(eventArg)); }
409
+ catch {
410
+ await listener.cleanup();
411
+ if (this.destroyed) this.callbackFailures.add(new Error('Zigbee2MQTT subscriber callback failed while draining.'));
412
+ else this.emitError('Zigbee2MQTT subscriber callback failed.');
413
+ }
414
+ });
415
+ this.pendingHandlers.add(work);
416
+ void work.then(() => this.pendingHandlers.delete(work), () => this.pendingHandlers.delete(work));
404
417
  }
405
418
  }
406
419
 
@@ -1,18 +1,18 @@
1
1
  import type * as plugins from '../../plugins.js';
2
2
  import type { IIntegrationEntity } from '../../core/types.js';
3
3
  import type { IZigbee2MqttDevice, IZigbee2MqttExpose, IZigbee2MqttSnapshot, TZigbee2MqttPrimitive } from './zigbee2mqtt.types.js';
4
- import { zigbee2MqttPrimitive, zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
4
+ import { zigbee2MqttControlKey, zigbee2MqttPrimitive, zigbee2MqttRecord } from './zigbee2mqtt.validation.js';
5
5
 
6
6
  const specificTypes = new Set(['light', 'switch', 'fan', 'cover', 'lock', 'climate']);
7
7
  const forbiddenProperties = new Set(['__proto__', 'constructor', 'prototype', 'read', 'write', 'command', 'commands', 'get', 'set']);
8
8
 
9
9
  export class Zigbee2MqttMapper {
10
- public static deviceId(ieeeAddressArg: string): string {
11
- return `zigbee2mqtt.${ieeeAddressArg}`;
10
+ public static deviceId(homeIdArg: string, ieeeAddressArg: string): string {
11
+ return `${encodeURIComponent(homeIdArg)}:zigbee2mqtt.${ieeeAddressArg}`;
12
12
  }
13
13
 
14
- public static entityId(ieeeAddressArg: string, propertyArg: string): string {
15
- return `${this.deviceId(ieeeAddressArg)}.${propertyArg}`;
14
+ public static entityId(homeIdArg: string, ieeeAddressArg: string, propertyArg: string): string {
15
+ return `${this.deviceId(homeIdArg, ieeeAddressArg)}.${propertyArg}`;
16
16
  }
17
17
 
18
18
  public static parseInventory(payloadArg: unknown, maxDevicesArg: number): IZigbee2MqttDevice[] {
@@ -54,12 +54,12 @@ export class Zigbee2MqttMapper {
54
54
 
55
55
  public static toDevices(snapshotArg: IZigbee2MqttSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
56
56
  return snapshotArg.devices.map((deviceArg) => ({
57
- id: this.deviceId(deviceArg.ieeeAddress), integrationDomain: 'zigbee2mqtt', name: deviceArg.friendlyName,
57
+ id: this.deviceId(snapshotArg.homeId, deviceArg.ieeeAddress), integrationDomain: 'zigbee2mqtt', name: deviceArg.friendlyName,
58
58
  manufacturer: deviceArg.manufacturer, model: deviceArg.model, protocol: 'zigbee',
59
59
  online: this.available(snapshotArg, deviceArg),
60
60
  features: deviceArg.exposes.map((exposeArg) => ({
61
61
  id: exposeArg.property, name: exposeArg.name, capability: exposeArg.capability,
62
- readable: exposeArg.readable, writable: exposeArg.writable && deviceArg.supported && !deviceArg.disabled,
62
+ readable: exposeArg.readable, writable: this.writable(snapshotArg, deviceArg, exposeArg),
63
63
  unit: exposeArg.unit,
64
64
  })),
65
65
  state: deviceArg.exposes.filter((exposeArg) => exposeArg.readable).flatMap((exposeArg) => {
@@ -67,6 +67,7 @@ export class Zigbee2MqttMapper {
67
67
  return state ? [{ featureId: exposeArg.property, value: state.value, updatedAt: state.updatedAt }] : [];
68
68
  }),
69
69
  metadata: {
70
+ homeId: snapshotArg.homeId,
70
71
  ieeeAddress: deviceArg.ieeeAddress, supported: deviceArg.supported, disabled: deviceArg.disabled,
71
72
  unsupportedExposes: deviceArg.unsupportedExposes,
72
73
  availability: snapshotArg.availability.has(deviceArg.ieeeAddress) ? snapshotArg.availability.get(deviceArg.ieeeAddress) ? 'online' : 'offline' : 'unknown',
@@ -76,18 +77,23 @@ export class Zigbee2MqttMapper {
76
77
 
77
78
  public static toEntities(snapshotArg: IZigbee2MqttSnapshot): IIntegrationEntity[] {
78
79
  return snapshotArg.devices.flatMap((deviceArg) => deviceArg.exposes.map((exposeArg) => ({
79
- id: this.entityId(deviceArg.ieeeAddress, exposeArg.property),
80
- uniqueId: this.entityId(deviceArg.ieeeAddress, exposeArg.property),
81
- integrationDomain: 'zigbee2mqtt', deviceId: this.deviceId(deviceArg.ieeeAddress),
80
+ id: this.entityId(snapshotArg.homeId, deviceArg.ieeeAddress, exposeArg.property),
81
+ uniqueId: this.entityId(snapshotArg.homeId, deviceArg.ieeeAddress, exposeArg.property),
82
+ integrationDomain: 'zigbee2mqtt', deviceId: this.deviceId(snapshotArg.homeId, deviceArg.ieeeAddress),
82
83
  name: `${deviceArg.friendlyName} ${exposeArg.name}`, platform: exposeArg.platform,
83
84
  available: this.available(snapshotArg, deviceArg),
84
85
  state: exposeArg.readable ? snapshotArg.state.get(deviceArg.ieeeAddress)?.get(exposeArg.property)?.value ?? null : null,
85
- attributes: { property: exposeArg.property, readable: exposeArg.readable, writable: exposeArg.writable && deviceArg.supported && !deviceArg.disabled,
86
+ attributes: { homeId: snapshotArg.homeId, property: exposeArg.property, readable: exposeArg.readable, writable: this.writable(snapshotArg, deviceArg, exposeArg),
86
87
  unit: exposeArg.unit, minimum: exposeArg.minimum, maximum: exposeArg.maximum, step: exposeArg.step,
87
88
  values: exposeArg.values ? [...exposeArg.values] : undefined },
88
89
  })));
89
90
  }
90
91
 
92
+ public static writable(snapshotArg: IZigbee2MqttSnapshot, deviceArg: IZigbee2MqttDevice, exposeArg: IZigbee2MqttExpose): boolean {
93
+ return snapshotArg.inventoryCurrent && exposeArg.writable && deviceArg.supported && !deviceArg.disabled &&
94
+ snapshotArg.controls.has(zigbee2MqttControlKey(deviceArg.ieeeAddress, exposeArg.property));
95
+ }
96
+
91
97
  public static available(snapshotArg: IZigbee2MqttSnapshot, deviceArg: IZigbee2MqttDevice): boolean {
92
98
  return snapshotArg.connected && snapshotArg.bridgeOnline && snapshotArg.inventoryCurrent &&
93
99
  deviceArg.supported && !deviceArg.disabled && snapshotArg.availability.get(deviceArg.ieeeAddress) === true;
@@ -1,13 +1,17 @@
1
1
  import type { TEntityPlatform } from '../../core/types.js';
2
2
  import type * as plugins from '../../plugins.js';
3
+ import type { INativeIntegrationOwnerOptions } from '../../core/classes.nativeintegrationowner.js';
4
+ import type { IMqttCredentialRequest, IMqttTransportCredentials } from '../mqtt/mqtt.types.js';
3
5
 
4
6
  export interface IZigbee2MqttConfig {
7
+ homeId: string;
8
+ authentication: 'anonymous' | 'provider' | 'config-store';
9
+ credentialKey?: string;
10
+ allowUnencryptedLan?: boolean;
5
11
  host: string;
6
12
  baseTopic: string;
7
13
  port?: number;
8
14
  protocol?: 'mqtt' | 'mqtts';
9
- username?: string;
10
- password?: string;
11
15
  clientId?: string;
12
16
  connectTimeoutMs?: number;
13
17
  operationTimeoutMs?: number;
@@ -18,6 +22,33 @@ export interface IZigbee2MqttConfig {
18
22
  maxBufferedBytes?: number;
19
23
  }
20
24
 
25
+ /** Runtime-only owner permission; it is never learned from MQTT or editable config. */
26
+ export interface IZigbee2MqttControlPermission {
27
+ readonly ieeeAddress: string;
28
+ readonly property: string;
29
+ }
30
+
31
+ export interface IZigbee2MqttCredentialRequest extends IMqttCredentialRequest {
32
+ readonly host: string;
33
+ readonly port: number;
34
+ readonly protocol: 'mqtt' | 'mqtts';
35
+ readonly baseTopic: string;
36
+ }
37
+
38
+ export interface IZigbee2MqttStoredCredential {
39
+ version: 1;
40
+ host: string;
41
+ port: number;
42
+ protocol: 'mqtt' | 'mqtts';
43
+ baseTopic: string;
44
+ credentials: IMqttTransportCredentials;
45
+ }
46
+
47
+ export interface IZigbee2MqttIntegrationOptions extends INativeIntegrationOwnerOptions {
48
+ readonly controls?: readonly IZigbee2MqttControlPermission[];
49
+ readonly resolveCredentials?: (requestArg: IZigbee2MqttCredentialRequest) => Promise<IMqttTransportCredentials>;
50
+ }
51
+
21
52
  export type TZigbee2MqttPrimitive = string | number | boolean;
22
53
 
23
54
  export interface IZigbee2MqttExpose {
@@ -54,6 +85,8 @@ export interface IZigbee2MqttReportedState {
54
85
  }
55
86
 
56
87
  export interface IZigbee2MqttSnapshot {
88
+ homeId: string;
89
+ controls: ReadonlySet<string>;
57
90
  devices: IZigbee2MqttDevice[];
58
91
  connected: boolean;
59
92
  bridgeOnline: boolean;