@smarthome.exchange/integrations 1.0.0 → 3.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 (529) hide show
  1. package/changelog.md +84 -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 +81 -0
  8. package/dist_ts/core/classes.nativeintegrationowner.js +273 -0
  9. package/dist_ts/core/classes.runtimemanager.d.ts +2 -0
  10. package/dist_ts/core/classes.runtimemanager.js +11 -3
  11. package/dist_ts/core/index.d.ts +2 -0
  12. package/dist_ts/core/index.js +3 -1
  13. package/dist_ts/core/types.d.ts +16 -0
  14. package/dist_ts/integrations/braviatv/braviatv.classes.client.d.ts +23 -72
  15. package/dist_ts/integrations/braviatv/braviatv.classes.client.js +265 -625
  16. package/dist_ts/integrations/braviatv/braviatv.classes.configflow.d.ts +7 -7
  17. package/dist_ts/integrations/braviatv/braviatv.classes.configflow.js +51 -68
  18. package/dist_ts/integrations/braviatv/braviatv.classes.integration.d.ts +8 -11
  19. package/dist_ts/integrations/braviatv/braviatv.classes.integration.js +186 -135
  20. package/dist_ts/integrations/braviatv/braviatv.discovery.js +12 -11
  21. package/dist_ts/integrations/braviatv/braviatv.mapper.d.ts +6 -14
  22. package/dist_ts/integrations/braviatv/braviatv.mapper.js +37 -131
  23. package/dist_ts/integrations/braviatv/braviatv.types.d.ts +55 -133
  24. package/dist_ts/integrations/braviatv/braviatv.validation.d.ts +7 -0
  25. package/dist_ts/integrations/braviatv/braviatv.validation.js +72 -0
  26. package/dist_ts/integrations/broadlink/broadlink.actions.d.ts +2 -0
  27. package/dist_ts/integrations/broadlink/broadlink.actions.js +36 -0
  28. package/dist_ts/integrations/broadlink/broadlink.classes.client.d.ts +21 -19
  29. package/dist_ts/integrations/broadlink/broadlink.classes.client.js +185 -124
  30. package/dist_ts/integrations/broadlink/broadlink.classes.configflow.d.ts +5 -4
  31. package/dist_ts/integrations/broadlink/broadlink.classes.configflow.js +64 -83
  32. package/dist_ts/integrations/broadlink/broadlink.classes.integration.d.ts +16 -19
  33. package/dist_ts/integrations/broadlink/broadlink.classes.integration.js +114 -59
  34. package/dist_ts/integrations/broadlink/broadlink.mapper.d.ts +3 -2
  35. package/dist_ts/integrations/broadlink/broadlink.mapper.js +2 -1
  36. package/dist_ts/integrations/broadlink/broadlink.selected.mapper.d.ts +10 -0
  37. package/dist_ts/integrations/broadlink/broadlink.selected.mapper.js +41 -0
  38. package/dist_ts/integrations/broadlink/broadlink.types.d.ts +43 -7
  39. package/dist_ts/integrations/broadlink/broadlink.validation.d.ts +7 -0
  40. package/dist_ts/integrations/broadlink/broadlink.validation.js +63 -0
  41. package/dist_ts/integrations/daikin/daikin.classes.client.d.ts +10 -52
  42. package/dist_ts/integrations/daikin/daikin.classes.client.js +55 -614
  43. package/dist_ts/integrations/daikin/daikin.classes.configflow.d.ts +8 -12
  44. package/dist_ts/integrations/daikin/daikin.classes.configflow.js +77 -138
  45. package/dist_ts/integrations/daikin/daikin.classes.integration.d.ts +7 -24
  46. package/dist_ts/integrations/daikin/daikin.classes.integration.js +175 -67
  47. package/dist_ts/integrations/daikin/daikin.discovery.d.ts +11 -24
  48. package/dist_ts/integrations/daikin/daikin.discovery.js +98 -305
  49. package/dist_ts/integrations/daikin/daikin.mapper.d.ts +6 -7
  50. package/dist_ts/integrations/daikin/daikin.mapper.js +95 -155
  51. package/dist_ts/integrations/daikin/daikin.types.d.ts +24 -44
  52. package/dist_ts/integrations/daikin/daikin.types.js +1 -1
  53. package/dist_ts/integrations/daikin/daikin.validation.d.ts +5 -0
  54. package/dist_ts/integrations/daikin/daikin.validation.js +53 -0
  55. package/dist_ts/integrations/deconz/deconz.classes.client.d.ts +37 -29
  56. package/dist_ts/integrations/deconz/deconz.classes.client.js +521 -294
  57. package/dist_ts/integrations/deconz/deconz.classes.configflow.d.ts +6 -1
  58. package/dist_ts/integrations/deconz/deconz.classes.configflow.js +63 -33
  59. package/dist_ts/integrations/deconz/deconz.classes.integration.d.ts +4 -3
  60. package/dist_ts/integrations/deconz/deconz.classes.integration.js +85 -40
  61. package/dist_ts/integrations/deconz/deconz.mapper.d.ts +1 -0
  62. package/dist_ts/integrations/deconz/deconz.mapper.js +24 -6
  63. package/dist_ts/integrations/deconz/deconz.types.d.ts +43 -6
  64. package/dist_ts/integrations/deconz/deconz.validation.d.ts +7 -1
  65. package/dist_ts/integrations/deconz/deconz.validation.js +215 -4
  66. package/dist_ts/integrations/denonavr/denonavr.classes.client.d.ts +31 -40
  67. package/dist_ts/integrations/denonavr/denonavr.classes.client.js +149 -404
  68. package/dist_ts/integrations/denonavr/denonavr.classes.configflow.d.ts +7 -2
  69. package/dist_ts/integrations/denonavr/denonavr.classes.configflow.js +44 -36
  70. package/dist_ts/integrations/denonavr/denonavr.classes.integration.d.ts +6 -11
  71. package/dist_ts/integrations/denonavr/denonavr.classes.integration.js +192 -113
  72. package/dist_ts/integrations/denonavr/denonavr.discovery.d.ts +10 -18
  73. package/dist_ts/integrations/denonavr/denonavr.discovery.js +71 -193
  74. package/dist_ts/integrations/denonavr/denonavr.mapper.d.ts +5 -13
  75. package/dist_ts/integrations/denonavr/denonavr.mapper.js +35 -159
  76. package/dist_ts/integrations/denonavr/denonavr.types.d.ts +55 -90
  77. package/dist_ts/integrations/denonavr/denonavr.validation.d.ts +10 -0
  78. package/dist_ts/integrations/denonavr/denonavr.validation.js +86 -0
  79. package/dist_ts/integrations/esphome/esphome.classes.client.d.ts +19 -7
  80. package/dist_ts/integrations/esphome/esphome.classes.client.js +183 -89
  81. package/dist_ts/integrations/esphome/esphome.classes.configflow.d.ts +5 -2
  82. package/dist_ts/integrations/esphome/esphome.classes.configflow.js +43 -33
  83. package/dist_ts/integrations/esphome/esphome.classes.integration.d.ts +8 -6
  84. package/dist_ts/integrations/esphome/esphome.classes.integration.js +71 -43
  85. package/dist_ts/integrations/esphome/esphome.discovery.js +4 -12
  86. package/dist_ts/integrations/esphome/esphome.mapper.d.ts +2 -2
  87. package/dist_ts/integrations/esphome/esphome.mapper.js +6 -4
  88. package/dist_ts/integrations/esphome/esphome.types.d.ts +31 -6
  89. package/dist_ts/integrations/esphome/esphome.types.js +1 -1
  90. package/dist_ts/integrations/esphome/esphome.validation.d.ts +7 -0
  91. package/dist_ts/integrations/esphome/esphome.validation.js +66 -0
  92. package/dist_ts/integrations/homeassistant/homeassistant.classes.client.d.ts +32 -4
  93. package/dist_ts/integrations/homeassistant/homeassistant.classes.client.js +345 -116
  94. package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.d.ts +8 -5
  95. package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.js +34 -8
  96. package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.d.ts +7 -4
  97. package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.js +44 -56
  98. package/dist_ts/integrations/homeassistant/homeassistant.types.d.ts +12 -1
  99. package/dist_ts/integrations/hue/hue.classes.client.d.ts +6 -1
  100. package/dist_ts/integrations/hue/hue.classes.client.js +120 -45
  101. package/dist_ts/integrations/hue/hue.classes.configflow.js +16 -7
  102. package/dist_ts/integrations/hue/hue.classes.integration.d.ts +2 -0
  103. package/dist_ts/integrations/hue/hue.classes.integration.js +103 -30
  104. package/dist_ts/integrations/hue/hue.validation.d.ts +1 -0
  105. package/dist_ts/integrations/hue/hue.validation.js +14 -4
  106. package/dist_ts/integrations/kodi/kodi.classes.client.d.ts +11 -45
  107. package/dist_ts/integrations/kodi/kodi.classes.client.js +84 -310
  108. package/dist_ts/integrations/kodi/kodi.classes.configflow.d.ts +9 -8
  109. package/dist_ts/integrations/kodi/kodi.classes.configflow.js +53 -48
  110. package/dist_ts/integrations/kodi/kodi.classes.integration.d.ts +11 -11
  111. package/dist_ts/integrations/kodi/kodi.classes.integration.js +114 -171
  112. package/dist_ts/integrations/kodi/kodi.discovery.js +53 -29
  113. package/dist_ts/integrations/kodi/kodi.mapper.d.ts +7 -2
  114. package/dist_ts/integrations/kodi/kodi.mapper.js +48 -10
  115. package/dist_ts/integrations/kodi/kodi.types.d.ts +32 -28
  116. package/dist_ts/integrations/kodi/kodi.validation.d.ts +9 -0
  117. package/dist_ts/integrations/kodi/kodi.validation.js +62 -0
  118. package/dist_ts/integrations/modbus/modbus.classes.client.d.ts +15 -61
  119. package/dist_ts/integrations/modbus/modbus.classes.client.js +117 -776
  120. package/dist_ts/integrations/modbus/modbus.classes.configflow.d.ts +9 -5
  121. package/dist_ts/integrations/modbus/modbus.classes.configflow.js +70 -38
  122. package/dist_ts/integrations/modbus/modbus.classes.integration.d.ts +6 -17
  123. package/dist_ts/integrations/modbus/modbus.classes.integration.js +133 -57
  124. package/dist_ts/integrations/modbus/modbus.discovery.d.ts +5 -8
  125. package/dist_ts/integrations/modbus/modbus.discovery.js +38 -93
  126. package/dist_ts/integrations/modbus/modbus.mapper.d.ts +8 -32
  127. package/dist_ts/integrations/modbus/modbus.mapper.js +71 -431
  128. package/dist_ts/integrations/modbus/modbus.types.d.ts +82 -332
  129. package/dist_ts/integrations/modbus/modbus.validation.d.ts +10 -0
  130. package/dist_ts/integrations/modbus/modbus.validation.js +128 -0
  131. package/dist_ts/integrations/mqtt/mqtt.classes.client.d.ts +15 -2
  132. package/dist_ts/integrations/mqtt/mqtt.classes.client.js +173 -23
  133. package/dist_ts/integrations/mqtt/mqtt.classes.configflow.d.ts +5 -1
  134. package/dist_ts/integrations/mqtt/mqtt.classes.configflow.js +53 -33
  135. package/dist_ts/integrations/mqtt/mqtt.classes.integration.d.ts +4 -1
  136. package/dist_ts/integrations/mqtt/mqtt.classes.integration.js +127 -56
  137. package/dist_ts/integrations/mqtt/mqtt.classes.session.d.ts +13 -0
  138. package/dist_ts/integrations/mqtt/mqtt.classes.session.js +130 -0
  139. package/dist_ts/integrations/mqtt/mqtt.mapper.d.ts +3 -0
  140. package/dist_ts/integrations/mqtt/mqtt.mapper.js +44 -1
  141. package/dist_ts/integrations/mqtt/mqtt.types.d.ts +46 -0
  142. package/dist_ts/integrations/mqtt/mqtt.validation.d.ts +7 -0
  143. package/dist_ts/integrations/mqtt/mqtt.validation.js +180 -0
  144. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.client.d.ts +12 -34
  145. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.client.js +143 -231
  146. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.d.ts +6 -1
  147. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.js +105 -18
  148. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.d.ts +8 -8
  149. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.js +141 -155
  150. package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.d.ts +9 -0
  151. package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.js +35 -0
  152. package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.d.ts +6 -18
  153. package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.js +45 -220
  154. package/dist_ts/integrations/nanoleaf/nanoleaf.types.d.ts +33 -147
  155. package/dist_ts/integrations/nanoleaf/nanoleaf.validation.d.ts +6 -0
  156. package/dist_ts/integrations/nanoleaf/nanoleaf.validation.js +82 -0
  157. package/dist_ts/integrations/onkyo/onkyo.classes.client.d.ts +15 -20
  158. package/dist_ts/integrations/onkyo/onkyo.classes.client.js +161 -404
  159. package/dist_ts/integrations/onkyo/onkyo.classes.configflow.d.ts +9 -4
  160. package/dist_ts/integrations/onkyo/onkyo.classes.configflow.js +51 -6
  161. package/dist_ts/integrations/onkyo/onkyo.classes.integration.d.ts +22 -6
  162. package/dist_ts/integrations/onkyo/onkyo.classes.integration.js +144 -11
  163. package/dist_ts/integrations/onkyo/onkyo.discovery.d.ts +6 -1
  164. package/dist_ts/integrations/onkyo/onkyo.discovery.js +40 -4
  165. package/dist_ts/integrations/onkyo/onkyo.mapper.d.ts +7 -8
  166. package/dist_ts/integrations/onkyo/onkyo.mapper.js +45 -12
  167. package/dist_ts/integrations/onkyo/onkyo.types.d.ts +58 -8
  168. package/dist_ts/integrations/onkyo/onkyo.types.js +13 -102
  169. package/dist_ts/integrations/onkyo/onkyo.validation.d.ts +16 -0
  170. package/dist_ts/integrations/onkyo/onkyo.validation.js +77 -0
  171. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.d.ts +25 -25
  172. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.js +116 -360
  173. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.d.ts +11 -3
  174. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.js +76 -6
  175. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.d.ts +26 -4
  176. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.js +142 -9
  177. package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.d.ts +2 -1
  178. package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.js +31 -4
  179. package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.d.ts +9 -9
  180. package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.js +31 -149
  181. package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.d.ts +51 -11
  182. package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.js +22 -87
  183. package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.d.ts +6 -0
  184. package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.js +56 -0
  185. package/dist_ts/integrations/pioneer/pioneer.classes.client.d.ts +31 -16
  186. package/dist_ts/integrations/pioneer/pioneer.classes.client.js +167 -409
  187. package/dist_ts/integrations/pioneer/pioneer.classes.configflow.d.ts +10 -3
  188. package/dist_ts/integrations/pioneer/pioneer.classes.configflow.js +66 -6
  189. package/dist_ts/integrations/pioneer/pioneer.classes.integration.d.ts +13 -5
  190. package/dist_ts/integrations/pioneer/pioneer.classes.integration.js +239 -11
  191. package/dist_ts/integrations/pioneer/pioneer.discovery.d.ts +14 -1
  192. package/dist_ts/integrations/pioneer/pioneer.discovery.js +51 -4
  193. package/dist_ts/integrations/pioneer/pioneer.mapper.d.ts +14 -8
  194. package/dist_ts/integrations/pioneer/pioneer.mapper.js +54 -12
  195. package/dist_ts/integrations/pioneer/pioneer.types.d.ts +79 -12
  196. package/dist_ts/integrations/pioneer/pioneer.types.js +12 -76
  197. package/dist_ts/integrations/pioneer/pioneer.validation.d.ts +10 -0
  198. package/dist_ts/integrations/pioneer/pioneer.validation.js +85 -0
  199. package/dist_ts/integrations/samsungtv/samsungtv.classes.client.d.ts +4 -2
  200. package/dist_ts/integrations/samsungtv/samsungtv.classes.client.js +41 -16
  201. package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.d.ts +5 -0
  202. package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.js +20 -7
  203. package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.d.ts +1 -0
  204. package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.js +53 -50
  205. package/dist_ts/integrations/samsungtv/samsungtv.types.d.ts +4 -1
  206. package/dist_ts/integrations/shelly/shelly.classes.client.d.ts +26 -30
  207. package/dist_ts/integrations/shelly/shelly.classes.client.js +504 -276
  208. package/dist_ts/integrations/shelly/shelly.classes.configflow.d.ts +8 -3
  209. package/dist_ts/integrations/shelly/shelly.classes.configflow.js +43 -38
  210. package/dist_ts/integrations/shelly/shelly.classes.integration.d.ts +6 -6
  211. package/dist_ts/integrations/shelly/shelly.classes.integration.js +103 -101
  212. package/dist_ts/integrations/shelly/shelly.classes.subscription.d.ts +4 -0
  213. package/dist_ts/integrations/shelly/shelly.classes.subscription.js +24 -2
  214. package/dist_ts/integrations/shelly/shelly.mapper.d.ts +1 -2
  215. package/dist_ts/integrations/shelly/shelly.mapper.js +21 -20
  216. package/dist_ts/integrations/shelly/shelly.rpc.js +4 -1
  217. package/dist_ts/integrations/shelly/shelly.types.d.ts +42 -2
  218. package/dist_ts/integrations/shelly/shelly.types.js +11 -2
  219. package/dist_ts/integrations/shelly/shelly.validation.d.ts +7 -0
  220. package/dist_ts/integrations/shelly/shelly.validation.js +79 -0
  221. package/dist_ts/integrations/sonos/sonos.classes.client.d.ts +79 -22
  222. package/dist_ts/integrations/sonos/sonos.classes.client.js +427 -139
  223. package/dist_ts/integrations/sonos/sonos.classes.configflow.d.ts +9 -2
  224. package/dist_ts/integrations/sonos/sonos.classes.configflow.js +26 -28
  225. package/dist_ts/integrations/sonos/sonos.classes.integration.d.ts +7 -6
  226. package/dist_ts/integrations/sonos/sonos.classes.integration.js +289 -224
  227. package/dist_ts/integrations/sonos/sonos.config.d.ts +6 -1
  228. package/dist_ts/integrations/sonos/sonos.config.js +23 -2
  229. package/dist_ts/integrations/sonos/sonos.mapper.d.ts +1 -1
  230. package/dist_ts/integrations/sonos/sonos.mapper.js +14 -9
  231. package/dist_ts/integrations/sonos/sonos.matchers.js +3 -3
  232. package/dist_ts/integrations/sonos/sonos.types.d.ts +26 -1
  233. package/dist_ts/integrations/soundtouch/soundtouch.classes.client.d.ts +13 -36
  234. package/dist_ts/integrations/soundtouch/soundtouch.classes.client.js +179 -502
  235. package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.d.ts +6 -1
  236. package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.js +50 -40
  237. package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.d.ts +14 -15
  238. package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.js +108 -128
  239. package/dist_ts/integrations/soundtouch/soundtouch.discovery.d.ts +2 -20
  240. package/dist_ts/integrations/soundtouch/soundtouch.discovery.js +23 -113
  241. package/dist_ts/integrations/soundtouch/soundtouch.mapper.d.ts +3 -12
  242. package/dist_ts/integrations/soundtouch/soundtouch.mapper.js +35 -189
  243. package/dist_ts/integrations/soundtouch/soundtouch.types.d.ts +57 -142
  244. package/dist_ts/integrations/soundtouch/soundtouch.types.js +18 -3
  245. package/dist_ts/integrations/soundtouch/soundtouch.validation.d.ts +22 -0
  246. package/dist_ts/integrations/soundtouch/soundtouch.validation.js +65 -0
  247. package/dist_ts/integrations/tasmota/tasmota.classes.integration.d.ts +5 -0
  248. package/dist_ts/integrations/tasmota/tasmota.classes.integration.js +23 -7
  249. package/dist_ts/integrations/tasmota/tasmota.classes.runtime.d.ts +19 -5
  250. package/dist_ts/integrations/tasmota/tasmota.classes.runtime.js +115 -41
  251. package/dist_ts/integrations/tasmota/tasmota.config.js +23 -1
  252. package/dist_ts/integrations/tasmota/tasmota.credentials.d.ts +11 -0
  253. package/dist_ts/integrations/tasmota/tasmota.credentials.js +97 -0
  254. package/dist_ts/integrations/tasmota/tasmota.discovery.d.ts +7 -1
  255. package/dist_ts/integrations/tasmota/tasmota.discovery.js +108 -30
  256. package/dist_ts/integrations/tasmota/tasmota.mapper.d.ts +2 -2
  257. package/dist_ts/integrations/tasmota/tasmota.mapper.js +7 -7
  258. package/dist_ts/integrations/tasmota/tasmota.types.d.ts +41 -2
  259. package/dist_ts/integrations/tuya/tuya.classes.integration.d.ts +4 -1
  260. package/dist_ts/integrations/tuya/tuya.classes.integration.js +79 -110
  261. package/dist_ts/integrations/tuya/tuya.classes.runtime.d.ts +5 -2
  262. package/dist_ts/integrations/tuya/tuya.classes.runtime.js +73 -26
  263. package/dist_ts/integrations/tuya/tuya.mapper.js +2 -2
  264. package/dist_ts/integrations/tuya/tuya.types.d.ts +3 -0
  265. package/dist_ts/integrations/tuya/tuya.validation.d.ts +2 -2
  266. package/dist_ts/integrations/tuya/tuya.validation.js +6 -11
  267. package/dist_ts/integrations/webostv/webostv.classes.client.d.ts +6 -5
  268. package/dist_ts/integrations/webostv/webostv.classes.client.js +147 -76
  269. package/dist_ts/integrations/webostv/webostv.classes.configflow.d.ts +7 -2
  270. package/dist_ts/integrations/webostv/webostv.classes.configflow.js +36 -20
  271. package/dist_ts/integrations/webostv/webostv.classes.integration.d.ts +4 -4
  272. package/dist_ts/integrations/webostv/webostv.classes.integration.js +46 -54
  273. package/dist_ts/integrations/webostv/webostv.mapper.js +5 -5
  274. package/dist_ts/integrations/webostv/webostv.types.d.ts +16 -3
  275. package/dist_ts/integrations/webostv/webostv.types.js +1 -1
  276. package/dist_ts/integrations/webostv/webostv.validation.d.ts +3 -1
  277. package/dist_ts/integrations/webostv/webostv.validation.js +23 -1
  278. package/dist_ts/integrations/wiz/index.d.ts +1 -0
  279. package/dist_ts/integrations/wiz/index.js +2 -1
  280. package/dist_ts/integrations/wiz/wiz.classes.client.d.ts +14 -28
  281. package/dist_ts/integrations/wiz/wiz.classes.client.js +93 -229
  282. package/dist_ts/integrations/wiz/wiz.classes.configflow.d.ts +6 -4
  283. package/dist_ts/integrations/wiz/wiz.classes.configflow.js +38 -67
  284. package/dist_ts/integrations/wiz/wiz.classes.integration.d.ts +10 -21
  285. package/dist_ts/integrations/wiz/wiz.classes.integration.js +204 -44
  286. package/dist_ts/integrations/wiz/wiz.discovery.d.ts +10 -31
  287. package/dist_ts/integrations/wiz/wiz.discovery.js +48 -249
  288. package/dist_ts/integrations/wiz/wiz.mapper.d.ts +7 -51
  289. package/dist_ts/integrations/wiz/wiz.mapper.js +82 -668
  290. package/dist_ts/integrations/wiz/wiz.types.d.ts +34 -208
  291. package/dist_ts/integrations/wiz/wiz.validation.d.ts +8 -0
  292. package/dist_ts/integrations/wiz/wiz.validation.js +72 -0
  293. package/dist_ts/integrations/yamaha/index.d.ts +1 -0
  294. package/dist_ts/integrations/yamaha/index.js +2 -1
  295. package/dist_ts/integrations/yamaha/yamaha.classes.client.d.ts +30 -4
  296. package/dist_ts/integrations/yamaha/yamaha.classes.client.js +162 -6
  297. package/dist_ts/integrations/yamaha/yamaha.classes.configflow.d.ts +10 -3
  298. package/dist_ts/integrations/yamaha/yamaha.classes.configflow.js +106 -6
  299. package/dist_ts/integrations/yamaha/yamaha.classes.integration.d.ts +22 -4
  300. package/dist_ts/integrations/yamaha/yamaha.classes.integration.js +243 -8
  301. package/dist_ts/integrations/yamaha/yamaha.discovery.d.ts +13 -1
  302. package/dist_ts/integrations/yamaha/yamaha.discovery.js +57 -4
  303. package/dist_ts/integrations/yamaha/yamaha.mapper.d.ts +16 -8
  304. package/dist_ts/integrations/yamaha/yamaha.mapper.js +58 -12
  305. package/dist_ts/integrations/yamaha/yamaha.types.d.ts +73 -6
  306. package/dist_ts/integrations/yamaha/yamaha.types.js +8 -94
  307. package/dist_ts/integrations/yamaha/yamaha.validation.d.ts +12 -0
  308. package/dist_ts/integrations/yamaha/yamaha.validation.js +89 -0
  309. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.d.ts +6 -1
  310. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.js +18 -8
  311. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.d.ts +4 -1
  312. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.js +54 -6
  313. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.d.ts +12 -6
  314. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.js +73 -59
  315. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.d.ts +3 -2
  316. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.js +17 -12
  317. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.types.d.ts +31 -2
  318. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.d.ts +9 -1
  319. package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.js +57 -7
  320. package/dist_ts/integrations/zwave_js/zwave_js.classes.client.d.ts +10 -4
  321. package/dist_ts/integrations/zwave_js/zwave_js.classes.client.js +167 -31
  322. package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.d.ts +6 -1
  323. package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.js +57 -35
  324. package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.d.ts +3 -1
  325. package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.js +59 -37
  326. package/dist_ts/integrations/zwave_js/zwave_js.discovery.js +3 -3
  327. package/dist_ts/integrations/zwave_js/zwave_js.mapper.js +41 -24
  328. package/dist_ts/integrations/zwave_js/zwave_js.types.d.ts +25 -3
  329. package/dist_ts/integrations/zwave_js/zwave_js.validation.d.ts +6 -2
  330. package/dist_ts/integrations/zwave_js/zwave_js.validation.js +52 -17
  331. package/dist_ts/plugins.d.ts +2 -1
  332. package/dist_ts/plugins.js +3 -2
  333. package/package.json +9 -9
  334. package/readme.md +539 -8
  335. package/ts/00_commitinfo_data.ts +1 -1
  336. package/ts/core/classes.configstore.ts +84 -27
  337. package/ts/core/classes.nativehomeadmission.ts +96 -0
  338. package/ts/core/classes.nativeintegrationowner.ts +302 -0
  339. package/ts/core/classes.runtimemanager.ts +11 -2
  340. package/ts/core/index.ts +2 -0
  341. package/ts/core/types.ts +18 -0
  342. package/ts/integrations/braviatv/braviatv.classes.client.ts +245 -709
  343. package/ts/integrations/braviatv/braviatv.classes.configflow.ts +47 -76
  344. package/ts/integrations/braviatv/braviatv.classes.integration.ts +167 -147
  345. package/ts/integrations/braviatv/braviatv.discovery.ts +11 -10
  346. package/ts/integrations/braviatv/braviatv.mapper.ts +39 -142
  347. package/ts/integrations/braviatv/braviatv.types.ts +53 -173
  348. package/ts/integrations/braviatv/braviatv.validation.ts +68 -0
  349. package/ts/integrations/broadlink/broadlink.actions.ts +31 -0
  350. package/ts/integrations/broadlink/broadlink.classes.client.ts +170 -144
  351. package/ts/integrations/broadlink/broadlink.classes.configflow.ts +59 -89
  352. package/ts/integrations/broadlink/broadlink.classes.integration.ts +110 -64
  353. package/ts/integrations/broadlink/broadlink.mapper.ts +5 -4
  354. package/ts/integrations/broadlink/broadlink.selected.mapper.ts +46 -0
  355. package/ts/integrations/broadlink/broadlink.types.ts +48 -8
  356. package/ts/integrations/broadlink/broadlink.validation.ts +66 -0
  357. package/ts/integrations/daikin/daikin.classes.client.ts +52 -661
  358. package/ts/integrations/daikin/daikin.classes.configflow.ts +72 -152
  359. package/ts/integrations/daikin/daikin.classes.integration.ts +166 -71
  360. package/ts/integrations/daikin/daikin.discovery.ts +94 -346
  361. package/ts/integrations/daikin/daikin.mapper.ts +83 -166
  362. package/ts/integrations/daikin/daikin.types.ts +27 -48
  363. package/ts/integrations/daikin/daikin.validation.ts +51 -0
  364. package/ts/integrations/daikin/readme.md +83 -0
  365. package/ts/integrations/deconz/deconz.classes.client.ts +348 -252
  366. package/ts/integrations/deconz/deconz.classes.configflow.ts +57 -29
  367. package/ts/integrations/deconz/deconz.classes.integration.ts +69 -37
  368. package/ts/integrations/deconz/deconz.mapper.ts +22 -5
  369. package/ts/integrations/deconz/deconz.types.ts +49 -6
  370. package/ts/integrations/deconz/deconz.validation.ts +177 -4
  371. package/ts/integrations/deconz/readme.md +15 -1
  372. package/ts/integrations/denonavr/denonavr.classes.client.ts +132 -421
  373. package/ts/integrations/denonavr/denonavr.classes.configflow.ts +44 -37
  374. package/ts/integrations/denonavr/denonavr.classes.integration.ts +171 -117
  375. package/ts/integrations/denonavr/denonavr.discovery.ts +67 -220
  376. package/ts/integrations/denonavr/denonavr.mapper.ts +36 -182
  377. package/ts/integrations/denonavr/denonavr.types.ts +48 -111
  378. package/ts/integrations/denonavr/denonavr.validation.ts +87 -0
  379. package/ts/integrations/esphome/esphome.classes.client.ts +179 -94
  380. package/ts/integrations/esphome/esphome.classes.configflow.ts +40 -33
  381. package/ts/integrations/esphome/esphome.classes.integration.ts +63 -36
  382. package/ts/integrations/esphome/esphome.discovery.ts +3 -12
  383. package/ts/integrations/esphome/esphome.mapper.ts +9 -7
  384. package/ts/integrations/esphome/esphome.types.ts +35 -6
  385. package/ts/integrations/esphome/esphome.validation.ts +66 -0
  386. package/ts/integrations/esphome/readme.md +20 -6
  387. package/ts/integrations/homeassistant/homeassistant.classes.client.ts +353 -125
  388. package/ts/integrations/homeassistant/homeassistant.classes.integration.ts +35 -18
  389. package/ts/integrations/homeassistant/homeassistant.classes.runtime.ts +50 -63
  390. package/ts/integrations/homeassistant/homeassistant.types.ts +17 -1
  391. package/ts/integrations/homeassistant/readme.md +22 -6
  392. package/ts/integrations/hue/hue.classes.client.ts +110 -41
  393. package/ts/integrations/hue/hue.classes.configflow.ts +12 -6
  394. package/ts/integrations/hue/hue.classes.integration.ts +92 -26
  395. package/ts/integrations/hue/hue.validation.ts +13 -3
  396. package/ts/integrations/hue/readme.md +3 -3
  397. package/ts/integrations/kodi/kodi.classes.client.ts +72 -352
  398. package/ts/integrations/kodi/kodi.classes.configflow.ts +52 -55
  399. package/ts/integrations/kodi/kodi.classes.integration.ts +97 -178
  400. package/ts/integrations/kodi/kodi.discovery.ts +49 -28
  401. package/ts/integrations/kodi/kodi.mapper.ts +52 -10
  402. package/ts/integrations/kodi/kodi.types.ts +33 -58
  403. package/ts/integrations/kodi/kodi.validation.ts +60 -0
  404. package/ts/integrations/modbus/modbus.classes.client.ts +100 -842
  405. package/ts/integrations/modbus/modbus.classes.configflow.ts +70 -41
  406. package/ts/integrations/modbus/modbus.classes.integration.ts +115 -60
  407. package/ts/integrations/modbus/modbus.discovery.ts +38 -106
  408. package/ts/integrations/modbus/modbus.mapper.ts +64 -467
  409. package/ts/integrations/modbus/modbus.types.ts +64 -381
  410. package/ts/integrations/modbus/modbus.validation.ts +119 -0
  411. package/ts/integrations/mqtt/mqtt.classes.client.ts +136 -19
  412. package/ts/integrations/mqtt/mqtt.classes.configflow.ts +45 -32
  413. package/ts/integrations/mqtt/mqtt.classes.integration.ts +99 -38
  414. package/ts/integrations/mqtt/mqtt.classes.session.ts +118 -0
  415. package/ts/integrations/mqtt/mqtt.mapper.ts +43 -1
  416. package/ts/integrations/mqtt/mqtt.types.ts +52 -0
  417. package/ts/integrations/mqtt/mqtt.validation.ts +157 -0
  418. package/ts/integrations/mqtt/readme.md +12 -3
  419. package/ts/integrations/nanoleaf/nanoleaf.classes.client.ts +115 -249
  420. package/ts/integrations/nanoleaf/nanoleaf.classes.configflow.ts +90 -19
  421. package/ts/integrations/nanoleaf/nanoleaf.classes.integration.ts +130 -181
  422. package/ts/integrations/nanoleaf/nanoleaf.credentials.ts +39 -0
  423. package/ts/integrations/nanoleaf/nanoleaf.mapper.ts +47 -260
  424. package/ts/integrations/nanoleaf/nanoleaf.types.ts +29 -147
  425. package/ts/integrations/nanoleaf/nanoleaf.validation.ts +77 -0
  426. package/ts/integrations/onkyo/onkyo.classes.client.ts +148 -457
  427. package/ts/integrations/onkyo/onkyo.classes.configflow.ts +52 -6
  428. package/ts/integrations/onkyo/onkyo.classes.integration.ts +121 -12
  429. package/ts/integrations/onkyo/onkyo.discovery.ts +41 -3
  430. package/ts/integrations/onkyo/onkyo.mapper.ts +46 -19
  431. package/ts/integrations/onkyo/onkyo.types.ts +55 -110
  432. package/ts/integrations/onkyo/onkyo.validation.ts +85 -0
  433. package/ts/integrations/onkyo/readme.md +126 -0
  434. package/ts/integrations/panasonic_viera/panasonic_viera.classes.client.ts +101 -398
  435. package/ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.ts +69 -5
  436. package/ts/integrations/panasonic_viera/panasonic_viera.classes.integration.ts +141 -9
  437. package/ts/integrations/panasonic_viera/panasonic_viera.discovery.ts +32 -3
  438. package/ts/integrations/panasonic_viera/panasonic_viera.mapper.ts +34 -161
  439. package/ts/integrations/panasonic_viera/panasonic_viera.types.ts +52 -96
  440. package/ts/integrations/panasonic_viera/panasonic_viera.validation.ts +56 -0
  441. package/ts/integrations/pioneer/pioneer.classes.client.ts +132 -428
  442. package/ts/integrations/pioneer/pioneer.classes.configflow.ts +67 -5
  443. package/ts/integrations/pioneer/pioneer.classes.integration.ts +225 -11
  444. package/ts/integrations/pioneer/pioneer.discovery.ts +45 -3
  445. package/ts/integrations/pioneer/pioneer.mapper.ts +57 -13
  446. package/ts/integrations/pioneer/pioneer.types.ts +87 -85
  447. package/ts/integrations/pioneer/pioneer.validation.ts +84 -0
  448. package/ts/integrations/samsungtv/readme.md +11 -5
  449. package/ts/integrations/samsungtv/samsungtv.classes.client.ts +40 -15
  450. package/ts/integrations/samsungtv/samsungtv.classes.configflow.ts +21 -6
  451. package/ts/integrations/samsungtv/samsungtv.classes.integration.ts +53 -38
  452. package/ts/integrations/samsungtv/samsungtv.types.ts +4 -1
  453. package/ts/integrations/shelly/readme.md +22 -7
  454. package/ts/integrations/shelly/shelly.classes.client.ts +386 -269
  455. package/ts/integrations/shelly/shelly.classes.configflow.ts +43 -38
  456. package/ts/integrations/shelly/shelly.classes.integration.ts +92 -111
  457. package/ts/integrations/shelly/shelly.classes.subscription.ts +24 -1
  458. package/ts/integrations/shelly/shelly.mapper.ts +19 -20
  459. package/ts/integrations/shelly/shelly.rpc.ts +2 -0
  460. package/ts/integrations/shelly/shelly.types.ts +50 -2
  461. package/ts/integrations/shelly/shelly.validation.ts +76 -0
  462. package/ts/integrations/sonos/readme.md +117 -38
  463. package/ts/integrations/sonos/sonos.classes.client.ts +392 -123
  464. package/ts/integrations/sonos/sonos.classes.configflow.ts +25 -28
  465. package/ts/integrations/sonos/sonos.classes.integration.ts +211 -186
  466. package/ts/integrations/sonos/sonos.config.ts +24 -2
  467. package/ts/integrations/sonos/sonos.mapper.ts +13 -8
  468. package/ts/integrations/sonos/sonos.matchers.ts +2 -2
  469. package/ts/integrations/sonos/sonos.types.ts +30 -1
  470. package/ts/integrations/soundtouch/soundtouch.classes.client.ts +152 -571
  471. package/ts/integrations/soundtouch/soundtouch.classes.configflow.ts +47 -42
  472. package/ts/integrations/soundtouch/soundtouch.classes.integration.ts +95 -150
  473. package/ts/integrations/soundtouch/soundtouch.discovery.ts +21 -133
  474. package/ts/integrations/soundtouch/soundtouch.mapper.ts +36 -207
  475. package/ts/integrations/soundtouch/soundtouch.types.ts +50 -195
  476. package/ts/integrations/soundtouch/soundtouch.validation.ts +60 -0
  477. package/ts/integrations/tasmota/readme.md +16 -5
  478. package/ts/integrations/tasmota/tasmota.classes.integration.ts +24 -6
  479. package/ts/integrations/tasmota/tasmota.classes.runtime.ts +112 -43
  480. package/ts/integrations/tasmota/tasmota.config.ts +17 -0
  481. package/ts/integrations/tasmota/tasmota.credentials.ts +92 -0
  482. package/ts/integrations/tasmota/tasmota.discovery.ts +87 -24
  483. package/ts/integrations/tasmota/tasmota.mapper.ts +6 -6
  484. package/ts/integrations/tasmota/tasmota.types.ts +43 -2
  485. package/ts/integrations/tuya/readme.md +52 -10
  486. package/ts/integrations/tuya/tuya.classes.integration.ts +69 -98
  487. package/ts/integrations/tuya/tuya.classes.runtime.ts +70 -24
  488. package/ts/integrations/tuya/tuya.mapper.ts +1 -1
  489. package/ts/integrations/tuya/tuya.types.ts +4 -0
  490. package/ts/integrations/tuya/tuya.validation.ts +5 -8
  491. package/ts/integrations/webostv/readme.md +115 -22
  492. package/ts/integrations/webostv/webostv.classes.client.ts +123 -50
  493. package/ts/integrations/webostv/webostv.classes.configflow.ts +35 -15
  494. package/ts/integrations/webostv/webostv.classes.integration.ts +44 -37
  495. package/ts/integrations/webostv/webostv.mapper.ts +4 -4
  496. package/ts/integrations/webostv/webostv.types.ts +18 -3
  497. package/ts/integrations/webostv/webostv.validation.ts +22 -1
  498. package/ts/integrations/wiz/index.ts +1 -0
  499. package/ts/integrations/wiz/wiz.classes.client.ts +84 -254
  500. package/ts/integrations/wiz/wiz.classes.configflow.ts +39 -70
  501. package/ts/integrations/wiz/wiz.classes.integration.ts +189 -45
  502. package/ts/integrations/wiz/wiz.discovery.ts +48 -291
  503. package/ts/integrations/wiz/wiz.mapper.ts +81 -754
  504. package/ts/integrations/wiz/wiz.types.ts +24 -224
  505. package/ts/integrations/wiz/wiz.validation.ts +73 -0
  506. package/ts/integrations/yamaha/index.ts +1 -0
  507. package/ts/integrations/yamaha/yamaha.classes.client.ts +150 -6
  508. package/ts/integrations/yamaha/yamaha.classes.configflow.ts +100 -5
  509. package/ts/integrations/yamaha/yamaha.classes.integration.ts +236 -8
  510. package/ts/integrations/yamaha/yamaha.discovery.ts +52 -3
  511. package/ts/integrations/yamaha/yamaha.mapper.ts +65 -13
  512. package/ts/integrations/yamaha/yamaha.types.ts +77 -97
  513. package/ts/integrations/yamaha/yamaha.validation.ts +86 -0
  514. package/ts/integrations/zigbee2mqtt/readme.md +22 -27
  515. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.ts +16 -7
  516. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.ts +53 -6
  517. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.ts +62 -49
  518. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.ts +17 -11
  519. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.types.ts +35 -2
  520. package/ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.ts +53 -7
  521. package/ts/integrations/zwave_js/readme.md +42 -9
  522. package/ts/integrations/zwave_js/zwave_js.classes.client.ts +155 -31
  523. package/ts/integrations/zwave_js/zwave_js.classes.configflow.ts +55 -31
  524. package/ts/integrations/zwave_js/zwave_js.classes.integration.ts +57 -35
  525. package/ts/integrations/zwave_js/zwave_js.discovery.ts +2 -2
  526. package/ts/integrations/zwave_js/zwave_js.mapper.ts +37 -23
  527. package/ts/integrations/zwave_js/zwave_js.types.ts +26 -3
  528. package/ts/integrations/zwave_js/zwave_js.validation.ts +52 -18
  529. package/ts/plugins.ts +2 -1
@@ -1,581 +1,162 @@
1
- import type {
2
- ISoundtouchCommandRequest,
3
- ISoundtouchConfig,
4
- ISoundtouchContentItem,
5
- ISoundtouchDeviceInfo,
6
- ISoundtouchPreset,
7
- ISoundtouchRawCommandRequest,
8
- ISoundtouchSnapshot,
9
- ISoundtouchStatus,
10
- ISoundtouchVolume,
11
- ISoundtouchZoneMember,
12
- ISoundtouchZoneStatus,
13
- TSoundtouchCommand,
14
- } from './soundtouch.types.js';
15
- import { soundtouchDefaultDlnaPort, soundtouchDefaultPort, soundtouchDefaultSourceList } from './soundtouch.types.js';
16
-
17
- const keyCommands: Record<Exclude<TSoundtouchCommand, 'turn_on' | 'turn_off' | 'set_volume' | 'mute' | 'select_source' | 'play_media' | 'play_preset'>, string> = {
18
- play: 'PLAY',
19
- pause: 'PAUSE',
20
- play_pause: 'PLAY_PAUSE',
21
- previous_track: 'PREV_TRACK',
22
- next_track: 'NEXT_TRACK',
23
- volume_up: 'VOLUME_UP',
24
- volume_down: 'VOLUME_DOWN',
25
- };
1
+ import * as plugins from '../../plugins.js';
2
+ import { NativeHomeAdmission } from '../../core/classes.nativehomeadmission.js';
3
+ import type { ISoundtouchConfig, ISoundtouchSetupContext, ISoundtouchSnapshot, TSoundtouchClientEvent } from './soundtouch.types.js';
4
+ import { normalizeSoundtouchConfig, soundtouchBoolean, soundtouchInteger } from './soundtouch.validation.js';
5
+
6
+ interface ISoundtouchSubscriber {
7
+ handler: (eventArg: TSoundtouchClientEvent) => void;
8
+ active: boolean;
9
+ failed: boolean;
10
+ failure?: unknown;
11
+ detach: () => void;
12
+ unsubscribe: () => void;
13
+ }
26
14
 
15
+ /** Owns one selected published protocol and a bounded logical operation lifetime. */
27
16
  export class SoundtouchClient {
28
- constructor(private readonly config: ISoundtouchConfig) {}
29
-
30
- public async getSnapshot(): Promise<ISoundtouchSnapshot> {
31
- if (this.config.snapshot) {
32
- return this.normalizeSnapshot(this.cloneValue(this.config.snapshot));
33
- }
34
- if (!this.config.host && !this.config.commandExecutor) {
35
- return this.normalizeSnapshot(this.manualSnapshot(false, 'SoundTouch refresh requires config.host, config.snapshot, or commandExecutor.'));
36
- }
37
-
38
- const infoXml = await this.fetchText('/info');
39
- const deviceInfo = this.parseDeviceInfo(infoXml);
40
- const [status, volume, presets, zone] = await Promise.all([
41
- this.optionalText('/now_playing').then((xmlArg) => xmlArg ? this.parseStatus(xmlArg) : undefined),
42
- this.optionalText('/volume').then((xmlArg) => xmlArg ? this.parseVolume(xmlArg) : undefined),
43
- this.optionalText('/presets').then((xmlArg) => xmlArg ? this.parsePresets(xmlArg) : []),
44
- this.optionalText('/getZone').then((xmlArg) => xmlArg ? this.parseZoneStatus(xmlArg) : undefined),
45
- ]);
46
-
47
- return this.normalizeSnapshot({
48
- config: deviceInfo,
49
- status,
50
- volume,
51
- presets,
52
- zone,
53
- sourceList: this.config.sourceList,
54
- available: true,
55
- lastUpdated: new Date().toISOString(),
17
+ readonly #config: ReturnType<typeof normalizeSoundtouchConfig>;
18
+ readonly #protocol: plugins.ecobridgeDeviceManager.BoseSoundTouchProtocol;
19
+ readonly #lifetime = new AbortController();
20
+ readonly #operations = new Set<Promise<unknown>>();
21
+ readonly #subscribers = new Set<ISoundtouchSubscriber>();
22
+ readonly #parentSignal?: AbortSignal;
23
+ readonly #onParentAbort: () => void;
24
+ #tail: Promise<void> = Promise.resolve();
25
+ #healthy = false;
26
+ #closed = false;
27
+ #closePromise?: Promise<void>;
28
+ #retryPromise?: Promise<void>;
29
+ #protocolClosed = false;
30
+
31
+ public constructor(configArg: ISoundtouchConfig, contextArg: ISoundtouchSetupContext) {
32
+ this.#config = normalizeSoundtouchConfig(configArg);
33
+ const context = new NativeHomeAdmission('soundtouch').admit(contextArg, this.#config.homeId);
34
+ const signalArg = context.abortSignal;
35
+ this.#protocol = new plugins.ecobridgeDeviceManager.BoseSoundTouchProtocol({
36
+ address: this.#config.host, port: this.#config.port, expectedDeviceId: this.#config.deviceId, expectedType: this.#config.type,
37
+ allowPlaintext: true, timeoutMs: this.#config.requestTimeoutMs, maxPendingOperations: this.#config.maxPendingOperations,
38
+ maxResponseBytes: this.#config.maxResponseBytes,
39
+ controlPolicy: { maximumVolume: this.#config.volumeLimit },
56
40
  });
57
- }
58
-
59
- public async validateConnection(): Promise<ISoundtouchSnapshot> {
60
- const snapshot = await this.getSnapshot();
61
- if (!snapshot.config.deviceId && !snapshot.config.name) {
62
- throw new Error('SoundTouch device did not provide a device identifier.');
63
- }
64
- return snapshot;
65
- }
66
-
67
- public async execute(requestArg: ISoundtouchCommandRequest): Promise<unknown> {
68
- this.ensureCommandTransport();
69
-
70
- if (requestArg.command === 'turn_on') {
71
- return this.setPower(true);
72
- }
73
- if (requestArg.command === 'turn_off') {
74
- return this.setPower(false);
75
- }
76
- if (requestArg.command in keyCommands) {
77
- return this.sendKey(keyCommands[requestArg.command as keyof typeof keyCommands], requestArg.command);
78
- }
79
- if (requestArg.command === 'set_volume') {
80
- return this.setVolume(requestArg);
81
- }
82
- if (requestArg.command === 'mute') {
83
- return this.setMute(requestArg.muted);
84
- }
85
- if (requestArg.command === 'select_source') {
86
- return this.selectSource(requestArg.source);
87
- }
88
- if (requestArg.command === 'play_preset') {
89
- return this.playPreset(requestArg);
90
- }
91
- if (requestArg.command === 'play_media') {
92
- return this.playMedia(requestArg);
93
- }
94
-
95
- throw new Error(`Unsupported SoundTouch command: ${requestArg.command}`);
96
- }
97
-
98
- public async destroy(): Promise<void> {}
99
-
100
- private async setPower(poweredArg: boolean): Promise<unknown> {
101
- const snapshot = await this.getSnapshot().catch(() => undefined);
102
- const isStandby = snapshot?.status?.source === 'STANDBY';
103
- if (poweredArg && snapshot?.status && !isStandby) {
104
- return { skipped: true, reason: 'SoundTouch device is already on.' };
105
- }
106
- if (!poweredArg && isStandby) {
107
- return { skipped: true, reason: 'SoundTouch device is already off.' };
108
- }
109
- return this.sendKey('POWER', poweredArg ? 'turn_on' : 'turn_off');
110
- }
111
-
112
- private async setVolume(requestArg: ISoundtouchCommandRequest): Promise<string> {
113
- const volume = this.volumePercent(requestArg);
114
- return this.requestText('POST', '/volume', 'set_volume', `<volume>${volume}</volume>`, { 'content-type': 'text/xml; charset="utf-8"' });
115
- }
116
-
117
- private async setMute(mutedArg: boolean | undefined): Promise<unknown> {
118
- if (typeof mutedArg === 'boolean') {
119
- const snapshot = await this.getSnapshot().catch(() => undefined);
120
- if (typeof snapshot?.volume?.muted === 'boolean' && snapshot.volume.muted === mutedArg) {
121
- return { skipped: true, reason: `SoundTouch mute is already ${mutedArg ? 'on' : 'off'}.` };
122
- }
123
- }
124
- return this.sendKey('MUTE', 'mute');
125
- }
126
-
127
- private async selectSource(sourceArg: string | undefined): Promise<string> {
128
- const source = sourceArg?.toUpperCase();
129
- if (source !== 'AUX' && source !== 'BLUETOOTH') {
130
- throw new Error('SoundTouch select_source supports AUX or BLUETOOTH.');
131
- }
132
- return this.sendSelectXml(this.contentItemXml({ source, sourceAccount: source === 'AUX' ? 'AUX' : undefined }), 'select_source');
133
- }
134
-
135
- private async playPreset(requestArg: ISoundtouchCommandRequest): Promise<string> {
136
- const preset = requestArg.contentItem ? { presetId: requestArg.presetId || '', ...requestArg.contentItem } : await this.findPreset(requestArg);
137
- const body = preset.sourceXml || this.contentItemXml(preset);
138
- return this.sendSelectXml(body, 'play_preset');
139
- }
140
-
141
- private async playMedia(requestArg: ISoundtouchCommandRequest): Promise<unknown> {
142
- const mediaId = requestArg.url || requestArg.mediaId;
143
- if (mediaId?.toLowerCase().startsWith('http://')) {
144
- return this.playUrl(mediaId);
145
- }
146
- if (mediaId?.toLowerCase().startsWith('https://')) {
147
- throw new Error('SoundTouch play_media supports only http:// URLs for DLNA playback.');
148
- }
149
- return this.playPreset({ ...requestArg, presetId: requestArg.presetId || mediaId });
150
- }
151
-
152
- private async playUrl(urlArg: string): Promise<string> {
153
- const body = `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><CurrentURIMetaData></CurrentURIMetaData><CurrentURI>${escapeXml(urlArg)}</CurrentURI></u:SetAVTransportURI></s:Body></s:Envelope>`;
154
- return this.requestText('POST', '/AVTransport/Control', 'play_media', body, {
155
- 'user-agent': 'smarthome.exchange',
156
- accept: '*/*',
157
- 'content-type': 'text/xml; charset="utf-8"',
158
- host: this.config.host ? `${this.config.host}:${this.dlnaPort()}` : `:${this.dlnaPort()}`,
159
- soapaction: 'urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI',
160
- }, this.dlnaPort());
161
- }
162
-
163
- private async sendKey(keyArg: string, commandArg: TSoundtouchCommand): Promise<string[]> {
164
- const body = (stateArg: 'press' | 'release') => `<key state="${stateArg}" sender="Gabbo">${keyArg}</key>`;
165
- return [
166
- await this.requestText('POST', '/key', commandArg, body('press'), { 'content-type': 'text/xml; charset="utf-8"' }),
167
- await this.requestText('POST', '/key', commandArg, body('release'), { 'content-type': 'text/xml; charset="utf-8"' }),
168
- ];
169
- }
170
-
171
- private async sendSelectXml(bodyArg: string, commandArg: TSoundtouchCommand): Promise<string> {
172
- return this.requestText('POST', '/select', commandArg, bodyArg, { 'content-type': 'text/xml; charset="utf-8"' });
173
- }
174
-
175
- private async findPreset(requestArg: ISoundtouchCommandRequest): Promise<ISoundtouchPreset> {
176
- const snapshot = await this.getSnapshot();
177
- const preset = (snapshot.presets || []).find((presetArg) => {
178
- return Boolean(
179
- requestArg.presetId && String(presetArg.presetId) === String(requestArg.presetId)
180
- || requestArg.presetId && presetArg.name === requestArg.presetId
181
- || requestArg.presetName && presetArg.name === requestArg.presetName
182
- );
183
- });
184
- if (!preset) {
185
- throw new Error('SoundTouch play_preset requires a known preset id or name.');
186
- }
187
- return preset;
188
- }
189
-
190
- private contentItemXml(itemArg: ISoundtouchContentItem): string {
191
- if (!itemArg.source) {
192
- throw new Error('SoundTouch ContentItem requires source.');
193
- }
194
- const attributes: Record<string, string | undefined> = {
195
- source: itemArg.source,
196
- type: itemArg.type,
197
- location: itemArg.location,
198
- sourceAccount: itemArg.sourceAccount,
199
- isPresetable: typeof itemArg.isPresetable === 'boolean' ? String(itemArg.isPresetable) : undefined,
41
+ this.#parentSignal = signalArg;
42
+ this.#onParentAbort = () => {
43
+ // The owner receives the retained terminal close promise, including cleanup failures.
44
+ void this.destroy().catch(() => {});
200
45
  };
201
- const attrs = Object.entries(attributes)
202
- .filter((entryArg): entryArg is [string, string] => typeof entryArg[1] === 'string' && entryArg[1] !== '')
203
- .map(([keyArg, valueArg]) => `${keyArg}="${escapeXml(valueArg)}"`)
204
- .join(' ');
205
- const itemName = itemArg.name ? `<itemName>${escapeXml(itemArg.name)}</itemName>` : '';
206
- return `<ContentItem ${attrs}>${itemName}</ContentItem>`;
207
- }
208
-
209
- private async fetchText(pathArg: string): Promise<string> {
210
- return this.requestText('GET', pathArg, 'refresh');
211
- }
212
-
213
- private async optionalText(pathArg: string): Promise<string | undefined> {
214
- try {
215
- return await this.fetchText(pathArg);
216
- } catch {
217
- return undefined;
218
- }
219
- }
220
-
221
- private async requestText(methodArg: 'GET' | 'POST', pathArg: string, commandArg: TSoundtouchCommand | 'refresh', bodyArg?: string, headersArg?: Record<string, string>, portArg = this.port()): Promise<string> {
222
- const rawRequest = this.rawRequest(methodArg, pathArg, commandArg, bodyArg, headersArg, portArg);
223
- if (this.config.commandExecutor) {
224
- return this.executorResultToText(await this.config.commandExecutor.execute(rawRequest));
225
- }
226
- if (!rawRequest.host) {
227
- throw new Error('SoundTouch command transport requires config.host or commandExecutor.');
228
- }
229
-
230
- const controller = this.config.timeoutMs ? new AbortController() : undefined;
231
- const timeout = controller ? globalThis.setTimeout(() => controller.abort(), this.config.timeoutMs) : undefined;
232
- try {
233
- const response = await globalThis.fetch(rawRequest.url, {
234
- method: rawRequest.method,
235
- headers: rawRequest.headers,
236
- body: rawRequest.body,
237
- signal: controller?.signal,
46
+ signalArg?.addEventListener('abort', this.#onParentAbort, { once: true });
47
+ if (signalArg?.aborted) this.#onParentAbort();
48
+ }
49
+ public isHealthy(): boolean { return this.#healthy && !this.#closed && !this.#protocol.isClosed; }
50
+ public getSnapshot(signalArg?: AbortSignal): Promise<ISoundtouchSnapshot> { return this.#run((signal) => this.#inspect(signal), signalArg); }
51
+ public setVolume(valueArg: number, signalArg?: AbortSignal): Promise<plugins.ecobridgeDeviceManager.IBoseSoundTouchControlResult> {
52
+ const volume = soundtouchInteger(valueArg, 0, this.#config.volumeLimit, 'absolute volume');
53
+ return this.#run((signal) => this.#control({ volume }, signal), signalArg);
54
+ }
55
+ public setMuted(valueArg: boolean, signalArg?: AbortSignal): Promise<plugins.ecobridgeDeviceManager.IBoseSoundTouchControlResult> {
56
+ const muted = soundtouchBoolean(valueArg);
57
+ return this.#run((signal) => this.#control({ muted }, signal), signalArg);
58
+ }
59
+ public writeFeature(idArg: string, valueArg: plugins.shxInterfaces.data.TDeviceStateValue, signalArg?: AbortSignal): Promise<plugins.ecobridgeDeviceManager.IBoseSoundTouchControlResult> {
60
+ if (idArg === 'volume') return this.setVolume(soundtouchInteger(valueArg, 0, this.#config.volumeLimit, 'absolute volume'), signalArg);
61
+ if (idArg === 'mute') return this.setMuted(soundtouchBoolean(valueArg), signalArg);
62
+ throw new TypeError('SoundTouch feature is not writable.');
63
+ }
64
+ public subscribe(handlerArg: (eventArg: TSoundtouchClientEvent) => void, signalArg?: AbortSignal): () => void {
65
+ if (this.#closed || this.#protocol.isClosed) throw new Error('SoundTouch client is closed.');
66
+ if (typeof handlerArg !== 'function') throw new TypeError('SoundTouch subscriber must be a function.');
67
+ if (signalArg !== undefined && !(signalArg instanceof AbortSignal)) throw new TypeError('SoundTouch subscription signal must be an AbortSignal.');
68
+ signalArg?.throwIfAborted();
69
+ if (this.#subscribers.size >= 128) throw new Error('SoundTouch subscription capacity is full.');
70
+ const aborted = (): void => { subscriber.detach(); if (!subscriber.failed) this.#subscribers.delete(subscriber); };
71
+ const subscriber: ISoundtouchSubscriber = { handler: handlerArg, active: true, failed: false,
72
+ detach: () => { subscriber.active = false; signalArg?.removeEventListener('abort', aborted); },
73
+ unsubscribe: () => {
74
+ subscriber.detach();
75
+ if (subscriber.failed) throw subscriber.failure;
76
+ this.#subscribers.delete(subscriber);
77
+ } };
78
+ this.#subscribers.add(subscriber); signalArg?.addEventListener('abort', aborted, { once: true });
79
+ if (signalArg?.aborted) aborted();
80
+ return subscriber.unsubscribe;
81
+ }
82
+ public destroy(): Promise<void> {
83
+ if (this.#closePromise) return this.#closePromise;
84
+ this.#closed = true; this.#healthy = false;
85
+ this.#closePromise = Promise.resolve().then(() => this.#drain());
86
+ this.#parentSignal?.removeEventListener('abort', this.#onParentAbort);
87
+ for (const subscriber of this.#subscribers) subscriber.detach();
88
+ this.#lifetime.abort(new plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError('SoundTouch client is destroyed.', 'closed'));
89
+ return this.#closePromise;
90
+ }
91
+ /** Explicit cleanup retry only; never repeats an HTTP control or observation. */
92
+ public retryCleanup(): Promise<void> {
93
+ if (!this.#closed) return Promise.reject(new Error('SoundTouch cleanup retry requires terminal close.'));
94
+ if (!this.#retryPromise) {
95
+ this.#retryPromise = Promise.resolve().then(async () => {
96
+ await Promise.allSettled([this.#closePromise]);
97
+ // A caller explicitly acknowledges retained subscriber failures by requesting cleanup retry.
98
+ for (const subscriber of this.#subscribers) { subscriber.detach(); this.#subscribers.delete(subscriber); }
99
+ await this.#drain();
238
100
  });
239
- const text = await response.text();
240
- if (!response.ok) {
241
- throw new Error(`SoundTouch request ${pathArg} failed with HTTP ${response.status}${text ? `: ${text}` : ''}`);
242
- }
243
- return text;
244
- } finally {
245
- if (timeout) {
246
- globalThis.clearTimeout(timeout);
247
- }
248
- }
249
- }
250
-
251
- private rawRequest(methodArg: 'GET' | 'POST', pathArg: string, commandArg: TSoundtouchCommand | 'refresh', bodyArg: string | undefined, headersArg: Record<string, string> | undefined, portArg: number): ISoundtouchRawCommandRequest {
252
- const path = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
253
- return {
254
- command: commandArg,
255
- method: methodArg,
256
- path,
257
- url: `${this.baseUrl(portArg)}${path}`,
258
- host: this.config.host,
259
- port: portArg,
260
- body: bodyArg,
261
- headers: headersArg,
262
- };
263
- }
264
-
265
- private parseDeviceInfo(xmlArg: string): ISoundtouchDeviceInfo {
266
- const infoElement = readFirstElement(xmlArg, 'info') || xmlArg;
267
- const infoAttrs = elementAttributes(infoElement, 'info');
268
- return {
269
- deviceId: infoAttrs.deviceID || this.config.deviceId,
270
- name: readTagValue(infoElement, 'name') || this.config.name,
271
- type: readTagValue(infoElement, 'type') || this.config.model,
272
- accountUuid: readTagValue(infoElement, 'margeAccountUUID'),
273
- moduleType: readTagValue(infoElement, 'moduleType'),
274
- variant: readTagValue(infoElement, 'variant'),
275
- variantMode: readTagValue(infoElement, 'variantMode'),
276
- countryCode: readTagValue(infoElement, 'countryCode'),
277
- regionCode: readTagValue(infoElement, 'regionCode'),
278
- host: this.config.host,
279
- port: this.port(),
280
- networks: readAllElements(infoElement, 'networkInfo').map((networkArg) => ({
281
- type: elementAttributes(networkArg, 'networkInfo').type,
282
- macAddress: readTagValue(networkArg, 'macAddress'),
283
- ipAddress: readTagValue(networkArg, 'ipAddress'),
284
- })),
285
- components: readAllElements(infoElement, 'component').map((componentArg) => ({
286
- category: readTagValue(componentArg, 'componentCategory'),
287
- softwareVersion: readTagValue(componentArg, 'softwareVersion'),
288
- serialNumber: readTagValue(componentArg, 'serialNumber'),
289
- })),
290
- };
291
- }
292
-
293
- private parseStatus(xmlArg: string): ISoundtouchStatus {
294
- const statusElement = readFirstElement(xmlArg, 'nowPlaying') || xmlArg;
295
- const statusAttrs = elementAttributes(statusElement, 'nowPlaying');
296
- const contentElement = readFirstElement(statusElement, 'ContentItem');
297
- const artElement = readFirstElement(statusElement, 'art');
298
- const artAttrs = artElement ? elementAttributes(artElement, 'art') : {};
299
- const timeElement = readFirstElement(statusElement, 'time');
300
- const timeAttrs = timeElement ? elementAttributes(timeElement, 'time') : {};
301
- return {
302
- source: statusAttrs.source,
303
- contentItem: contentElement ? this.parseContentItem(contentElement) : undefined,
304
- track: readTagValue(statusElement, 'track'),
305
- artist: readTagValue(statusElement, 'artist'),
306
- album: readTagValue(statusElement, 'album'),
307
- image: artAttrs.artImageStatus === 'IMAGE_PRESENT' ? readTagValue(statusElement, 'art') : undefined,
308
- duration: numberValue(timeAttrs.total),
309
- position: numberValue(timeElement ? innerText(timeElement, 'time') : undefined),
310
- playStatus: readTagValue(statusElement, 'playStatus'),
311
- shuffleSetting: readTagValue(statusElement, 'shuffleSetting'),
312
- repeatSetting: readTagValue(statusElement, 'repeatSetting'),
313
- streamType: readTagValue(statusElement, 'streamType'),
314
- trackId: readTagValue(statusElement, 'trackID'),
315
- stationName: readTagValue(statusElement, 'stationName'),
316
- description: readTagValue(statusElement, 'description'),
317
- stationLocation: readTagValue(statusElement, 'stationLocation'),
318
- };
319
- }
320
-
321
- private parseContentItem(xmlArg: string): ISoundtouchContentItem {
322
- const attrs = elementAttributes(xmlArg, 'ContentItem');
323
- return {
324
- name: readTagValue(xmlArg, 'itemName'),
325
- source: attrs.source,
326
- type: attrs.type,
327
- location: attrs.location,
328
- sourceAccount: attrs.sourceAccount,
329
- isPresetable: booleanValue(attrs.isPresetable),
330
- };
331
- }
332
-
333
- private parseVolume(xmlArg: string): ISoundtouchVolume {
334
- return {
335
- actual: numberValue(readTagValue(xmlArg, 'actualvolume')),
336
- target: numberValue(readTagValue(xmlArg, 'targetvolume')),
337
- muted: booleanValue(readTagValue(xmlArg, 'muteenabled')),
338
- };
339
- }
340
-
341
- private parsePresets(xmlArg: string): ISoundtouchPreset[] {
342
- return readAllElements(xmlArg, 'preset').map((presetArg) => {
343
- const attrs = elementAttributes(presetArg, 'preset');
344
- const contentElement = readFirstElement(presetArg, 'ContentItem');
345
- const content = contentElement ? this.parseContentItem(contentElement) : {};
346
- return {
347
- ...content,
348
- presetId: attrs.id || readTagValue(presetArg, 'id') || '',
349
- sourceXml: contentElement,
350
- };
351
- }).filter((presetArg) => presetArg.presetId);
352
- }
353
-
354
- private parseZoneStatus(xmlArg: string): ISoundtouchZoneStatus | undefined {
355
- const zoneElement = readFirstElement(xmlArg, 'zone') || xmlArg;
356
- const members = readAllElements(zoneElement, 'member');
357
- if (!members.length) {
358
- return undefined;
359
- }
360
- const attrs = elementAttributes(zoneElement, 'zone');
361
- return {
362
- masterId: attrs.master,
363
- masterIp: attrs.senderIPAddress,
364
- isMaster: attrs.master && this.config.deviceId ? attrs.master === this.config.deviceId : attrs.senderIPAddress === undefined,
365
- slaves: members.map((memberArg): ISoundtouchZoneMember => {
366
- const memberAttrs = elementAttributes(memberArg, 'member');
367
- return {
368
- deviceId: stripTags(innerXml(memberArg, 'member')).trim() || undefined,
369
- ipAddress: memberAttrs.ipaddress,
370
- role: memberAttrs.role,
371
- };
372
- }),
373
- };
374
- }
375
-
376
- private normalizeSnapshot(snapshotArg: ISoundtouchSnapshot): ISoundtouchSnapshot {
377
- const config = {
378
- ...snapshotArg.config,
379
- deviceId: snapshotArg.config.deviceId || this.config.deviceId || this.config.macAddress || this.config.host,
380
- name: snapshotArg.config.name || this.config.name || this.config.host || 'Bose SoundTouch',
381
- type: snapshotArg.config.type || this.config.model || 'SoundTouch',
382
- host: snapshotArg.config.host || this.config.host,
383
- port: snapshotArg.config.port || this.config.port || (this.config.host ? soundtouchDefaultPort : undefined),
384
- networks: snapshotArg.config.networks?.length ? snapshotArg.config.networks : this.config.macAddress || this.config.host ? [{ macAddress: this.config.macAddress, ipAddress: this.config.host }] : [],
385
- };
386
- const zone = snapshotArg.zone ? {
387
- ...snapshotArg.zone,
388
- isMaster: snapshotArg.zone.masterId && config.deviceId ? snapshotArg.zone.masterId === config.deviceId : snapshotArg.zone.isMaster,
389
- slaves: snapshotArg.zone.slaves || [],
390
- } : undefined;
391
- return {
392
- ...snapshotArg,
393
- config,
394
- presets: snapshotArg.presets || [],
395
- zone,
396
- sourceList: snapshotArg.sourceList?.length ? snapshotArg.sourceList : [...soundtouchDefaultSourceList],
397
- available: snapshotArg.available ?? Boolean(this.config.host || this.config.commandExecutor),
398
- lastUpdated: snapshotArg.lastUpdated || new Date().toISOString(),
399
- };
400
- }
401
-
402
- private manualSnapshot(availableArg: boolean, errorArg?: string): ISoundtouchSnapshot {
403
- return {
404
- config: {
405
- deviceId: this.config.deviceId || this.config.macAddress || this.config.host,
406
- name: this.config.name || this.config.host || 'Bose SoundTouch',
407
- type: this.config.model || 'SoundTouch',
408
- host: this.config.host,
409
- port: this.config.port || (this.config.host ? soundtouchDefaultPort : undefined),
410
- networks: this.config.macAddress || this.config.host ? [{ macAddress: this.config.macAddress, ipAddress: this.config.host }] : [],
411
- },
412
- presets: [],
413
- sourceList: this.config.sourceList || [...soundtouchDefaultSourceList],
414
- available: availableArg,
415
- lastUpdated: new Date().toISOString(),
416
- error: errorArg,
417
- };
418
- }
419
-
420
- private ensureCommandTransport(): void {
421
- if (!this.config.host && !this.config.commandExecutor) {
422
- throw new Error('SoundTouch command transport requires config.host or commandExecutor. Static snapshots are read-only.');
423
- }
424
- }
425
-
426
- private async executorResultToText(resultArg: unknown): Promise<string> {
427
- if (typeof resultArg === 'string') {
428
- return resultArg;
429
- }
430
- if (isFetchResponse(resultArg)) {
431
- return resultArg.text();
432
- }
433
- if (isRecord(resultArg) && typeof resultArg.body === 'string') {
434
- return resultArg.body;
435
- }
436
- if (isRecord(resultArg) && typeof resultArg.text === 'string') {
437
- return resultArg.text;
438
- }
439
- return resultArg === undefined ? '' : JSON.stringify(resultArg);
440
- }
441
-
442
- private volumePercent(requestArg: ISoundtouchCommandRequest): number {
443
- const value = requestArg.volume ?? (typeof requestArg.volumeLevel === 'number' ? requestArg.volumeLevel <= 1 ? requestArg.volumeLevel * 100 : requestArg.volumeLevel : undefined);
444
- if (typeof value !== 'number' || !Number.isFinite(value)) {
445
- throw new Error('SoundTouch volume command requires volumeLevel or volume.');
101
+ const settled = (): void => { this.#retryPromise = undefined; };
102
+ void this.#retryPromise.then(settled, settled);
446
103
  }
447
- return Math.max(0, Math.min(100, Math.round(value)));
104
+ return this.#retryPromise;
448
105
  }
449
-
450
- private port(): number {
451
- return this.config.port || soundtouchDefaultPort;
452
- }
453
-
454
- private dlnaPort(): number {
455
- return this.config.dlnaPort || soundtouchDefaultDlnaPort;
456
- }
457
-
458
- private baseUrl(portArg: number): string {
459
- const rawHost = this.config.host || 'localhost';
460
- if (/^https?:\/\//i.test(rawHost)) {
461
- const url = new URL(rawHost);
462
- if (!url.port) {
463
- url.port = String(portArg);
464
- }
465
- return url.origin;
466
- }
467
- const host = rawHost.replace(/\/.*$/, '');
468
- const hostPort = /^([^:]+):(\d+)$/.exec(host);
469
- if (hostPort) {
470
- return `http://${hostPort[1]}:${hostPort[2]}`;
106
+ async #drain(): Promise<void> {
107
+ const close = Promise.resolve().then(async () => {
108
+ if (!this.#protocolClosed) { await this.#protocol.close(); this.#protocolClosed = true; }
109
+ });
110
+ const [closed] = await Promise.allSettled([close, ...this.#operations]);
111
+ const errors: unknown[] = closed.status === 'rejected' ? [closed.reason as unknown] : [];
112
+ for (const subscriber of this.#subscribers) {
113
+ if (subscriber.failed) errors.push(subscriber.failure);
114
+ else this.#subscribers.delete(subscriber);
115
+ }
116
+ if (errors.length) throw new AggregateError(errors, 'SoundTouch cleanup or subscriber failed; explicit cleanup retry is required.');
117
+ }
118
+ async #inspect(signalArg: AbortSignal): Promise<ISoundtouchSnapshot> {
119
+ const state = await this.#protocol.readState({ signal: signalArg });
120
+ const snapshot = this.#snapshot(state); this.#emit({ type: 'snapshot', snapshot }); return snapshot;
121
+ }
122
+ #snapshot(stateArg: plugins.ecobridgeDeviceManager.IBoseSoundTouchState): ISoundtouchSnapshot {
123
+ return Object.freeze({ ...stateArg, homeId: this.#config.homeId, ...(this.#config.name === undefined ? {} : { displayName: this.#config.name }),
124
+ volumeLimit: this.#config.volumeLimit, online: true as const });
125
+ }
126
+ async #control(controlArg: plugins.ecobridgeDeviceManager.IBoseSoundTouchControl, signalArg: AbortSignal): Promise<plugins.ecobridgeDeviceManager.IBoseSoundTouchControlResult> {
127
+ const observation = await this.#protocol.setControl(controlArg, { signal: signalArg });
128
+ this.#emit({ type: 'control', observation });
129
+ this.#emit({ type: 'snapshot', snapshot: this.#snapshot(observation.observed) });
130
+ return observation;
131
+ }
132
+ #run<TResult>(workArg: (signalArg: AbortSignal) => Promise<TResult>, signalArg?: AbortSignal): Promise<TResult> {
133
+ if (this.#closed || this.#protocol.isClosed) return Promise.reject(new plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError('SoundTouch client is terminally closed.', 'closed'));
134
+ if (signalArg !== undefined && !(signalArg instanceof AbortSignal)) return Promise.reject(new TypeError('SoundTouch operation signal must be an AbortSignal.'));
135
+ if (signalArg?.aborted) return Promise.reject(new plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError('SoundTouch operation was cancelled before admission.', 'cancelled'));
136
+ if (this.#operations.size >= this.#config.maxPendingOperations) return Promise.reject(new plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError('SoundTouch client operation capacity is full.', 'capacity'));
137
+ const deadline = new AbortController();
138
+ const signal = AbortSignal.any([this.#lifetime.signal, deadline.signal, ...(signalArg ? [signalArg] : [])]);
139
+ const timer = setTimeout(() => deadline.abort(new plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError('SoundTouch consumer deadline expired.', 'timeout')), this.#config.requestTimeoutMs);
140
+ timer.unref();
141
+ const operation = this.#tail.then(() => { signal.throwIfAborted(); return workArg(signal); }).then((resultArg) => {
142
+ this.#healthy = !this.#closed && !signal.aborted && !this.#protocol.isClosed; return resultArg;
143
+ }, (errorArg: unknown) => {
144
+ this.#healthy = false;
145
+ const error = deadline.signal.aborted && errorArg instanceof plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError
146
+ ? new plugins.ecobridgeDeviceManager.BoseSoundTouchProtocolError('SoundTouch consumer deadline expired.', 'timeout', errorArg.effectOutcome) : errorArg;
147
+ if (!this.#closed && error instanceof Error) this.#emit({ type: 'error', error });
148
+ throw error;
149
+ }).finally(() => { clearTimeout(timer); this.#operations.delete(operation); });
150
+ this.#operations.add(operation);
151
+ this.#tail = operation.then(() => {}, () => {});
152
+ return operation;
153
+ }
154
+ #emit(eventArg: TSoundtouchClientEvent): void {
155
+ if (this.#closed) return;
156
+ for (const subscriber of [...this.#subscribers]) {
157
+ if (!subscriber.active || !this.#subscribers.has(subscriber)) continue;
158
+ try { subscriber.handler(structuredClone(eventArg)); }
159
+ catch (errorArg) { subscriber.failed = true; subscriber.failure = errorArg; subscriber.detach(); }
471
160
  }
472
- return `http://${formatHost(host)}:${portArg}`;
473
- }
474
-
475
- private cloneValue<TValue>(valueArg: TValue): TValue {
476
- return valueArg === undefined ? valueArg : JSON.parse(JSON.stringify(valueArg)) as TValue;
477
161
  }
478
162
  }
479
-
480
- const readFirstElement = (xmlArg: string, tagArg: string): string | undefined => {
481
- const escapedTag = escapeRegExp(tagArg);
482
- const match = new RegExp(`<(?:[A-Za-z0-9_]+:)?${escapedTag}\\b[^>]*>[\\s\\S]*?<\\/(?:[A-Za-z0-9_]+:)?${escapedTag}>`, 'i').exec(xmlArg)
483
- || new RegExp(`<(?:[A-Za-z0-9_]+:)?${escapedTag}\\b[^>]*/>`, 'i').exec(xmlArg);
484
- return match?.[0];
485
- };
486
-
487
- const readAllElements = (xmlArg: string, tagArg: string): string[] => {
488
- const escapedTag = escapeRegExp(tagArg);
489
- const regex = new RegExp(`<(?:[A-Za-z0-9_]+:)?${escapedTag}\\b[^>]*>[\\s\\S]*?<\\/(?:[A-Za-z0-9_]+:)?${escapedTag}>|<(?:[A-Za-z0-9_]+:)?${escapedTag}\\b[^>]*/>`, 'gi');
490
- const values: string[] = [];
491
- let match: RegExpExecArray | null;
492
- while ((match = regex.exec(xmlArg))) {
493
- values.push(match[0]);
494
- }
495
- return values;
496
- };
497
-
498
- const readTagValue = (xmlArg: string, tagArg: string): string | undefined => {
499
- const element = readFirstElement(xmlArg, tagArg);
500
- if (!element) {
501
- return undefined;
502
- }
503
- return stripTags(innerXml(element, tagArg)).trim() || undefined;
504
- };
505
-
506
- const innerText = (elementArg: string, tagArg: string): string | undefined => {
507
- return stripTags(innerXml(elementArg, tagArg)).trim() || undefined;
508
- };
509
-
510
- const innerXml = (elementArg: string, tagArg: string): string => {
511
- const escapedTag = escapeRegExp(tagArg);
512
- return elementArg
513
- .replace(new RegExp(`^<(?:[A-Za-z0-9_]+:)?${escapedTag}\\b[^>]*>`, 'i'), '')
514
- .replace(new RegExp(`<\\/(?:[A-Za-z0-9_]+:)?${escapedTag}>$`, 'i'), '')
515
- .replace(new RegExp(`^<(?:[A-Za-z0-9_]+:)?${escapedTag}\\b[^>]*/>$`, 'i'), '');
516
- };
517
-
518
- const elementAttributes = (elementArg: string, tagArg: string): Record<string, string> => {
519
- const escapedTag = escapeRegExp(tagArg);
520
- const match = new RegExp(`<(?:[A-Za-z0-9_]+:)?${escapedTag}\\b([^>]*)>`, 'i').exec(elementArg);
521
- return parseAttributes(match?.[1] || '');
522
- };
523
-
524
- const parseAttributes = (valueArg: string): Record<string, string> => {
525
- const attrs: Record<string, string> = {};
526
- const regex = /([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
527
- let match: RegExpExecArray | null;
528
- while ((match = regex.exec(valueArg))) {
529
- attrs[match[1]] = unescapeXml(match[2] ?? match[3] ?? '');
530
- }
531
- return attrs;
532
- };
533
-
534
- const stripTags = (valueArg: string): string => unescapeXml(valueArg.replace(/<[^>]+>/g, ''));
535
-
536
- const numberValue = (valueArg: unknown): number | undefined => {
537
- if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
538
- return valueArg;
539
- }
540
- if (typeof valueArg === 'string' && valueArg.trim()) {
541
- const parsed = Number(valueArg);
542
- return Number.isFinite(parsed) ? parsed : undefined;
543
- }
544
- return undefined;
545
- };
546
-
547
- const booleanValue = (valueArg: unknown): boolean | undefined => {
548
- if (typeof valueArg === 'boolean') {
549
- return valueArg;
550
- }
551
- if (typeof valueArg === 'string') {
552
- const normalized = valueArg.toLowerCase();
553
- if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') {
554
- return true;
555
- }
556
- if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') {
557
- return false;
558
- }
559
- }
560
- return undefined;
561
- };
562
-
563
- const escapeXml = (valueArg: string): string => {
564
- return valueArg.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
565
- };
566
-
567
- const unescapeXml = (valueArg: string): string => {
568
- return valueArg.replace(/&apos;/g, "'").replace(/&quot;/g, '"').replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&');
569
- };
570
-
571
- const escapeRegExp = (valueArg: string): string => valueArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
572
-
573
- const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => {
574
- return Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg));
575
- };
576
-
577
- const isFetchResponse = (valueArg: unknown): valueArg is Response => {
578
- return Boolean(valueArg && typeof valueArg === 'object' && typeof (valueArg as Response).text === 'function');
579
- };
580
-
581
- const formatHost = (hostArg: string): string => hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;