@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,51 +1,57 @@
1
- import type * as shxInterfaces from '@smarthome.exchange/interfaces';
1
+ import type * as plugins from '../../plugins.js';
2
+ import { NativeIntegrationOwner, type INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
2
3
  import { BaseIntegration } from '../../core/classes.baseintegration.js';
3
4
  import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
4
5
  import { HueClient } from './hue.classes.client.js';
5
6
  import { HueConfigFlow } from './hue.classes.configflow.js';
6
7
  import { createHueDiscoveryDescriptor } from './hue.discovery.js';
7
8
  import { HueMapper } from './hue.mapper.js';
8
- import { hueRecord } from './hue.validation.js';
9
- import type { IHueConfig, IHueLightStatePatch, IHueResourceResponse } from './hue.types.js';
9
+ import { captureHueOptions, hueRecord, normalizeHueConfig } from './hue.validation.js';
10
+ import type { IHueConfig, IHueLightStatePatch, IHueResourceResponse, IHueIntegrationOptions } from './hue.types.js';
10
11
 
11
12
  export class HueIntegration extends BaseIntegration<IHueConfig> {
12
13
  public readonly domain = 'hue';
13
14
  public readonly displayName = 'Philips Hue';
14
15
  public readonly status = 'control-runtime' as const;
15
16
  public readonly discoveryDescriptor = createHueDiscoveryDescriptor();
16
- public readonly configFlow = new HueConfigFlow();
17
- readonly #runtimes = new Set<HueIntegrationRuntime>();
18
- #closed = false;
17
+ public readonly configFlow: HueConfigFlow;
18
+ readonly #owner: NativeIntegrationOwner;
19
+ readonly #options: Readonly<IHueIntegrationOptions>;
20
+
21
+ constructor(optionsArg: IHueIntegrationOptions = {}) {
22
+ super();
23
+ this.#options = captureHueOptions(optionsArg);
24
+ this.#owner = new NativeIntegrationOwner('hue', { homeId: this.#options.homeId, capacity: this.#options.capacity });
25
+ this.configFlow = new HueConfigFlow(this.#options, this.#owner);
26
+ }
19
27
 
20
28
  public async setup(configArg: IHueConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
21
- if (this.#closed) throw new Error('Hue integration is closed.');
22
- const client = new HueClient(configArg, contextArg.abortSignal);
23
- const runtime = new HueIntegrationRuntime(client, () => this.#runtimes.delete(runtime));
24
- this.#runtimes.add(runtime);
25
- try {
29
+ const config = normalizeHueConfig(configArg);
30
+ return this.#owner.runSetup(contextArg, config.homeId, async (lease) => {
31
+ const client = new HueClient(config, { ...lease.context, abortSignal: lease.signal }, this.#options, this.#owner);
32
+ let cleanupStarted = false;
33
+ lease.own(() => { if (cleanupStarted) return client.retryCleanup(); cleanupStarted = true; return client.destroy(); });
26
34
  await client.getResources();
27
- if (this.#closed || contextArg.abortSignal?.aborted) throw new Error('Hue setup was cancelled.');
28
- return runtime;
29
- } catch (error) {
30
- await runtime.destroy();
31
- throw error;
32
- }
35
+ lease.assertOpen();
36
+ if (!client.isHealthy()) throw new Error('Hue setup cancelled.');
37
+ lease.promoteToRuntime();
38
+ return new HueIntegrationRuntime(client, lease);
39
+ });
33
40
  }
34
41
 
35
- public async destroy(): Promise<void> {
36
- this.#closed = true;
37
- await Promise.all([...this.#runtimes].map((runtimeArg) => runtimeArg.destroy()));
38
- }
42
+ public destroy(): Promise<void> { return this.#owner.close(); }
43
+ public retryCleanup(): Promise<void> { return this.#owner.retryCleanup(); }
44
+
39
45
  }
40
46
 
41
47
  class HueIntegrationRuntime implements IIntegrationRuntime {
42
48
  public readonly domain = 'hue';
43
49
 
44
- constructor(private readonly client: HueClient, private readonly released: () => void) {}
50
+ constructor(private readonly client: HueClient, private readonly lease: INativeSetupLease) {}
45
51
 
46
52
  public isHealthy(): boolean { return this.client.isHealthy(); }
47
53
 
48
- public async devices(abortSignalArg?: AbortSignal): Promise<shxInterfaces.data.IDeviceDefinition[]> {
54
+ public async devices(abortSignalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
49
55
  return HueMapper.toDevices(await this.client.getResources(abortSignalArg));
50
56
  }
51
57
 
@@ -59,57 +65,72 @@ class HueIntegrationRuntime implements IIntegrationRuntime {
59
65
  }
60
66
 
61
67
  public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
62
- if (requestArg.domain !== 'light' || !['turn_on', 'turn_off', 'set_percentage'].includes(requestArg.service)) {
68
+ const request = hueRecord(requestArg);
69
+ if (Reflect.ownKeys(request).some((keyArg) => !['domain', 'service', 'target', 'data'].includes(String(keyArg)))) return { success: false, error: 'Hue service contains unsupported fields.' };
70
+ if (request.domain !== 'light' || typeof request.service !== 'string' || !['turn_on', 'turn_off', 'set_percentage'].includes(request.service)) {
63
71
  return { success: false, error: 'Hue supports only light turn_on, turn_off and set_percentage.' };
64
72
  }
65
- const target = hueRecord(requestArg.target);
73
+ const target = hueRecord(request.target);
66
74
  if (Reflect.ownKeys(target).some((keyArg) => keyArg !== 'entityId' && keyArg !== 'deviceId')) {
67
75
  return { success: false, error: 'Hue service target contains unsupported fields.' };
68
76
  }
69
- const data = requestArg.data === undefined ? {} : hueRecord(requestArg.data);
70
- const allowedKey = requestArg.service === 'set_percentage' ? 'percentage' : requestArg.service === 'turn_on' ? 'brightness' : undefined;
77
+ if (Object.values(target).some((valueArg) => typeof valueArg !== 'string' || !valueArg)) return { success: false, error: 'Hue service targets must be identifiers.' };
78
+ const data = request.data === undefined ? {} : hueRecord(request.data);
79
+ const allowedKey = request.service === 'set_percentage' ? 'percentage' : request.service === 'turn_on' ? 'brightness' : undefined;
71
80
  if (Reflect.ownKeys(data).some((keyArg) => keyArg !== allowedKey)) return { success: false, error: 'Hue service data contains unsupported fields.' };
72
- const state: IHueLightStatePatch = { on: requestArg.service !== 'turn_off' };
73
- if (requestArg.service === 'set_percentage' || data.brightness !== undefined) {
74
- const brightness = data[requestArg.service === 'set_percentage' ? 'percentage' : 'brightness'];
81
+ const state: IHueLightStatePatch = { on: request.service !== 'turn_off' };
82
+ if (request.service === 'set_percentage' || data.brightness !== undefined) {
83
+ const brightness = data[request.service === 'set_percentage' ? 'percentage' : 'brightness'];
75
84
  if (typeof brightness !== 'number' || !Number.isFinite(brightness) || brightness < 0 || brightness > 100) {
76
85
  return { success: false, error: 'Hue brightness must be between 0 and 100 percent.' };
77
86
  }
78
87
  state.brightness = brightness;
79
- if (requestArg.service === 'set_percentage') state.on = brightness > 0;
88
+ if (request.service === 'set_percentage') state.on = brightness > 0;
80
89
  }
81
- const snapshot = await this.client.getResources();
82
- const lightId = this.resolveLight(snapshot, target.entityId, target.deviceId);
83
- if (!lightId) return { success: false, error: 'Hue service requires one known, unambiguous light target.' };
84
- await this.client.setLightState(lightId, state);
85
- return { success: true };
90
+ return this.client.execute(async (session) => {
91
+ const snapshot = await session.getResources();
92
+ const lightId = this.resolveLight(snapshot, target.entityId, target.deviceId);
93
+ if (!lightId) return { success: false, error: 'Hue service requires one known, unambiguous light target.' };
94
+ await session.setLightState(lightId, state);
95
+ return { success: true };
96
+ });
86
97
  }
87
98
 
88
99
  public async writeFeature(requestArg: IDeviceFeatureWriteRequest, abortSignalArg?: AbortSignal): Promise<IServiceCallResult> {
89
- const snapshot = await this.client.getResources(abortSignalArg);
90
- const device = HueMapper.toDevices(snapshot).find((deviceArg) => deviceArg.id === requestArg.device.id);
91
- const feature = device?.features.find((featureArg) => featureArg.id === requestArg.feature.id && featureArg.writable);
92
- const lightId = this.resolveLight(snapshot, undefined, device?.id);
93
- if (!device || !device.online || !feature || !lightId) return { success: false, error: 'Hue feature target is unknown, unavailable or not writable.' };
94
- let state: IHueLightStatePatch;
95
- if (feature.id === 'on' && typeof requestArg.value === 'boolean') state = { on: requestArg.value };
96
- else if (feature.id === 'brightness' && typeof requestArg.value === 'number') state = { brightness: requestArg.value };
97
- else return { success: false, error: 'Hue feature value has an invalid type.' };
98
- await this.client.setLightState(lightId, state, abortSignalArg);
99
- return { success: true };
100
+ const request = hueRecord(requestArg);
101
+ if (Reflect.ownKeys(request).some((keyArg) => !['device', 'feature', 'value'].includes(String(keyArg)))) return { success: false, error: 'Hue feature request contains unsupported fields.' };
102
+ const selectedDevice = hueRecord(request.device);
103
+ const selectedFeature = hueRecord(request.feature);
104
+ const deviceId = selectedDevice.id;
105
+ const featureId = selectedFeature.id;
106
+ const capability = selectedFeature.capability;
107
+ const value = request.value;
108
+ if (selectedDevice.integrationDomain !== this.domain || typeof deviceId !== 'string' || typeof featureId !== 'string') {
109
+ return { success: false, error: 'Hue feature requires one exact Hue device and feature.' };
110
+ }
111
+ return this.client.execute(async (session) => {
112
+ const snapshot = await session.getResources(abortSignalArg);
113
+ const device = HueMapper.toDevices(snapshot).find((deviceArg) => deviceArg.id === deviceId);
114
+ const feature = device?.features.find((featureArg) => featureArg.id === featureId && featureArg.capability === capability && featureArg.writable);
115
+ const lightId = this.resolveLight(snapshot, undefined, device?.id);
116
+ if (!device || !device.online || !feature || !lightId) return { success: false, error: 'Hue feature target is unknown, unavailable or not writable.' };
117
+ let state: IHueLightStatePatch;
118
+ if (feature.id === 'on' && typeof value === 'boolean') state = { on: value };
119
+ else if (feature.id === 'brightness' && typeof value === 'number') state = { brightness: value };
120
+ else return { success: false, error: 'Hue feature value has an invalid type.' };
121
+ await session.setLightState(lightId, state, abortSignalArg);
122
+ return { success: true };
123
+ }, abortSignalArg);
100
124
  }
101
125
 
102
- public async destroy(): Promise<void> {
103
- await this.client.destroy();
104
- this.released();
105
- }
126
+ public destroy(): Promise<void> { return this.lease.close(); }
106
127
 
107
128
  private resolveLight(snapshotArg: IHueResourceResponse, entityIdArg: unknown, deviceIdArg: unknown): string | undefined {
108
129
  if (entityIdArg === undefined && deviceIdArg === undefined) return undefined;
109
130
  const entity = entityIdArg === undefined ? undefined : HueMapper.toEntities(snapshotArg).find((entityArg) => entityArg.id === entityIdArg);
110
- const device = deviceIdArg === undefined ? undefined : snapshotArg.lights.find((lightArg) => `hue.light.${lightArg.id}` === deviceIdArg);
131
+ const device = deviceIdArg === undefined ? undefined : snapshotArg.lights.find((lightArg) => HueMapper.lightDeviceId(snapshotArg.homeId, lightArg.id) === deviceIdArg);
111
132
  if (entityIdArg !== undefined && !entity || deviceIdArg !== undefined && !device) return undefined;
112
- if (entity && device && entity.deviceId !== `hue.light.${device.id}`) return undefined;
113
- return device?.id || (entity ? snapshotArg.lights.find((lightArg) => entity.deviceId === `hue.light.${lightArg.id}`)?.id : undefined);
133
+ if (entity && device && entity.deviceId !== HueMapper.lightDeviceId(snapshotArg.homeId, device.id)) return undefined;
134
+ return device?.id || (entity ? snapshotArg.lights.find((lightArg) => entity.deviceId === HueMapper.lightDeviceId(snapshotArg.homeId, lightArg.id))?.id : undefined);
114
135
  }
115
136
  }
@@ -3,10 +3,13 @@ import type { IIntegrationEntity, IIntegrationEvent } from '../../core/types.js'
3
3
  import type { IHueEvent, IHueResourceResponse } from './hue.types.js';
4
4
 
5
5
  export class HueMapper {
6
+ public static lightDeviceId(homeIdArg: string, lightIdArg: string): string { return `hue.light.${encodeURIComponent(homeIdArg)}.${lightIdArg}`; }
7
+ public static lightEntityId(homeIdArg: string, lightIdArg: string): string { return `light.hue.${encodeURIComponent(homeIdArg)}.${lightIdArg}`; }
8
+
6
9
  public static toDevices(resourcesArg: IHueResourceResponse): plugins.shxInterfaces.data.IDeviceDefinition[] {
7
10
  const timestamp = new Date().toISOString();
8
11
  const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [{
9
- id: `hue.bridge.${resourcesArg.bridge.id}`,
12
+ id: `hue.bridge.${encodeURIComponent(resourcesArg.homeId)}.${resourcesArg.bridge.id}`,
10
13
  integrationDomain: 'hue', name: resourcesArg.bridge.metadata?.name || 'Hue Bridge', protocol: 'hue',
11
14
  manufacturer: resourcesArg.bridge.productData?.manufacturerName,
12
15
  model: resourcesArg.bridge.productData?.productName || resourcesArg.bridge.productData?.modelId,
@@ -17,7 +20,7 @@ export class HueMapper {
17
20
  for (const light of resourcesArg.lights) {
18
21
  const owner = resourcesArg.devices.find((deviceArg) => deviceArg.id === light.owner.rid);
19
22
  devices.push({
20
- id: `hue.light.${light.id}`, integrationDomain: 'hue',
23
+ id: HueMapper.lightDeviceId(resourcesArg.homeId, light.id), integrationDomain: 'hue',
21
24
  name: light.metadata?.name || owner?.metadata?.name || `Hue Light ${light.id}`, protocol: 'hue',
22
25
  manufacturer: owner?.productData?.manufacturerName,
23
26
  model: owner?.productData?.productName || owner?.productData?.modelId,
@@ -26,7 +29,7 @@ export class HueMapper {
26
29
  ...(light.dimming ? [{ id: 'brightness', capability: 'light' as const, name: 'Brightness', readable: true, writable: true, unit: '%' }] : [])],
27
30
  state: [{ featureId: 'on', value: light.on.on, updatedAt: timestamp },
28
31
  ...(light.dimming ? [{ featureId: 'brightness', value: light.dimming.brightness, updatedAt: timestamp }] : [])],
29
- metadata: { hueOwnerRid: light.owner.rid },
32
+ metadata: { homeId: resourcesArg.homeId, hueOwnerRid: light.owner.rid },
30
33
  });
31
34
  }
32
35
  return devices;
@@ -34,8 +37,8 @@ export class HueMapper {
34
37
 
35
38
  public static toEntities(resourcesArg: IHueResourceResponse): IIntegrationEntity[] {
36
39
  return resourcesArg.lights.map((lightArg) => ({
37
- id: `light.${lightArg.id}`, uniqueId: `hue_light_${lightArg.id}`, integrationDomain: 'hue',
38
- deviceId: `hue.light.${lightArg.id}`, platform: 'light',
40
+ id: HueMapper.lightEntityId(resourcesArg.homeId, lightArg.id), uniqueId: `hue_light_${encodeURIComponent(resourcesArg.homeId)}_${lightArg.id}`, integrationDomain: 'hue',
41
+ deviceId: HueMapper.lightDeviceId(resourcesArg.homeId, lightArg.id), platform: 'light',
39
42
  name: lightArg.metadata?.name || resourcesArg.devices.find((deviceArg) => deviceArg.id === lightArg.owner.rid)?.metadata?.name || `Hue Light ${lightArg.id}`,
40
43
  state: lightArg.on.on ? 'on' : 'off',
41
44
  attributes: lightArg.dimming ? { brightness: lightArg.dimming.brightness } : {}, available: lightArg.available,
@@ -1,13 +1,48 @@
1
+ import type * as plugins from '../../plugins.js';
2
+ import type { IConfigEncryptionKeyProvider } from '../../core/classes.configstore.js';
3
+
1
4
  export interface IHueConfig {
5
+ homeId: string;
2
6
  bridgeId: string;
3
7
  host: string;
4
- appKey: string;
8
+ credentialReference: string;
5
9
  port?: number;
6
10
  /** PEM CA supplied by the operator from a trusted source. */
7
11
  trustedCa?: string;
8
12
  requestTimeoutMs?: number;
9
13
  }
10
14
 
15
+ export interface IHueCredentialRequest {
16
+ readonly homeId: string;
17
+ readonly origin: string;
18
+ readonly bridgeId: string;
19
+ readonly credentialReference: string;
20
+ readonly signal: AbortSignal;
21
+ /** Monotonic deadline shared by provider work and HTTPS dispatch. */
22
+ readonly deadline: number;
23
+ }
24
+
25
+ export interface IHueCredentialProvider {
26
+ resolve(requestArg: IHueCredentialRequest): Promise<string>;
27
+ /** Creates a reference without replacing an existing credential. */
28
+ create?(requestArg: IHueCredentialRequest, appKeyArg: string): Promise<void>;
29
+ }
30
+
31
+ export interface IHueControlPermission {
32
+ readonly resource: 'config' | 'lights';
33
+ readonly resourceId: string;
34
+ readonly fields: readonly string[];
35
+ }
36
+
37
+ export interface IHueIntegrationOptions {
38
+ readonly homeId?: string;
39
+ readonly capacity?: number;
40
+ readonly credentialProvider?: IHueCredentialProvider;
41
+ readonly controlPermissions?: readonly IHueControlPermission[];
42
+ readonly db?: plugins.nosqldb.SmartdataDb;
43
+ readonly keyProvider?: IConfigEncryptionKeyProvider;
44
+ }
45
+
11
46
  export interface IHueLightStatePatch {
12
47
  on?: boolean;
13
48
  /** Hue v2 brightness is a percentage, from 0 through 100. */
@@ -40,6 +75,7 @@ export interface IHueLightResource {
40
75
  }
41
76
 
42
77
  export interface IHueResourceResponse {
78
+ homeId: string;
43
79
  bridge: IHueDeviceResource;
44
80
  devices: IHueDeviceResource[];
45
81
  lights: IHueLightResource[];
@@ -1,5 +1,5 @@
1
1
  import * as plugins from '../../plugins.js';
2
- import type { IHueConfig, IHueDeviceResource, IHueEvent, IHueLightResource, IHueLightStatePatch, IHueResourceResponse } from './hue.types.js';
2
+ import type { IHueConfig, IHueDeviceResource, IHueEvent, IHueLightResource, IHueLightStatePatch, IHueResourceResponse, IHueIntegrationOptions } from './hue.types.js';
3
3
 
4
4
  export const hueContentType = (valueArg: unknown, expectedArg: string): boolean =>
5
5
  typeof valueArg === 'string' && valueArg.split(';', 1)[0].trim().toLowerCase() === expectedArg;
@@ -10,13 +10,101 @@ export const hueUuid = (valueArg: unknown): valueArg is string =>
10
10
  export const hueBridgeId = (valueArg: unknown): valueArg is string =>
11
11
  typeof valueArg === 'string' && /^[0-9a-f]{16}$/i.test(valueArg);
12
12
 
13
+ /** Capture plain data fields without invoking caller accessors or retaining mutable objects. */
13
14
  export const hueRecord = (valueArg: unknown): Record<string, unknown> => {
14
- if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
15
- throw new Error('Hue returned an invalid object.');
15
+ if (!valueArg || typeof valueArg !== 'object'
16
+ || (Object.getPrototypeOf(valueArg) !== Object.prototype && Object.getPrototypeOf(valueArg) !== null)) {
17
+ throw new Error('Hue requires a plain data object.');
16
18
  }
17
- return valueArg as Record<string, unknown>;
19
+ const record: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
20
+ for (const key of Reflect.ownKeys(valueArg)) {
21
+ const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
22
+ if (typeof key !== 'string' || !descriptor || !descriptor.enumerable || !('value' in descriptor)) {
23
+ throw new Error('Hue data contains an unsupported field or accessor.');
24
+ }
25
+ record[key] = descriptor.value as unknown;
26
+ }
27
+ return record;
28
+ };
29
+
30
+
31
+ export const hueData = (valueArg: unknown, allowedArg: readonly string[]): Record<string, unknown> => {
32
+ const value = hueRecord(valueArg);
33
+ if (Object.keys(value).some((key) => !allowedArg.includes(key))) throw new TypeError('Hue data contains unsupported fields.');
34
+ return value;
35
+ };
36
+
37
+ export const hueText = (valueArg: unknown, nameArg: string, maximumArg = 256): string => {
38
+ if (typeof valueArg !== 'string' || !valueArg || valueArg.length > maximumArg || valueArg.trim() !== valueArg || /[\p{Cc}\p{Cs}]/u.test(valueArg)) throw new TypeError(`Invalid Hue ${nameArg}.`);
39
+ return valueArg;
40
+ };
41
+
42
+ export const hueKey = (valueArg: unknown): string => {
43
+ if (typeof valueArg !== 'string' || !/^[a-z0-9_-]{1,256}$/i.test(valueArg)) throw new TypeError('Invalid Hue application credential.');
44
+ return valueArg;
18
45
  };
19
46
 
47
+ export const hueOrigin = (configArg: IHueConfig): string => new URL(`https://${plugins.net.isIP(configArg.host) === 6 ? `[${configArg.host}]` : configArg.host}:${configArg.port ?? 443}`).origin;
48
+
49
+ function method(valueArg: unknown, keyArg: string): ((...argsArg: never[]) => unknown) | undefined {
50
+ if (!valueArg || typeof valueArg !== 'object') throw new TypeError('Invalid Hue credential provider.');
51
+ let target: object | null = valueArg;
52
+ for (let depth = 0; target && depth < 16; depth++) {
53
+ const descriptor = Object.getOwnPropertyDescriptor(target, keyArg);
54
+ if (descriptor) {
55
+ if (!('value' in descriptor) || typeof descriptor.value !== 'function') throw new TypeError('Hue provider requires data methods.');
56
+ return Function.prototype.bind.call(descriptor.value, valueArg) as (...argsArg: never[]) => unknown;
57
+ }
58
+ target = Object.getPrototypeOf(target) as object | null;
59
+ }
60
+ return undefined;
61
+ }
62
+
63
+ export function captureHueOptions(valueArg: IHueIntegrationOptions = {}): Readonly<IHueIntegrationOptions> {
64
+ const value = hueData(valueArg, ['homeId', 'capacity', 'credentialProvider', 'controlPermissions', 'db', 'keyProvider']);
65
+ const result: IHueIntegrationOptions = {};
66
+ if (value.homeId !== undefined) Object.assign(result, { homeId: hueText(value.homeId, 'constructor home') });
67
+ if (value.capacity !== undefined) {
68
+ if (typeof value.capacity !== 'number' || !Number.isSafeInteger(value.capacity) || value.capacity < 1 || value.capacity > 16) throw new TypeError('Hue capacity must be from 1 through 16.');
69
+ Object.assign(result, { capacity: value.capacity });
70
+ }
71
+ if (value.credentialProvider !== undefined) {
72
+ const resolve = method(value.credentialProvider, 'resolve');
73
+ const create = method(value.credentialProvider, 'create');
74
+ if (!resolve) throw new TypeError('Hue credential provider must resolve references.');
75
+ Object.assign(result, { credentialProvider: Object.freeze({ resolve, ...(create ? { create } : {}) }) });
76
+ }
77
+ if (value.controlPermissions !== undefined) {
78
+ if (!Array.isArray(value.controlPermissions) || value.controlPermissions.length > 128) throw new TypeError('Invalid Hue control permissions.');
79
+ const permissions = dataArray(value.controlPermissions).map((permissionArg: unknown) => {
80
+ const permission = hueData(permissionArg, ['resource', 'resourceId', 'fields']);
81
+ if (typeof permission.resource !== 'string' || !['config', 'lights'].includes(permission.resource)) throw new TypeError('Invalid Hue permission resource.');
82
+ const resourceId = hueText(permission.resourceId, 'permission resource', 128).toLowerCase();
83
+ if (permission.resource === 'lights' ? !hueUuid(resourceId) : !hueBridgeId(resourceId)) throw new TypeError('Invalid Hue permission identity.');
84
+ if (!Array.isArray(permission.fields) || !permission.fields.length || permission.fields.length > 32) throw new TypeError('Invalid Hue permission fields.');
85
+ const fields = dataArray(permission.fields).map((fieldArg: unknown) => hueText(fieldArg, 'permission field', 64));
86
+ const supported = permission.resource === 'lights' ? ['on', 'brightness'] : ['pair', 'credentials:create'];
87
+ if (fields.some((field) => !supported.includes(field))) throw new TypeError('Invalid Hue permission field.');
88
+ return Object.freeze({ resource: permission.resource, resourceId, fields: Object.freeze(fields) });
89
+ });
90
+ Object.assign(result, { controlPermissions: Object.freeze(permissions) });
91
+ }
92
+ if (value.db !== undefined) Object.assign(result, { db: value.db });
93
+ if (value.keyProvider !== undefined) {
94
+ const currentKey = method(value.keyProvider, 'currentKey');
95
+ const keyForId = method(value.keyProvider, 'keyForId');
96
+ if (!currentKey || !keyForId) throw new TypeError('Hue encrypted storage requires data-method key providers.');
97
+ Object.assign(result, { keyProvider: Object.freeze({ currentKey, keyForId }) });
98
+ }
99
+ if (Boolean(value.db) !== Boolean(value.keyProvider)) throw new TypeError('Hue owned encrypted storage requires both database and key provider.');
100
+ return Object.freeze(result);
101
+ }
102
+
103
+ function dataArray(valueArg: unknown[]): unknown[] {
104
+ if (Reflect.ownKeys(valueArg).some((key) => key !== 'length' && (typeof key !== 'string' || !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= valueArg.length))) throw new TypeError('Hue arrays require dense data fields.');
105
+ return Array.from({ length: valueArg.length }, (_, index) => { const descriptor = Object.getOwnPropertyDescriptor(valueArg, String(index)); if (!descriptor?.enumerable || !('value' in descriptor)) throw new TypeError('Hue arrays require dense data fields.'); return descriptor.value as unknown; });
106
+ }
107
+
20
108
  export const normalizeHueHost = (valueArg: unknown): string => {
21
109
  if (typeof valueArg !== 'string' || valueArg.length > 253 || valueArg !== valueArg.trim()) {
22
110
  throw new Error('Hue host must be a hostname or IP address.');
@@ -30,16 +118,17 @@ export const normalizeHueHost = (valueArg: unknown): string => {
30
118
  return host;
31
119
  };
32
120
 
33
- export const normalizeHueConfig = (configArg: IHueConfig): Required<Pick<IHueConfig, 'bridgeId' | 'host' | 'appKey' | 'port' | 'requestTimeoutMs'>> & Pick<IHueConfig, 'trustedCa'> => {
121
+ export const normalizeHueConfig = (configArg: IHueConfig): Readonly<Required<Omit<IHueConfig, 'trustedCa'>> & Pick<IHueConfig, 'trustedCa'>> => {
34
122
  const record = hueRecord(configArg);
35
- const allowed = new Set(['bridgeId', 'host', 'appKey', 'port', 'trustedCa', 'requestTimeoutMs']);
123
+ if (Object.hasOwn(record, 'appKey')) throw new Error('Hue raw appKey configuration is no longer supported. Migrate the key to a home-bound credential reference.');
124
+ const allowed = new Set(['homeId', 'bridgeId', 'host', 'credentialReference', 'port', 'trustedCa', 'requestTimeoutMs']);
36
125
  if (Reflect.ownKeys(record).some((keyArg) => typeof keyArg !== 'string' || !allowed.has(keyArg))) {
37
126
  throw new Error('Hue configuration contains unsupported fields.');
38
127
  }
39
128
  if (!hueBridgeId(record.bridgeId)) throw new Error('Hue bridgeId must contain 16 hexadecimal characters.');
40
- if (typeof record.appKey !== 'string' || !/^[a-z0-9_-]{1,256}$/i.test(record.appKey)) {
41
- throw new Error('Hue appKey must be a nonempty application key.');
42
- }
129
+ const homeId = hueText(record.homeId, 'configuration home');
130
+ const credentialReference = hueText(record.credentialReference, 'credential reference');
131
+ if (encodeURIComponent(credentialReference).length > 900) throw new Error('Hue credential reference is too long.');
43
132
  const port = record.port ?? 443;
44
133
  const requestTimeoutMs = record.requestTimeoutMs ?? 10_000;
45
134
  if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65_535) throw new Error('Hue port is invalid.');
@@ -48,8 +137,8 @@ export const normalizeHueConfig = (configArg: IHueConfig): Required<Pick<IHueCon
48
137
  }
49
138
  if (record.trustedCa !== undefined && (typeof record.trustedCa !== 'string' || record.trustedCa.length > 65_536
50
139
  || !record.trustedCa.includes('-----BEGIN CERTIFICATE-----'))) throw new Error('Hue trustedCa must contain a PEM certificate.');
51
- return { bridgeId: record.bridgeId.toLowerCase(), host: normalizeHueHost(record.host), appKey: record.appKey, port, requestTimeoutMs,
52
- trustedCa: typeof record.trustedCa === 'string' ? record.trustedCa : undefined };
140
+ return Object.freeze({ homeId, credentialReference, bridgeId: record.bridgeId.toLowerCase(), host: normalizeHueHost(record.host), port, requestTimeoutMs,
141
+ trustedCa: typeof record.trustedCa === 'string' ? record.trustedCa : undefined });
53
142
  };
54
143
 
55
144
  export const validateHuePatch = (patchArg: IHueLightStatePatch): void => {
@@ -86,7 +175,7 @@ const deviceResource = (resourceArg: Record<string, unknown>): IHueDeviceResourc
86
175
  productName: optionalString(product.product_name), softwareVersion: optionalString(product.software_version) } : undefined };
87
176
  };
88
177
 
89
- export const parseHueResources = (dataArg: unknown[], bridgeIdArg: string): IHueResourceResponse => {
178
+ export const parseHueResources = (dataArg: unknown[], bridgeIdArg: string, homeIdArg: string): IHueResourceResponse => {
90
179
  const devices = new Map<string, IHueDeviceResource>();
91
180
  const connectivity = new Map<string, boolean>();
92
181
  const lightResources: Record<string, unknown>[] = [];
@@ -133,7 +222,7 @@ export const parseHueResources = (dataArg: unknown[], bridgeIdArg: string): IHue
133
222
  on: { on: power.on }, dimming: dimming ? { brightness: dimming.brightness as number } : undefined,
134
223
  available: connectivity.get(owner.rid.toLowerCase()) === true };
135
224
  });
136
- return { bridge, devices: [...devices.values()], lights };
225
+ return { homeId: hueText(homeIdArg, 'resource home'), bridge, devices: [...devices.values()], lights };
137
226
  };
138
227
 
139
228
  export const parseHueEvents = (valueArg: unknown): IHueEvent[] => {
@@ -6,19 +6,27 @@ Native TypeScript integration for Philips Hue API v2 bridges. It discovers bridg
6
6
 
7
7
  For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
8
8
 
9
- ## Configuration and TLS
9
+ ## Configuration, credentials and TLS
10
10
 
11
- `IHueConfig` requires `bridgeId` (the bridge's 16-character hexadecimal ID), `host` (an IP address or hostname), and `appKey`. Optional fields are `port` (443 by default), `trustedCa` (a PEM CA certificate), and `requestTimeoutMs` (10000 by default, permitted range 50–60000).
11
+ `IHueConfig` requires `homeId`, `bridgeId` (16 hexadecimal characters), `host` and `credentialReference`. Optional fields are `port` (443), `trustedCa` (operator-supplied PEM CA) and `requestTimeoutMs` (10000, permitted range 50–60000). Setup, config flows and the public `HueClient(config, context, options)` require an independently selected `context.homeId`. Configuration and discovery cannot choose that authority. Constructor `options.homeId`, configuration home, and any supplied store must match it. The first admitted home remains bound even when readiness fails.
12
12
 
13
- Create an application key using the bridge's physical link button and the [Hue developer setup procedure](https://developers.meethue.com/develop/get-started-2/). This integration accepts that key; it does not generate or print credentials. Its config flow verifies a live resource read before returning a completed configuration. The application key travels only in the `hue-application-key` request header. Inject the host's encrypted `IConfigStore` into the normal integration setup flow when storing the resulting configuration.
13
+ Supply an `IHueCredentialProvider` in owner options or an actual `NoSqlConfigStore` scoped to that home and the `hue` domain in the context. A structural store is rejected. A provider receives a frozen request containing `homeId`, HTTPS `origin`, `bridgeId`, `credentialReference`, `signal` and a monotonic `deadline`; it must resolve the exact selected credential and honor its own non-replacing `create` contract. Provider methods and control permissions are captured at construction. Resolution is joined even if cancellation wins, and a late result cannot open HTTPS. Provider errors are redacted. Keys are resolved per operation and never retained in configuration.
14
14
 
15
- Certificate-chain verification remains enabled. The certificate must identify the configured bridge ID, even when the connection uses an IP address. Hue bridges using the Signify private CA require the operator to supply the appropriate trusted CA from the [Hue HTTPS guidance](https://developers.meethue.com/develop/application-design-guidance/using-https/). The system trust store is used when `trustedCa` is absent. No trust-on-first-use, HTTP downgrade, disabled verification or caller-supplied resource snapshots are supported. Discovery metadata does not establish a trusted bridge identity.
15
+ The encrypted-store reference key is `hue.credential.<encodeURIComponent(credentialReference)>`. Its encrypted value is `{ version: 1, homeId, origin, bridgeId, credentialReference, appKey }`; every scope field must match before the key is used. Creation uses insert-only `NoSqlConfigStore.create`, after TLS and live bridge-resource validation. `options.db` plus `options.keyProvider` can instead create an owned store wrapper at the admitted home/domain. The wrapper is closed with the client; a supplied store, database and key provider remain caller-owned. Normal persistence uses the public document-client APIs.
16
16
 
17
- Use `HueIntegration` from `@smarthome.exchange/integrations` with operator-supplied configuration:
17
+ The config flow accepts an existing key only for one-time credential insertion. Without a supplied key, it uses HTTPS `POST /api` with the physical link button, validates the resulting key against API v2 resources and passes it to the provider/store. Error type 101 offers an explicit link-button retry. An indeterminate dispatched pairing or credential creation is terminal; shutdown and cleanup never replay it. Completed config and flow output contain only the reference, and events/errors never expose the application key. Pairing requires exact `config` permission for the selected bridge ID with both `pair` and `credentials:create`; insertion of an existing key requires `credentials:create`. The application key is sent in the `hue-application-key` header for authenticated requests.
18
+
19
+ Certificate-chain verification stays enabled. The certificate must identify the configured bridge ID even when connecting by IP. Hue bridges using the Signify private CA require the trusted CA from the [Hue HTTPS guidance](https://developers.meethue.com/develop/application-design-guidance/using-https/). The system trust store is used when `trustedCa` is absent. Discovery metadata is a selection hint; setup verifies TLS identity and the live `bridge_id`.
18
20
 
19
21
  ```typescript
20
- const integration = new HueIntegration();
21
- const runtime = await integration.setup({ bridgeId, host, appKey, trustedCa }, { configStore });
22
+ import { HueIntegration } from '@smarthome.exchange/integrations';
23
+
24
+ // homeId and the actual home/domain-scoped configStore are selected by the host.
25
+ const integration = new HueIntegration({
26
+ homeId,
27
+ controlPermissions: [{ resource: 'lights', resourceId: selectedLightUuid, fields: ['on', 'brightness'] }],
28
+ });
29
+ const runtime = await integration.setup({ homeId, bridgeId, host, credentialReference, trustedCa }, { homeId, configStore });
22
30
  try {
23
31
  const lights = (await runtime.devices()).filter((device) => device.id.startsWith('hue.light.'));
24
32
  console.log(lights.map((device) => ({ id: device.id, name: device.name, online: device.online })));
@@ -27,13 +35,38 @@ try {
27
35
  }
28
36
  ```
29
37
 
38
+ ## Migration from raw appKey configurations
39
+
40
+ Raw `appKey` configurations are refused with a migration error, including when a reference is also supplied. Migrate explicitly by inserting the existing key into the selected host store and saving only the completed configuration. `configStore` must be an actual encrypted store scoped to `homeId` and `hue`:
41
+
42
+ ```typescript
43
+ const integration = new HueIntegration({
44
+ homeId,
45
+ controlPermissions: [{ resource: 'config', resourceId: bridgeId, fields: ['credentials:create'] }],
46
+ });
47
+ try {
48
+ const form = await integration.configFlow.start({
49
+ source: 'manual', integrationDomain: 'hue', id: bridgeId, host, port,
50
+ }, { homeId, configStore });
51
+ const result = await form.submit!({ credentialReference, appKey: existingAppKey, trustedCa });
52
+ if (result.kind !== 'done' || !result.config) throw new Error('Hue credential migration failed.');
53
+ // Persist result.config through the host config store; retire the old raw-key config.
54
+ } finally {
55
+ await integration.destroy();
56
+ }
57
+ ```
58
+
59
+ Use a new reference if the selected reference already exists; insertion never replaces it. Update stored entity/device targets to the home-qualified identities below. Construct a runtime owner with the desired light grants after migration; pairing permissions do not grant light controls.
60
+
30
61
  ## Reads and controls
31
62
 
32
- Setup checks both the TLS identity and the `bridge_id` returned by `/clip/v2/resource`. Device/light IDs are validated UUIDs. Entities use `light.<resource UUID>` and devices use `hue.light.<resource UUID>`; changing a display name preserves identity. This replaces the previous display-name entity IDs. The `resources` configuration fixture field has been removed.
63
+ Setup checks both the TLS identity and the `bridge_id` returned by `/clip/v2/resource`. Device/light IDs are validated UUIDs. Entities use `light.hue.<encoded home>.<resource UUID>`, light devices use `hue.light.<encoded home>.<resource UUID>`, and bridge devices use `hue.bridge.<encoded home>.<bridge-device UUID>`; changing a display name preserves identity. Entity unique IDs also include the admitted home. Two homes cannot share a routing identity. The `resources` configuration fixture field has been removed.
33
64
 
34
65
  Lights advertise brightness only when the bridge reports the `dimming` capability. Availability follows the owning device's Zigbee connectivity resource; missing or disconnected connectivity is unavailable. Manufacturer and model come from the owning device, including supported third-party Hue lights.
35
66
 
36
- Light services are `turn_on`, `turn_off` and `set_percentage`. `turn_on` accepts optional `brightness` from 0 to 100; `set_percentage` requires `percentage` in that same range. A target must match the current entity/device exactly; supplying both requires them to identify the same light. `writeFeature` supports boolean `on` and numeric `brightness` only for an available light's declared writable capabilities. Unsupported fields, unknown targets and values outside the protocol range are rejected.
67
+ Controls default to denied. Each write requires one immutable `lights` permission matching the raw light UUID and every selected field (`on`, `brightness`). No wildcard grant is accepted. The public client enforces the same permission boundary as the runtime.
68
+
69
+ Light services are `turn_on`, `turn_off` and `set_percentage`. `turn_on` accepts optional `brightness` from 0 to 100; `set_percentage` requires `percentage` in that same range. A target must match the current entity/device exactly; supplying both requires them to identify the same light. `writeFeature` supports boolean `on` and numeric `brightness` only for an available light's declared writable capabilities. Unsupported fields, unknown targets and values outside the protocol range are rejected. Controls capture plain-data target IDs, domain, capability and value before awaiting a fresh read; later caller mutation cannot retarget a write. Accessors, symbols, hidden fields and class instances are rejected without invoking getters. Setup cancellation and config-flow discovery endpoint hints are also captured before awaiting readiness or displaying a form.
37
70
 
38
71
  Writes use `PUT /clip/v2/resource/light/<UUID>` and require the bridge's matching resource acknowledgement. Success means the bridge accepted the write; it does not guarantee physical delivery. Accepted writes never change local read state. Each `devices()` or `entities()` call reads current authoritative resources, and writes are not automatically retried.
39
72
 
@@ -41,13 +74,17 @@ Writes use `PUT /clip/v2/resource/light/<UUID>` and require the bridge's matchin
41
74
 
42
75
  `subscribe()` opens authenticated HTTPS `/eventstream/clip/v2` and returns an asynchronous, idempotent unsubscribe function. Change notifications contain only relevant resource IDs/types; consumers reread authoritative state. Unknown resource payloads are omitted because they may contain credentials. Invalid frames, stream termination and transport failures produce an error event and mark the runtime unhealthy. Reads do not silently restart a failed subscription; the host may explicitly establish a new one or replace the runtime.
43
76
 
44
- Requests have whole-response deadlines and a 4 MiB body limit. Discovery uses the fixed documented `https://discovery.meethue.com/` broker with a five-second deadline and 256 KiB body limit. SSE frames are limited to 1 MiB and batches to 256 events. A client permits at most 32 outstanding connections, two event subscriptions, and three simultaneous HTTPS sockets. Caller cancellation and `destroy()` close owned requests/streams; destroy waits for them to close. The integration also owns pending setups and destroys their clients on failure or shutdown.
77
+ One shared `NativeIntegrationOwner` bounds pending setups, promoted runtimes, subscriptions, finite operations and retained cleanup resources (default capacity 16). A setup lease owns the client before readiness and promotes to a runtime only after validation. Config flows use terminal child scopes on that same owner. Previously returned forms cannot submit after flow/integration shutdown; candidate endpoint and host context are captured before any await. A runtime command holds one operation lease and permits one PUT attempt. Setup/submission also occupy a lease while their client operation runs, so capacity must leave room for both.
78
+
79
+ Requests share a whole-operation deadline with credential resolution and have a 4 MiB body limit. Discovery uses the fixed documented `https://discovery.meethue.com/` broker with a five-second deadline and 256 KiB body limit. SSE frames are limited to 1 MiB and batches to 256 events. A client permits at most 32 outstanding connections, two event subscriptions, three simultaneous HTTPS sockets and 32 pending callbacks per stream. An established stream retains shared capacity until its transport and callbacks settle; its connection deadline does not expire a live subscription.
80
+
81
+ Caller cancellation and `destroy()` close owned requests/streams and join actual provider, request and subscriber continuations. Subscriber callbacks may return promises. Unsubscribe and destroy have stable promise identity; later callbacks are fenced. Synchronous throws and async rejections are observed and use redacted subscriber errors. Failed transport/store cleanup and callback failures remain retained until explicit cleanup-only retry. `HueClient.retryCleanup()`, `HueConfigFlow.retryCleanup()` and `HueIntegration.retryCleanup()` never replay GETs, PUTs, pairing, credential creation, subscriptions or callbacks. Integration shutdown drains sibling runtimes and pending submissions before reporting cleanup failure. Runtime `destroy()` closes its promoted lease.
45
82
 
46
83
  ## Verification and limits
47
84
 
48
- Tests use disposable CA-signed local TLS bridges, including untrusted issuers, a wrong bridge certificate, resource identity mismatch, exact PUT payloads, non-optimistic reads, unsupported controls, unavailable devices, malformed/oversized bodies, cancellation and event-stream cleanup. No physical bridge or owner's home was accessed.
85
+ Tests preserve the existing 46 cases and use disposable CA-signed local TLS bridges, including untrusted issuers, a wrong bridge certificate, resource identity mismatch, exact PUT payloads, non-optimistic reads, unsupported controls, unavailable devices, malformed/oversized bodies, cancellation, plain-data/accessor validation, blocked async subscribers, sibling drainage, reentrant close, retained cleanup failures and cleanup-only retry without HTTPS replay. Ownership regressions additionally cover home-qualified routing, default-deny exact grants, shared capacity, scoped encrypted references, late provider drainage, flow shutdown, link-button pairing and secret-free completion. No physical bridge or owner's home was accessed.
49
86
 
50
- The public [Hue API announcement](https://developers.meethue.com/new-hue-api/) establishes API v2 HTTPS, local proactive events and the supported discovery methods. Detailed Hue API and HTTPS pages require a developer login and returned login/403 here. Implemented wire resource models and event paths were cross-checked against [aiohue's upstream v2 implementation](https://github.com/home-assistant-libs/aiohue/tree/main/aiohue/v2). Aiohue's disabled-TLS and geofence keepalive workaround are not used. Physical bridge firmware compatibility, operator CA provisioning and physical command delivery remain acceptance checks. Color, color temperature, scenes, entertainment streaming, grouped lights and accessory sensors are not exposed by this runtime.
87
+ The public [Hue API announcement](https://developers.meethue.com/new-hue-api/) establishes API v2 HTTPS, local proactive events and the supported discovery methods. Detailed Hue API and HTTPS pages require a developer login and returned login/403 here. The pairing wire shape (`POST /api`, `devicetype`, `success.username`) was checked against [aiohue util.py](https://github.com/home-assistant-libs/aiohue/blob/main/aiohue/util.py). Its HTTP fallback and disabled TLS are not used. Implemented wire resource models and event paths were cross-checked against [aiohue's upstream v2 implementation](https://github.com/home-assistant-libs/aiohue/tree/main/aiohue/v2). Aiohue's disabled-TLS and geofence keepalive workaround are not used. Physical bridge firmware compatibility, operator CA provisioning and physical command delivery remain acceptance checks. Color, color temperature, scenes, entertainment streaming, grouped lights and accessory sensors are not exposed by this runtime.
51
88
 
52
89
  ## License and Legal Information
53
90