@smarthome.exchange/integrations 1.0.0 → 2.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 (294) hide show
  1. package/changelog.md +49 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/core/classes.configstore.d.ts +2 -0
  4. package/dist_ts/core/classes.configstore.js +84 -27
  5. package/dist_ts/core/classes.nativehomeadmission.d.ts +19 -0
  6. package/dist_ts/core/classes.nativehomeadmission.js +84 -0
  7. package/dist_ts/core/classes.nativeintegrationowner.d.ts +79 -0
  8. package/dist_ts/core/classes.nativeintegrationowner.js +269 -0
  9. package/dist_ts/core/index.d.ts +2 -0
  10. package/dist_ts/core/index.js +3 -1
  11. package/dist_ts/core/types.d.ts +14 -0
  12. package/dist_ts/integrations/braviatv/braviatv.classes.client.d.ts +23 -72
  13. package/dist_ts/integrations/braviatv/braviatv.classes.client.js +265 -625
  14. package/dist_ts/integrations/braviatv/braviatv.classes.configflow.d.ts +7 -7
  15. package/dist_ts/integrations/braviatv/braviatv.classes.configflow.js +51 -68
  16. package/dist_ts/integrations/braviatv/braviatv.classes.integration.d.ts +8 -11
  17. package/dist_ts/integrations/braviatv/braviatv.classes.integration.js +186 -135
  18. package/dist_ts/integrations/braviatv/braviatv.discovery.js +12 -11
  19. package/dist_ts/integrations/braviatv/braviatv.mapper.d.ts +6 -14
  20. package/dist_ts/integrations/braviatv/braviatv.mapper.js +37 -131
  21. package/dist_ts/integrations/braviatv/braviatv.types.d.ts +55 -133
  22. package/dist_ts/integrations/braviatv/braviatv.validation.d.ts +7 -0
  23. package/dist_ts/integrations/braviatv/braviatv.validation.js +72 -0
  24. package/dist_ts/integrations/broadlink/broadlink.actions.d.ts +2 -0
  25. package/dist_ts/integrations/broadlink/broadlink.actions.js +36 -0
  26. package/dist_ts/integrations/broadlink/broadlink.classes.client.d.ts +21 -19
  27. package/dist_ts/integrations/broadlink/broadlink.classes.client.js +185 -124
  28. package/dist_ts/integrations/broadlink/broadlink.classes.configflow.d.ts +5 -4
  29. package/dist_ts/integrations/broadlink/broadlink.classes.configflow.js +64 -83
  30. package/dist_ts/integrations/broadlink/broadlink.classes.integration.d.ts +16 -19
  31. package/dist_ts/integrations/broadlink/broadlink.classes.integration.js +114 -59
  32. package/dist_ts/integrations/broadlink/broadlink.mapper.d.ts +3 -2
  33. package/dist_ts/integrations/broadlink/broadlink.mapper.js +2 -1
  34. package/dist_ts/integrations/broadlink/broadlink.selected.mapper.d.ts +10 -0
  35. package/dist_ts/integrations/broadlink/broadlink.selected.mapper.js +41 -0
  36. package/dist_ts/integrations/broadlink/broadlink.types.d.ts +43 -7
  37. package/dist_ts/integrations/broadlink/broadlink.validation.d.ts +7 -0
  38. package/dist_ts/integrations/broadlink/broadlink.validation.js +63 -0
  39. package/dist_ts/integrations/daikin/daikin.classes.client.d.ts +10 -52
  40. package/dist_ts/integrations/daikin/daikin.classes.client.js +55 -614
  41. package/dist_ts/integrations/daikin/daikin.classes.configflow.d.ts +8 -12
  42. package/dist_ts/integrations/daikin/daikin.classes.configflow.js +77 -138
  43. package/dist_ts/integrations/daikin/daikin.classes.integration.d.ts +7 -24
  44. package/dist_ts/integrations/daikin/daikin.classes.integration.js +175 -67
  45. package/dist_ts/integrations/daikin/daikin.discovery.d.ts +11 -24
  46. package/dist_ts/integrations/daikin/daikin.discovery.js +98 -305
  47. package/dist_ts/integrations/daikin/daikin.mapper.d.ts +6 -7
  48. package/dist_ts/integrations/daikin/daikin.mapper.js +95 -155
  49. package/dist_ts/integrations/daikin/daikin.types.d.ts +24 -44
  50. package/dist_ts/integrations/daikin/daikin.types.js +1 -1
  51. package/dist_ts/integrations/daikin/daikin.validation.d.ts +5 -0
  52. package/dist_ts/integrations/daikin/daikin.validation.js +53 -0
  53. package/dist_ts/integrations/denonavr/denonavr.classes.client.d.ts +31 -40
  54. package/dist_ts/integrations/denonavr/denonavr.classes.client.js +149 -404
  55. package/dist_ts/integrations/denonavr/denonavr.classes.configflow.d.ts +7 -2
  56. package/dist_ts/integrations/denonavr/denonavr.classes.configflow.js +44 -36
  57. package/dist_ts/integrations/denonavr/denonavr.classes.integration.d.ts +6 -11
  58. package/dist_ts/integrations/denonavr/denonavr.classes.integration.js +192 -113
  59. package/dist_ts/integrations/denonavr/denonavr.discovery.d.ts +10 -18
  60. package/dist_ts/integrations/denonavr/denonavr.discovery.js +71 -193
  61. package/dist_ts/integrations/denonavr/denonavr.mapper.d.ts +5 -13
  62. package/dist_ts/integrations/denonavr/denonavr.mapper.js +35 -159
  63. package/dist_ts/integrations/denonavr/denonavr.types.d.ts +55 -90
  64. package/dist_ts/integrations/denonavr/denonavr.validation.d.ts +10 -0
  65. package/dist_ts/integrations/denonavr/denonavr.validation.js +86 -0
  66. package/dist_ts/integrations/kodi/kodi.classes.client.d.ts +11 -45
  67. package/dist_ts/integrations/kodi/kodi.classes.client.js +84 -310
  68. package/dist_ts/integrations/kodi/kodi.classes.configflow.d.ts +9 -8
  69. package/dist_ts/integrations/kodi/kodi.classes.configflow.js +53 -48
  70. package/dist_ts/integrations/kodi/kodi.classes.integration.d.ts +11 -11
  71. package/dist_ts/integrations/kodi/kodi.classes.integration.js +114 -171
  72. package/dist_ts/integrations/kodi/kodi.discovery.js +53 -29
  73. package/dist_ts/integrations/kodi/kodi.mapper.d.ts +7 -2
  74. package/dist_ts/integrations/kodi/kodi.mapper.js +48 -10
  75. package/dist_ts/integrations/kodi/kodi.types.d.ts +32 -28
  76. package/dist_ts/integrations/kodi/kodi.validation.d.ts +9 -0
  77. package/dist_ts/integrations/kodi/kodi.validation.js +62 -0
  78. package/dist_ts/integrations/modbus/modbus.classes.client.d.ts +15 -61
  79. package/dist_ts/integrations/modbus/modbus.classes.client.js +117 -776
  80. package/dist_ts/integrations/modbus/modbus.classes.configflow.d.ts +9 -5
  81. package/dist_ts/integrations/modbus/modbus.classes.configflow.js +70 -38
  82. package/dist_ts/integrations/modbus/modbus.classes.integration.d.ts +6 -17
  83. package/dist_ts/integrations/modbus/modbus.classes.integration.js +133 -57
  84. package/dist_ts/integrations/modbus/modbus.discovery.d.ts +5 -8
  85. package/dist_ts/integrations/modbus/modbus.discovery.js +38 -93
  86. package/dist_ts/integrations/modbus/modbus.mapper.d.ts +8 -32
  87. package/dist_ts/integrations/modbus/modbus.mapper.js +71 -431
  88. package/dist_ts/integrations/modbus/modbus.types.d.ts +82 -332
  89. package/dist_ts/integrations/modbus/modbus.validation.d.ts +10 -0
  90. package/dist_ts/integrations/modbus/modbus.validation.js +128 -0
  91. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.client.d.ts +12 -34
  92. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.client.js +143 -231
  93. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.d.ts +6 -1
  94. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.js +105 -18
  95. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.d.ts +8 -8
  96. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.js +141 -155
  97. package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.d.ts +9 -0
  98. package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.js +35 -0
  99. package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.d.ts +6 -18
  100. package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.js +45 -220
  101. package/dist_ts/integrations/nanoleaf/nanoleaf.types.d.ts +33 -147
  102. package/dist_ts/integrations/nanoleaf/nanoleaf.validation.d.ts +6 -0
  103. package/dist_ts/integrations/nanoleaf/nanoleaf.validation.js +82 -0
  104. package/dist_ts/integrations/onkyo/onkyo.classes.client.d.ts +15 -20
  105. package/dist_ts/integrations/onkyo/onkyo.classes.client.js +161 -404
  106. package/dist_ts/integrations/onkyo/onkyo.classes.configflow.d.ts +9 -4
  107. package/dist_ts/integrations/onkyo/onkyo.classes.configflow.js +51 -6
  108. package/dist_ts/integrations/onkyo/onkyo.classes.integration.d.ts +22 -6
  109. package/dist_ts/integrations/onkyo/onkyo.classes.integration.js +144 -11
  110. package/dist_ts/integrations/onkyo/onkyo.discovery.d.ts +6 -1
  111. package/dist_ts/integrations/onkyo/onkyo.discovery.js +40 -4
  112. package/dist_ts/integrations/onkyo/onkyo.mapper.d.ts +7 -8
  113. package/dist_ts/integrations/onkyo/onkyo.mapper.js +45 -12
  114. package/dist_ts/integrations/onkyo/onkyo.types.d.ts +58 -8
  115. package/dist_ts/integrations/onkyo/onkyo.types.js +13 -102
  116. package/dist_ts/integrations/onkyo/onkyo.validation.d.ts +16 -0
  117. package/dist_ts/integrations/onkyo/onkyo.validation.js +77 -0
  118. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.d.ts +25 -25
  119. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.js +116 -360
  120. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.d.ts +11 -3
  121. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.js +76 -6
  122. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.d.ts +26 -4
  123. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.js +142 -9
  124. package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.d.ts +2 -1
  125. package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.js +31 -4
  126. package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.d.ts +9 -9
  127. package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.js +31 -149
  128. package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.d.ts +51 -11
  129. package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.js +22 -87
  130. package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.d.ts +6 -0
  131. package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.js +56 -0
  132. package/dist_ts/integrations/pioneer/pioneer.classes.client.d.ts +31 -16
  133. package/dist_ts/integrations/pioneer/pioneer.classes.client.js +167 -409
  134. package/dist_ts/integrations/pioneer/pioneer.classes.configflow.d.ts +10 -3
  135. package/dist_ts/integrations/pioneer/pioneer.classes.configflow.js +66 -6
  136. package/dist_ts/integrations/pioneer/pioneer.classes.integration.d.ts +13 -5
  137. package/dist_ts/integrations/pioneer/pioneer.classes.integration.js +239 -11
  138. package/dist_ts/integrations/pioneer/pioneer.discovery.d.ts +14 -1
  139. package/dist_ts/integrations/pioneer/pioneer.discovery.js +51 -4
  140. package/dist_ts/integrations/pioneer/pioneer.mapper.d.ts +14 -8
  141. package/dist_ts/integrations/pioneer/pioneer.mapper.js +54 -12
  142. package/dist_ts/integrations/pioneer/pioneer.types.d.ts +79 -12
  143. package/dist_ts/integrations/pioneer/pioneer.types.js +12 -76
  144. package/dist_ts/integrations/pioneer/pioneer.validation.d.ts +10 -0
  145. package/dist_ts/integrations/pioneer/pioneer.validation.js +85 -0
  146. package/dist_ts/integrations/soundtouch/soundtouch.classes.client.d.ts +13 -36
  147. package/dist_ts/integrations/soundtouch/soundtouch.classes.client.js +179 -502
  148. package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.d.ts +6 -1
  149. package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.js +50 -40
  150. package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.d.ts +14 -15
  151. package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.js +108 -128
  152. package/dist_ts/integrations/soundtouch/soundtouch.discovery.d.ts +2 -20
  153. package/dist_ts/integrations/soundtouch/soundtouch.discovery.js +23 -113
  154. package/dist_ts/integrations/soundtouch/soundtouch.mapper.d.ts +3 -12
  155. package/dist_ts/integrations/soundtouch/soundtouch.mapper.js +35 -189
  156. package/dist_ts/integrations/soundtouch/soundtouch.types.d.ts +57 -142
  157. package/dist_ts/integrations/soundtouch/soundtouch.types.js +18 -3
  158. package/dist_ts/integrations/soundtouch/soundtouch.validation.d.ts +22 -0
  159. package/dist_ts/integrations/soundtouch/soundtouch.validation.js +65 -0
  160. package/dist_ts/integrations/wiz/index.d.ts +1 -0
  161. package/dist_ts/integrations/wiz/index.js +2 -1
  162. package/dist_ts/integrations/wiz/wiz.classes.client.d.ts +14 -28
  163. package/dist_ts/integrations/wiz/wiz.classes.client.js +93 -229
  164. package/dist_ts/integrations/wiz/wiz.classes.configflow.d.ts +6 -4
  165. package/dist_ts/integrations/wiz/wiz.classes.configflow.js +38 -67
  166. package/dist_ts/integrations/wiz/wiz.classes.integration.d.ts +10 -21
  167. package/dist_ts/integrations/wiz/wiz.classes.integration.js +204 -44
  168. package/dist_ts/integrations/wiz/wiz.discovery.d.ts +10 -31
  169. package/dist_ts/integrations/wiz/wiz.discovery.js +48 -249
  170. package/dist_ts/integrations/wiz/wiz.mapper.d.ts +7 -51
  171. package/dist_ts/integrations/wiz/wiz.mapper.js +82 -668
  172. package/dist_ts/integrations/wiz/wiz.types.d.ts +34 -208
  173. package/dist_ts/integrations/wiz/wiz.validation.d.ts +8 -0
  174. package/dist_ts/integrations/wiz/wiz.validation.js +72 -0
  175. package/dist_ts/integrations/yamaha/index.d.ts +1 -0
  176. package/dist_ts/integrations/yamaha/index.js +2 -1
  177. package/dist_ts/integrations/yamaha/yamaha.classes.client.d.ts +30 -4
  178. package/dist_ts/integrations/yamaha/yamaha.classes.client.js +162 -6
  179. package/dist_ts/integrations/yamaha/yamaha.classes.configflow.d.ts +10 -3
  180. package/dist_ts/integrations/yamaha/yamaha.classes.configflow.js +106 -6
  181. package/dist_ts/integrations/yamaha/yamaha.classes.integration.d.ts +22 -4
  182. package/dist_ts/integrations/yamaha/yamaha.classes.integration.js +243 -8
  183. package/dist_ts/integrations/yamaha/yamaha.discovery.d.ts +13 -1
  184. package/dist_ts/integrations/yamaha/yamaha.discovery.js +57 -4
  185. package/dist_ts/integrations/yamaha/yamaha.mapper.d.ts +16 -8
  186. package/dist_ts/integrations/yamaha/yamaha.mapper.js +58 -12
  187. package/dist_ts/integrations/yamaha/yamaha.types.d.ts +73 -6
  188. package/dist_ts/integrations/yamaha/yamaha.types.js +8 -94
  189. package/dist_ts/integrations/yamaha/yamaha.validation.d.ts +12 -0
  190. package/dist_ts/integrations/yamaha/yamaha.validation.js +89 -0
  191. package/package.json +3 -3
  192. package/readme.md +514 -2
  193. package/ts/00_commitinfo_data.ts +1 -1
  194. package/ts/core/classes.configstore.ts +84 -27
  195. package/ts/core/classes.nativehomeadmission.ts +96 -0
  196. package/ts/core/classes.nativeintegrationowner.ts +297 -0
  197. package/ts/core/index.ts +2 -0
  198. package/ts/core/types.ts +16 -0
  199. package/ts/integrations/braviatv/braviatv.classes.client.ts +245 -709
  200. package/ts/integrations/braviatv/braviatv.classes.configflow.ts +47 -76
  201. package/ts/integrations/braviatv/braviatv.classes.integration.ts +167 -147
  202. package/ts/integrations/braviatv/braviatv.discovery.ts +11 -10
  203. package/ts/integrations/braviatv/braviatv.mapper.ts +39 -142
  204. package/ts/integrations/braviatv/braviatv.types.ts +53 -173
  205. package/ts/integrations/braviatv/braviatv.validation.ts +68 -0
  206. package/ts/integrations/broadlink/broadlink.actions.ts +31 -0
  207. package/ts/integrations/broadlink/broadlink.classes.client.ts +170 -144
  208. package/ts/integrations/broadlink/broadlink.classes.configflow.ts +59 -89
  209. package/ts/integrations/broadlink/broadlink.classes.integration.ts +110 -64
  210. package/ts/integrations/broadlink/broadlink.mapper.ts +5 -4
  211. package/ts/integrations/broadlink/broadlink.selected.mapper.ts +46 -0
  212. package/ts/integrations/broadlink/broadlink.types.ts +48 -8
  213. package/ts/integrations/broadlink/broadlink.validation.ts +66 -0
  214. package/ts/integrations/daikin/daikin.classes.client.ts +52 -661
  215. package/ts/integrations/daikin/daikin.classes.configflow.ts +72 -152
  216. package/ts/integrations/daikin/daikin.classes.integration.ts +166 -71
  217. package/ts/integrations/daikin/daikin.discovery.ts +94 -346
  218. package/ts/integrations/daikin/daikin.mapper.ts +83 -166
  219. package/ts/integrations/daikin/daikin.types.ts +27 -48
  220. package/ts/integrations/daikin/daikin.validation.ts +51 -0
  221. package/ts/integrations/daikin/readme.md +83 -0
  222. package/ts/integrations/denonavr/denonavr.classes.client.ts +132 -421
  223. package/ts/integrations/denonavr/denonavr.classes.configflow.ts +44 -37
  224. package/ts/integrations/denonavr/denonavr.classes.integration.ts +171 -117
  225. package/ts/integrations/denonavr/denonavr.discovery.ts +67 -220
  226. package/ts/integrations/denonavr/denonavr.mapper.ts +36 -182
  227. package/ts/integrations/denonavr/denonavr.types.ts +48 -111
  228. package/ts/integrations/denonavr/denonavr.validation.ts +87 -0
  229. package/ts/integrations/kodi/kodi.classes.client.ts +72 -352
  230. package/ts/integrations/kodi/kodi.classes.configflow.ts +52 -55
  231. package/ts/integrations/kodi/kodi.classes.integration.ts +97 -178
  232. package/ts/integrations/kodi/kodi.discovery.ts +49 -28
  233. package/ts/integrations/kodi/kodi.mapper.ts +52 -10
  234. package/ts/integrations/kodi/kodi.types.ts +33 -58
  235. package/ts/integrations/kodi/kodi.validation.ts +60 -0
  236. package/ts/integrations/modbus/modbus.classes.client.ts +100 -842
  237. package/ts/integrations/modbus/modbus.classes.configflow.ts +70 -41
  238. package/ts/integrations/modbus/modbus.classes.integration.ts +115 -60
  239. package/ts/integrations/modbus/modbus.discovery.ts +38 -106
  240. package/ts/integrations/modbus/modbus.mapper.ts +64 -467
  241. package/ts/integrations/modbus/modbus.types.ts +64 -381
  242. package/ts/integrations/modbus/modbus.validation.ts +119 -0
  243. package/ts/integrations/nanoleaf/nanoleaf.classes.client.ts +115 -249
  244. package/ts/integrations/nanoleaf/nanoleaf.classes.configflow.ts +90 -19
  245. package/ts/integrations/nanoleaf/nanoleaf.classes.integration.ts +130 -181
  246. package/ts/integrations/nanoleaf/nanoleaf.credentials.ts +39 -0
  247. package/ts/integrations/nanoleaf/nanoleaf.mapper.ts +47 -260
  248. package/ts/integrations/nanoleaf/nanoleaf.types.ts +29 -147
  249. package/ts/integrations/nanoleaf/nanoleaf.validation.ts +77 -0
  250. package/ts/integrations/onkyo/onkyo.classes.client.ts +148 -457
  251. package/ts/integrations/onkyo/onkyo.classes.configflow.ts +52 -6
  252. package/ts/integrations/onkyo/onkyo.classes.integration.ts +121 -12
  253. package/ts/integrations/onkyo/onkyo.discovery.ts +41 -3
  254. package/ts/integrations/onkyo/onkyo.mapper.ts +46 -19
  255. package/ts/integrations/onkyo/onkyo.types.ts +55 -110
  256. package/ts/integrations/onkyo/onkyo.validation.ts +85 -0
  257. package/ts/integrations/onkyo/readme.md +126 -0
  258. package/ts/integrations/panasonic_viera/panasonic_viera.classes.client.ts +101 -398
  259. package/ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.ts +69 -5
  260. package/ts/integrations/panasonic_viera/panasonic_viera.classes.integration.ts +141 -9
  261. package/ts/integrations/panasonic_viera/panasonic_viera.discovery.ts +32 -3
  262. package/ts/integrations/panasonic_viera/panasonic_viera.mapper.ts +34 -161
  263. package/ts/integrations/panasonic_viera/panasonic_viera.types.ts +52 -96
  264. package/ts/integrations/panasonic_viera/panasonic_viera.validation.ts +56 -0
  265. package/ts/integrations/pioneer/pioneer.classes.client.ts +132 -428
  266. package/ts/integrations/pioneer/pioneer.classes.configflow.ts +67 -5
  267. package/ts/integrations/pioneer/pioneer.classes.integration.ts +225 -11
  268. package/ts/integrations/pioneer/pioneer.discovery.ts +45 -3
  269. package/ts/integrations/pioneer/pioneer.mapper.ts +57 -13
  270. package/ts/integrations/pioneer/pioneer.types.ts +87 -85
  271. package/ts/integrations/pioneer/pioneer.validation.ts +84 -0
  272. package/ts/integrations/soundtouch/soundtouch.classes.client.ts +152 -571
  273. package/ts/integrations/soundtouch/soundtouch.classes.configflow.ts +47 -42
  274. package/ts/integrations/soundtouch/soundtouch.classes.integration.ts +95 -150
  275. package/ts/integrations/soundtouch/soundtouch.discovery.ts +21 -133
  276. package/ts/integrations/soundtouch/soundtouch.mapper.ts +36 -207
  277. package/ts/integrations/soundtouch/soundtouch.types.ts +50 -195
  278. package/ts/integrations/soundtouch/soundtouch.validation.ts +60 -0
  279. package/ts/integrations/wiz/index.ts +1 -0
  280. package/ts/integrations/wiz/wiz.classes.client.ts +84 -254
  281. package/ts/integrations/wiz/wiz.classes.configflow.ts +39 -70
  282. package/ts/integrations/wiz/wiz.classes.integration.ts +189 -45
  283. package/ts/integrations/wiz/wiz.discovery.ts +48 -291
  284. package/ts/integrations/wiz/wiz.mapper.ts +81 -754
  285. package/ts/integrations/wiz/wiz.types.ts +24 -224
  286. package/ts/integrations/wiz/wiz.validation.ts +73 -0
  287. package/ts/integrations/yamaha/index.ts +1 -0
  288. package/ts/integrations/yamaha/yamaha.classes.client.ts +150 -6
  289. package/ts/integrations/yamaha/yamaha.classes.configflow.ts +100 -5
  290. package/ts/integrations/yamaha/yamaha.classes.integration.ts +236 -8
  291. package/ts/integrations/yamaha/yamaha.discovery.ts +52 -3
  292. package/ts/integrations/yamaha/yamaha.mapper.ts +65 -13
  293. package/ts/integrations/yamaha/yamaha.types.ts +77 -97
  294. package/ts/integrations/yamaha/yamaha.validation.ts +86 -0
@@ -1,280 +1,146 @@
1
- import type {
2
- INanoleafAuthTokenResult,
3
- INanoleafConfig,
4
- INanoleafControllerInfo,
5
- INanoleafEffects,
6
- INanoleafEffectsCommand,
7
- INanoleafPanelLayout,
8
- INanoleafRhythmInfo,
9
- INanoleafSnapshot,
10
- INanoleafState,
11
- INanoleafStateCommand,
12
- } from './nanoleaf.types.js';
13
-
14
- const DEFAULT_NANOLEAF_PORT = 16021;
1
+ import * as plugins from '../../plugins.js';
2
+ import { NoSqlConfigStore } from '../../core/classes.configstore.js';
3
+ import { createNanoleafCredentialStore, nanoleafCredentialKey, readNanoleafCredential } from './nanoleaf.credentials.js';
4
+ import type { INanoleafConfig, INanoleafIntegrationOptions, INanoleafSnapshot, INanoleafWriteResult } from './nanoleaf.types.js';
5
+ import { normalizeNanoleafConfig } from './nanoleaf.validation.js';
15
6
 
16
7
  export class NanoleafClient {
17
- constructor(private readonly config: INanoleafConfig) {}
18
-
19
- public async getSnapshot(): Promise<INanoleafSnapshot> {
20
- if (this.config.snapshot) {
21
- return this.normalizeSnapshot(this.config.snapshot);
22
- }
23
-
24
- if (this.canRequest()) {
25
- const controllerInfo = await this.requestJson<INanoleafControllerInfo>(this.authenticatedPath(''));
26
- return this.normalizeSnapshot({
27
- controllerInfo,
28
- state: controllerInfo.state ?? {},
29
- effects: controllerInfo.effects ?? {},
30
- panelLayout: controllerInfo.panelLayout,
31
- rhythm: controllerInfo.rhythm,
32
- });
33
- }
34
-
35
- return this.normalizeSnapshot({
36
- controllerInfo: this.config.controllerInfo ?? this.defaultControllerInfo(),
37
- state: this.config.state ?? this.config.controllerInfo?.state ?? {},
38
- effects: this.config.effects ?? this.config.controllerInfo?.effects ?? {},
39
- panelLayout: this.config.panelLayout ?? this.config.controllerInfo?.panelLayout,
40
- rhythm: this.config.rhythm ?? this.config.controllerInfo?.rhythm,
41
- });
42
- }
43
-
44
- public async getControllerInfo(): Promise<INanoleafControllerInfo> {
45
- return (await this.getSnapshot()).controllerInfo;
46
- }
47
-
48
- public async getState(): Promise<INanoleafState> {
49
- if (this.config.snapshot || this.config.state || this.config.controllerInfo?.state || !this.canRequest()) {
50
- return (await this.getSnapshot()).state;
51
- }
52
- return this.requestJson<INanoleafState>(this.authenticatedPath('/state'));
53
- }
54
-
55
- public async getEffects(): Promise<INanoleafEffects> {
56
- if (this.config.snapshot || this.config.effects || this.config.controllerInfo?.effects || !this.canRequest()) {
57
- return (await this.getSnapshot()).effects;
58
- }
59
- return this.requestJson<INanoleafEffects>(this.authenticatedPath('/effects'));
60
- }
61
-
62
- public async getPanelLayout(): Promise<INanoleafPanelLayout | undefined> {
63
- return (await this.getSnapshot()).panelLayout;
64
- }
65
-
66
- public async getRhythm(): Promise<INanoleafRhythmInfo | undefined> {
67
- return (await this.getSnapshot()).rhythm;
68
- }
69
-
70
- public async turnOn(): Promise<void> {
71
- await this.setState({ on: { value: true } });
72
- }
73
-
74
- public async turnOff(transitionArg?: number): Promise<void> {
75
- if (transitionArg === undefined) {
76
- await this.setState({ on: { value: false } });
77
- return;
78
- }
79
- await this.setState({ on: { value: false }, brightness: { value: 0, duration: transitionArg } });
80
- }
81
-
82
- public async setBrightness(percentArg: number, transitionArg?: number): Promise<void> {
83
- await this.setState({
84
- brightness: {
85
- value: this.clamp(Math.round(percentArg), 0, 100),
86
- duration: transitionArg,
8
+ readonly #config: Readonly<INanoleafConfig>;
9
+ readonly #protocol: plugins.ecobridgeDeviceManager.NanoleafProtocol;
10
+ readonly #ownsStore: boolean;
11
+ readonly #lifetime = new AbortController();
12
+ readonly #operations = new Set<Promise<unknown>>();
13
+ #store?: NoSqlConfigStore;
14
+ #healthy = false;
15
+ #closed = false;
16
+ #closePromise?: Promise<void>;
17
+ #retryPromise?: Promise<void>;
18
+ #protocolClosed = false;
19
+ #removeParentAbort: () => void = () => undefined;
20
+
21
+ constructor(configArg: INanoleafConfig, ownerArg?: INanoleafIntegrationOptions | NoSqlConfigStore, signalArg?: AbortSignal, homeIdArg?: string) {
22
+ this.#config = normalizeNanoleafConfig(configArg);
23
+ if (signalArg !== undefined && !(signalArg instanceof AbortSignal)) throw new TypeError('Nanoleaf lifetime signal must be an AbortSignal.');
24
+ if (homeIdArg === undefined) throw new TypeError('Nanoleaf client requires the admitted host home.');
25
+ const store = createNanoleafCredentialStore(this.#config, homeIdArg, ownerArg);
26
+ this.#store = store.store;
27
+ this.#ownsStore = store.owned;
28
+ this.#protocol = new plugins.ecobridgeDeviceManager.NanoleafProtocol({
29
+ origin: this.#config.origin, expectedSerialNumber: this.#config.expectedSerialNumber,
30
+ expectedModel: this.#config.expectedModel, allowUnencryptedHttp: this.#config.allowUnencryptedHttp,
31
+ ca: this.#config.certificateAuthority, timeoutMs: this.#config.timeoutMs,
32
+ maxResponseBytes: this.#config.maxResponseBytes, maxPendingOperations: this.#config.maxPendingOperations,
33
+ getAuthToken: async (signal) => {
34
+ signal.throwIfAborted();
35
+ const currentStore = this.#store;
36
+ if (!currentStore) throw new Error('Nanoleaf client is destroyed.');
37
+ await currentStore.initialize({ signal });
38
+ signal.throwIfAborted();
39
+ const credential: unknown = await currentStore.get(nanoleafCredentialKey(this.#config));
40
+ signal.throwIfAborted();
41
+ return readNanoleafCredential(this.#config, credential);
87
42
  },
88
43
  });
89
- }
90
-
91
- public async setHue(hueArg: number): Promise<void> {
92
- await this.setState({ hue: { value: this.clamp(Math.round(hueArg), 0, 360) } });
93
- }
94
-
95
- public async setSaturation(saturationArg: number): Promise<void> {
96
- await this.setState({ sat: { value: this.clamp(Math.round(saturationArg), 0, 100) } });
97
- }
98
-
99
- public async setColorTemperature(kelvinArg: number): Promise<void> {
100
- await this.setState({ ct: { value: Math.round(kelvinArg) } });
101
- }
102
-
103
- public async setState(stateArg: INanoleafStateCommand): Promise<void> {
104
- this.applyStatePatch(stateArg);
105
- if (this.canRequest()) {
106
- await this.requestJson<void>(this.authenticatedPath('/state'), {
107
- method: 'PUT',
108
- body: stateArg,
109
- });
110
- return;
111
- }
112
- this.assertFixtureMode();
113
- }
114
-
115
- public async setEffect(effectArg: string): Promise<void> {
116
- this.applyEffectsPatch({ select: effectArg });
117
- if (this.canRequest()) {
118
- await this.requestJson<void>(this.authenticatedPath('/effects'), {
119
- method: 'PUT',
120
- body: { select: effectArg },
121
- });
122
- return;
123
- }
124
- this.assertFixtureMode();
125
- }
126
-
127
- public async writeEffectsCommand(commandArg: INanoleafEffectsCommand): Promise<void> {
128
- if (this.canRequest()) {
129
- await this.requestJson<void>(this.authenticatedPath('/effects'), {
130
- method: 'PUT',
131
- body: { write: commandArg },
132
- });
133
- return;
134
- }
135
- this.assertFixtureMode();
136
- }
137
-
138
- public async identify(): Promise<void> {
139
- if (this.canRequest()) {
140
- await this.requestJson<void>(this.authenticatedPath('/identify'), { method: 'PUT' });
141
- return;
44
+ if (signalArg) {
45
+ const aborted = (): void => { void this.destroy(); };
46
+ signalArg.addEventListener('abort', aborted, { once: true });
47
+ this.#removeParentAbort = () => signalArg.removeEventListener('abort', aborted);
48
+ if (signalArg.aborted) aborted();
142
49
  }
143
- this.assertFixtureMode();
144
- }
145
-
146
- public async createAuthToken(): Promise<INanoleafAuthTokenResult> {
147
- return {
148
- success: false,
149
- error: 'Nanoleaf pairing/token generation is not implemented. Put the controller in pairing mode and provide an existing authToken.',
150
- };
151
50
  }
152
51
 
153
- public async destroy(): Promise<void> {}
52
+ public isHealthy(): boolean { return this.#healthy && !this.#closed; }
154
53
 
155
- private normalizeSnapshot(snapshotArg: INanoleafSnapshot): INanoleafSnapshot {
156
- const controllerInfo = snapshotArg.controllerInfo;
157
- const state = snapshotArg.state ?? controllerInfo.state ?? {};
158
- const effects = snapshotArg.effects ?? controllerInfo.effects ?? {};
159
- const panelLayout = snapshotArg.panelLayout ?? controllerInfo.panelLayout;
160
- const rhythm = snapshotArg.rhythm ?? controllerInfo.rhythm;
161
- controllerInfo.state = state;
162
- controllerInfo.effects = effects;
163
- controllerInfo.panelLayout = panelLayout;
164
- controllerInfo.rhythm = rhythm;
165
- return { controllerInfo, state, effects, panelLayout, rhythm };
166
- }
167
-
168
- private applyStatePatch(stateArg: INanoleafStateCommand): void {
169
- const state = this.ensureMutableState();
170
- if (stateArg.on) {
171
- state.on = { ...state.on, ...stateArg.on };
172
- }
173
- if (stateArg.brightness) {
174
- state.brightness = { ...state.brightness, ...stateArg.brightness };
175
- }
176
- if (stateArg.hue) {
177
- state.hue = { ...state.hue, ...stateArg.hue };
178
- }
179
- if (stateArg.sat) {
180
- state.sat = { ...state.sat, ...stateArg.sat };
181
- }
182
- if (stateArg.ct) {
183
- state.ct = { ...state.ct, ...stateArg.ct };
184
- }
54
+ public getSnapshot(signalArg?: AbortSignal): Promise<INanoleafSnapshot> {
55
+ return this.#run(async (signal) => this.#snapshot(await this.#protocol.readSnapshot({ signal })), signalArg);
185
56
  }
186
57
 
187
- private applyEffectsPatch(effectsArg: Partial<INanoleafEffects>): void {
188
- const effects = this.ensureMutableEffects();
189
- Object.assign(effects, effectsArg);
58
+ public setPower(powerArg: boolean, signalArg?: AbortSignal): Promise<INanoleafWriteResult> {
59
+ return this.#setState({ power: powerArg }, signalArg);
190
60
  }
191
61
 
192
- private ensureMutableState(): INanoleafState {
193
- if (this.config.snapshot) {
194
- this.config.snapshot.state ||= {};
195
- this.config.snapshot.controllerInfo.state = this.config.snapshot.state;
196
- return this.config.snapshot.state;
197
- }
198
- this.config.state ||= this.config.controllerInfo?.state ?? {};
199
- if (this.config.controllerInfo) {
200
- this.config.controllerInfo.state = this.config.state;
201
- }
202
- return this.config.state;
62
+ public setBrightness(brightnessArg: number, signalArg?: AbortSignal): Promise<INanoleafWriteResult> {
63
+ return this.#setState({ brightness: brightnessArg }, signalArg);
203
64
  }
204
65
 
205
- private ensureMutableEffects(): INanoleafEffects {
206
- if (this.config.snapshot) {
207
- this.config.snapshot.effects ||= {};
208
- this.config.snapshot.controllerInfo.effects = this.config.snapshot.effects;
209
- return this.config.snapshot.effects;
210
- }
211
- this.config.effects ||= this.config.controllerInfo?.effects ?? {};
212
- if (this.config.controllerInfo) {
213
- this.config.controllerInfo.effects = this.config.effects;
214
- }
215
- return this.config.effects;
66
+ public setColorTemperature(kelvinArg: number, signalArg?: AbortSignal): Promise<INanoleafWriteResult> {
67
+ return this.#setState({ colorTemperatureKelvin: kelvinArg }, signalArg);
216
68
  }
217
69
 
218
- private assertFixtureMode(): void {
219
- if (!this.hasFixtureData()) {
220
- throw new Error('Nanoleaf host and authToken are required when snapshot/manual config data is not provided.');
221
- }
70
+ #setState(commandArg: plugins.ecobridgeDeviceManager.INanoleafStateCommand, signalArg?: AbortSignal): Promise<INanoleafWriteResult> {
71
+ return this.#run(async (signal) => {
72
+ const result = await this.#protocol.setState(commandArg, { signal });
73
+ return Object.freeze({ acceptance: result.acceptance, snapshot: this.#snapshot(result.snapshot) });
74
+ }, signalArg);
222
75
  }
223
76
 
224
- private hasFixtureData(): boolean {
225
- return Boolean(this.config.snapshot || this.config.controllerInfo || this.config.state || this.config.effects || this.config.panelLayout || this.config.rhythm);
77
+ public selectEffect(effectArg: string, signalArg?: AbortSignal): Promise<INanoleafWriteResult> {
78
+ return this.#run(async (signal) => {
79
+ const result = await this.#protocol.selectEffect(effectArg, { signal });
80
+ return Object.freeze({ acceptance: result.acceptance, snapshot: this.#snapshot(result.snapshot) });
81
+ }, signalArg);
226
82
  }
227
83
 
228
- private canRequest(): boolean {
229
- return Boolean(this.config.host && this.authToken());
84
+ public destroy(): Promise<void> {
85
+ if (this.#closePromise) return this.#closePromise;
86
+ this.#closed = true;
87
+ this.#healthy = false;
88
+ this.#removeParentAbort();
89
+ this.#closePromise = Promise.resolve().then(() => this.#drainCleanup());
90
+ this.#lifetime.abort();
91
+ return this.#closePromise;
230
92
  }
231
93
 
232
- private authToken(): string | undefined {
233
- return this.config.authToken || this.config.token;
94
+ /** Explicitly retries retained cleanup; no device operation is replayed. */
95
+ public retryCleanup(): Promise<void> {
96
+ if (!this.#closePromise) return this.destroy();
97
+ if (this.#retryPromise) return this.#retryPromise;
98
+ const retry = this.#closePromise.then(() => undefined, () => this.#drainCleanup(true));
99
+ this.#retryPromise = retry;
100
+ void retry.finally(() => { if (this.#retryPromise === retry) this.#retryPromise = undefined; }).catch(() => undefined);
101
+ return retry;
234
102
  }
235
103
 
236
- private authenticatedPath(pathArg: string): string {
237
- const token = this.authToken();
238
- if (!token) {
239
- throw new Error('Nanoleaf authToken is required for local HTTP requests.');
240
- }
241
- return `/api/v1/${encodeURIComponent(token)}${pathArg}`;
104
+ async #drainCleanup(retryArg = false): Promise<void> {
105
+ const results = await Promise.allSettled([
106
+ Promise.resolve().then(async () => {
107
+ if (!this.#protocolClosed) {
108
+ await this.#protocol.close();
109
+ this.#protocolClosed = true;
110
+ }
111
+ }),
112
+ Promise.allSettled([...this.#operations]),
113
+ ]);
114
+ if (this.#ownsStore && this.#store) {
115
+ results.push(...await Promise.allSettled([Promise.resolve().then(async () => {
116
+ if (retryArg) await this.#store?.retryCleanup();
117
+ else await this.#store?.close();
118
+ this.#store = undefined;
119
+ })]));
120
+ } else if (this.#protocolClosed) this.#store = undefined;
121
+ const failures = results.flatMap((resultArg) => resultArg.status === 'rejected' ? [resultArg.reason as unknown] : []);
122
+ if (failures.length) throw new AggregateError(failures, 'Nanoleaf client cleanup failed.');
242
123
  }
243
124
 
244
- private async requestJson<TResult>(pathArg: string, optionsArg: { method?: string; body?: unknown } = {}): Promise<TResult> {
245
- const response = await globalThis.fetch(`${this.baseUrl()}${pathArg}`, {
246
- method: optionsArg.method ?? (optionsArg.body === undefined ? 'GET' : 'POST'),
247
- headers: optionsArg.body === undefined ? undefined : { 'content-type': 'application/json' },
248
- body: optionsArg.body === undefined ? undefined : JSON.stringify(optionsArg.body),
249
- });
250
- const text = await response.text();
251
- if (!response.ok) {
252
- throw new Error(`Nanoleaf request ${pathArg} failed with HTTP ${response.status}: ${text}`);
253
- }
254
- if (!text) {
255
- return undefined as TResult;
256
- }
257
- return JSON.parse(text) as TResult;
125
+ #snapshot(snapshotArg: plugins.ecobridgeDeviceManager.INanoleafSnapshot): INanoleafSnapshot {
126
+ return Object.freeze({ ...snapshotArg, homeId: this.#config.homeId, origin: this.#config.origin });
258
127
  }
259
128
 
260
- private baseUrl(): string {
261
- if (!this.config.host) {
262
- throw new Error('Nanoleaf host is required for local HTTP requests.');
129
+ #run<TResult>(workArg: (signalArg: AbortSignal) => Promise<TResult>, signalArg?: AbortSignal): Promise<TResult> {
130
+ if (this.#closed) return Promise.reject(new plugins.ecobridgeDeviceManager.NanoleafProtocolError('CLOSED', 'Nanoleaf client is destroyed.'));
131
+ if (this.#operations.size >= (this.#config.maxPendingOperations ?? 32)) {
132
+ return Promise.reject(new plugins.ecobridgeDeviceManager.NanoleafProtocolError('BUSY', 'Nanoleaf client operation capacity is exhausted.'));
263
133
  }
264
- const protocol = this.config.protocol ?? 'http';
265
- const port = this.config.port ?? DEFAULT_NANOLEAF_PORT;
266
- return `${protocol}://${this.config.host}:${port}`;
267
- }
268
-
269
- private defaultControllerInfo(): INanoleafControllerInfo {
270
- return {
271
- name: 'Nanoleaf',
272
- manufacturer: 'Nanoleaf',
273
- model: 'Nanoleaf Controller',
274
- };
275
- }
276
-
277
- private clamp(valueArg: number, minArg: number, maxArg: number): number {
278
- return Math.max(minArg, Math.min(maxArg, valueArg));
134
+ if (signalArg !== undefined && !(signalArg instanceof AbortSignal)) return Promise.reject(new TypeError('Nanoleaf operation signal must be an AbortSignal.'));
135
+ const signal = signalArg ? AbortSignal.any([this.#lifetime.signal, signalArg]) : this.#lifetime.signal;
136
+ const operation = Promise.resolve().then(async () => {
137
+ if (signal.aborted) throw new plugins.ecobridgeDeviceManager.NanoleafProtocolError('ABORTED', 'Nanoleaf operation was cancelled before admission.');
138
+ return workArg(signal);
139
+ }).then((resultArg) => { this.#healthy = !this.#closed && !signal.aborted; return resultArg; }, (errorArg: unknown) => {
140
+ this.#healthy = false;
141
+ throw errorArg;
142
+ }).finally(() => this.#operations.delete(operation));
143
+ this.#operations.add(operation);
144
+ return operation;
279
145
  }
280
146
  }
@@ -1,29 +1,100 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
3
+ import type { INativeAdmissionContext } from '../../core/classes.nativehomeadmission.js';
1
4
  import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
2
- import type { INanoleafConfig } from './nanoleaf.types.js';
3
-
4
- const DEFAULT_NANOLEAF_PORT = 16021;
5
+ import { createNanoleafCredentialStore, nanoleafCredential, nanoleafCredentialKey } from './nanoleaf.credentials.js';
6
+ import type { INanoleafConfig, INanoleafIntegrationOptions } from './nanoleaf.types.js';
7
+ import { nanoleafKeys, nanoleafRecord, normalizeNanoleafConfig } from './nanoleaf.validation.js';
5
8
 
6
9
  export class NanoleafConfigFlow implements IConfigFlow<INanoleafConfig> {
10
+ readonly #scope: NativeIntegrationScope;
11
+ readonly #credentials?: INanoleafIntegrationOptions;
12
+
13
+ constructor(ownerArg?: INanoleafIntegrationOptions, scopeArg?: NativeIntegrationScope) {
14
+ this.#credentials = ownerArg && Object.freeze({ db: ownerArg.db, keyProvider: ownerArg.keyProvider });
15
+ this.#scope = scopeArg ?? new NativeIntegrationOwner('nanoleaf', { homeId: ownerArg?.homeId }).createFlowScope();
16
+ }
17
+
18
+ public destroy(): Promise<void> { return this.#scope.close(); }
19
+ public retryCleanup(): Promise<void> { return this.#scope.retryCleanup(); }
20
+
7
21
  public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<INanoleafConfig>> {
8
- void contextArg;
22
+ if (candidateArg.integrationDomain !== undefined && candidateArg.integrationDomain !== 'nanoleaf') {
23
+ return { kind: 'error', error: 'Nanoleaf config flow requires a Nanoleaf candidate.' };
24
+ }
25
+ let context: INativeAdmissionContext;
26
+ try { context = this.#scope.admit(contextArg); }
27
+ catch { return { kind: 'error', error: 'Nanoleaf config flow requires an open instance and the explicit matching host home.' }; }
9
28
  return {
10
- kind: 'form',
11
- title: 'Connect Nanoleaf',
12
- description: 'Configure the local Nanoleaf HTTP endpoint with an existing auth token from the controller.',
29
+ kind: 'form', title: 'Connect selected Nanoleaf controller',
30
+ description: 'Provide the selected local IPv4 origin, serial and documented model. An existing token is validated and stored encrypted for the host-selected home; plaintext vendor HTTP requires explicit consent.',
13
31
  fields: [
14
- { name: 'host', label: 'Host', type: 'text', required: true },
15
- { name: 'port', label: 'Port', type: 'number', required: true },
16
- { name: 'authToken', label: 'Auth token', type: 'password', required: true },
32
+ { name: 'origin', label: 'Selected HTTP(S) IPv4 origin', type: 'text', required: true },
33
+ { name: 'expectedSerialNumber', label: 'Expected serial number', type: 'text', required: true },
34
+ { name: 'expectedModel', label: 'Expected documented controller model', type: 'text', required: true },
35
+ { name: 'credentialKey', label: 'New encrypted credential reference', type: 'text', required: true },
36
+ { name: 'authToken', label: 'Existing controller token', type: 'password', required: true },
37
+ { name: 'allowUnencryptedHttp', label: 'Consent to plaintext local HTTP', type: 'boolean' },
38
+ { name: 'certificateAuthority', label: 'Trusted HTTPS controller PEM certificate', type: 'text' },
17
39
  ],
18
- submit: async (valuesArg) => ({
19
- kind: 'done',
20
- title: 'Nanoleaf configured',
21
- config: {
22
- host: String(valuesArg.host || candidateArg.host || ''),
23
- port: typeof valuesArg.port === 'number' ? valuesArg.port : candidateArg.port || DEFAULT_NANOLEAF_PORT,
24
- authToken: String(valuesArg.authToken || ''),
25
- },
26
- }),
40
+ submit: (valuesArg) => this.#submit(valuesArg, context),
27
41
  };
28
42
  }
43
+
44
+ async #submit(valuesArg: Record<string, unknown>, contextArg: INativeAdmissionContext): Promise<IConfigFlowStep<INanoleafConfig>> {
45
+ let config: Readonly<INanoleafConfig>;
46
+ let credential: ReturnType<typeof nanoleafCredential>;
47
+ try {
48
+ const values = nanoleafRecord(valuesArg, 'config flow values');
49
+ nanoleafKeys(values, ['homeId', 'origin', 'expectedSerialNumber', 'expectedModel', 'credentialKey', 'authToken',
50
+ 'allowUnencryptedHttp', 'certificateAuthority', 'timeoutMs', 'maxResponseBytes', 'maxPendingOperations']);
51
+ if (values.homeId !== undefined && values.homeId !== contextArg.homeId) throw new TypeError('Nanoleaf form home conflicts with the host home.');
52
+ const { authToken, ...configuration } = values;
53
+ config = normalizeNanoleafConfig({ ...configuration, homeId: contextArg.homeId });
54
+ credential = nanoleafCredential(config, authToken);
55
+ this.#scope.admit(contextArg, config.homeId);
56
+ } catch { return { kind: 'error', error: 'Nanoleaf config flow values or host home are invalid, cancelled or closed.' }; }
57
+ return this.#scope.run(contextArg, config.homeId, async (leaseArg): Promise<IConfigFlowStep<INanoleafConfig>> => {
58
+ try {
59
+ const handle = createNanoleafCredentialStore(config, leaseArg.context.homeId, leaseArg.context.configStore ?? this.#credentials);
60
+ let protocol: plugins.ecobridgeDeviceManager.NanoleafProtocol | undefined;
61
+ let protocolClosed = false;
62
+ let storeClosed = !handle.owned;
63
+ let firstStoreCleanup = true;
64
+ // One admitted credential-validation operation owns its protocol and optional store.
65
+ leaseArg.own(async () => {
66
+ const results = await Promise.allSettled([
67
+ Promise.resolve().then(async () => {
68
+ if (protocol && !protocolClosed) { await protocol.close(); protocolClosed = true; }
69
+ }),
70
+ Promise.resolve().then(async () => {
71
+ if (!storeClosed) {
72
+ // Failed lease callbacks run again only on explicit owner cleanup retry.
73
+ if (firstStoreCleanup) { firstStoreCleanup = false; await handle.store.close(); }
74
+ else await handle.store.retryCleanup();
75
+ storeClosed = true;
76
+ }
77
+ }),
78
+ ]);
79
+ const failures = results.flatMap((resultArg) => resultArg.status === 'rejected' ? [resultArg.reason as unknown] : []);
80
+ if (failures.length) throw new AggregateError(failures, 'Nanoleaf flow resource cleanup failed.');
81
+ });
82
+ await handle.store.initialize({ signal: leaseArg.signal });
83
+ leaseArg.assertOpen();
84
+ protocol = new plugins.ecobridgeDeviceManager.NanoleafProtocol({ origin: config.origin,
85
+ expectedSerialNumber: config.expectedSerialNumber, expectedModel: config.expectedModel,
86
+ getAuthToken: () => credential.authToken, allowUnencryptedHttp: config.allowUnencryptedHttp,
87
+ ca: config.certificateAuthority, timeoutMs: config.timeoutMs, maxResponseBytes: config.maxResponseBytes,
88
+ maxPendingOperations: config.maxPendingOperations });
89
+ await protocol.readSnapshot({ signal: leaseArg.signal });
90
+ leaseArg.assertOpen();
91
+ await handle.store.create(nanoleafCredentialKey(config), credential);
92
+ leaseArg.assertOpen();
93
+ return { kind: 'done', title: 'Nanoleaf identity verified and token stored encrypted', config };
94
+ } catch { return { kind: 'error', error: 'Nanoleaf controller validation or encrypted credential persistence failed.' }; }
95
+ }).catch((errorArg: unknown): IConfigFlowStep<INanoleafConfig> => {
96
+ if (errorArg instanceof AggregateError) throw errorArg;
97
+ return { kind: 'error', error: 'Nanoleaf config flow was cancelled or closed.' };
98
+ });
99
+ }
29
100
  }