@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,55 +1,60 @@
1
+ import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
1
2
  import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
2
- import type { ISoundtouchConfig } from './soundtouch.types.js';
3
- import { soundtouchDefaultDlnaPort, soundtouchDefaultPort } from './soundtouch.types.js';
3
+ import { SoundtouchClient } from './soundtouch.classes.client.js';
4
+ import { createSoundtouchDiscoveryDescriptor } from './soundtouch.discovery.js';
5
+ import type { ISoundtouchConfig, ISoundtouchHomeOptions } from './soundtouch.types.js';
6
+ import { normalizeSoundtouchConfig, soundtouchKeys, soundtouchRecord, soundtouchText } from './soundtouch.validation.js';
4
7
 
5
8
  export class SoundtouchConfigFlow implements IConfigFlow<ISoundtouchConfig> {
9
+ readonly #owner: NativeIntegrationOwner;
10
+ readonly #scope: NativeIntegrationScope;
11
+ readonly #standalone: boolean;
12
+ public constructor(optionsArg: ISoundtouchHomeOptions | NativeIntegrationOwner = {}) {
13
+ this.#standalone = !(optionsArg instanceof NativeIntegrationOwner);
14
+ this.#owner = optionsArg instanceof NativeIntegrationOwner ? optionsArg : new NativeIntegrationOwner('soundtouch', optionsArg);
15
+ this.#scope = this.#owner.createFlowScope();
16
+ }
6
17
  public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<ISoundtouchConfig>> {
7
- void contextArg;
18
+ // Capture host authority synchronously before the matcher or any submission can await.
19
+ const context = this.#scope.admit(contextArg);
20
+ const matched = await createSoundtouchDiscoveryDescriptor().getValidators()[0].validate(candidateArg, {});
21
+ if (!matched.matched || !matched.candidate) return { kind: 'error', error: matched.reason };
22
+ const candidate = matched.candidate;
8
23
  return {
9
- kind: 'form',
10
- title: 'Connect Bose SoundTouch',
11
- description: candidateArg.source === 'manual'
12
- ? 'Configure a local Bose SoundTouch speaker.'
13
- : 'Confirm or adjust the discovered local Bose SoundTouch speaker.',
24
+ kind: 'form', title: 'Connect selected Bose SoundTouch speaker',
25
+ description: 'Acknowledge the plaintext LAN connection and choose an absolute integer volume ceiling. The host home and selected endpoint/deviceID/type stay fixed.',
14
26
  fields: [
15
- { name: 'host', label: 'Host', type: 'text', required: true },
16
- { name: 'port', label: 'Port', type: 'number' },
17
- { name: 'name', label: 'Name', type: 'text' },
27
+ { name: 'allowUnencryptedLan', label: 'Acknowledge unauthenticated plaintext LAN', type: 'boolean', required: true },
28
+ { name: 'volumeLimit', label: 'Maximum absolute volume (0–100)', type: 'number', required: true },
29
+ { name: 'name', label: 'Local display label', type: 'text' },
18
30
  ],
19
31
  submit: async (valuesArg) => {
20
- const host = stringValue(valuesArg.host) || candidateArg.host;
21
- if (!host) {
22
- return { kind: 'error', title: 'SoundTouch host required', error: 'SoundTouch setup requires a host.' };
32
+ try {
33
+ const values = soundtouchRecord(valuesArg, 'config flow values');
34
+ soundtouchKeys(values, ['homeId', 'allowUnencryptedLan', 'volumeLimit', 'name', 'requestTimeoutMs', 'maxPendingOperations', 'maxResponseBytes']);
35
+ if (values.homeId !== undefined && soundtouchText(values.homeId, 'submitted homeId', 256) !== context.homeId) {
36
+ throw new TypeError('SoundTouch submitted home conflicts with the captured host home.');
37
+ }
38
+ const config = normalizeSoundtouchConfig({ ...values, homeId: context.homeId,
39
+ host: candidate.host, port: candidate.port, deviceId: candidate.id, type: candidate.metadata?.type });
40
+ return await this.#scope.run(context, config.homeId, async (leaseArg) => {
41
+ const client = new SoundtouchClient(config, { ...leaseArg.context, abortSignal: leaseArg.signal });
42
+ let cleanupAttempted = false;
43
+ leaseArg.own(async () => {
44
+ if (cleanupAttempted) return client.retryCleanup();
45
+ cleanupAttempted = true;
46
+ await client.destroy();
47
+ });
48
+ await client.getSnapshot(leaseArg.signal);
49
+ leaseArg.assertOpen();
50
+ return { kind: 'done' as const, title: 'Selected SoundTouch speaker verified', config };
51
+ });
52
+ } catch (errorArg) {
53
+ return { kind: 'error', error: errorArg instanceof Error ? errorArg.message : 'SoundTouch selected setup failed.' };
23
54
  }
24
- return {
25
- kind: 'done',
26
- title: 'Bose SoundTouch configured',
27
- config: {
28
- host,
29
- port: numberValue(valuesArg.port) || candidateArg.port || soundtouchDefaultPort,
30
- dlnaPort: soundtouchDefaultDlnaPort,
31
- name: stringValue(valuesArg.name) || candidateArg.name,
32
- deviceId: candidateArg.id,
33
- model: candidateArg.model,
34
- macAddress: candidateArg.macAddress,
35
- },
36
- };
37
55
  },
38
56
  };
39
57
  }
58
+ public destroy(): Promise<void> { return this.#standalone ? this.#owner.close() : this.#scope.close(); }
59
+ public retryCleanup(): Promise<void> { return this.#standalone ? this.#owner.retryCleanup() : this.#scope.retryCleanup(); }
40
60
  }
41
-
42
- const stringValue = (valueArg: unknown): string | undefined => {
43
- return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
44
- };
45
-
46
- const numberValue = (valueArg: unknown): number | undefined => {
47
- if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
48
- return valueArg;
49
- }
50
- if (typeof valueArg === 'string' && valueArg.trim()) {
51
- const parsed = Number(valueArg);
52
- return Number.isFinite(parsed) ? parsed : undefined;
53
- }
54
- return undefined;
55
- };
@@ -1,178 +1,123 @@
1
- import type * as shxInterfaces from '@smarthome.exchange/interfaces';
1
+ import * as plugins from '../../plugins.js';
2
2
  import { BaseIntegration } from '../../core/classes.baseintegration.js';
3
- import type { IIntegrationEntity, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
3
+ import { NativeIntegrationOwner, type INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
4
+ import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
4
5
  import { SoundtouchClient } from './soundtouch.classes.client.js';
5
6
  import { SoundtouchConfigFlow } from './soundtouch.classes.configflow.js';
6
7
  import { createSoundtouchDiscoveryDescriptor } from './soundtouch.discovery.js';
7
8
  import { SoundtouchMapper } from './soundtouch.mapper.js';
8
- import type { ISoundtouchCommandRequest, ISoundtouchConfig, TSoundtouchCommand } from './soundtouch.types.js';
9
+ import type { ISoundtouchConfig, ISoundtouchHomeOptions, ISoundtouchSnapshot } from './soundtouch.types.js';
10
+ import { soundtouchProfile } from './soundtouch.types.js';
11
+ import { normalizeSoundtouchConfig, soundtouchBoolean, soundtouchInteger, soundtouchKeys, soundtouchRecord } from './soundtouch.validation.js';
9
12
 
10
13
  export class SoundtouchIntegration extends BaseIntegration<ISoundtouchConfig> {
11
14
  public readonly domain = 'soundtouch';
12
15
  public readonly displayName = 'Bose SoundTouch';
13
16
  public readonly status = 'control-runtime' as const;
17
+ public readonly metadata = soundtouchProfile.metadata;
14
18
  public readonly discoveryDescriptor = createSoundtouchDiscoveryDescriptor();
15
- public readonly configFlow = new SoundtouchConfigFlow();
16
- public readonly metadata = {
17
- source: 'home-assistant/core',
18
- upstreamPath: 'homeassistant/components/soundtouch',
19
- upstreamDomain: 'soundtouch',
20
- integrationType: 'device',
21
- iotClass: 'local_polling',
22
- requirements: ['libsoundtouch==0.8'],
23
- dependencies: [],
24
- afterDependencies: [],
25
- codeowners: ['@kroimon'],
26
- configFlow: true,
27
- documentation: 'https://www.home-assistant.io/integrations/soundtouch',
28
- zeroconf: ['_soundtouch._tcp.local.'],
29
- };
19
+ public readonly configFlow: SoundtouchConfigFlow;
20
+ readonly #owner: NativeIntegrationOwner;
30
21
 
31
- public async setup(configArg: ISoundtouchConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
32
- void contextArg;
33
- return new SoundtouchRuntime(new SoundtouchClient(configArg));
22
+ public constructor(optionsArg: ISoundtouchHomeOptions = {}) {
23
+ super();
24
+ this.#owner = new NativeIntegrationOwner('soundtouch', optionsArg);
25
+ this.configFlow = new SoundtouchConfigFlow(this.#owner);
34
26
  }
35
-
36
- public async destroy(): Promise<void> {}
27
+ public setup(configArg: ISoundtouchConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
28
+ const config = normalizeSoundtouchConfig(configArg);
29
+ return this.#owner.runSetup(contextArg, config.homeId, async (leaseArg) => {
30
+ const client = new SoundtouchClient(config, { ...leaseArg.context, abortSignal: leaseArg.signal });
31
+ let cleanupAttempted = false;
32
+ leaseArg.own(async () => {
33
+ if (cleanupAttempted) return client.retryCleanup();
34
+ cleanupAttempted = true;
35
+ await client.destroy();
36
+ });
37
+ const snapshot = await client.getSnapshot(leaseArg.signal);
38
+ leaseArg.assertOpen();
39
+ const runtime = new SoundtouchRuntime(client, snapshot, leaseArg);
40
+ leaseArg.promoteToRuntime();
41
+ return runtime;
42
+ });
43
+ }
44
+ public destroy(): Promise<void> { return this.#owner.close(); }
45
+ public retryCleanup(): Promise<void> { return this.#owner.retryCleanup(); }
37
46
  }
38
-
39
47
  export class HomeAssistantSoundtouchIntegration extends SoundtouchIntegration {}
40
48
 
41
49
  class SoundtouchRuntime implements IIntegrationRuntime {
42
- public domain = 'soundtouch';
43
-
44
- constructor(private readonly client: SoundtouchClient) {}
45
-
46
- public async devices(): Promise<shxInterfaces.data.IDeviceDefinition[]> {
47
- return SoundtouchMapper.toDevices(await this.client.getSnapshot());
50
+ public readonly domain = 'soundtouch';
51
+ readonly #deviceId: string;
52
+ readonly #entityId: string;
53
+ constructor(private readonly client: SoundtouchClient, snapshotArg: ISoundtouchSnapshot, private readonly lease: INativeSetupLease) {
54
+ this.#deviceId = SoundtouchMapper.deviceId(snapshotArg);
55
+ this.#entityId = SoundtouchMapper.mediaEntityId(snapshotArg);
56
+ }
57
+ public isHealthy(): boolean { return !this.lease.signal.aborted && this.client.isHealthy(); }
58
+ public async devices(signalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
59
+ this.lease.assertOpen(); return SoundtouchMapper.toDevices(await this.client.getSnapshot(signalArg));
48
60
  }
49
-
50
61
  public async entities(): Promise<IIntegrationEntity[]> {
51
- return SoundtouchMapper.toEntities(await this.client.getSnapshot());
62
+ this.lease.assertOpen(); return SoundtouchMapper.toEntities(await this.client.getSnapshot());
63
+ }
64
+ public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void, signalArg?: AbortSignal): Promise<() => Promise<void>> {
65
+ this.lease.assertOpen();
66
+ if (typeof handlerArg !== 'function') throw new TypeError('SoundTouch runtime subscriber must be a function.');
67
+ const unsubscribe = this.client.subscribe((eventArg) => {
68
+ handlerArg({ type: eventArg.type === 'error' ? 'error' : 'state_changed', integrationDomain: 'soundtouch',
69
+ deviceId: this.#deviceId, entityId: this.#entityId, timestamp: Date.now(),
70
+ data: eventArg.type === 'snapshot' ? { snapshot: eventArg.snapshot, entity: SoundtouchMapper.toEntities(eventArg.snapshot)[0] }
71
+ : eventArg.type === 'control' ? { observation: eventArg.observation, effectOutcome: 'indeterminate' }
72
+ : { message: eventArg.error.message } });
73
+ }, signalArg);
74
+ return async () => unsubscribe();
52
75
  }
53
-
54
76
  public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
55
77
  try {
56
- if (requestArg.domain === 'media_player') {
57
- return await this.callMediaPlayerService(requestArg);
78
+ this.lease.assertOpen();
79
+ const request = soundtouchRecord(requestArg, 'service request');
80
+ soundtouchKeys(request, ['domain', 'service', 'target', 'data']);
81
+ if (typeof request.domain !== 'string' || typeof request.service !== 'string') throw new TypeError('SoundTouch service domain and name must be strings.');
82
+ const target = soundtouchRecord(request.target, 'service target'); soundtouchKeys(target, ['deviceId', 'entityId']);
83
+ if (target.deviceId === undefined && target.entityId === undefined
84
+ || target.deviceId !== undefined && target.deviceId !== this.#deviceId
85
+ || target.entityId !== undefined && target.entityId !== this.#entityId) throw new TypeError('SoundTouch services require an exact owned home/device or speaker entity target.');
86
+ const data = request.data === undefined ? {} : soundtouchRecord(request.data, 'service data');
87
+ if (request.domain === 'soundtouch' && ['status', 'snapshot', 'refresh'].includes(request.service)) {
88
+ soundtouchKeys(data, []); return { success: true, data: await this.client.getSnapshot() };
58
89
  }
59
- if (requestArg.domain === 'soundtouch') {
60
- return await this.callSoundtouchService(requestArg);
90
+ if (request.domain !== 'soundtouch' && request.domain !== 'media_player') throw new TypeError('SoundTouch service domain is unsupported.');
91
+ if (request.service === 'set_volume' || request.service === 'volume_set') {
92
+ soundtouchKeys(data, ['volume']); return observed(await this.client.setVolume(soundtouchInteger(data.volume, 0, 100, 'absolute volume')));
61
93
  }
62
- return { success: false, error: `Unsupported SoundTouch service domain: ${requestArg.domain}` };
63
- } catch (errorArg) {
64
- return { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg) };
65
- }
66
- }
67
-
68
- public async destroy(): Promise<void> {
69
- await this.client.destroy();
70
- }
71
-
72
- private async callMediaPlayerService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
73
- const command = this.commandFromMediaService(requestArg.service);
74
- if (!command) {
75
- return { success: false, error: `Unsupported SoundTouch media_player service: ${requestArg.service}` };
76
- }
77
- const result = await this.client.execute(this.commandRequest(command, requestArg));
78
- return { success: true, data: result };
79
- }
80
-
81
- private async callSoundtouchService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
82
- if (requestArg.service === 'play_preset' || requestArg.service === 'select_preset') {
83
- const presetId = this.stringData(requestArg, 'preset_id') || this.stringData(requestArg, 'preset') || this.stringData(requestArg, 'presetId') || this.numberStringData(requestArg, 'preset');
84
- const presetName = this.stringData(requestArg, 'preset_name') || this.stringData(requestArg, 'presetName') || this.stringData(requestArg, 'name');
85
- const result = await this.client.execute({ command: 'play_preset', presetId, presetName });
86
- return { success: true, data: result };
87
- }
88
- if (requestArg.service === 'play' || requestArg.service === 'pause' || requestArg.service === 'media_play' || requestArg.service === 'media_pause' || requestArg.service === 'volume_set' || requestArg.service === 'volume_mute' || requestArg.service === 'select_source' || requestArg.service === 'play_media') {
89
- return this.callMediaPlayerService({ ...requestArg, domain: 'media_player' });
90
- }
91
- return { success: false, error: `Unsupported SoundTouch service: ${requestArg.service}` };
92
- }
93
-
94
- private commandFromMediaService(serviceArg: string): TSoundtouchCommand | undefined {
95
- if (serviceArg === 'turn_on') {
96
- return 'turn_on';
97
- }
98
- if (serviceArg === 'turn_off') {
99
- return 'turn_off';
100
- }
101
- if (serviceArg === 'media_play' || serviceArg === 'play') {
102
- return 'play';
103
- }
104
- if (serviceArg === 'media_pause' || serviceArg === 'pause') {
105
- return 'pause';
106
- }
107
- if (serviceArg === 'media_play_pause' || serviceArg === 'play_pause') {
108
- return 'play_pause';
109
- }
110
- if (serviceArg === 'media_previous_track' || serviceArg === 'previous_track') {
111
- return 'previous_track';
112
- }
113
- if (serviceArg === 'media_next_track' || serviceArg === 'next_track') {
114
- return 'next_track';
115
- }
116
- if (serviceArg === 'volume_up') {
117
- return 'volume_up';
118
- }
119
- if (serviceArg === 'volume_down') {
120
- return 'volume_down';
121
- }
122
- if (serviceArg === 'volume_set' || serviceArg === 'set_volume') {
123
- return 'set_volume';
124
- }
125
- if (serviceArg === 'volume_mute' || serviceArg === 'mute') {
126
- return 'mute';
127
- }
128
- if (serviceArg === 'select_source' || serviceArg === 'select_input') {
129
- return 'select_source';
130
- }
131
- if (serviceArg === 'play_media') {
132
- return 'play_media';
133
- }
134
- return undefined;
135
- }
136
-
137
- private commandRequest(commandArg: TSoundtouchCommand, requestArg: IServiceCallRequest): ISoundtouchCommandRequest {
138
- const request: ISoundtouchCommandRequest = { command: commandArg };
139
- if (commandArg === 'set_volume') {
140
- request.volumeLevel = this.numberData(requestArg, 'volume_level') ?? this.numberData(requestArg, 'volumeLevel');
141
- request.volume = this.numberData(requestArg, 'volume');
142
- }
143
- if (commandArg === 'mute') {
144
- request.muted = this.boolData(requestArg, 'is_volume_muted') ?? this.boolData(requestArg, 'mute') ?? this.boolData(requestArg, 'muted');
145
- }
146
- if (commandArg === 'select_source') {
147
- request.source = this.stringData(requestArg, 'source') || this.stringData(requestArg, 'input');
148
- }
149
- if (commandArg === 'play_media') {
150
- request.mediaId = this.stringData(requestArg, 'media_content_id') || this.stringData(requestArg, 'mediaId') || this.stringData(requestArg, 'uri') || this.numberStringData(requestArg, 'media_content_id');
151
- request.mediaType = this.stringData(requestArg, 'media_content_type') || this.stringData(requestArg, 'mediaType');
152
- request.url = this.stringData(requestArg, 'url');
153
- request.presetId = this.stringData(requestArg, 'preset_id') || this.stringData(requestArg, 'presetId') || this.numberStringData(requestArg, 'preset');
154
- request.presetName = this.stringData(requestArg, 'preset_name') || this.stringData(requestArg, 'presetName');
155
- }
156
- return request;
157
- }
158
-
159
- private stringData(requestArg: IServiceCallRequest, keyArg: string): string | undefined {
160
- const value = requestArg.data?.[keyArg];
161
- return typeof value === 'string' && value ? value : undefined;
162
- }
163
-
164
- private numberData(requestArg: IServiceCallRequest, keyArg: string): number | undefined {
165
- const value = requestArg.data?.[keyArg];
166
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
94
+ if (request.service === 'set_mute' || request.service === 'volume_mute') {
95
+ soundtouchKeys(data, ['muted']); return observed(await this.client.setMuted(soundtouchBoolean(data.muted)));
96
+ }
97
+ throw new TypeError('SoundTouch supports status reads and explicit absolute volume/mute only.');
98
+ } catch (errorArg) { return failed(errorArg); }
167
99
  }
168
-
169
- private numberStringData(requestArg: IServiceCallRequest, keyArg: string): string | undefined {
170
- const value = this.numberData(requestArg, keyArg);
171
- return typeof value === 'number' ? String(value) : undefined;
100
+ public async writeFeature(requestArg: IDeviceFeatureWriteRequest, signalArg?: AbortSignal): Promise<IServiceCallResult> {
101
+ try {
102
+ this.lease.assertOpen();
103
+ const request = soundtouchRecord(requestArg, 'feature write'); soundtouchKeys(request, ['device', 'feature', 'value']);
104
+ const device = soundtouchRecord(request.device, 'feature device');
105
+ const feature = soundtouchRecord(request.feature, 'feature');
106
+ if (device.id !== this.#deviceId || device.integrationDomain !== 'soundtouch') throw new TypeError('SoundTouch feature device is outside this home runtime.');
107
+ if ((feature.id !== 'volume' && feature.id !== 'mute') || feature.capability !== 'media') throw new TypeError('SoundTouch feature is not an admitted absolute media control.');
108
+ const value = feature.id === 'volume' ? soundtouchInteger(request.value, 0, 100, 'absolute volume') : soundtouchBoolean(request.value);
109
+ return observed(await this.client.writeFeature(feature.id, value, signalArg));
110
+ } catch (errorArg) { return failed(errorArg); }
172
111
  }
112
+ public destroy(): Promise<void> { return this.lease.close(); }
113
+ }
173
114
 
174
- private boolData(requestArg: IServiceCallRequest, keyArg: string): boolean | undefined {
175
- const value = requestArg.data?.[keyArg];
176
- return typeof value === 'boolean' ? value : undefined;
177
- }
115
+ function observed(observationArg: plugins.ecobridgeDeviceManager.IBoseSoundTouchControlResult): IServiceCallResult {
116
+ return { success: false, data: { ...observationArg, effectOutcome: 'indeterminate' } };
117
+ }
118
+ function failed(errorArg: unknown): IServiceCallResult {
119
+ return { success: false, error: errorArg instanceof Error ? errorArg.message : 'SoundTouch operation failed.', data: {
120
+ effectOutcome: errorArg instanceof plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError ? errorArg.effectOutcome : 'not-sent',
121
+ code: errorArg instanceof plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError ? errorArg.code : 'INVALID_OPERATION',
122
+ } };
178
123
  }
@@ -1,137 +1,25 @@
1
1
  import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
2
- import type { IDiscoveryCandidate, IDiscoveryContext, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
3
- import type { ISoundtouchManualEntry, ISoundtouchMdnsRecord } from './soundtouch.types.js';
4
- import { soundtouchDefaultPort } from './soundtouch.types.js';
5
-
6
- const soundtouchDomain = 'soundtouch';
7
- const soundtouchMdnsType = '_soundtouch._tcp.local.';
8
-
9
- export class SoundtouchMdnsMatcher implements IDiscoveryMatcher<ISoundtouchMdnsRecord> {
10
- public id = 'soundtouch-mdns-match';
11
- public source = 'mdns' as const;
12
- public description = 'Recognize Bose SoundTouch zeroconf advertisements.';
13
-
14
- public async matches(recordArg: ISoundtouchMdnsRecord, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
15
- void contextArg;
16
- const type = recordArg.type || '';
17
- const txt = recordArg.txt || {};
18
- const matched = normalizeMdnsType(type) === normalizeMdnsType(soundtouchMdnsType);
19
- if (!matched) {
20
- return { matched: false, confidence: 'low', reason: 'mDNS record is not a SoundTouch advertisement.' };
21
- }
22
-
23
- const id = valueForKey(txt, 'deviceId') || valueForKey(txt, 'deviceID') || valueForKey(txt, 'id') || recordArg.name;
24
- return {
25
- matched: true,
26
- confidence: recordArg.host ? 'certain' : 'high',
27
- reason: 'mDNS record matches Bose SoundTouch zeroconf metadata.',
28
- normalizedDeviceId: id,
29
- candidate: {
30
- source: 'mdns',
31
- integrationDomain: soundtouchDomain,
32
- id,
33
- host: recordArg.host,
34
- port: recordArg.port || soundtouchDefaultPort,
35
- name: recordArg.name,
36
- manufacturer: 'Bose Corporation',
37
- model: valueForKey(txt, 'type') || valueForKey(txt, 'model'),
38
- macAddress: valueForKey(txt, 'mac') || valueForKey(txt, 'macAddress'),
39
- metadata: { mdnsName: recordArg.name, mdnsType: type, txt },
40
- },
41
- };
42
- }
43
- }
44
-
45
- export class SoundtouchManualMatcher implements IDiscoveryMatcher<ISoundtouchManualEntry> {
46
- public id = 'soundtouch-manual-match';
47
- public source = 'manual' as const;
48
- public description = 'Recognize manual Bose SoundTouch setup entries.';
49
-
50
- public async matches(inputArg: ISoundtouchManualEntry, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
51
- void contextArg;
52
- const haystack = `${inputArg.name || ''} ${inputArg.manufacturer || ''} ${inputArg.model || ''}`.toLowerCase();
53
- const matched = Boolean(inputArg.host || inputArg.metadata?.soundtouch || haystack.includes('soundtouch') || haystack.includes('bose'));
54
- if (!matched) {
55
- return { matched: false, confidence: 'low', reason: 'Manual entry does not contain SoundTouch setup hints.' };
2
+ import type { IDiscoveryCandidate, IDiscoveryMatch } from '../../core/types.js';
3
+ import { readSoundtouchEndpoint, soundtouchKeys, soundtouchRecord, soundtouchText } from './soundtouch.validation.js';
4
+
5
+ function matchSoundtouchCandidate(valueArg: unknown): IDiscoveryMatch {
6
+ try {
7
+ const input = soundtouchRecord(valueArg, 'selected candidate'); soundtouchKeys(input, ['source', 'integrationDomain', 'id', 'host', 'port', 'name', 'metadata']);
8
+ if (input.integrationDomain !== 'soundtouch' || input.source !== 'manual' && input.source !== 'custom') {
9
+ return { matched: false, confidence: 'low', reason: 'SoundTouch requires an explicitly selected manual/custom candidate.' };
56
10
  }
57
-
58
- const id = inputArg.deviceId || inputArg.macAddress || inputArg.id || (inputArg.host ? `${inputArg.host}:${inputArg.port || soundtouchDefaultPort}` : undefined);
59
- return {
60
- matched: true,
61
- confidence: inputArg.host ? 'high' : 'medium',
62
- reason: 'Manual entry can start SoundTouch setup.',
63
- normalizedDeviceId: id,
64
- candidate: {
65
- source: 'manual',
66
- integrationDomain: soundtouchDomain,
67
- id,
68
- host: inputArg.host,
69
- port: inputArg.port || soundtouchDefaultPort,
70
- name: inputArg.name,
71
- manufacturer: normalizedManufacturer(inputArg.manufacturer),
72
- model: inputArg.model,
73
- macAddress: inputArg.macAddress,
74
- metadata: inputArg.metadata,
75
- },
76
- };
77
- }
11
+ const metadata = soundtouchRecord(input.metadata, 'candidate metadata'); soundtouchKeys(metadata, ['type']);
12
+ const endpoint = readSoundtouchEndpoint(input.host, input.id, metadata.type, input.port);
13
+ const candidate: IDiscoveryCandidate = Object.freeze({ source: input.source, integrationDomain: 'soundtouch', id: endpoint.deviceId,
14
+ host: endpoint.host, port: endpoint.port, ...(input.name === undefined ? {} : { name: soundtouchText(input.name, 'candidate name') }),
15
+ metadata: Object.freeze({ type: endpoint.type }) });
16
+ return { matched: true, confidence: 'medium', reason: 'Selected endpoint/deviceID/type; live identity is verified during setup.', candidate, normalizedDeviceId: endpoint.deviceId };
17
+ } catch (errorArg) { return { matched: false, confidence: 'low', reason: errorArg instanceof Error ? errorArg.message : 'Invalid SoundTouch candidate.' }; }
78
18
  }
79
-
80
- export class SoundtouchCandidateValidator implements IDiscoveryValidator {
81
- public id = 'soundtouch-candidate-validator';
82
- public description = 'Validate Bose SoundTouch candidates.';
83
-
84
- public async validate(candidateArg: IDiscoveryCandidate, contextArg: IDiscoveryContext): Promise<IDiscoveryMatch> {
85
- void contextArg;
86
- const metadata = candidateArg.metadata || {};
87
- const matched = candidateArg.integrationDomain === soundtouchDomain
88
- || includesSoundtouch(candidateArg.name)
89
- || includesSoundtouch(candidateArg.model)
90
- || includesBose(candidateArg.manufacturer)
91
- || Boolean(metadata.soundtouch)
92
- || normalizeMdnsType(stringMetadata(metadata.mdnsType) || '') === normalizeMdnsType(soundtouchMdnsType);
93
-
94
- return {
95
- matched,
96
- confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
97
- reason: matched ? 'Candidate has Bose SoundTouch metadata.' : 'Candidate is not SoundTouch.',
98
- candidate: matched ? { ...candidateArg, port: candidateArg.port || soundtouchDefaultPort } : undefined,
99
- normalizedDeviceId: candidateArg.id || candidateArg.macAddress,
100
- };
101
- }
19
+ /** Matching only; this subset performs no network discovery. */
20
+ export function createSoundtouchDiscoveryDescriptor(): DiscoveryDescriptor {
21
+ return new DiscoveryDescriptor({ integrationDomain: 'soundtouch', displayName: 'Bose SoundTouch' })
22
+ .addMatcher({ id: 'soundtouch-manual-match', source: 'manual', matches: async (inputArg) => matchSoundtouchCandidate(inputArg) })
23
+ .addMatcher({ id: 'soundtouch-custom-match', source: 'custom', matches: async (inputArg) => matchSoundtouchCandidate(inputArg) })
24
+ .addValidator({ id: 'soundtouch-selected-endpoint', validate: async (candidateArg) => matchSoundtouchCandidate(candidateArg) });
102
25
  }
103
-
104
- export const createSoundtouchDiscoveryDescriptor = (): DiscoveryDescriptor => {
105
- return new DiscoveryDescriptor({ integrationDomain: soundtouchDomain, displayName: 'Bose SoundTouch' })
106
- .addMatcher(new SoundtouchMdnsMatcher())
107
- .addMatcher(new SoundtouchManualMatcher())
108
- .addValidator(new SoundtouchCandidateValidator());
109
- };
110
-
111
- const valueForKey = (recordArg: Record<string, string | undefined> | undefined, keyArg: string): string | undefined => {
112
- if (!recordArg) {
113
- return undefined;
114
- }
115
- const lowerKey = keyArg.toLowerCase();
116
- for (const [key, value] of Object.entries(recordArg)) {
117
- if (key.toLowerCase() === lowerKey) {
118
- return value;
119
- }
120
- }
121
- return undefined;
122
- };
123
-
124
- const normalizeMdnsType = (valueArg: string): string => valueArg.toLowerCase().replace(/\.$/, '');
125
-
126
- const normalizedManufacturer = (valueArg: string | undefined): string => {
127
- if (!valueArg || valueArg.toLowerCase().includes('bose')) {
128
- return 'Bose Corporation';
129
- }
130
- return valueArg;
131
- };
132
-
133
- const includesSoundtouch = (valueArg: string | undefined): boolean => Boolean(valueArg?.toLowerCase().includes('soundtouch'));
134
-
135
- const includesBose = (valueArg: string | undefined): boolean => Boolean(valueArg?.toLowerCase().includes('bose'));
136
-
137
- const stringMetadata = (valueArg: unknown): string | undefined => typeof valueArg === 'string' ? valueArg : undefined;