@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,14 +1,17 @@
1
1
  import * as plugins from '../../plugins.js';
2
+ import { NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
3
+ import { normalizeHomeAssistantConfig } from './homeassistant.classes.client.js';
4
+ import { homeAssistantRecord } from './homeassistant.validation.js';
2
5
  import { BaseIntegration } from '../../core/classes.baseintegration.js';
3
6
  import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
4
7
  import type {
5
- IIntegrationRuntime,
6
8
  IIntegrationSetupContext,
7
9
  } from '../../core/types.js';
8
10
  import { HomeAssistantClient } from './homeassistant.classes.client.js';
9
11
  import { HomeAssistantRuntime } from './homeassistant.classes.runtime.js';
10
12
  import type {
11
13
  IHomeAssistantHomeassistantConfig,
14
+ IHomeAssistantIntegrationOptions,
12
15
  THomeAssistantWebSocketFactory,
13
16
  } from './homeassistant.types.js';
14
17
 
@@ -29,26 +32,40 @@ export class HomeAssistantHomeassistantIntegration extends BaseIntegration<IHome
29
32
  transport: 'websocket',
30
33
  };
31
34
 
32
- constructor(
33
- private readonly webSocketFactory: THomeAssistantWebSocketFactory = (
34
- urlArg
35
- ) => new globalThis.WebSocket(urlArg)
36
- ) {
35
+ private readonly owner: NativeIntegrationOwner;
36
+ private readonly webSocketFactory: THomeAssistantWebSocketFactory;
37
+
38
+ constructor(optionsOrFactoryArg: IHomeAssistantIntegrationOptions | THomeAssistantWebSocketFactory = {}) {
37
39
  super();
40
+ const options = homeAssistantRecord(typeof optionsOrFactoryArg === 'function'
41
+ ? { webSocketFactory: optionsOrFactoryArg } : optionsOrFactoryArg);
42
+ if (Object.keys(options).some((key) => !['homeId', 'capacity', 'webSocketFactory'].includes(key))) throw new TypeError('Home Assistant integration options contain unsupported fields.');
43
+ if (options.webSocketFactory !== undefined && typeof options.webSocketFactory !== 'function') throw new TypeError('Home Assistant webSocketFactory must be a function.');
44
+ this.webSocketFactory = options.webSocketFactory as THomeAssistantWebSocketFactory | undefined ?? ((urlArg) => new globalThis.WebSocket(urlArg));
45
+ this.owner = new NativeIntegrationOwner(this.domain, { homeId: options.homeId as string | undefined, capacity: options.capacity as number | undefined });
38
46
  }
39
47
 
40
- public async setup(
41
- configArg: IHomeAssistantHomeassistantConfig,
42
- contextArg: IIntegrationSetupContext
43
- ): Promise<IIntegrationRuntime> {
44
- const client = new HomeAssistantClient(
45
- configArg,
46
- plugins.homeAssistantWebsocket,
47
- this.webSocketFactory
48
- );
49
- await client.init(contextArg.abortSignal);
50
- return new HomeAssistantRuntime(client);
48
+ public async setup(configArg: IHomeAssistantHomeassistantConfig, contextArg: IIntegrationSetupContext): Promise<HomeAssistantRuntime> {
49
+ // Validate/capture before deferred setup so later caller mutation cannot change scope.
50
+ normalizeHomeAssistantConfig(configArg);
51
+ const config = structuredClone(configArg);
52
+ return this.owner.runSetup(contextArg, config.homeId, async (leaseArg) => {
53
+ const client = new HomeAssistantClient(config, leaseArg.context, plugins.homeAssistantWebsocket,
54
+ this.webSocketFactory, { owner: this.owner, lease: leaseArg });
55
+ let cleanupStarted = false;
56
+ leaseArg.own(() => {
57
+ const retry = cleanupStarted;
58
+ cleanupStarted = true;
59
+ return retry ? client.retryCleanup() : client.destroy();
60
+ });
61
+ await client.init(leaseArg.signal);
62
+ leaseArg.assertOpen();
63
+ const runtime = new HomeAssistantRuntime(client, leaseArg, this.owner);
64
+ leaseArg.promoteToRuntime();
65
+ return runtime;
66
+ });
51
67
  }
52
68
 
53
- public async destroy(): Promise<void> {}
69
+ public destroy(): Promise<void> { return this.owner.close(); }
70
+ public retryCleanup(): Promise<void> { return this.owner.retryCleanup(); }
54
71
  }
@@ -1,3 +1,4 @@
1
+ import type { INativeSetupLease, NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
1
2
  import type * as shxInterfaces from '@smarthome.exchange/interfaces';
2
3
  import type {
3
4
  IDeviceFeatureWriteRequest,
@@ -10,6 +11,7 @@ import { HomeAssistantMapper } from './homeassistant.mapper.js';
10
11
  import { homeAssistantRecord } from './homeassistant.validation.js';
11
12
  import type {
12
13
  IHomeAssistantClient,
14
+ IHomeAssistantOperationSession,
13
15
  IHomeAssistantTargetScope,
14
16
  } from './homeassistant.types.js';
15
17
 
@@ -21,43 +23,44 @@ const abortError = (abortSignalArg: AbortSignal): Error => {
21
23
 
22
24
  export class HomeAssistantRuntime implements IIntegrationRuntime {
23
25
  public readonly domain = 'homeassistant';
24
- private readonly subscriptions = new Set<() => void>();
25
26
  private destroyed = false;
26
- private cleanupComplete = false;
27
27
  private destroyPromise?: Promise<void>;
28
28
  private readonly targetScope: IHomeAssistantTargetScope;
29
29
 
30
- constructor(private readonly client: IHomeAssistantClient) {
30
+ constructor(private readonly client: IHomeAssistantClient, private readonly lease?: INativeSetupLease, private readonly owner?: NativeIntegrationOwner) {
31
31
  this.targetScope = Object.freeze({ ...client.getTargetScope() });
32
32
  }
33
33
 
34
34
  public isHealthy(): boolean {
35
- return !this.destroyed && this.client.connected;
35
+ return !this.destroyed && !this.lease?.signal.aborted && this.client.connected;
36
36
  }
37
37
 
38
38
  public async devices(
39
39
  abortSignalArg?: AbortSignal
40
40
  ): Promise<shxInterfaces.data.IDeviceDefinition[]> {
41
41
  this.assertActive();
42
- if (abortSignalArg?.aborted) {
43
- throw abortError(abortSignalArg);
44
- }
45
- return HomeAssistantMapper.toDevices(
46
- this.client.getSnapshot(),
47
- this.client.connected,
48
- this.client.getEntityPlatforms(),
49
- this.client.getServices(),
50
- this.targetScope
51
- );
42
+ return this.client.withOperation(async () => {
43
+ this.assertActive();
44
+ return HomeAssistantMapper.toDevices(
45
+ this.client.getSnapshot(),
46
+ this.client.connected,
47
+ this.client.getEntityPlatforms(),
48
+ this.client.getServices(),
49
+ this.targetScope
50
+ );
51
+ }, abortSignalArg);
52
52
  }
53
53
 
54
54
  public async entities(): Promise<IIntegrationEntity[]> {
55
55
  this.assertActive();
56
- return HomeAssistantMapper.toEntities(this.client.getSnapshot(), this.client.connected, this.targetScope);
56
+ return this.client.withOperation(async () => {
57
+ this.assertActive();
58
+ return HomeAssistantMapper.toEntities(this.client.getSnapshot(), this.client.connected, this.targetScope);
59
+ });
57
60
  }
58
61
 
59
62
  public async subscribe(
60
- handlerArg: (eventArg: IIntegrationEvent) => void,
63
+ handlerArg: (eventArg: IIntegrationEvent) => void | Promise<void>,
61
64
  abortSignalArg?: AbortSignal
62
65
  ): Promise<() => Promise<void>> {
63
66
  this.assertActive();
@@ -65,32 +68,14 @@ export class HomeAssistantRuntime implements IIntegrationRuntime {
65
68
  throw abortError(abortSignalArg);
66
69
  }
67
70
 
68
- let subscribed = true;
69
- const unsubscribeClient = this.client.subscribe(() => {
70
- handlerArg({
71
+ if (typeof handlerArg !== 'function') throw new TypeError('Home Assistant subscriber must be a function.');
72
+ return this.client.subscribe(() => {
73
+ return handlerArg({
71
74
  type: 'state_changed',
72
75
  integrationDomain: this.domain,
73
76
  timestamp: Date.now(),
74
77
  });
75
- });
76
- const unsubscribe = (): void => {
77
- if (!subscribed) {
78
- return;
79
- }
80
- subscribed = false;
81
- this.subscriptions.delete(unsubscribe);
82
- abortSignalArg?.removeEventListener('abort', unsubscribe);
83
- unsubscribeClient();
84
- };
85
- this.subscriptions.add(unsubscribe);
86
- abortSignalArg?.addEventListener('abort', unsubscribe, { once: true });
87
- if (abortSignalArg?.aborted) {
88
- unsubscribe();
89
- }
90
-
91
- return async () => {
92
- unsubscribe();
93
- };
78
+ }, abortSignalArg);
94
79
  }
95
80
 
96
81
  public async writeFeature(
@@ -101,6 +86,19 @@ export class HomeAssistantRuntime implements IIntegrationRuntime {
101
86
  if (abortSignalArg?.aborted) {
102
87
  throw abortError(abortSignalArg);
103
88
  }
89
+ homeAssistantRecord(requestArg);
90
+ homeAssistantRecord(requestArg.device);
91
+ homeAssistantRecord(requestArg.feature);
92
+ if (requestArg.device.metadata !== undefined) homeAssistantRecord(requestArg.device.metadata);
93
+ const request = structuredClone(requestArg);
94
+ return this.client.withOperation((sessionArg) => this.writeAdmitted(request, sessionArg), abortSignalArg);
95
+ }
96
+
97
+ private async writeAdmitted(
98
+ requestArg: IDeviceFeatureWriteRequest,
99
+ sessionArg: IHomeAssistantOperationSession
100
+ ): Promise<IServiceCallResult> {
101
+ this.assertActive();
104
102
  if (Object.keys(homeAssistantRecord(requestArg)).some((key) => !['device', 'feature', 'value'].includes(key))) {
105
103
  return { success: false, error: 'Home Assistant feature requests contain unsupported fields.' };
106
104
  }
@@ -145,10 +143,8 @@ export class HomeAssistantRuntime implements IIntegrationRuntime {
145
143
  return { success: false, error: mappedCall.error || 'Unsupported Home Assistant write.' };
146
144
  }
147
145
 
148
- const data = await this.client.callService(mappedCall.request, abortSignalArg);
149
- if (abortSignalArg?.aborted) {
150
- throw abortError(abortSignalArg);
151
- }
146
+ const data = await sessionArg.callService(mappedCall.request);
147
+ sessionArg.signal.throwIfAborted();
152
148
  return {
153
149
  success: true,
154
150
  data: {
@@ -159,34 +155,25 @@ export class HomeAssistantRuntime implements IIntegrationRuntime {
159
155
  }
160
156
 
161
157
  private assertActive(): void {
158
+ this.lease?.assertOpen();
162
159
  if (this.destroyed) throw new Error('Home Assistant runtime is destroyed.');
163
160
  }
164
161
 
165
162
  public destroy(): Promise<void> {
166
- if (this.destroyPromise) {
167
- return this.destroyPromise;
168
- }
169
- if (this.cleanupComplete) {
170
- return Promise.resolve();
163
+ if (!this.destroyPromise) {
164
+ this.destroyed = true;
165
+ this.destroyPromise = Promise.resolve().then(() => this.performDestroy());
171
166
  }
172
- this.destroyed = true;
173
- const destroyPromise = this.performDestroy();
174
- this.destroyPromise = destroyPromise;
175
- void destroyPromise.then(() => {
176
- this.cleanupComplete = true;
177
- }).finally(() => {
178
- if (this.destroyPromise === destroyPromise) {
179
- this.destroyPromise = undefined;
180
- }
181
- }).catch(() => undefined);
182
- return destroyPromise;
167
+ return this.destroyPromise;
168
+ }
169
+
170
+ public retryCleanup(): Promise<void> {
171
+ if (!this.destroyPromise) return Promise.reject(new Error('Home Assistant cleanup retry requires destruction.'));
172
+ return this.owner ? this.owner.retryCleanup() : this.client.retryCleanup();
183
173
  }
184
174
 
185
175
  private async performDestroy(): Promise<void> {
186
- for (const unsubscribe of [...this.subscriptions]) {
187
- unsubscribe();
188
- }
189
- this.subscriptions.clear();
190
- await this.client.destroy();
176
+ // The client closes transport before joining its owned subscriber continuations.
177
+ await (this.lease ? this.lease.close() : this.client.destroy());
191
178
  }
192
179
  }
@@ -1,3 +1,4 @@
1
+ import type { INativeIntegrationOwnerOptions } from '../../core/classes.nativeintegrationowner.js';
1
2
  import type * as homeAssistantWebsocket from 'home-assistant-js-websocket';
2
3
 
3
4
  export type THomeAssistantVacuumCommand =
@@ -8,6 +9,10 @@ export type THomeAssistantVacuumCommand =
8
9
  | 'locate'
9
10
  | 'clean_spot';
10
11
 
12
+ export interface IHomeAssistantIntegrationOptions extends INativeIntegrationOwnerOptions {
13
+ readonly webSocketFactory?: THomeAssistantWebSocketFactory;
14
+ }
15
+
11
16
  export interface IHomeAssistantHomeassistantConfig {
12
17
  homeId: string;
13
18
  baseUrl: string;
@@ -72,12 +77,23 @@ export interface IHomeAssistantClient {
72
77
  getEntityPlatforms(): Record<string, string>;
73
78
  getServices(): ReadonlySet<string>;
74
79
  getTargetScope(): IHomeAssistantTargetScope;
75
- subscribe(handlerArg: () => void): () => void;
80
+ subscribe(handlerArg: () => void | Promise<void>, abortSignalArg?: AbortSignal): () => Promise<void>;
81
+ withOperation<TResult>(
82
+ workArg: (sessionArg: IHomeAssistantOperationSession) => Promise<TResult>,
83
+ abortSignalArg?: AbortSignal
84
+ ): Promise<TResult>;
76
85
  callService(
77
86
  requestArg: IHomeAssistantServiceCall,
78
87
  abortSignalArg?: AbortSignal
79
88
  ): Promise<unknown>;
80
89
  destroy(): Promise<void>;
90
+ retryCleanup(): Promise<void>;
91
+ }
92
+
93
+ /** A service session remains usable only inside its admitted continuation. */
94
+ export interface IHomeAssistantOperationSession {
95
+ readonly signal: AbortSignal;
96
+ callService(requestArg: IHomeAssistantServiceCall): Promise<unknown>;
81
97
  }
82
98
 
83
99
  export interface IHomeAssistantTargetScope {
@@ -1,9 +1,9 @@
1
1
  # Home Assistant runtime
2
2
 
3
- `HomeAssistantHomeassistantIntegration` connects directly to Home Assistant's authenticated `/api/websocket` endpoint through the installed official `home-assistant-js-websocket` client and Node's native WebSocket. Its existing optional WebSocket-factory constructor remains available. Production contains no fixture entities or credentials.
3
+ `HomeAssistantHomeassistantIntegration` connects directly to Home Assistant's authenticated `/api/websocket` endpoint through the installed official `home-assistant-js-websocket` client and Node's native WebSocket. Its existing WebSocket-factory constructor remains available; an options object additionally accepts `homeId`, `capacity` and `webSocketFactory`. Production contains no fixture entities or credentials.
4
4
 
5
5
  ```typescript
6
- const integration = new HomeAssistantHomeassistantIntegration();
6
+ const integration = new HomeAssistantHomeassistantIntegration({ capacity: 16 });
7
7
  const runtime = await integration.setup({
8
8
  homeId: 'house-a',
9
9
  baseUrl: 'https://ha.example.test:8123',
@@ -14,15 +14,19 @@ const runtime = await integration.setup({
14
14
  setupRetry: 0,
15
15
  initialSnapshotTimeoutMs: 10_000,
16
16
  serviceCallTimeoutMs: 15_000,
17
- }, { abortSignal });
17
+ }, { homeId: 'house-a', abortSignal });
18
18
  try {
19
19
  const devices = await runtime.devices();
20
20
  // Send selected advertised features through the hub's scoped tool boundary.
21
21
  } finally {
22
- await runtime.destroy();
22
+ await integration.destroy();
23
23
  }
24
24
  ```
25
25
 
26
+ Setup requires an independent host-selected `context.homeId`, even if the constructor constrains a home. Configuration home is an exact consistency check. The first admitted home remains fixed after failed readiness; context is frozen before allocation. An optional borrowed `NoSqlConfigStore` must be a real instance with exactly that home and the `homeassistant` domain; HA never closes it, its database, logger or caller-owned signal.
27
+
28
+ For a host-owned encrypted config store, pass `{ homeId: selectedHome, configStore: homeAssistantStore, abortSignal }` as context. The store's scope must be `{ homeId: selectedHome, integrationDomain: 'homeassistant' }`. The host closes that store after its integrations finish. Supplying a store does not replace the required inline runtime token or create a credential-reference API.
29
+
26
30
  `homeId`, an HTTP(S) origin without user information, path, query or fragment, and a nonempty configured access token are required. Home IDs contain 1–128 alphanumeric, underscore, period, colon or hyphen characters and begin with an alphanumeric character. The exact configured ID is preserved, including case and colons in IDs such as `home:studio` and `home:unconfigured`; it is never trimmed or renamed. Optional entity IDs and domains form intersecting allowlists. Credentials stay in runtime configuration; a caller that persists configuration must use the core encrypted NoSql config store and its external key provider. This integration writes no application data to the filesystem.
27
31
 
28
32
  Setup uses one deadline for authentication, subscription acknowledgements, an authoritative `get_states` snapshot, `get_services`, and `config/entity_registry/list_for_display`. Registry provenance is fetched for selected entities even when strict registry enforcement is disabled. Strict enforcement additionally requires every explicitly configured entity to exist in the state and registry snapshots. Home Assistant must support these documented commands; an unsupported or malformed response rejects setup.
@@ -33,8 +37,20 @@ Supported writes are exact light/switch/input-boolean `turn_on` and `turn_off`,
33
37
 
34
38
  `state_changed` updates are authoritative. Service acknowledgements never synthesize state. `service_registered`, `service_removed` and `entity_registry_updated` events revoke snapshot readiness until fresh state, service and registry snapshots complete. Reconnects do the same before health or writes recover. Missing required entities, offline entities, malformed states or inconsistent event targets block controls. A malformed state event closes its connection and leaves retained devices offline.
35
39
 
36
- Snapshots and buffered events are bounded at 10,000 entities, services at 10,000 actions and 1,000 domains, and entity attributes at depth 16 and 4,096 values. Caller aborts and write deadlines cancel the pending command through the owning connection; commands are never retried or replayed by this adapter. Cancellation after dispatch leaves the physical effect uncertain. Destroy aborts pending callers, releases all subscriptions, closes sockets, waits for physical closure up to one second, clears retained data and makes the runtime terminal. Failed cleanup remains retryable through `destroy()`.
40
+ Snapshots and buffered events are bounded at 10,000 entities, services at 10,000 actions and 1,000 domains, and entity attributes at depth 16 and 4,096 values. Caller aborts and write deadlines cancel the pending command through the owning connection; commands are never retried or replayed by this adapter. Cancellation after dispatch leaves the physical effect uncertain. Destroy aborts pending callers, releases all subscriptions, closes sockets, waits for physical closure up to one second, clears retained data and makes the runtime terminal. Repeated `destroy()` calls return the same permanent result, including a failure. `retryCleanup()` retries only failed cleanup handles and never reopens, authenticates or replays a command. Integration `destroy()` drains setup and all promoted runtimes, returns one permanent result and rejects reopening; integration `retryCleanup()` performs cleanup only. Runtime destruction closes its setup lease. Cancellation of the setup context remains attached after promotion. A shared capacity (default 16) counts pending work, live runtimes and retained cleanup; rejection happens before allocation. The exported controlling client also requires independent context: `new HomeAssistantClient(config, { homeId: selectedHome }, api?, webSocketFactory?)`.
41
+
42
+ Close joins actual connection, subscription, refresh and service settlement even when caller cancellation has already rejected. A late handle remains owned until its cleanup completes. Sibling cleanup proceeds even when one unsubscribe fails.
43
+
44
+ Runtime reads, feature writes, direct service calls and subscriber delivery use the same owner capacity as live runtimes. `client.withOperation(async (session) => ...)` owns the entire continuation and provides `session.signal` and a scoped `session.callService(request)`; a session cannot dispatch after that continuation finishes. Caller cancellation rejects promptly while the actual continuation and any dispatched service promise still occupy capacity until settlement. Requests are captured before deferred admission and validated against current authoritative state at dispatch.
45
+
46
+ Subscribers may return `Promise<void>`. The returned unsubscribe function fences new and queued delivery immediately, returns one stable promise and joins callbacks already running. Runtime and integration close shut down transport before joining callbacks. Synchronous and asynchronous callback failures, and rejected callback admission, remain observable through unsubscribe and close; a failed subscriber stops receiving events while siblings continue. Explicit cleanup retry clears reported callback failures and retries retained cleanup without replaying callbacks or commands. A callback must finish its own work before awaiting destruction of its owning runtime.
47
+
48
+ The local test fixtures use real HTTP upgrades and native WebSockets with test-only tokens. Their delayed replies preserve the result captured when a command was received. They verify exact request bytes, acknowledgement gates, catalogue changes, registry refresh, reconnect readiness, authoritative state, malformed inputs, cross-home and mutated-target rejection, shared capacity, callback drainage, cancellation and physical closure. The five suites contain 62 tests: injected behavior 20, native protocol 15, lifecycle 6, native ownership 9, finite operations/subscribers 12. The domain has seven production TypeScript roots and seven test/support roots for strict type and emit checks.
49
+
50
+ Qualification commands are `pnpm exec tstest test/homeassistant/ --verbose --logfile --timeout 60`, `pnpm run build`, and `pnpm exec tsbuild check 'test/**/*'` without `skipLibCheck`. For the domain's 14 roots, use the installed `@git.zone/tsbuild` `TsCompiler.checkTypes(roots, { strict: true })` and `checkEmit(roots, { strict: true })`; the package build also covers the shared owner and generated domains.
51
+
52
+ One upstream contract remains blocked. In installed `home-assistant-js-websocket` 9.7.0, terminal `Connection.close()` leaves an unacknowledged `subscribeEvents()` promise pending forever. `Connection._handleClose` preserves subscription commands for reconnect before checking `closeRequested`, then stops reconnecting. A fixture probe sent one subscription, closed its socket and observed no subscription settlement. The published 9.7.1 artifact was inspected on 2026-10-11 and retains the same implementation. HA's actual-work ledger deliberately continues owning that promise; shutdown during an unacknowledged subscription cannot be qualified as complete. The proper path is a fix and published release in the official client that rejects unresolved subscriptions on terminal close, followed by a published consumer update and native terminal-subscription regressions. No local shim, command replay or fabricated settlement is used.
37
53
 
38
- The local test fixtures use real HTTP upgrades and native WebSockets with test-only tokens. They verify exact request bytes, catalogue changes, registry refresh, reconnect readiness, authoritative state, malformed inputs, cross-home and mutated-target rejection, cancellation and physical closure. Hardware acceptance against the owner's Home Assistant remains outside this lane.
54
+ Real Home Assistant authentication, registry compatibility, credential revocation, commissioning/control and physical shutdown acceptance remain owner-gated. Credential references and config flow, explicit control/plaintext consent, public offline-mapper defaults and `reconnect-error` reporting remain separate public-contract decisions.
39
55
 
40
56
  API references: [Home Assistant WebSocket API](https://developers.home-assistant.io/docs/api/websocket/), [REST API](https://developers.home-assistant.io/docs/api/rest/), and [official JavaScript client](https://github.com/home-assistant/home-assistant-js-websocket).