@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,372 +1,92 @@
1
- import type {
2
- IKodiActivePlayer,
3
- IKodiApplicationProperties,
4
- IKodiConfig,
5
- IKodiDeviceInfo,
6
- IKodiJsonRpcRequest,
7
- IKodiJsonRpcResponse,
8
- IKodiMediaItem,
9
- IKodiPlayerProperties,
10
- IKodiSnapshot,
11
- IKodiTime,
12
- TKodiInputCommand,
13
- TKodiJsonRpcParams,
14
- TKodiMediaType,
15
- } from './kodi.types.js';
16
-
17
- const defaultPort = 8080;
18
- const defaultWsPort = 9090;
19
- const defaultTimeoutMs = 5000;
20
-
21
- const playerProperties = ['time', 'totaltime', 'speed', 'live', 'percentage', 'playlistid', 'position', 'repeat', 'shuffled'];
22
- const itemProperties = ['title', 'file', 'uniqueid', 'thumbnail', 'artist', 'albumartist', 'showtitle', 'album', 'season', 'episode', 'streamdetails'];
23
-
24
- export class KodiJsonRpcError extends Error {
25
- constructor(public readonly method: string, public readonly code: number | undefined, messageArg: string, public readonly data?: unknown) {
26
- super(`Kodi JSON-RPC ${method} failed${typeof code === 'number' ? ` (${code})` : ''}: ${messageArg}`);
27
- this.name = 'KodiJsonRpcError';
28
- }
29
- }
1
+ import * as plugins from '../../plugins.js';
2
+ import { NativeHomeAdmission, type INativeAdmissionContext } from '../../core/classes.nativehomeadmission.js';
3
+ import type { IKodiClientOptions, IKodiConfig, IKodiNativeSnapshot, IKodiSetupContext } from './kodi.types.js';
4
+ import { kodiKeys, kodiRecord, kodiSignal, normalizeKodiConfig } from './kodi.validation.js';
30
5
 
6
+ /** Consumer lifecycle and home projection; the foundation owns all protocol work. */
31
7
  export class KodiClient {
32
- private requestId = 1;
33
-
34
- constructor(private readonly config: IKodiConfig) {}
35
-
36
- public async getSnapshot(): Promise<IKodiSnapshot> {
37
- if (this.config.snapshot) {
38
- return this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot));
39
- }
40
- if (!this.config.host) {
41
- return this.normalizeSnapshot({
42
- deviceInfo: this.deviceInfoFromConfig(),
43
- players: [],
44
- online: false,
45
- updatedAt: new Date().toISOString(),
46
- });
47
- }
48
-
49
- const [application, players] = await Promise.all([
50
- this.getApplicationProperties().catch(() => undefined),
51
- this.getActivePlayers(),
52
- ]);
53
- const player = players[0];
54
- const [playerPropertiesResult, item] = player ? await Promise.all([
55
- this.getPlayerProperties(player.playerid).catch(() => undefined),
56
- this.getPlayerItem(player.playerid).catch(() => undefined),
57
- ]) : [undefined, undefined];
58
-
59
- return this.normalizeSnapshot({
60
- deviceInfo: {
61
- ...this.deviceInfoFromConfig(),
62
- name: this.config.name || this.config.deviceInfo?.name || application?.name || this.config.host,
63
- version: this.versionString(application),
64
- },
65
- application,
66
- players,
67
- player,
68
- playerProperties: playerPropertiesResult,
69
- item,
70
- online: true,
71
- updatedAt: new Date().toISOString(),
72
- });
73
- }
74
-
75
- public async ping(): Promise<boolean> {
76
- const result = await this.callMethod<string>('JSONRPC.Ping');
77
- return result === 'pong';
78
- }
79
-
80
- public async getApplicationProperties(): Promise<IKodiApplicationProperties> {
81
- return this.callMethod<IKodiApplicationProperties>('Application.GetProperties', {
82
- properties: ['name', 'version', 'volume', 'muted'],
83
- });
84
- }
85
-
86
- public async getActivePlayers(): Promise<IKodiActivePlayer[]> {
87
- return this.callMethod<IKodiActivePlayer[]>('Player.GetActivePlayers');
88
- }
89
-
90
- public async getPlayerProperties(playerIdArg: number): Promise<IKodiPlayerProperties> {
91
- return this.callMethod<IKodiPlayerProperties>('Player.GetProperties', {
92
- playerid: playerIdArg,
93
- properties: playerProperties,
8
+ readonly #config: Readonly<IKodiConfig>;
9
+ readonly #context: INativeAdmissionContext;
10
+ readonly #protocol: plugins.ecobridgeDeviceManager.KodiProtocol;
11
+ readonly #lifetime = new AbortController();
12
+ readonly #parentSignal?: AbortSignal;
13
+ readonly #onParentAbort: () => void;
14
+ readonly #operations = new Set<Promise<unknown>>();
15
+ #closed = false;
16
+ #healthy = false;
17
+ #closePromise?: Promise<void>;
18
+
19
+ public constructor(config: IKodiConfig, context: IKodiSetupContext, options: IKodiClientOptions = {}) {
20
+ this.#config = normalizeKodiConfig(config);
21
+ this.#context = new NativeHomeAdmission('kodi').admit(context, this.#config.homeId);
22
+ const value = kodiRecord(options, 'client options');
23
+ kodiKeys(value, ['getBasicCredentials']);
24
+ this.#parentSignal = this.#context.abortSignal;
25
+ if (value.getBasicCredentials !== undefined && typeof value.getBasicCredentials !== 'function') throw new TypeError('Kodi Basic provider must be a function.');
26
+ this.#parentSignal?.throwIfAborted();
27
+ this.#protocol = new plugins.ecobridgeDeviceManager.KodiProtocol({
28
+ origin: this.#config.origin, expectedApplicationName: this.#config.expectedApplicationName,
29
+ expectedApplicationMajor: this.#config.expectedApplicationMajor, allowUnencryptedHttp: true,
30
+ control: this.#config.control, timeoutMs: this.#config.timeoutMs,
31
+ maxQueuedOperations: this.#config.maxQueuedOperations, maxResponseBytes: this.#config.maxResponseBytes,
32
+ getBasicCredentials: value.getBasicCredentials as IKodiClientOptions['getBasicCredentials'],
94
33
  });
34
+ this.#onParentAbort = () => { void this.destroy().catch(() => {}); };
35
+ this.#parentSignal?.addEventListener('abort', this.#onParentAbort, { once: true });
36
+ if (this.#parentSignal?.aborted) this.#onParentAbort();
95
37
  }
96
38
 
97
- public async getPlayerItem(playerIdArg: number): Promise<IKodiMediaItem | undefined> {
98
- const result = await this.callMethod<{ item?: IKodiMediaItem }>('Player.GetItem', {
99
- playerid: playerIdArg,
100
- properties: itemProperties,
101
- });
102
- return result.item;
103
- }
104
-
105
- public async setVolumeLevel(volumeLevelArg: number): Promise<number> {
106
- return this.callMethod<number>('Application.SetVolume', {
107
- volume: Math.max(0, Math.min(100, Math.round(volumeLevelArg * 100))),
108
- });
109
- }
110
-
111
- public async stepVolume(directionArg: 'increment' | 'decrement'): Promise<number> {
112
- return this.callMethod<number>('Application.SetVolume', { volume: directionArg });
113
- }
114
-
115
- public async setMuted(mutedArg: boolean): Promise<boolean> {
116
- return this.callMethod<boolean>('Application.SetMute', { mute: mutedArg });
117
- }
118
-
119
- public async playPause(): Promise<unknown> {
120
- return this.playState('toggle');
121
- }
122
-
123
- public async play(): Promise<unknown> {
124
- return this.playState(true);
125
- }
126
-
127
- public async pause(): Promise<unknown> {
128
- return this.playState(false);
129
- }
130
-
131
- public async stop(): Promise<string> {
132
- return this.callMethod<string>('Player.Stop', { playerid: await this.activePlayerId() });
133
- }
134
-
135
- public async nextTrack(): Promise<string> {
136
- return this.callMethod<string>('Player.GoTo', { playerid: await this.activePlayerId(), to: 'next' });
137
- }
138
-
139
- public async previousTrack(): Promise<string> {
140
- return this.callMethod<string>('Player.GoTo', { playerid: await this.activePlayerId(), to: 'previous' });
141
- }
142
-
143
- public async seek(positionSecondsArg: number): Promise<unknown> {
144
- return this.callMethod('Player.Seek', {
145
- playerid: await this.activePlayerId(),
146
- value: { time: this.secondsToTime(positionSecondsArg) },
147
- });
148
- }
39
+ public isHealthy(): boolean { return this.#healthy && !this.#closed && !this.#protocol.isClosed; }
149
40
 
150
- public async playMedia(mediaTypeArg: TKodiMediaType, mediaIdArg: string | number): Promise<string> {
151
- return this.callMethod<string>('Player.Open', { item: this.mediaItem(mediaTypeArg, mediaIdArg) });
41
+ public getSnapshot(signal?: AbortSignal): Promise<IKodiNativeSnapshot> {
42
+ return this.run(async (signal) => this.project(await this.#protocol.readSnapshot({ signal })), signal);
152
43
  }
153
44
 
154
- public async clearPlaylist(playlistIdArg = 0): Promise<string> {
155
- return this.callMethod<string>('Playlist.Clear', { playlistid: playlistIdArg });
45
+ public setVolume(volume: number, signal?: AbortSignal): Promise<plugins.ecobridgeDeviceManager.IKodiControlReceipt> {
46
+ return this.run((signal) => this.#protocol.setVolume(volume, { signal }), signal);
156
47
  }
157
48
 
158
- public async addToPlaylist(mediaTypeArg: TKodiMediaType, mediaIdArg: string | number, playlistIdArg = 0): Promise<string> {
159
- return this.callMethod<string>('Playlist.Add', {
160
- playlistid: playlistIdArg,
161
- item: this.playlistItem(mediaTypeArg, mediaIdArg),
162
- });
49
+ public setMuted(muted: boolean, signal?: AbortSignal): Promise<plugins.ecobridgeDeviceManager.IKodiControlReceipt> {
50
+ return this.run((signal) => this.#protocol.setMuted(muted, { signal }), signal);
163
51
  }
164
52
 
165
- public async showNotification(titleArg: string, messageArg: string, iconArg = 'info', displayTimeMsArg = 10000): Promise<string> {
166
- return this.callMethod<string>('GUI.ShowNotification', {
167
- title: titleArg,
168
- message: messageArg,
169
- image: iconArg,
170
- displaytime: Math.max(1500, Math.round(displayTimeMsArg)),
53
+ public destroy(): Promise<void> {
54
+ if (this.#closePromise) return this.#closePromise;
55
+ this.#closed = true;
56
+ this.#healthy = false;
57
+ this.#closePromise = Promise.resolve().then(async () => {
58
+ const [protocolCleanup] = await Promise.allSettled([this.#protocol.close(), ...this.#operations]);
59
+ // Operation failures belong to their callers; this close still joins every projection.
60
+ if (protocolCleanup.status === 'rejected') throw protocolCleanup.reason;
171
61
  });
62
+ this.#parentSignal?.removeEventListener('abort', this.#onParentAbort);
63
+ this.#lifetime.abort(new Error('Kodi client is closed.'));
64
+ return this.#closePromise;
172
65
  }
173
66
 
174
- public async input(commandArg: TKodiInputCommand): Promise<string | unknown> {
175
- const method = this.inputMethod(commandArg);
176
- return this.callMethod(method);
67
+ private project(state: plugins.ecobridgeDeviceManager.IKodiSnapshot): IKodiNativeSnapshot {
68
+ return Object.freeze({ ...state, homeId: this.#context.homeId, name: this.#config.name ?? state.applicationName, online: true });
177
69
  }
178
70
 
179
- public async quit(): Promise<string> {
180
- return this.callMethod<string>('Application.Quit');
181
- }
182
-
183
- public async callMethod<T = unknown>(methodArg: string, paramsArg?: TKodiJsonRpcParams): Promise<T> {
184
- if (!this.config.host) {
185
- throw new Error('Kodi host is required when snapshot data is not provided.');
186
- }
187
-
188
- const request: IKodiJsonRpcRequest = {
189
- jsonrpc: '2.0',
190
- method: methodArg,
191
- id: this.requestId++,
192
- };
193
- if (paramsArg !== undefined) {
194
- request.params = paramsArg;
195
- }
196
-
197
- const abortController = new AbortController();
198
- const timeout = setTimeout(() => abortController.abort(), this.config.timeoutMs || defaultTimeoutMs);
71
+ private run<TResult>(work: (signal: AbortSignal) => Promise<TResult>, callerSignal?: AbortSignal): Promise<TResult> {
72
+ if (this.#closed || this.#protocol.isClosed) return Promise.reject(new plugins.ecobridgeDeviceManager.KodiProtocolError('closed', 'Kodi client is terminally closed.'));
73
+ let signal: AbortSignal;
199
74
  try {
200
- const response = await globalThis.fetch(this.jsonRpcUrl(), {
201
- method: 'POST',
202
- headers: this.headers(),
203
- body: JSON.stringify(request),
204
- signal: abortController.signal,
205
- });
206
- const text = await response.text();
207
- if (!response.ok) {
208
- throw new Error(`Kodi request ${methodArg} failed with HTTP ${response.status}: ${text}`);
209
- }
210
- const payload = JSON.parse(text) as IKodiJsonRpcResponse<T>;
211
- if (payload.error) {
212
- throw new KodiJsonRpcError(methodArg, payload.error.code, payload.error.message || 'Unknown JSON-RPC error', payload.error.data);
213
- }
214
- return payload.result as T;
215
- } finally {
216
- clearTimeout(timeout);
217
- }
218
- }
219
-
220
- public async destroy(): Promise<void> {}
221
-
222
- private async playState(playArg: boolean | 'toggle'): Promise<unknown> {
223
- return this.callMethod('Player.PlayPause', {
224
- playerid: await this.activePlayerId(),
225
- play: playArg,
226
- });
227
- }
228
-
229
- private async activePlayerId(): Promise<number> {
230
- const snapshot = await this.getSnapshot();
231
- const playerId = snapshot.player?.playerid ?? snapshot.players[0]?.playerid;
232
- if (typeof playerId !== 'number') {
233
- throw new Error('Kodi has no active player.');
234
- }
235
- return playerId;
236
- }
237
-
238
- private mediaItem(mediaTypeArg: TKodiMediaType, mediaIdArg: string | number): Record<string, unknown> {
239
- const type = mediaTypeArg.toLowerCase();
240
- if (type === 'playlist') {
241
- return { playlistid: Number(mediaIdArg) };
242
- }
243
- if (type === 'channel') {
244
- return { channelid: Number(mediaIdArg) };
245
- }
246
- if (type === 'directory') {
247
- return { path: String(mediaIdArg), recursive: true };
248
- }
249
- if (type === 'movie') {
250
- return { movieid: Number(mediaIdArg) };
251
- }
252
- if (type === 'episode') {
253
- return { episodeid: Number(mediaIdArg) };
254
- }
255
- if (type === 'season') {
256
- return { seasonid: Number(mediaIdArg) };
257
- }
258
- if (type === 'tvshow') {
259
- return { tvshowid: Number(mediaIdArg) };
260
- }
261
- if (type === 'album') {
262
- return { albumid: Number(mediaIdArg) };
263
- }
264
- if (type === 'artist') {
265
- return { artistid: Number(mediaIdArg) };
266
- }
267
- if (type === 'song' || type === 'track') {
268
- return { songid: Number(mediaIdArg) };
269
- }
270
- return { file: String(mediaIdArg) };
271
- }
272
-
273
- private playlistItem(mediaTypeArg: TKodiMediaType, mediaIdArg: string | number): Record<string, unknown> {
274
- const type = mediaTypeArg.toLowerCase();
275
- if (type === 'album') {
276
- return { albumid: Number(mediaIdArg) };
277
- }
278
- if (type === 'artist') {
279
- return { artistid: Number(mediaIdArg) };
280
- }
281
- if (type === 'song' || type === 'track') {
282
- return { songid: Number(mediaIdArg) };
283
- }
284
- return this.mediaItem(type, mediaIdArg);
285
- }
286
-
287
- private secondsToTime(secondsArg: number): IKodiTime {
288
- const safeSeconds = Math.max(0, Math.floor(secondsArg));
289
- return {
290
- hours: Math.floor(safeSeconds / 3600),
291
- minutes: Math.floor((safeSeconds % 3600) / 60),
292
- seconds: safeSeconds % 60,
293
- milliseconds: Math.max(0, Math.round((secondsArg - Math.floor(secondsArg)) * 1000)),
294
- };
295
- }
296
-
297
- private inputMethod(commandArg: string): string {
298
- const normalized = commandArg.replace(/[_\s-]+/g, '').toLowerCase();
299
- const methods: Record<string, string> = {
300
- up: 'Input.Up',
301
- down: 'Input.Down',
302
- left: 'Input.Left',
303
- right: 'Input.Right',
304
- select: 'Input.Select',
305
- ok: 'Input.Select',
306
- enter: 'Input.Select',
307
- back: 'Input.Back',
308
- home: 'Input.Home',
309
- info: 'Input.Info',
310
- contextmenu: 'Input.ContextMenu',
311
- };
312
- return methods[normalized] || (commandArg.includes('.') ? commandArg : `Input.${commandArg}`);
313
- }
314
-
315
- private normalizeSnapshot(snapshotArg: IKodiSnapshot): IKodiSnapshot {
316
- const deviceInfo = {
317
- ...this.deviceInfoFromConfig(),
318
- ...snapshotArg.deviceInfo,
319
- };
320
- if (!deviceInfo.name) {
321
- deviceInfo.name = snapshotArg.application?.name || this.config.name || this.config.host || 'Kodi';
322
- }
323
- if (!deviceInfo.version) {
324
- deviceInfo.version = this.versionString(snapshotArg.application);
325
- }
326
- return {
327
- ...snapshotArg,
328
- deviceInfo,
329
- players: snapshotArg.players || [],
330
- player: snapshotArg.player || snapshotArg.players?.[0],
331
- online: snapshotArg.online,
332
- };
333
- }
334
-
335
- private deviceInfoFromConfig(): IKodiDeviceInfo {
336
- return {
337
- ...this.config.deviceInfo,
338
- id: this.config.deviceInfo?.id || this.config.uniqueId,
339
- uuid: this.config.deviceInfo?.uuid || this.config.uniqueId,
340
- name: this.config.deviceInfo?.name || this.config.name,
341
- host: this.config.deviceInfo?.host || this.config.host,
342
- port: this.config.deviceInfo?.port || this.config.port || defaultPort,
343
- wsPort: this.config.deviceInfo?.wsPort || this.config.wsPort || defaultWsPort,
344
- manufacturer: this.config.deviceInfo?.manufacturer || 'Kodi',
345
- };
346
- }
347
-
348
- private versionString(applicationArg: IKodiApplicationProperties | undefined): string | undefined {
349
- const version = applicationArg?.version;
350
- if (!version || typeof version.major !== 'number') {
351
- return undefined;
352
- }
353
- return `${version.major}.${typeof version.minor === 'number' ? version.minor : 0}${version.revision ? `-${version.revision}` : ''}`;
354
- }
355
-
356
- private cloneSnapshot(snapshotArg: IKodiSnapshot): IKodiSnapshot {
357
- return JSON.parse(JSON.stringify(snapshotArg)) as IKodiSnapshot;
358
- }
359
-
360
- private jsonRpcUrl(): string {
361
- const protocol = this.config.ssl ? 'https' : 'http';
362
- return `${protocol}://${this.config.host}:${this.config.port || defaultPort}/jsonrpc`;
363
- }
364
-
365
- private headers(): Record<string, string> {
366
- const headers: Record<string, string> = { 'content-type': 'application/json' };
367
- if (this.config.username !== undefined && this.config.password !== undefined) {
368
- headers.authorization = `Basic ${globalThis.btoa(`${this.config.username}:${this.config.password}`)}`;
369
- }
370
- return headers;
75
+ kodiSignal(callerSignal);
76
+ if (callerSignal?.aborted) throw new plugins.ecobridgeDeviceManager.KodiProtocolError('cancelled', 'Kodi operation was cancelled before admission.');
77
+ signal = AbortSignal.any([this.#lifetime.signal, ...(callerSignal ? [callerSignal] : [])]);
78
+ } catch (error) { return Promise.reject(error); }
79
+ let operation: Promise<TResult>;
80
+ try { operation = work(signal).then((result) => {
81
+ this.#healthy = !this.#closed && !signal.aborted && !this.#protocol.isClosed;
82
+ return result;
83
+ }, (error: unknown) => {
84
+ this.#healthy = false;
85
+ throw error;
86
+ }); } catch (error) { return Promise.reject(error); }
87
+ this.#operations.add(operation);
88
+ const settled = (): void => { this.#operations.delete(operation); };
89
+ void operation.then(settled, settled);
90
+ return operation;
371
91
  }
372
92
  }
@@ -1,63 +1,60 @@
1
- import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
2
- import type { IKodiConfig } from './kodi.types.js';
3
-
4
- const defaultPort = 8080;
5
- const defaultWsPort = 9090;
6
- const defaultTimeoutMs = 5000;
1
+ import * as plugins from '../../plugins.js';
2
+ import type { IConfigFlow, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
3
+ import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
4
+ import { KodiClient } from './kodi.classes.client.js';
5
+ import type { IKodiConfig, IKodiConfigFlowContext } from './kodi.types.js';
6
+ import { kodiInteger, kodiKeys, kodiRecord, kodiText, normalizeKodiConfig } from './kodi.validation.js';
7
7
 
8
+ /** Captures selected configuration. Setup separately owns live readiness admission. */
8
9
  export class KodiConfigFlow implements IConfigFlow<IKodiConfig> {
9
- public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IKodiConfig>> {
10
- void contextArg;
10
+ readonly #scope: NativeIntegrationScope;
11
+ public constructor(owner: NativeIntegrationOwner = new NativeIntegrationOwner('kodi')) {
12
+ this.#scope = owner.createFlowScope();
13
+ }
14
+
15
+ public destroy(): Promise<void> { return this.#scope.close(); }
16
+ public retryCleanup(): Promise<void> { return this.#scope.retryCleanup(); }
17
+
18
+ public async start(input: IDiscoveryCandidate, context: IKodiConfigFlowContext): Promise<IConfigFlowStep<IKodiConfig>> {
19
+ const candidate = kodiRecord(input, 'config candidate');
20
+ const host = kodiText(candidate.host, 'selected candidate host', 128);
21
+ const port = kodiInteger(candidate.port === undefined ? 8080 : candidate.port, 1, 65535, 'candidate port');
22
+ const origin = plugins.ecobridgeDeviceManager.validateKodiOrigin(`http://${host}:${port}`);
23
+ const captured = this.#scope.admit(context);
11
24
  return {
12
- kind: 'form',
13
- title: 'Connect Kodi',
14
- description: 'Configure the local Kodi JSON-RPC endpoint.',
25
+ kind: 'form', title: 'Connect selected Kodi application',
26
+ description: 'Specify the expected application name and major for the host-selected home. Accept plaintext HTTP explicitly. Controls default to disabled; enabling them requires endpoint trust and an integer volume ceiling. Live readiness is checked during setup.',
15
27
  fields: [
16
- { name: 'host', label: 'Host', type: 'text', required: true },
17
- { name: 'port', label: 'HTTP JSON-RPC port', type: 'number' },
18
- { name: 'ssl', label: 'Use HTTPS', type: 'boolean' },
19
- { name: 'wsPort', label: 'WebSocket port', type: 'number' },
20
- { name: 'username', label: 'Username', type: 'text' },
21
- { name: 'password', label: 'Password', type: 'password' },
22
- { name: 'name', label: 'Name', type: 'text' },
28
+ { name: 'expectedApplicationName', label: 'Exact application name', type: 'text', required: true },
29
+ { name: 'expectedApplicationMajor', label: 'Application major version', type: 'number', required: true },
30
+ { name: 'allowUnencryptedHttp', label: 'Accept plaintext HTTP', type: 'boolean', required: true },
31
+ { name: 'allowControls', label: 'Enable application volume and mute controls', type: 'boolean' },
32
+ { name: 'trustSelectedEndpoint', label: 'Trust this selected endpoint for controls', type: 'boolean' },
33
+ { name: 'maximumVolume', label: 'Maximum integer application volume (0–100)', type: 'number' },
34
+ { name: 'name', label: 'Display name', type: 'text' },
23
35
  ],
24
- submit: async (valuesArg) => ({
25
- kind: 'done',
26
- title: 'Kodi configured',
27
- config: {
28
- host: this.stringValue(valuesArg.host) || candidateArg.host || '',
29
- port: this.numberValue(valuesArg.port) || candidateArg.port || defaultPort,
30
- wsPort: this.numberValue(valuesArg.wsPort) || this.numberMetadata(candidateArg, 'wsPort') || defaultWsPort,
31
- ssl: this.booleanValue(valuesArg.ssl) ?? this.booleanMetadata(candidateArg, 'ssl') ?? false,
32
- username: this.stringValue(valuesArg.username),
33
- password: this.stringValue(valuesArg.password),
34
- name: this.stringValue(valuesArg.name) || candidateArg.name,
35
- uniqueId: candidateArg.id,
36
- timeoutMs: defaultTimeoutMs,
37
- },
38
- }),
36
+ submit: async (input) => {
37
+ try {
38
+ const values = kodiRecord(input, 'config-flow values');
39
+ return await this.#scope.run(captured, captured.homeId, async (lease): Promise<IConfigFlowStep<IKodiConfig>> => {
40
+ kodiKeys(values, ['homeId', 'expectedApplicationName', 'expectedApplicationMajor', 'allowUnencryptedHttp', 'allowControls', 'trustSelectedEndpoint', 'maximumVolume', 'name', 'timeoutMs', 'maxQueuedOperations', 'maxResponseBytes']);
41
+ if (values.homeId !== undefined && values.homeId !== lease.context.homeId) throw new TypeError('Kodi form home differs from the host-selected home.');
42
+ if (values.allowControls !== undefined && typeof values.allowControls !== 'boolean') throw new TypeError('Kodi controls require an explicit boolean opt-in.');
43
+ if (values.allowControls !== true && (values.trustSelectedEndpoint === true || values.maximumVolume !== undefined)) throw new TypeError('Kodi owner control policy requires an explicit controls opt-in.');
44
+ const config = normalizeKodiConfig({ homeId: lease.context.homeId, origin,
45
+ expectedApplicationName: values.expectedApplicationName, expectedApplicationMajor: values.expectedApplicationMajor,
46
+ allowUnencryptedHttp: values.allowUnencryptedHttp,
47
+ control: values.allowControls === true ? { trustSelectedEndpoint: values.trustSelectedEndpoint, maximumVolume: values.maximumVolume } : undefined,
48
+ name: values.name, timeoutMs: values.timeoutMs, maxQueuedOperations: values.maxQueuedOperations, maxResponseBytes: values.maxResponseBytes });
49
+ // Construction delegates endpoint validation without opening a connection.
50
+ const client = new KodiClient(config, { ...lease.context, abortSignal: lease.signal });
51
+ lease.own(() => client.destroy());
52
+ return { kind: 'done', title: 'Kodi configuration captured', config };
53
+ });
54
+ } catch (error) {
55
+ return { kind: 'error', error: error instanceof Error ? error.message : 'Kodi configuration was refused.' };
56
+ }
57
+ },
39
58
  };
40
59
  }
41
-
42
- private stringValue(valueArg: unknown): string | undefined {
43
- return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
44
- }
45
-
46
- private numberValue(valueArg: unknown): number | undefined {
47
- return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;
48
- }
49
-
50
- private booleanValue(valueArg: unknown): boolean | undefined {
51
- return typeof valueArg === 'boolean' ? valueArg : undefined;
52
- }
53
-
54
- private numberMetadata(candidateArg: IDiscoveryCandidate, keyArg: string): number | undefined {
55
- const value = candidateArg.metadata?.[keyArg];
56
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
57
- }
58
-
59
- private booleanMetadata(candidateArg: IDiscoveryCandidate, keyArg: string): boolean | undefined {
60
- const value = candidateArg.metadata?.[keyArg];
61
- return typeof value === 'boolean' ? value : undefined;
62
- }
63
60
  }