@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,38 +1,35 @@
1
- import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
1
+ import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
2
+ import type { IConfigFlow, IConfigFlowStep, IDiscoveryCandidate, INativeConfigFlowContext } from '../../core/types.js';
2
3
  import type { ISonosConfig } from './sonos.types.js';
3
4
  import { isSonosDataRecord, normalizeSonosConfig } from './sonos.config.js';
4
5
 
5
6
  export class SonosConfigFlow implements IConfigFlow<ISonosConfig> {
6
- public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<ISonosConfig>> {
7
- void contextArg;
7
+ private readonly scope: NativeIntegrationScope;
8
+ constructor(scope?: NativeIntegrationScope, options: { readonly homeId?: string } = {}) {
9
+ this.scope = scope ?? new NativeIntegrationOwner('sonos', options).createFlowScope();
10
+ }
11
+ public async start(candidateArg: IDiscoveryCandidate, contextArg: INativeConfigFlowContext): Promise<IConfigFlowStep<ISonosConfig>> {
8
12
  if (!isSonosDataRecord(candidateArg) || candidateArg.integrationDomain !== 'sonos') return { kind: 'error', error: 'Candidate does not belong to Sonos.' };
9
- candidateArg = { source: candidateArg.source, integrationDomain: 'sonos', host: candidateArg.host, port: candidateArg.port, id: candidateArg.id };
13
+ const candidate = { source: candidateArg.source, host: candidateArg.host, port: candidateArg.port, id: candidateArg.id };
14
+ const admitted = this.scope.admit(contextArg);
10
15
  return {
11
- kind: 'form',
12
- title: 'Connect Sonos',
13
- description: 'Configure the local Sonos player HTTP endpoint discovered by SSDP or mDNS.',
14
- fields: [
15
- { name: 'host', label: 'Host', type: 'text', required: true },
16
- { name: 'port', label: 'Port', type: 'number' },
17
- ],
18
- submit: async (valuesArg) => {
19
- if (!isSonosDataRecord(valuesArg)) throw new Error('Sonos form values must be a plain object.');
20
- for (const key of Reflect.ownKeys(valuesArg)) {
21
- const descriptor = Object.getOwnPropertyDescriptor(valuesArg, key);
22
- if (typeof key !== 'string' || !['host', 'port'].includes(key) || !descriptor || !('value' in descriptor)) throw new Error('Sonos form contains an unsupported field.');
23
- }
24
- if (valuesArg.host !== undefined && typeof valuesArg.host !== 'string') throw new Error('Sonos host must be text.');
25
- if (valuesArg.port !== undefined && typeof valuesArg.port !== 'number') throw new Error('Sonos port must be numeric.');
26
- return {
27
- kind: 'done',
28
- title: 'Sonos configured',
29
- config: normalizeSonosConfig({
30
- host: typeof valuesArg.host === 'string' ? valuesArg.host : candidateArg.host ?? '',
31
- port: typeof valuesArg.port === 'number' ? valuesArg.port : candidateArg.port ?? 1400,
32
- ...(candidateArg.id ? { expectedUid: candidateArg.id } : {}),
33
- }),
34
- };
16
+ kind: 'form', title: 'Connect Sonos', description: 'Configure the selected local Sonos HTTP endpoint for the host-selected home.',
17
+ fields: [{ name: 'host', label: 'Host', type: 'text', required: true }, { name: 'port', label: 'Port', type: 'number' }],
18
+ submit: (values) => {
19
+ if (!isSonosDataRecord(values)) throw new Error('Sonos form values must be a plain object.');
20
+ if (Object.keys(values).some((key) => !['homeId', 'host', 'port'].includes(key))) throw new Error('Sonos form contains an unsupported field.');
21
+ if (values.homeId !== undefined && values.homeId !== admitted.homeId) throw new Error('Sonos submitted home conflicts with the host-selected home.');
22
+ if (values.host !== undefined && typeof values.host !== 'string') throw new Error('Sonos host must be text.');
23
+ if (values.port !== undefined && typeof values.port !== 'number') throw new Error('Sonos port must be numeric.');
24
+ const config = normalizeSonosConfig({ homeId: admitted.homeId, host: typeof values.host === 'string' ? values.host : candidate.host ?? '',
25
+ port: typeof values.port === 'number' ? values.port : candidate.port ?? 1400, ...(candidate.id ? { expectedUid: candidate.id } : {}) });
26
+ return this.scope.run(admitted, config.homeId, async (lease) => {
27
+ lease.assertOpen();
28
+ return { kind: 'done', title: 'Sonos configured', config };
29
+ });
35
30
  },
36
31
  };
37
32
  }
33
+ public destroy(): Promise<void> { return this.scope.close(); }
34
+ public retryCleanup(): Promise<void> { return this.scope.retryCleanup(); }
38
35
  }
@@ -1,60 +1,66 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { BaseIntegration } from '../../core/classes.baseintegration.js';
3
- import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
4
- import { SonosClient } from './sonos.classes.client.js';
3
+ import { NativeIntegrationOwner, type INativeSetupLease } from '../../core/classes.nativeintegrationowner.js';
4
+ import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, INativeIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
5
+ import { SonosClient, SonosCommandError, type ISonosOperationSession } from './sonos.classes.client.js';
5
6
  import { SonosConfigFlow } from './sonos.classes.configflow.js';
6
7
  import { createSonosDiscoveryDescriptor } from './sonos.discovery.js';
7
8
  import { SonosMapper } from './sonos.mapper.js';
8
- import { isSonosDataRecord } from './sonos.config.js';
9
- import type { ISonosConfig, ISonosSnapshot } from './sonos.types.js';
9
+ import { isSonosDataRecord, normalizeSonosConfig, normalizeSonosOwnerOptions } from './sonos.config.js';
10
+ import type { ISonosConfig, ISonosControlResult, ISonosOwnerOptions, ISonosSnapshot } from './sonos.types.js';
10
11
 
11
12
  export class SonosIntegration extends BaseIntegration<ISonosConfig> {
12
13
  public readonly domain = 'sonos';
13
14
  public readonly displayName = 'Sonos';
14
15
  public readonly status = 'control-runtime' as const;
15
16
  public readonly discoveryDescriptor = createSonosDiscoveryDescriptor();
16
- public readonly configFlow = new SonosConfigFlow();
17
+ public readonly configFlow: SonosConfigFlow;
17
18
  public readonly metadata = { source: 'native-typescript', integrationType: 'device', iotClass: 'local_poll' };
18
- private readonly runtimes = new Set<SonosRuntime>();
19
- private readonly setups = new Set<Promise<IIntegrationRuntime>>();
20
- private readonly lifetime = new AbortController();
19
+ private readonly owner: NativeIntegrationOwner;
20
+ private readonly options: Readonly<ISonosOwnerOptions>;
21
21
  private closePromise?: Promise<void>;
22
22
 
23
- public setup(config: ISonosConfig, context: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
24
- if (this.lifetime.signal.aborted) return Promise.reject(new Error('Sonos integration is closed.'));
25
- const signal = AbortSignal.any([this.lifetime.signal, ...(context.abortSignal ? [context.abortSignal] : [])]);
26
- const pending = (async () => {
27
- const client = new SonosClient(config, signal);
28
- try {
29
- const initial = await client.getSnapshot(signal);
30
- signal.throwIfAborted();
31
- const runtime = new SonosRuntime(client, initial, { ...context, abortSignal: signal }, () => { this.runtimes.delete(runtime); });
32
- this.runtimes.add(runtime);
23
+ constructor(options: ISonosOwnerOptions = {}) {
24
+ super();
25
+ this.options = normalizeSonosOwnerOptions(options);
26
+ this.owner = new NativeIntegrationOwner('sonos', this.options.homeId === undefined ? {} : { homeId: this.options.homeId });
27
+ this.configFlow = new SonosConfigFlow(this.owner.createFlowScope());
28
+ }
29
+
30
+ public setup(config: ISonosConfig, context: INativeIntegrationSetupContext): Promise<IIntegrationRuntime> {
31
+ try {
32
+ const selected = normalizeSonosConfig(config);
33
+ return this.owner.runSetup(context, selected.homeId, async (lease) => {
34
+ const client = new SonosClient(selected, lease.context, this.options, { owner: this.owner, lease });
35
+ let runtime: SonosRuntime | undefined;
36
+ let cleanupStarted = false;
37
+ lease.own(() => {
38
+ const retry = cleanupStarted;
39
+ cleanupStarted = true;
40
+ return runtime ? runtime.closeOwnedResources(retry) : retry ? client.retryCleanup() : client.destroy();
41
+ });
42
+ const initial = await client.withSetupLease(lease, (session) => session.getSnapshot());
43
+ lease.assertOpen();
44
+ runtime = new SonosRuntime(client, initial, lease);
45
+ lease.promoteToRuntime();
33
46
  return runtime;
34
- } catch (error) {
35
- try { await client.destroy(); }
36
- catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Sonos setup and cleanup failed.'); }
37
- throw error;
38
- }
39
- })();
40
- this.setups.add(pending);
41
- void pending.then(() => this.setups.delete(pending), () => this.setups.delete(pending));
42
- return pending;
47
+ });
48
+ } catch (error) { return Promise.reject(error); }
43
49
  }
44
50
 
45
51
  public destroy(): Promise<void> {
46
52
  if (!this.closePromise) {
47
- this.lifetime.abort(new Error('Sonos integration is closed.'));
48
- this.closePromise = (async () => {
49
- await Promise.allSettled([...this.setups]);
50
- const results = await Promise.allSettled([...this.runtimes].map((runtime) => runtime.destroy()));
51
- this.runtimes.clear();
53
+ let closing: Promise<void>[] = [];
54
+ this.closePromise = Promise.resolve().then(async () => {
55
+ const results = await Promise.allSettled(closing);
52
56
  const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
53
- if (failures.length) throw new AggregateError(failures.map((result) => result.reason), 'Sonos runtime cleanup failed.');
54
- })();
57
+ if (failures.length) throw new AggregateError(failures.map((result) => result.reason), 'Sonos owned cleanup failed; handles remain retained.');
58
+ });
59
+ closing = [this.configFlow.destroy(), this.owner.close()];
55
60
  }
56
61
  return this.closePromise;
57
62
  }
63
+ public retryCleanup(): Promise<void> { return this.owner.retryCleanup(); }
58
64
  }
59
65
 
60
66
  export class HomeAssistantSonosIntegration extends SonosIntegration {}
@@ -64,6 +70,7 @@ interface ISonosSubscription {
64
70
  job: Promise<void>;
65
71
  failed: boolean;
66
72
  failure?: unknown;
73
+ stopPromise?: Promise<void>;
67
74
  stop(): Promise<void>;
68
75
  }
69
76
 
@@ -74,178 +81,198 @@ class SonosRuntime implements IIntegrationRuntime {
74
81
  private healthy = true;
75
82
  private closed = false;
76
83
  private closePromise?: Promise<void>;
77
- private readonly parentAbort?: () => void;
78
-
79
- constructor(private readonly client: SonosClient, private snapshot: ISonosSnapshot, private readonly context: IIntegrationSetupContext = {}, private readonly onClosed?: () => void) {
80
- if (context.abortSignal) {
81
- this.parentAbort = () => this.lifetime.abort(context.abortSignal?.reason);
82
- context.abortSignal.addEventListener('abort', this.parentAbort, { once: true });
83
- if (context.abortSignal.aborted) this.parentAbort();
84
- }
85
- }
86
84
 
87
- public isHealthy(): boolean { return this.healthy && !this.closed && !this.lifetime.signal.aborted; }
85
+ constructor(private readonly client: SonosClient, private snapshot: ISonosSnapshot, private readonly lease: INativeSetupLease) {}
86
+ public isHealthy(): boolean { return this.healthy && !this.closed && !this.lease.signal.aborted; }
88
87
 
89
- public async devices(signal?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
90
- return SonosMapper.toDevices(await this.read(signal));
88
+ public devices(signal?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
89
+ return this.observe(async (session) => SonosMapper.toDevices(await this.read(session)), signal);
91
90
  }
91
+ public entities(): Promise<IIntegrationEntity[]> { return this.observe(async (session) => SonosMapper.toEntities(await this.read(session))); }
92
92
 
93
- public async entities(): Promise<IIntegrationEntity[]> { return SonosMapper.toEntities(await this.read()); }
94
-
95
- public async writeFeature(request: IDeviceFeatureWriteRequest, signal?: AbortSignal): Promise<IServiceCallResult> {
96
- assertDataRecord(request, ['device', 'feature', 'value']);
97
- assertDataRecord(request.device);
98
- assertDataRecord(request.feature);
99
- const deviceId = request.device.id;
100
- const integrationDomain = request.device.integrationDomain;
101
- const featureId = request.feature.id;
102
- const value = request.value;
103
- const snapshot = await this.read(signal);
104
- const device = SonosMapper.toDevices(snapshot)[0];
105
- if (integrationDomain !== this.domain || deviceId !== device.id) throw new Error('Sonos feature target does not belong to the selected renderer.');
106
- const feature = device.features.find((feature) => feature.id === featureId);
107
- if (!feature?.writable || feature.capability !== 'media') throw new Error('Sonos feature is not writable for this renderer.');
108
- if (feature.id === 'volume') {
109
- if (typeof value !== 'number') throw new Error('Sonos volume must be numeric.');
110
- await this.client.setVolume(value, this.signal(signal));
111
- } else if (feature.id === 'muted') {
112
- if (typeof value !== 'boolean') throw new Error('Sonos mute must be boolean.');
113
- await this.client.setMute(value, this.signal(signal));
114
- } else if (feature.id === 'playback') {
115
- if (value === 'playing') await this.client.play(this.signal(signal));
116
- else if (value === 'paused') await this.client.pause(this.signal(signal));
117
- else if (value === 'stopped') await this.client.stop(this.signal(signal));
118
- else throw new Error('Sonos playback must be playing, paused or stopped.');
119
- } else throw new Error('Unsupported Sonos feature.');
120
- return this.readback(signal);
93
+ public writeFeature(request: IDeviceFeatureWriteRequest, signal?: AbortSignal): Promise<IServiceCallResult> {
94
+ try {
95
+ assertDataRecord(request, ['device', 'feature', 'value']); assertDataRecord(request.device); assertDataRecord(request.feature);
96
+ const deviceId = request.device.id; const integrationDomain = request.device.integrationDomain; const featureId = request.feature.id; const value = request.value;
97
+ this.assertControlAllowed();
98
+ return this.observe(async (session) => {
99
+ const device = SonosMapper.toDevices(await this.read(session))[0];
100
+ if (integrationDomain !== this.domain || deviceId !== device.id) throw new Error('Sonos feature target does not belong to the selected renderer.');
101
+ const feature = device.features.find((feature) => feature.id === featureId);
102
+ if (!feature?.writable || feature.capability !== 'media') throw new Error('Sonos feature is not writable for this renderer.');
103
+ let result: ISonosControlResult;
104
+ if (feature.id === 'volume') {
105
+ if (typeof value !== 'number') throw new Error('Sonos volume must be numeric.');
106
+ result = await session.setVolume(value);
107
+ } else if (feature.id === 'muted') {
108
+ if (typeof value !== 'boolean') throw new Error('Sonos mute must be boolean.');
109
+ result = await session.setMute(value);
110
+ } else if (feature.id === 'playback') {
111
+ if (value === 'playing') result = await session.play();
112
+ else if (value === 'paused') result = await session.pause();
113
+ else if (value === 'stopped') result = await session.stop();
114
+ else throw new Error('Sonos playback must be playing, paused or stopped.');
115
+ } else throw new Error('Unsupported Sonos feature.');
116
+ return this.result(result);
117
+ }, signal);
118
+ } catch (error) { return Promise.reject(error); }
121
119
  }
122
120
 
123
- public async callService(request: IServiceCallRequest): Promise<IServiceCallResult> {
124
- assertDataRecord(request, ['domain', 'service', 'target', 'data']);
125
- assertDataRecord(request.target, ['deviceId', 'entityId']);
126
- if (request.data !== undefined) assertDataRecord(request.data);
127
- request = { domain: request.domain, service: request.service,
128
- target: { ...(request.target.deviceId === undefined ? {} : { deviceId: request.target.deviceId }),
129
- ...(request.target.entityId === undefined ? {} : { entityId: request.target.entityId }) },
130
- ...(request.data === undefined ? {} : { data: { ...request.data } }) };
131
- if (request.domain !== 'media_player' || typeof request.service !== 'string') throw new Error('Unsupported Sonos service domain.');
132
- const snapshot = await this.read();
133
- const deviceId = SonosMapper.deviceId(snapshot);
134
- const entityId = SonosMapper.entityId(snapshot.speakerInfo.uid);
135
- if ((!request.target.deviceId && !request.target.entityId) || (request.target.deviceId !== undefined && request.target.deviceId !== deviceId)
136
- || (request.target.entityId !== undefined && request.target.entityId !== entityId)) throw new Error('Sonos service target does not belong to the selected renderer.');
137
- const data = request.data ?? {};
138
- assertDataRecord(data);
139
- const signal = this.signal();
140
- if (['play', 'pause', 'stop', 'next_track', 'previous_track'].includes(request.service)) {
141
- assertDataRecord(data, []);
142
- if (request.service === 'play') await this.client.play(signal);
143
- else if (request.service === 'pause') await this.client.pause(signal);
144
- else if (request.service === 'stop') await this.client.stop(signal);
145
- else if (request.service === 'next_track') await this.client.next(signal);
146
- else await this.client.previous(signal);
147
- } else if (request.service === 'volume_set') {
148
- assertDataRecord(data, ['volume', 'volume_level']);
149
- if (Object.hasOwn(data, 'volume') === Object.hasOwn(data, 'volume_level')) throw new Error('Sonos volume_set requires exactly one volume argument.');
150
- const value = Object.hasOwn(data, 'volume') ? data.volume : data.volume_level;
151
- if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error('Sonos volume must be finite.');
152
- const percent = Math.round(value * 100);
153
- if (Object.hasOwn(data, 'volume_level') && (value < 0 || value > 1 || Math.abs(value * 100 - percent) > 1e-8)) throw new Error('Sonos volume_level must represent an integer percent from 0 to 100.');
154
- await this.client.setVolume(Object.hasOwn(data, 'volume') ? value : percent, signal);
155
- } else if (request.service === 'volume_mute') {
156
- assertDataRecord(data, ['muted', 'is_volume_muted']);
157
- if (Object.hasOwn(data, 'muted') === Object.hasOwn(data, 'is_volume_muted')) throw new Error('Sonos volume_mute requires exactly one mute argument.');
158
- const value = Object.hasOwn(data, 'muted') ? data.muted : data.is_volume_muted;
159
- if (typeof value !== 'boolean') throw new Error('Sonos mute must be boolean.');
160
- await this.client.setMute(value, signal);
161
- } else if (request.service === 'play_media') {
162
- assertDataRecord(data, ['uri', 'media_content_id', 'title', 'mime_type']);
163
- if (Object.hasOwn(data, 'uri') === Object.hasOwn(data, 'media_content_id')) throw new Error('Sonos play_media requires exactly one URI argument.');
164
- const uri = Object.hasOwn(data, 'uri') ? data.uri : data.media_content_id;
165
- if (typeof uri !== 'string' || (data.title !== undefined && typeof data.title !== 'string') || (data.mime_type !== undefined && typeof data.mime_type !== 'string')) throw new Error('Sonos media URI, title and MIME type must be text.');
166
- await this.client.playUri(uri, { title: data.title, mimeType: data.mime_type }, signal);
167
- } else throw new Error(`Unsupported Sonos service: ${request.service}`);
168
- return this.readback();
121
+ public callService(request: IServiceCallRequest): Promise<IServiceCallResult> {
122
+ try {
123
+ assertDataRecord(request, ['domain', 'service', 'target', 'data']); assertDataRecord(request.target, ['deviceId', 'entityId']);
124
+ if (request.data !== undefined) assertDataRecord(request.data);
125
+ const selected: IServiceCallRequest = { domain: request.domain, service: request.service,
126
+ target: { ...(request.target.deviceId === undefined ? {} : { deviceId: request.target.deviceId }), ...(request.target.entityId === undefined ? {} : { entityId: request.target.entityId }) },
127
+ ...(request.data === undefined ? {} : { data: { ...request.data } }) };
128
+ if (selected.domain !== 'media_player' || typeof selected.service !== 'string') throw new Error('Unsupported Sonos service domain.');
129
+ this.assertControlAllowed();
130
+ return this.observe(async (session) => {
131
+ const snapshot = await this.read(session);
132
+ const deviceId = SonosMapper.deviceId(snapshot); const entityId = SonosMapper.entityId(snapshot.homeId, snapshot.speakerInfo.uid);
133
+ if ((!selected.target.deviceId && !selected.target.entityId) || (selected.target.deviceId !== undefined && selected.target.deviceId !== deviceId)
134
+ || (selected.target.entityId !== undefined && selected.target.entityId !== entityId)) throw new Error('Sonos service target does not belong to the selected renderer.');
135
+ const data = selected.data ?? {}; assertDataRecord(data);
136
+ let result: ISonosControlResult;
137
+ if (['play', 'pause', 'stop', 'next_track', 'previous_track'].includes(selected.service)) {
138
+ assertDataRecord(data, []);
139
+ if (selected.service === 'play') result = await session.play();
140
+ else if (selected.service === 'pause') result = await session.pause();
141
+ else if (selected.service === 'stop') result = await session.stop();
142
+ else if (selected.service === 'next_track') result = await session.next();
143
+ else result = await session.previous();
144
+ } else if (selected.service === 'volume_set') {
145
+ assertDataRecord(data, ['volume', 'volume_level']);
146
+ if (Object.hasOwn(data, 'volume') === Object.hasOwn(data, 'volume_level')) throw new Error('Sonos volume_set requires exactly one volume argument.');
147
+ const value = Object.hasOwn(data, 'volume') ? data.volume : data.volume_level;
148
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error('Sonos volume must be finite.');
149
+ const percent = Math.round(value * 100);
150
+ if (Object.hasOwn(data, 'volume_level') && (value < 0 || value > 1 || Math.abs(value * 100 - percent) > 1e-8)) throw new Error('Sonos volume_level must represent an integer percent from 0 to 100.');
151
+ result = await session.setVolume(Object.hasOwn(data, 'volume') ? value : percent);
152
+ } else if (selected.service === 'volume_mute') {
153
+ assertDataRecord(data, ['muted', 'is_volume_muted']);
154
+ if (Object.hasOwn(data, 'muted') === Object.hasOwn(data, 'is_volume_muted')) throw new Error('Sonos volume_mute requires exactly one mute argument.');
155
+ const value = Object.hasOwn(data, 'muted') ? data.muted : data.is_volume_muted;
156
+ if (typeof value !== 'boolean') throw new Error('Sonos mute must be boolean.');
157
+ result = await session.setMute(value);
158
+ } else if (selected.service === 'play_media') {
159
+ assertDataRecord(data, ['uri', 'media_content_id', 'title', 'mime_type']);
160
+ if (Object.hasOwn(data, 'uri') === Object.hasOwn(data, 'media_content_id')) throw new Error('Sonos play_media requires exactly one URI argument.');
161
+ const uri = Object.hasOwn(data, 'uri') ? data.uri : data.media_content_id;
162
+ if (typeof uri !== 'string' || (data.title !== undefined && typeof data.title !== 'string') || (data.mime_type !== undefined && typeof data.mime_type !== 'string')) throw new Error('Sonos media URI, title and MIME type must be text.');
163
+ result = await session.playUri(uri, { title: data.title, mimeType: data.mime_type });
164
+ } else throw new Error(`Unsupported Sonos service: ${selected.service}`);
165
+ return this.result(result);
166
+ });
167
+ } catch (error) { return Promise.reject(error); }
169
168
  }
170
169
 
171
- public async subscribe(handler: (event: IIntegrationEvent) => void, signal?: AbortSignal): Promise<() => Promise<void>> {
172
- if (typeof handler !== 'function') throw new Error('Sonos subscription handler must be callable.');
173
- if (this.subscriptions.size >= 16) throw new Error('Sonos runtime supports at most 16 subscriptions.');
174
- const controller = new AbortController();
175
- const combined = AbortSignal.any([this.signal(signal), controller.signal]);
176
- const initial = await this.read(combined);
177
- combined.throwIfAborted();
178
- if (this.subscriptions.size >= 16) throw new Error('Sonos runtime supports at most 16 subscriptions.');
179
- handler(this.event('state_changed', initial));
180
- combined.throwIfAborted();
181
- let subscription: ISonosSubscription;
182
- const job = (async () => {
183
- while (!combined.aborted) {
184
- await wait(this.client.config.pollIntervalMs, combined);
185
- if (combined.aborted) return;
186
- let snapshot: ISonosSnapshot;
187
- try { snapshot = await this.read(combined); }
188
- catch (error) {
170
+ public subscribe(handler: (event: IIntegrationEvent) => void, signal?: AbortSignal): Promise<() => Promise<void>> {
171
+ try {
172
+ this.lease.assertOpen();
173
+ if (typeof handler !== 'function') throw new Error('Sonos subscription handler must be callable.');
174
+ if (this.subscriptions.size >= 16) throw new Error('Sonos runtime supports at most 16 subscriptions.');
175
+ const controller = new AbortController();
176
+ const combined = AbortSignal.any([this.lease.signal, this.lifetime.signal, controller.signal, ...(signal ? [signal] : [])]);
177
+ combined.throwIfAborted();
178
+ const subscription: ISonosSubscription = {
179
+ controller, job: Promise.resolve(), failed: false,
180
+ stop: () => {
181
+ if (!subscription.stopPromise) {
182
+ subscription.stopPromise = Promise.resolve().then(async () => {
183
+ await subscription.job;
184
+ this.subscriptions.delete(subscription);
185
+ if (subscription.failed) throw subscription.failure;
186
+ });
187
+ controller.abort(new Error('Sonos subscription stopped.'));
188
+ }
189
+ return subscription.stopPromise;
190
+ },
191
+ };
192
+ // Reservation precedes the initial read and the first caller callback.
193
+ this.subscriptions.add(subscription);
194
+ const publish = async (type: IIntegrationEvent['type'], data: unknown): Promise<void> => {
195
+ try { await handler(this.event(type, data)); }
196
+ catch (error) { subscription.failed = true; subscription.failure = error; throw error; }
197
+ };
198
+ const initial = this.observe(async (session) => {
199
+ const snapshot = await this.read(session); combined.throwIfAborted();
200
+ await publish('state_changed', snapshot); combined.throwIfAborted();
201
+ }, combined);
202
+ subscription.job = initial.then(async () => {
203
+ while (!combined.aborted) {
204
+ await wait(this.client.config.pollIntervalMs, combined);
189
205
  if (combined.aborted) return;
190
- handler(this.event('availability_changed', { available: false }));
191
- handler(this.event('error', { message: error instanceof Error ? error.message : 'Sonos read failed.' }));
192
- continue;
206
+ await this.observe(async (session) => {
207
+ let snapshot: ISonosSnapshot;
208
+ try { snapshot = await this.read(session); }
209
+ catch (error) {
210
+ if (combined.aborted) return;
211
+ await publish('availability_changed', { available: false });
212
+ await publish('error', { message: error instanceof Error ? error.message : 'Sonos read failed.' });
213
+ return;
214
+ }
215
+ await publish('state_changed', snapshot);
216
+ }, combined);
193
217
  }
194
- handler(this.event('state_changed', snapshot));
195
- }
196
- })().catch((error: unknown) => {
197
- if (!combined.aborted) { subscription.failed = true; subscription.failure = error; this.healthy = false; controller.abort(error); }
198
- }).then(() => {
199
- if (!subscription.failed) this.subscriptions.delete(subscription);
200
- });
201
- subscription = {
202
- controller, job, failed: false,
203
- stop: async () => {
204
- controller.abort(new Error('Sonos subscription stopped.'));
205
- await job;
206
- this.subscriptions.delete(subscription);
207
- if (subscription.failed) throw subscription.failure;
208
- },
209
- };
210
- this.subscriptions.add(subscription);
211
- return subscription.stop;
218
+ }).catch((error: unknown) => {
219
+ if (!combined.aborted || subscription.failed) { subscription.failed = true; subscription.failure = error; this.healthy = false; controller.abort(error); }
220
+ }).then(() => { if (!subscription.failed) this.subscriptions.delete(subscription); });
221
+ return initial.then(() => subscription.stop, async (error: unknown) => {
222
+ try { await subscription.stop(); } catch (cleanupError) { if (cleanupError !== error) throw new AggregateError([error, cleanupError], 'Sonos subscription admission and cleanup failed.'); }
223
+ throw error;
224
+ });
225
+ } catch (error) { return Promise.reject(error); }
212
226
  }
213
227
 
214
- public destroy(): Promise<void> {
228
+ public destroy(): Promise<void> { return this.lease.close(); }
229
+
230
+ /** The owner's retained composite handle invokes this, including explicit cleanup retry. */
231
+ public closeOwnedResources(retry = false): Promise<void> {
232
+ if (retry) return this.drain(true);
215
233
  if (!this.closePromise) {
216
- this.closed = true;
217
- this.healthy = false;
234
+ this.closed = true; this.healthy = false;
235
+ this.closePromise = Promise.resolve().then(() => this.drain(false));
218
236
  this.lifetime.abort(new Error('Sonos runtime is closed.'));
219
- if (this.parentAbort) this.context.abortSignal?.removeEventListener('abort', this.parentAbort);
220
- this.closePromise = (async () => {
221
- const results = await Promise.allSettled([this.client.destroy(), ...[...this.subscriptions].map((subscription) => subscription.stop())]);
222
- this.onClosed?.();
223
- const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
224
- if (failures.length) throw new AggregateError(failures.map((result) => result.reason), 'Sonos cleanup failed.');
225
- })();
226
237
  }
227
238
  return this.closePromise;
228
239
  }
240
+ private async drain(retry: boolean): Promise<void> {
241
+ const results = await Promise.allSettled([
242
+ Promise.resolve().then(() => retry ? this.client.retryCleanup() : this.client.destroy()),
243
+ ...[...this.subscriptions].map((subscription) => Promise.resolve().then(() => subscription.stop())),
244
+ ]);
245
+ const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
246
+ if (failures.length) throw new AggregateError(failures.map((result) => result.reason), 'Sonos cleanup or subscriber failed; ownership remains retained.');
247
+ }
229
248
 
230
- private async read(signal?: AbortSignal): Promise<ISonosSnapshot> {
231
- try { this.snapshot = await this.client.getSnapshot(this.signal(signal)); this.healthy = true; return this.snapshot; }
249
+ private observe<TResult>(work: (session: ISonosOperationSession) => Promise<TResult>, signal?: AbortSignal): Promise<TResult> {
250
+ try {
251
+ this.lease.assertOpen();
252
+ return this.client.withOperation(async (session) => {
253
+ try { return await work(session); }
254
+ catch (error) { if (error instanceof SonosCommandError && error.effectOutcome === 'indeterminate') this.healthy = false; throw error; }
255
+ }, signal);
256
+ }
257
+ catch (error) { return Promise.reject(error); }
258
+ }
259
+ private async read(session: ISonosOperationSession): Promise<ISonosSnapshot> {
260
+ try { this.snapshot = await session.getSnapshot(); this.healthy = true; return this.snapshot; }
232
261
  catch (error) { this.healthy = false; throw error; }
233
262
  }
234
-
235
- private async readback(signal?: AbortSignal): Promise<IServiceCallResult> {
236
- const snapshot = await this.read(signal);
237
- return { success: true, data: { devices: SonosMapper.toDevices(snapshot), entities: SonosMapper.toEntities(snapshot) } };
263
+ private assertControlAllowed(): void {
264
+ this.lease.assertOpen();
265
+ if (!this.client.controlAllowed) throw new Error('Sonos control requires explicit runtime owner permission.');
238
266
  }
239
-
240
- private signal(signal?: AbortSignal): AbortSignal {
241
- const combined = AbortSignal.any([this.lifetime.signal, ...(signal ? [signal] : [])]);
242
- combined.throwIfAborted();
243
- return combined;
267
+ private result(result: ISonosControlResult): IServiceCallResult {
268
+ this.snapshot = result.snapshot; this.healthy = true;
269
+ return { success: true, data: { acknowledgedActions: result.acknowledgedActions, outcome: result.outcome, requestedValue: result.requestedValue, observedValue: result.observedValue,
270
+ physicalEffectConfirmed: false, observationAtomic: false, observedAt: result.snapshot.observedAt,
271
+ devices: SonosMapper.toDevices(result.snapshot), entities: SonosMapper.toEntities(result.snapshot) } };
244
272
  }
245
-
246
273
  private event(type: IIntegrationEvent['type'], data: unknown): IIntegrationEvent {
247
- return { type, integrationDomain: this.domain, deviceId: SonosMapper.deviceId(this.snapshot),
248
- entityId: SonosMapper.entityId(this.snapshot.speakerInfo.uid), data: structuredClone(data), timestamp: Date.now() };
274
+ return { type, integrationDomain: this.domain, deviceId: SonosMapper.deviceId(this.snapshot), entityId: SonosMapper.entityId(this.snapshot.homeId, this.snapshot.speakerInfo.uid),
275
+ data: structuredClone(data), timestamp: Date.now() };
249
276
  }
250
277
  }
251
278
 
@@ -256,12 +283,10 @@ function assertDataRecord(value: unknown, allowed?: string[]): asserts value is
256
283
  if (typeof key !== 'string' || !descriptor || !('value' in descriptor) || (allowed && !allowed.includes(key))) throw new Error('Sonos input contains an unsupported field.');
257
284
  }
258
285
  }
259
-
260
286
  function wait(milliseconds: number, signal: AbortSignal): Promise<void> {
261
287
  return new Promise((resolve) => {
262
288
  const finish = (): void => { clearTimeout(timer); signal.removeEventListener('abort', finish); resolve(); };
263
289
  const timer = setTimeout(finish, milliseconds);
264
- signal.addEventListener('abort', finish, { once: true });
265
- if (signal.aborted) finish();
290
+ signal.addEventListener('abort', finish, { once: true }); if (signal.aborted) finish();
266
291
  });
267
292
  }
@@ -1,6 +1,7 @@
1
- import type { ISonosConfig } from './sonos.types.js';
1
+ import type { ISonosConfig, ISonosEndpointConfig, ISonosOwnerOptions } from './sonos.types.js';
2
2
 
3
3
  export interface INormalizedSonosConfig extends ISonosConfig {
4
+ readonly homeId: string;
4
5
  readonly host: string;
5
6
  readonly port: number;
6
7
  readonly expectedUid?: string;
@@ -9,6 +10,19 @@ export interface INormalizedSonosConfig extends ISonosConfig {
9
10
  readonly pollIntervalMs: number;
10
11
  }
11
12
 
13
+ export function normalizeSonosHome(value: unknown): string {
14
+ if (typeof value !== 'string' || !value || value.length > 256 || value.trim() !== value || /[\p{Cc}\p{Cs}]/u.test(value)) {
15
+ throw new Error('Sonos homeId must be a bounded nonempty identifier without surrounding whitespace or control characters.');
16
+ }
17
+ return value;
18
+ }
19
+
20
+ export function normalizeSonosOwnerOptions(input: ISonosOwnerOptions): Readonly<ISonosOwnerOptions> {
21
+ if (!isSonosDataRecord(input) || Object.keys(input).some((key) => !['homeId', 'allowControl'].includes(key))) throw new Error('Sonos owner options contain unsupported fields.');
22
+ if (input.allowControl !== undefined && typeof input.allowControl !== 'boolean') throw new Error('Sonos allowControl must be boolean.');
23
+ return Object.freeze({ ...(input.homeId === undefined ? {} : { homeId: normalizeSonosHome(input.homeId) }), allowControl: input.allowControl === true });
24
+ }
25
+
12
26
  export function normalizeSonosUid(value: unknown): string {
13
27
  if (typeof value !== 'string' || !/^RINCON_[0-9a-f]{12}0?1400$/i.test(value)) {
14
28
  throw new Error('Sonos player identity must be a RINCON UUID.');
@@ -20,11 +34,19 @@ export function isSonosDataRecord(value: unknown): value is Record<string, unkno
20
34
  if (!value || typeof value !== 'object' || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return false;
21
35
  return Reflect.ownKeys(value).every((key) => {
22
36
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
23
- return typeof key === 'string' && descriptor !== undefined && 'value' in descriptor;
37
+ return typeof key === 'string' && descriptor !== undefined && descriptor.enumerable === true && 'value' in descriptor;
24
38
  });
25
39
  }
26
40
 
27
41
  export function normalizeSonosConfig(input: ISonosConfig): INormalizedSonosConfig {
42
+ if (!isSonosDataRecord(input) || !Object.hasOwn(input, 'homeId')) throw new Error('Sonos configuration requires its own homeId.');
43
+ const homeId = normalizeSonosHome(input.homeId);
44
+ const { homeId: _homeId, ...endpoint } = input;
45
+ return Object.freeze({ ...normalizeSonosEndpoint(endpoint), homeId });
46
+ }
47
+
48
+ /** Discovery validates an endpoint only; it cannot select an owning home. */
49
+ export function normalizeSonosEndpoint(input: ISonosEndpointConfig): Omit<INormalizedSonosConfig, 'homeId'> {
28
50
  if (!isSonosDataRecord(input) || !Object.hasOwn(input, 'host')) throw new Error('Sonos configuration must be a plain object containing its own host field.');
29
51
  const allowed = new Set(['host', 'port', 'expectedUid', 'timeoutMs', 'maxResponseBytes', 'pollIntervalMs']);
30
52
  for (const key of Reflect.ownKeys(input)) {