@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,69 +1,83 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { BaseIntegration } from '../../core/classes.baseintegration.js';
3
+ import { NativeIntegrationOwner, type INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
3
4
  import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
4
5
  import { ZwaveJsClient } from './zwave_js.classes.client.js';
5
6
  import { ZwaveJsConfigFlow } from './zwave_js.classes.configflow.js';
6
7
  import { createZwaveJsDiscoveryDescriptor } from './zwave_js.discovery.js';
7
8
  import { ZwaveJsMapper } from './zwave_js.mapper.js';
8
- import type { IZwaveJsConfig, IZwaveJsServerCommand, IZwaveJsWriteResult } from './zwave_js.types.js';
9
+ import type { IZwaveJsConfig, IZwaveJsIntegrationOptions, IZwaveJsServerCommand, IZwaveJsWriteResult } from './zwave_js.types.js';
10
+ import { normalizeZwaveJsOptions, validateZwaveJsConfig } from './zwave_js.validation.js';
9
11
 
10
12
  export class ZwaveJsIntegration extends BaseIntegration<IZwaveJsConfig> {
11
13
  public readonly domain = 'zwave_js';
12
14
  public readonly displayName = 'Z-Wave';
13
15
  public readonly status = 'control-runtime' as const;
14
16
  public readonly discoveryDescriptor = createZwaveJsDiscoveryDescriptor();
15
- public readonly configFlow = new ZwaveJsConfigFlow();
17
+ public readonly configFlow: ZwaveJsConfigFlow;
16
18
  public readonly metadata = {
17
19
  source: 'zwave-js/zwave-js-server', upstreamPath: 'src/lib', upstreamDomain: 'zwave_js',
18
20
  integrationType: 'hub', iotClass: 'local_push', qualityScale: 'unknown',
19
21
  };
20
- #clients = new Set<ZwaveJsClient>();
21
- #destroyed = false;
22
+ readonly #owner: NativeIntegrationOwner;
23
+ readonly #options: Readonly<IZwaveJsIntegrationOptions>;
22
24
 
23
- public async setup(configArg: IZwaveJsConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
24
- if (this.#destroyed) throw new Error('Z-Wave JS integration is destroyed.');
25
- const client = new ZwaveJsClient(configArg, contextArg.abortSignal);
26
- this.#clients.add(client);
27
- try {
28
- await client.start();
29
- if (this.#destroyed) throw new Error('Z-Wave JS integration is destroyed.');
30
- return new ZwaveJsRuntime(client, () => { this.#clients.delete(client); });
31
- } catch (errorArg) {
32
- await client.destroy();
33
- this.#clients.delete(client);
34
- throw errorArg;
35
- }
25
+ constructor(optionsArg: IZwaveJsIntegrationOptions = {}) {
26
+ super();
27
+ this.#options = normalizeZwaveJsOptions(optionsArg);
28
+ this.#owner = new NativeIntegrationOwner('zwave_js', { homeId: this.#options.homeId, capacity: this.#options.capacity });
29
+ this.configFlow = new ZwaveJsConfigFlow(this.#options, this.#owner.createFlowScope());
36
30
  }
37
31
 
38
- public async destroy(): Promise<void> {
39
- this.#destroyed = true;
40
- await Promise.all([...this.#clients].map((clientArg) => clientArg.destroy()));
41
- this.#clients.clear();
32
+ public setup(configArg: IZwaveJsConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
33
+ try {
34
+ const config = validateZwaveJsConfig(configArg);
35
+ return this.#owner.runSetup(contextArg, config.homeId, async (lease) => {
36
+ const client = new ZwaveJsClient(config, { ...lease.context, abortSignal: lease.signal }, this.#options);
37
+ let runtime: ZwaveJsRuntime | undefined;
38
+ lease.own(async () => {
39
+ const results = await Promise.allSettled([Promise.resolve().then(() => client.retryCleanup()),
40
+ ...(runtime ? [Promise.resolve().then(() => runtime!.closeSubscriptions())] : [])]);
41
+ const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
42
+ if (failures.length) throw new AggregateError(failures.map((result) => result.reason as unknown), 'Z-Wave JS owned cleanup failed.');
43
+ });
44
+ await client.start();
45
+ lease.assertOpen();
46
+ runtime = new ZwaveJsRuntime(client, lease);
47
+ lease.promoteToRuntime();
48
+ return runtime;
49
+ });
50
+ } catch (errorArg) { return Promise.reject(errorArg); }
42
51
  }
52
+
53
+ public destroy(): Promise<void> { return this.#owner.close(); }
54
+ public retryCleanup(): Promise<void> { return this.#owner.retryCleanup(); }
43
55
  }
44
56
 
45
57
  export class HomeAssistantZwaveJsIntegration extends ZwaveJsIntegration {}
46
58
 
47
59
  class ZwaveJsRuntime implements IIntegrationRuntime {
48
60
  public readonly domain = 'zwave_js';
49
- #subscriptions = new Set<() => void>();
61
+ #subscriptions = new Set<() => Promise<void>>();
50
62
  #destroyPromise?: Promise<void>;
51
63
 
52
- constructor(private readonly client: ZwaveJsClient, private readonly released: () => void) {}
64
+ constructor(private readonly client: ZwaveJsClient, private readonly lease: INativeSetupLease) {}
53
65
 
54
66
  public isHealthy(): boolean { return this.client.isHealthy(); }
55
67
 
56
68
  public async devices(abortSignalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
69
+ this.lease.assertOpen();
57
70
  return ZwaveJsMapper.toDevices(await this.client.getSnapshot(abortSignalArg));
58
71
  }
59
72
 
60
- public async entities(): Promise<IIntegrationEntity[]> { return ZwaveJsMapper.toEntities(await this.client.getSnapshot()); }
73
+ public async entities(): Promise<IIntegrationEntity[]> { this.lease.assertOpen(); return ZwaveJsMapper.toEntities(await this.client.getSnapshot()); }
61
74
 
62
75
  public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void, abortSignalArg?: AbortSignal): Promise<() => Promise<void>> {
76
+ this.lease.assertOpen();
63
77
  if (abortSignalArg?.aborted) throw new Error('Z-Wave JS subscription was aborted.');
64
78
  const snapshot = await this.client.getSnapshot(abortSignalArg);
65
79
  const unsubscribe = this.client.onEvent((eventArg) => {
66
- handlerArg({
80
+ return handlerArg({
67
81
  type: eventArg.source === 'connection' || ['alive', 'dead', 'sleep', 'wake up'].includes(eventArg.event) ?
68
82
  'availability_changed' : 'state_changed',
69
83
  integrationDomain: this.domain,
@@ -71,36 +85,44 @@ class ZwaveJsRuntime implements IIntegrationRuntime {
71
85
  data: eventArg, timestamp: Date.now(),
72
86
  });
73
87
  });
74
- const release = () => {
75
- unsubscribe();
76
- abortSignalArg?.removeEventListener('abort', release);
88
+ const abort = () => { void release().catch(() => undefined); };
89
+ const release = async () => {
90
+ abortSignalArg?.removeEventListener('abort', abort);
91
+ await unsubscribe();
77
92
  this.#subscriptions.delete(release);
78
93
  };
79
94
  this.#subscriptions.add(release);
80
- abortSignalArg?.addEventListener('abort', release, { once: true });
81
- if (abortSignalArg?.aborted) release();
82
- return async () => { release(); };
95
+ abortSignalArg?.addEventListener('abort', abort, { once: true });
96
+ if (abortSignalArg?.aborted) await release();
97
+ this.lease.assertOpen();
98
+ return release;
83
99
  }
84
100
 
85
101
  public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
102
+ this.lease.assertOpen();
86
103
  const command = ZwaveJsMapper.commandForService(await this.client.getSnapshot(), requestArg);
87
104
  if (!command) return { success: false, error: 'Unsupported, missing or ambiguous Z-Wave JS control target.' };
88
105
  return this.execute(command);
89
106
  }
90
107
 
91
108
  public async writeFeature(requestArg: IDeviceFeatureWriteRequest, abortSignalArg?: AbortSignal): Promise<IServiceCallResult> {
109
+ this.lease.assertOpen();
92
110
  const command = ZwaveJsMapper.commandForFeature(await this.client.getSnapshot(abortSignalArg), requestArg);
93
111
  return this.execute(command, abortSignalArg);
94
112
  }
95
113
 
96
114
  public destroy(): Promise<void> {
97
- if (!this.#destroyPromise) {
98
- for (const release of this.#subscriptions) release();
99
- this.#destroyPromise = this.client.destroy().finally(() => this.released());
100
- }
115
+ if (!this.#destroyPromise) this.#destroyPromise = this.lease.close();
101
116
  return this.#destroyPromise;
102
117
  }
103
118
 
119
+ public async closeSubscriptions(): Promise<void> {
120
+ const results = await Promise.allSettled([...this.#subscriptions].map((release) => Promise.resolve().then(release)));
121
+ this.#subscriptions.clear(); // Callback work/failures remain owned by the client ledger.
122
+ const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
123
+ if (failures.length) throw new AggregateError(failures.map((result) => result.reason as unknown), 'Z-Wave JS subscription cleanup failed.');
124
+ }
125
+
104
126
  private async execute(commandArg: IZwaveJsServerCommand, abortSignalArg?: AbortSignal): Promise<IServiceCallResult> {
105
127
  const data: IZwaveJsWriteResult = await this.client.sendCommand(commandArg, abortSignalArg);
106
128
  return { success: data.outcome === 'confirmed', data,
@@ -1,7 +1,7 @@
1
1
  import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
2
2
  import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
3
3
  import type { IZwaveJsManualEntry, IZwaveJsMdnsRecord, IZwaveJsUsbRecord } from './zwave_js.types.js';
4
- import { validateZwaveJsConfig, zwaveJsRecord } from './zwave_js.validation.js';
4
+ import { validateZwaveJsEndpoint, zwaveJsRecord } from './zwave_js.validation.js';
5
5
 
6
6
  const knownUsbIds = new Set(['0658:0200', '10c4:8a2a', '303a:4001', '10c4:ea60']);
7
7
 
@@ -133,7 +133,7 @@ export const createZwaveJsDiscoveryDescriptor = (): DiscoveryDescriptor => {
133
133
  };
134
134
 
135
135
  const safeUrl = (urlArg: string): string | undefined => {
136
- try { return validateZwaveJsConfig({ url: urlArg, expectedHomeId: 1 }).url.replace(/\/$/, ''); }
136
+ try { return validateZwaveJsEndpoint(urlArg).replace(/\/$/, ''); }
137
137
  catch { return undefined; }
138
138
  };
139
139
 
@@ -1,7 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
3
3
  import type { IZwaveJsNode, IZwaveJsServerCommand, IZwaveJsSnapshot, IZwaveJsValue, IZwaveJsValueId, TZwaveJsControlValue } from './zwave_js.types.js';
4
- import { parseZwaveJsValueId, zwaveJsRecord, zwaveJsValueKey } from './zwave_js.validation.js';
4
+ import { captureZwaveJsData, parseZwaveJsValueId, zwaveJsRecord, zwaveJsValueKey } from './zwave_js.validation.js';
5
5
 
6
6
  interface IControlTarget { node: IZwaveJsNode; value: IZwaveJsValue }
7
7
 
@@ -9,14 +9,16 @@ export class ZwaveJsMapper {
9
9
  /** Pure mapping helper; runtime setup always obtains its state from the server. */
10
10
  public static toSnapshot(snapshotArg: Partial<IZwaveJsSnapshot>, connectedArg = false,
11
11
  eventsArg: IZwaveJsSnapshot['events'] = []): IZwaveJsSnapshot {
12
- return structuredClone({ ...snapshotArg, nodes: snapshotArg.nodes ?? [], events: eventsArg, connected: connectedArg });
12
+ if (typeof snapshotArg.homeId !== 'string' || !snapshotArg.homeId) throw new Error('Z-Wave JS snapshot requires its admitted SHX home.');
13
+ return structuredClone({ ...snapshotArg, homeId: snapshotArg.homeId, allowControls: snapshotArg.allowControls === true,
14
+ nodes: snapshotArg.nodes ?? [], events: eventsArg, connected: connectedArg });
13
15
  }
14
16
 
15
17
  public static toDevices(snapshotArg: IZwaveJsSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
16
18
  const updatedAt = new Date().toISOString();
17
19
  const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [{
18
- id: this.controllerDeviceId(snapshotArg), integrationDomain: 'zwave_js', name: 'Z-Wave controller',
19
- protocol: 'zwave', manufacturer: 'Z-Wave JS', model: 'Controller', online: snapshotArg.connected,
20
+ id: this.controllerDeviceId(snapshotArg), integrationDomain: 'zwave_js', name: 'Z-Wave JS server connection',
21
+ protocol: 'zwave', online: snapshotArg.connected,
20
22
  features: [
21
23
  { id: 'connection', capability: 'sensor', name: 'Connection', readable: true, writable: false },
22
24
  { id: 'node_count', capability: 'sensor', name: 'Node count', readable: true, writable: false },
@@ -25,7 +27,8 @@ export class ZwaveJsMapper {
25
27
  { featureId: 'connection', value: snapshotArg.connected ? 'connected' : 'disconnected', updatedAt },
26
28
  { featureId: 'node_count', value: snapshotArg.nodes.length, updatedAt },
27
29
  ],
28
- metadata: { homeId: this.homeId(snapshotArg), serverVersion: snapshotArg.version?.serverVersion, driverVersion: snapshotArg.version?.driverVersion },
30
+ metadata: { homeId: snapshotArg.homeId, zwaveHomeId: this.homeId(snapshotArg), ownNodeId: snapshotArg.controller?.ownNodeId,
31
+ serverVersion: snapshotArg.version?.serverVersion, driverVersion: snapshotArg.version?.driverVersion, adapterObservation: true },
29
32
  }];
30
33
  for (const node of snapshotArg.nodes) {
31
34
  const values = this.nodeValues(node).filter((valueArg) => valueArg.metadata?.secret !== true);
@@ -40,7 +43,7 @@ export class ZwaveJsMapper {
40
43
  id: this.featureId(valueArg), capability: this.platformForValue(valueArg) === 'light' ? 'light' :
41
44
  this.platformForValue(valueArg) === 'switch' ? 'switch' : 'sensor',
42
45
  name: this.valueLabel(valueArg), readable: valueArg.metadata?.readable === true,
43
- writable: this.valueWritable(valueArg), unit: valueArg.metadata?.unit,
46
+ writable: snapshotArg.allowControls && this.nodeAvailable(snapshotArg, node) && this.valueWritable(valueArg), unit: valueArg.metadata?.unit,
44
47
  })),
45
48
  ],
46
49
  state: [
@@ -49,7 +52,7 @@ export class ZwaveJsMapper {
49
52
  featureId: this.featureId(valueArg), value: this.stateValue(valueArg.value), updatedAt,
50
53
  })),
51
54
  ],
52
- metadata: { homeId: this.homeId(snapshotArg), nodeId: node.nodeId, manufacturerId: node.manufacturerId,
55
+ metadata: { homeId: snapshotArg.homeId, zwaveHomeId: this.homeId(snapshotArg), nodeId: node.nodeId, manufacturerId: node.manufacturerId,
53
56
  productId: node.productId, productType: node.productType, firmwareVersion: node.firmwareVersion },
54
57
  });
55
58
  }
@@ -66,14 +69,24 @@ export class ZwaveJsMapper {
66
69
  id: `${platform}.${identity}`, uniqueId: identity, integrationDomain: 'zwave_js',
67
70
  deviceId: this.nodeDeviceId(snapshotArg, nodeArg.nodeId), platform, name: this.valueLabel(valueArg),
68
71
  state: valueArg.metadata?.readable === true ? this.entityState(valueArg, platform) : 'unknown',
69
- attributes: { homeId: this.homeId(snapshotArg), nodeId: nodeArg.nodeId, valueId: this.valueId(valueArg),
70
- featureId: this.featureId(valueArg), unit: valueArg.metadata?.unit, writable: this.valueWritable(valueArg) },
72
+ attributes: { homeId: snapshotArg.homeId, zwaveHomeId: this.homeId(snapshotArg), nodeId: nodeArg.nodeId, valueId: this.valueId(valueArg),
73
+ featureId: this.featureId(valueArg), unit: valueArg.metadata?.unit,
74
+ writable: snapshotArg.allowControls && this.nodeAvailable(snapshotArg, nodeArg) && this.valueWritable(valueArg) },
71
75
  available: this.nodeAvailable(snapshotArg, nodeArg),
72
76
  };
73
77
  }));
74
78
  }
75
79
 
76
80
  public static commandForService(snapshotArg: IZwaveJsSnapshot, requestArg: IServiceCallRequest): IZwaveJsServerCommand | undefined {
81
+ const data = captureZwaveJsData(requestArg, ['domain', 'service', 'target', 'data']);
82
+ const targetData = captureZwaveJsData(data.target, ['entityId', 'deviceId']);
83
+ const serviceData = data.data === undefined ? undefined : captureZwaveJsData(data.data, ['featureId', 'value']);
84
+ if (typeof data.domain !== 'string' || typeof data.service !== 'string' ||
85
+ targetData.deviceId !== undefined && typeof targetData.deviceId !== 'string' ||
86
+ targetData.entityId !== undefined && typeof targetData.entityId !== 'string' ||
87
+ serviceData?.featureId !== undefined && typeof serviceData.featureId !== 'string' ||
88
+ data.service !== 'set_value' && serviceData?.value !== undefined) return undefined;
89
+ requestArg = { domain: data.domain, service: data.service, target: targetData, data: serviceData } as IServiceCallRequest;
77
90
  if (!['switch', 'light', 'zwave_js'].includes(requestArg.domain) ||
78
91
  !['turn_on', 'turn_off', 'set_value'].includes(requestArg.service)) return undefined;
79
92
  const target = this.findTargetValue(snapshotArg, requestArg);
@@ -86,25 +99,26 @@ export class ZwaveJsMapper {
86
99
  }
87
100
 
88
101
  public static commandForFeature(snapshotArg: IZwaveJsSnapshot, requestArg: IDeviceFeatureWriteRequest): IZwaveJsServerCommand {
89
- const node = snapshotArg.nodes.find((nodeArg) => this.nodeDeviceId(snapshotArg, nodeArg.nodeId) === requestArg.device.id);
90
- const value = node && this.nodeValues(node).find((valueArg) => this.featureId(valueArg) === requestArg.feature.id);
102
+ const request = captureZwaveJsData(requestArg, ['device', 'feature', 'value']);
103
+ const device = captureZwaveJsData(request.device);
104
+ const feature = captureZwaveJsData(request.feature);
105
+ if (device.integrationDomain !== 'zwave_js') throw new Error('Z-Wave JS device target belongs to another integration.');
106
+ const node = snapshotArg.nodes.find((nodeArg) => this.nodeDeviceId(snapshotArg, nodeArg.nodeId) === device.id);
107
+ const value = node && this.nodeValues(node).find((valueArg) => this.featureId(valueArg) === feature.id);
91
108
  if (!node || !value) throw new Error('Z-Wave JS feature target is not in the verified network.');
92
- return this.createCommand(snapshotArg, { node, value }, requestArg.value);
109
+ return this.createCommand(snapshotArg, { node, value }, request.value);
93
110
  }
94
111
 
95
112
  public static validateCommand(snapshotArg: IZwaveJsSnapshot, commandArg: IZwaveJsServerCommand): IZwaveJsServerCommand {
96
- if (!zwaveJsRecord(commandArg) || commandArg.command !== 'node.set_value' ||
97
- Object.keys(commandArg).some((keyArg) => !['command', 'nodeId', 'valueId', 'value'].includes(keyArg))) {
113
+ const command = captureZwaveJsData(commandArg, ['command', 'nodeId', 'valueId', 'value']);
114
+ if (command.command !== 'node.set_value') {
98
115
  throw new Error('Unsupported Z-Wave JS command.');
99
116
  }
100
- const id = parseZwaveJsValueId(commandArg.valueId);
101
- if (Object.keys(commandArg.valueId).some((keyArg) => !['commandClass', 'endpoint', 'property', 'propertyKey'].includes(keyArg))) {
102
- throw new Error('Invalid Z-Wave JS control identity fields.');
103
- }
104
- const node = snapshotArg.nodes.find((nodeArg) => nodeArg.nodeId === commandArg.nodeId);
117
+ const id = parseZwaveJsValueId(captureZwaveJsData(command.valueId, ['commandClass', 'endpoint', 'property', 'propertyKey']));
118
+ const node = snapshotArg.nodes.find((nodeArg) => nodeArg.nodeId === command.nodeId);
105
119
  const value = node && this.nodeValues(node).find((valueArg) => zwaveJsValueKey(valueArg) === zwaveJsValueKey(id));
106
120
  if (!node || !value) throw new Error('Z-Wave JS command target is not in the verified network.');
107
- return this.createCommand(snapshotArg, { node, value }, commandArg.value);
121
+ return this.createCommand(snapshotArg, { node, value }, command.value);
108
122
  }
109
123
 
110
124
  public static valueId(valueArg: IZwaveJsValue): IZwaveJsValueId {
@@ -120,11 +134,11 @@ export class ZwaveJsMapper {
120
134
  }
121
135
 
122
136
  public static nodeDeviceId(snapshotArg: IZwaveJsSnapshot, nodeIdArg: number): string {
123
- return `zwave_js.${this.homeId(snapshotArg).toString(16)}.node.${nodeIdArg}`;
137
+ return `zwave_js.${encodeURIComponent(snapshotArg.homeId)}.${this.homeId(snapshotArg).toString(16)}.node.${nodeIdArg}`;
124
138
  }
125
139
 
126
140
  private static createCommand(snapshotArg: IZwaveJsSnapshot, targetArg: IControlTarget, valueArg: unknown): IZwaveJsServerCommand {
127
- if (!this.nodeAvailable(snapshotArg, targetArg.node) || !this.valueWritable(targetArg.value)) {
141
+ if (!snapshotArg.allowControls || !this.nodeAvailable(snapshotArg, targetArg.node) || !this.valueWritable(targetArg.value)) {
128
142
  throw new Error('Z-Wave JS target is unavailable or is not a supported writable value.');
129
143
  }
130
144
  const metadata = targetArg.value.metadata!;
@@ -188,7 +202,7 @@ export class ZwaveJsMapper {
188
202
  return homeId;
189
203
  }
190
204
 
191
- private static controllerDeviceId(snapshotArg: IZwaveJsSnapshot): string { return `zwave_js.${this.homeId(snapshotArg).toString(16)}.controller`; }
205
+ private static controllerDeviceId(snapshotArg: IZwaveJsSnapshot): string { return `zwave_js.${encodeURIComponent(snapshotArg.homeId)}.${this.homeId(snapshotArg).toString(16)}.server`; }
192
206
  private static nodeName(nodeArg: IZwaveJsNode): string { return nodeArg.name || nodeArg.productLabel || `Z-Wave node ${nodeArg.nodeId}`; }
193
207
  private static valueLabel(valueArg: IZwaveJsValue): string { return valueArg.metadata?.label || valueArg.propertyName || String(valueArg.property); }
194
208
  }
@@ -1,13 +1,33 @@
1
1
  export interface IZwaveJsConfig {
2
+ /** Consistency with the independent home selected by the embedding host. */
3
+ homeId: string;
2
4
  url: string;
3
5
  expectedHomeId: number;
4
- /** Authorization enforced by the operator's TLS reverse proxy, not by Z-Wave JS. */
5
- authorization?: string;
6
+ /** Controls are disabled unless the owning host explicitly enables them. */
7
+ allowControls?: boolean;
8
+ credentialRef?: string;
9
+ /** Explicit alternatives; neither source falls back to the other. */
10
+ credentialSource?: 'provider' | 'config-store';
6
11
  /** Additional TLS trust anchor. Certificate and hostname verification stay enabled. */
7
12
  ca?: string;
8
13
  requestTimeoutMs?: number;
9
14
  }
10
15
 
16
+ export interface IZwaveJsCredentialRequest {
17
+ readonly homeId: string;
18
+ readonly credentialRef: string;
19
+ readonly signal: AbortSignal;
20
+ /** Monotonic performance.now() deadline in milliseconds. */
21
+ readonly deadline: number;
22
+ }
23
+ export type TZwaveJsAuthorizationResolver = (requestArg: IZwaveJsCredentialRequest) => Promise<string>;
24
+ export interface IZwaveJsIntegrationOptions {
25
+ readonly homeId?: string;
26
+ readonly capacity?: number;
27
+ /** Caller-owned; never persisted or closed by the integration. */
28
+ readonly resolveAuthorization?: TZwaveJsAuthorizationResolver;
29
+ }
30
+
11
31
  export interface IZwaveJsVersion {
12
32
  type: 'version';
13
33
  driverVersion: string;
@@ -89,6 +109,8 @@ export interface IZwaveJsValue {
89
109
  }
90
110
 
91
111
  export interface IZwaveJsSnapshot {
112
+ homeId: string;
113
+ allowControls: boolean;
92
114
  version?: IZwaveJsVersion;
93
115
  controller?: IZwaveJsController;
94
116
  nodes: IZwaveJsNode[];
@@ -107,9 +129,10 @@ export interface IZwaveJsServerCommand {
107
129
  }
108
130
 
109
131
  export interface IZwaveJsWriteResult {
110
- /** Only supervised status 255 proves completion. Status 1/254 remain indeterminate. */
132
+ /** Supervised status is a device report, not independent proof of physical causality. */
111
133
  outcome: 'confirmed' | 'working' | 'unsupervised' | 'rejected';
112
134
  status: number;
135
+ causalityConfirmed: false;
113
136
  }
114
137
 
115
138
  export interface IZwaveJsServerEvent {
@@ -1,4 +1,4 @@
1
- import type { IZwaveJsConfig, IZwaveJsNode, IZwaveJsState, IZwaveJsValue, IZwaveJsValueId, IZwaveJsValueMetadata, IZwaveJsVersion } from './zwave_js.types.js';
1
+ import type { IZwaveJsConfig, IZwaveJsIntegrationOptions, IZwaveJsNode, IZwaveJsState, IZwaveJsValue, IZwaveJsValueId, IZwaveJsValueMetadata, IZwaveJsVersion } from './zwave_js.types.js';
2
2
 
3
3
  export const zwaveJsSchemaVersion = 32;
4
4
 
@@ -8,34 +8,68 @@ export const zwaveJsRecord = (valueArg: unknown): valueArg is Record<string, unk
8
8
  export const zwaveJsInteger = (valueArg: unknown, minArg: number, maxArg: number): valueArg is number =>
9
9
  typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= minArg && valueArg <= maxArg;
10
10
 
11
- export const validateZwaveJsConfig = (configArg: IZwaveJsConfig): IZwaveJsConfig => {
12
- if (!zwaveJsRecord(configArg) || Object.keys(configArg).some((keyArg) =>
13
- !['url', 'expectedHomeId', 'authorization', 'ca', 'requestTimeoutMs'].includes(keyArg))) {
14
- throw new Error('Invalid Z-Wave JS configuration fields.');
15
- }
16
- if (typeof configArg.url !== 'string' || configArg.url.length > 2048) {
11
+ /** Capture foreign data without executing getters or accepting hidden/symbol fields. */
12
+ export const captureZwaveJsData = (valueArg: unknown, fieldsArg?: readonly string[]): Record<string, unknown> => {
13
+ if (!zwaveJsRecord(valueArg) || ![Object.prototype, null].includes(Object.getPrototypeOf(valueArg))) {
14
+ throw new TypeError('Z-Wave JS input must be a plain data object.');
15
+ }
16
+ const captured: Record<string, unknown> = {};
17
+ for (const key of Reflect.ownKeys(valueArg)) {
18
+ const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
19
+ if (typeof key !== 'string' || fieldsArg && !fieldsArg.includes(key) || !descriptor ||
20
+ !descriptor.enumerable || !('value' in descriptor)) throw new TypeError('Unsupported Z-Wave JS data field or accessor.');
21
+ Object.defineProperty(captured, key, { value: descriptor.value as unknown, enumerable: true, writable: true });
22
+ }
23
+ return captured;
24
+ };
25
+
26
+ export const validateZwaveJsEndpoint = (urlArg: unknown): string => {
27
+ if (typeof urlArg !== 'string' || urlArg.length > 2048) {
17
28
  throw new Error('Z-Wave JS requires an explicit WebSocket URL.');
18
29
  }
19
30
  let url: URL;
20
- try { url = new URL(configArg.url); } catch { throw new Error('Invalid Z-Wave JS WebSocket URL.'); }
31
+ try { url = new URL(urlArg); } catch { throw new Error('Invalid Z-Wave JS WebSocket URL.'); }
21
32
  if (!['ws:', 'wss:'].includes(url.protocol) || !url.hostname || url.username || url.password || url.search || url.hash) {
22
33
  throw new Error('Z-Wave JS URL must use ws/wss without credentials, query or fragment.');
23
34
  }
24
- if (!zwaveJsInteger(configArg.expectedHomeId, 1, 0xffffffff)) {
35
+ return url.href;
36
+ };
37
+
38
+ export const normalizeZwaveJsOptions = (optionsArg: IZwaveJsIntegrationOptions = {}): Readonly<IZwaveJsIntegrationOptions> => {
39
+ const options = captureZwaveJsData(optionsArg, ['homeId', 'capacity', 'resolveAuthorization']);
40
+ if (options.resolveAuthorization !== undefined && typeof options.resolveAuthorization !== 'function') {
41
+ throw new TypeError('Z-Wave JS authorization resolver must be a caller-owned function.');
42
+ }
43
+ return Object.freeze(options) as Readonly<IZwaveJsIntegrationOptions>;
44
+ };
45
+
46
+ export const validateZwaveJsConfig = (inputArg: IZwaveJsConfig | Record<string, unknown>): IZwaveJsConfig => {
47
+ const config = captureZwaveJsData(inputArg, ['homeId', 'url', 'expectedHomeId', 'allowControls', 'credentialRef', 'credentialSource', 'ca', 'requestTimeoutMs']);
48
+ if (typeof config.homeId !== 'string' || !config.homeId || config.homeId.length > 256 ||
49
+ config.homeId.trim() !== config.homeId || /[\p{Cc}\p{Cs}]/u.test(config.homeId)) throw new Error('Z-Wave JS configuration requires host home consistency.');
50
+ const url = validateZwaveJsEndpoint(config.url);
51
+ if (!zwaveJsInteger(config.expectedHomeId, 1, 0xffffffff)) {
25
52
  throw new Error('Z-Wave JS requires the expected network home ID.');
26
53
  }
27
- if (configArg.authorization !== undefined && (url.protocol !== 'wss:' ||
28
- typeof configArg.authorization !== 'string' || !configArg.authorization.trim() ||
29
- configArg.authorization.length > 4096 || /[\x00-\x1f\x7f]/.test(configArg.authorization))) {
30
- throw new Error('Z-Wave JS proxy authorization requires verified TLS and a valid header.');
54
+ if (config.allowControls !== undefined && typeof config.allowControls !== 'boolean') throw new Error('Invalid Z-Wave JS control authorization.');
55
+ if (config.credentialRef !== undefined && (typeof config.credentialRef !== 'string' || !config.credentialRef ||
56
+ config.credentialRef.length > 1024 || config.credentialRef.trim() !== config.credentialRef || /[\p{Cc}\p{Cs}]/u.test(config.credentialRef))) {
57
+ throw new Error('Invalid Z-Wave JS credential reference.');
58
+ }
59
+ if (config.credentialSource !== undefined && config.credentialSource !== 'provider' && config.credentialSource !== 'config-store' ||
60
+ (config.credentialRef === undefined) !== (config.credentialSource === undefined) || config.credentialRef !== undefined && !url.startsWith('wss:')) {
61
+ throw new Error('Z-Wave JS credentials require an explicit source, reference and verified TLS.');
31
62
  }
32
- if (configArg.ca !== undefined && (url.protocol !== 'wss:' || typeof configArg.ca !== 'string' ||
33
- configArg.ca.length > 65536 || !configArg.ca.includes('-----BEGIN CERTIFICATE-----'))) {
63
+ if (config.ca !== undefined && (!url.startsWith('wss:') || typeof config.ca !== 'string' ||
64
+ config.ca.length > 65536 || !config.ca.includes('-----BEGIN CERTIFICATE-----'))) {
34
65
  throw new Error('Z-Wave JS CA must be a PEM certificate for a wss endpoint.');
35
66
  }
36
- const requestTimeoutMs = configArg.requestTimeoutMs ?? 10000;
67
+ const requestTimeoutMs = config.requestTimeoutMs ?? 10000;
37
68
  if (!zwaveJsInteger(requestTimeoutMs, 50, 60000)) throw new Error('Invalid Z-Wave JS request deadline.');
38
- return { ...configArg, url: url.href, requestTimeoutMs };
69
+ return Object.freeze({ homeId: config.homeId, url, expectedHomeId: config.expectedHomeId,
70
+ allowControls: config.allowControls === true, requestTimeoutMs,
71
+ credentialRef: config.credentialRef as string | undefined,
72
+ credentialSource: config.credentialSource as IZwaveJsConfig['credentialSource'], ca: config.ca as string | undefined });
39
73
  };
40
74
 
41
75
  export const parseZwaveJsVersion = (valueArg: unknown, expectedHomeIdArg: number): IZwaveJsVersion => {
@@ -81,7 +115,7 @@ export const parseZwaveJsMetadata = (valueArg: unknown): IZwaveJsValueMetadata =
81
115
  }
82
116
  const metadata: IZwaveJsValueMetadata = {
83
117
  type: valueArg.type, readable: valueArg.readable, writeable: valueArg.writeable,
84
- secret: valueArg.secret === true, stateful: valueArg.stateful !== false,
118
+ secret: valueArg.secret as boolean | undefined, stateful: valueArg.stateful as boolean | undefined,
85
119
  };
86
120
  for (const key of ['label', 'unit'] as const) {
87
121
  if (typeof valueArg[key] === 'string' && valueArg[key].length <= 1024) metadata[key] = valueArg[key];
package/ts/plugins.ts CHANGED
@@ -4,9 +4,10 @@ import * as path from 'node:path';
4
4
  import * as crypto from 'node:crypto';
5
5
  import * as net from 'node:net';
6
6
  import * as tls from 'node:tls';
7
+ import * as http from 'node:http';
7
8
  import * as https from 'node:https';
8
9
  import * as os from 'node:os';
9
- export { crypto, fs, https, net, path, tls, os };
10
+ export { crypto, fs, http, https, net, path, tls, os };
10
11
 
11
12
  // Project scope
12
13
  import * as shxInterfaces from '@smarthome.exchange/interfaces';