@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,15 +1,21 @@
1
1
  import { BaseIntegration } from '../../core/classes.baseintegration.js';
2
+ import { NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
2
3
  import type { IIntegrationSetupContext } from '../../core/types.js';
3
4
  import { createTasmotaDiscoveryDescriptor, TasmotaConfigFlow } from './tasmota.discovery.js';
4
5
  import { TasmotaRuntime } from './tasmota.classes.runtime.js';
5
6
  import type { IHomeAssistantTasmotaConfig } from './tasmota.types.js';
7
+ import type { ITasmotaIntegrationOptions } from './tasmota.types.js';
8
+ import { normalizeTasmotaConfig } from './tasmota.config.js';
9
+ import { tasmotaOptions } from './tasmota.credentials.js';
6
10
 
7
11
  export class HomeAssistantTasmotaIntegration extends BaseIntegration<IHomeAssistantTasmotaConfig> {
8
12
  public readonly domain = 'tasmota';
9
13
  public readonly displayName = 'Tasmota';
10
14
  public readonly status = 'control-runtime' as const;
11
15
  public readonly discoveryDescriptor = createTasmotaDiscoveryDescriptor();
12
- public readonly configFlow = new TasmotaConfigFlow();
16
+ public readonly configFlow: TasmotaConfigFlow;
17
+ private readonly owner: NativeIntegrationOwner;
18
+ private readonly options: ITasmotaIntegrationOptions;
13
19
  public readonly metadata = {
14
20
  source: 'home-assistant/core',
15
21
  upstreamPath: 'homeassistant/components/tasmota',
@@ -19,12 +25,24 @@ export class HomeAssistantTasmotaIntegration extends BaseIntegration<IHomeAssist
19
25
  scope: 'selected-device-relays-brightness-sensors',
20
26
  };
21
27
 
28
+ constructor(optionsArg: ITasmotaIntegrationOptions = {}) {
29
+ super();
30
+ this.options = tasmotaOptions(optionsArg);
31
+ this.owner = new NativeIntegrationOwner(this.domain, { homeId: this.options.homeId, capacity: this.options.capacity });
32
+ this.configFlow = new TasmotaConfigFlow(this.options, this.owner);
33
+ }
34
+
22
35
  public async setup(configArg: IHomeAssistantTasmotaConfig, contextArg: IIntegrationSetupContext): Promise<TasmotaRuntime> {
23
- contextArg.abortSignal?.throwIfAborted();
24
- const runtime = new TasmotaRuntime(configArg);
25
- await runtime.start(contextArg.abortSignal);
26
- return runtime;
36
+ const config = normalizeTasmotaConfig(configArg);
37
+ return this.owner.runSetup(contextArg, config.homeId, async (leaseArg) => {
38
+ const runtime = new TasmotaRuntime(config, leaseArg, this.options, this.owner);
39
+ leaseArg.own(() => runtime.dispose());
40
+ await runtime.start(leaseArg.signal);
41
+ leaseArg.promoteToRuntime();
42
+ return runtime;
43
+ });
27
44
  }
28
45
 
29
- public async destroy(): Promise<void> {}
46
+ public destroy(): Promise<void> { return this.owner.close(); }
47
+ public retryCleanup(): Promise<void> { return this.owner.retryCleanup(); }
30
48
  }
@@ -1,32 +1,43 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { DeviceCommunicationError } from '../../core/errors.js';
3
+ import type { INativeSetupLease, NativeIntegrationOwner } from '../../core/classes.nativeintegrationowner.js';
3
4
  import type { IDeviceFeatureWriteRequest, IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IServiceCallResult } from '../../core/types.js';
4
- import { MqttClient } from '../mqtt/mqtt.classes.client.js';
5
- import type { IMqttMessage } from '../mqtt/mqtt.types.js';
5
+ import type { MqttClient } from '../mqtt/mqtt.classes.client.js';
6
+ import { MqttSession } from '../mqtt/mqtt.classes.session.js';
7
+ import type { IMqttMessage, TMqttCredentialResolver } from '../mqtt/mqtt.types.js';
6
8
  import { parseTasmotaRecord, tasmotaRecord, TasmotaMapper } from './tasmota.mapper.js';
7
- import { normalizeTasmotaConfig } from './tasmota.config.js';
8
- import type { IHomeAssistantTasmotaConfig, ITasmotaConfig, ITasmotaDeviceState, ITasmotaSelectedDevice } from './tasmota.types.js';
9
+ import { tasmotaBrokerIdentity, tasmotaCredential, tasmotaCredentialKey } from './tasmota.credentials.js';
10
+ import type { ITasmotaConfig, ITasmotaDeviceState, ITasmotaIntegrationOptions, ITasmotaSelectedDevice } from './tasmota.types.js';
9
11
 
10
12
  export class TasmotaRuntime implements IIntegrationRuntime {
11
13
  public readonly domain = 'tasmota';
12
- private readonly config: ITasmotaConfig;
13
- private readonly client: MqttClient;
14
+ public readonly homeId: string;
15
+ private session?: MqttSession;
16
+ private client?: MqttClient;
14
17
  private readonly lifetime = new AbortController();
15
18
  private readonly states = new Map<string, ITasmotaDeviceState>();
16
19
  private readonly listeners = new Set<(eventArg: IIntegrationEvent) => void>();
17
20
  private readonly changed = new Set<() => void>();
18
21
  private readonly subscriptionCleanups = new Set<() => void>();
19
- private readonly stopConnectionListener: () => void;
22
+ private stopConnectionListener?: () => void;
23
+ private readonly operations = new Set<Promise<unknown>>();
20
24
  private initializing = true;
21
25
  private started = false;
22
26
  private destroyed = false;
23
27
  private destroyPromise?: Promise<void>;
28
+ private disposePromise?: Promise<void>;
29
+ private cleanupFailed = false;
24
30
 
25
- constructor(configArg: IHomeAssistantTasmotaConfig) {
26
- this.config = normalizeTasmotaConfig(configArg);
27
- this.client = new MqttClient(this.config.broker);
31
+ constructor(private readonly config: ITasmotaConfig, private readonly lease: INativeSetupLease,
32
+ private readonly options: ITasmotaIntegrationOptions, private readonly owner: NativeIntegrationOwner) {
33
+ this.homeId = lease.context.homeId;
34
+ lease.assertOpen();
35
+ if (config.homeId !== this.homeId) throw new TypeError('Tasmota runtime must match its admitted host home.');
28
36
  for (const device of this.config.devices) this.states.set(device.id, { generation: 0, ready: false, online: false, queryPending: false, updatedAt: new Date(0).toISOString(), controls: {}, sensors: {} });
29
- this.stopConnectionListener = this.client.onConnectionState((stateArg) => {
37
+ }
38
+
39
+ private observeConnection(clientArg: MqttClient): void {
40
+ this.stopConnectionListener = clientArg.onConnectionState((stateArg) => {
30
41
  if (this.destroyed) return;
31
42
  if (!stateArg.connected) {
32
43
  for (const device of this.config.devices) this.markOffline(device);
@@ -41,33 +52,50 @@ export class TasmotaRuntime implements IIntegrationRuntime {
41
52
  this.checkActive(abortSignalArg);
42
53
  if (this.started) throw new DeviceCommunicationError('Tasmota runtime has already started.');
43
54
  this.started = true;
44
- try {
45
- await this.withDeadline(async (signalArg) => {
46
- await this.client.start(signalArg);
47
- for (const device of this.config.devices) {
48
- await this.client.subscribe(`${device.statusBase}#`, (messageArg) => this.receive(device, messageArg), signalArg);
49
- await this.client.subscribe(`${device.telemetryBase}#`, (messageArg) => this.receive(device, messageArg), signalArg);
50
- }
51
- await this.refresh(this.config.devices, signalArg);
52
- }, abortSignalArg);
53
- this.initializing = false;
54
- } catch (errorArg) {
55
- await this.destroy();
56
- throw errorArg;
57
- }
55
+ const deadlineMonotonicMs = performance.now() + this.config.initialSnapshotTimeoutMs;
56
+ await this.withDeadline(async (signalArg) => {
57
+ const broker = tasmotaBrokerIdentity(this.config);
58
+ const reference = this.config.authentication.mode === 'credential-reference' ? this.config.authentication.credentialRef : undefined;
59
+ const resolver = this.options.credentialResolver;
60
+ const store = this.lease.context.configStore;
61
+ if (reference && !resolver && !store) throw new DeviceCommunicationError('Tasmota credential reference requires an explicit credential source.');
62
+ const resolveCredentials: TMqttCredentialResolver | undefined = reference ? async (requestArg) => {
63
+ const request = Object.freeze({ homeId: requestArg.homeId, credentialRef: requestArg.credentialKey, broker,
64
+ signal: AbortSignal.any([requestArg.signal, signalArg]), deadlineMonotonicMs: Math.min(deadlineMonotonicMs, requestArg.deadline) });
65
+ const credential = resolver ? await resolver(request) : await store!.get<unknown>(tasmotaCredentialKey(requestArg.credentialKey));
66
+ return tasmotaCredential(credential, broker);
67
+ } : undefined;
68
+ this.checkActive(signalArg);
69
+ const remaining = Math.floor(deadlineMonotonicMs - performance.now());
70
+ if (remaining < 1) throw new DeviceCommunicationError('Tasmota fresh state deadline exceeded.');
71
+ const session = new MqttSession({ ...this.config.broker, homeId: this.homeId, credentialKey: reference,
72
+ connectTimeoutMs: Math.min(this.config.broker.connectTimeoutMs ?? remaining, remaining) }, this.lease, resolveCredentials);
73
+ // The runtime's lease owns dispose() before session startup enters the provider or allocates transport.
74
+ this.session = session;
75
+ const client = await session.start();
76
+ this.checkActive(signalArg);
77
+ this.client = client;
78
+ this.observeConnection(client);
79
+ for (const device of this.config.devices) {
80
+ await client.subscribe(`${device.statusBase}#`, (messageArg) => this.receive(device, messageArg), signalArg);
81
+ await client.subscribe(`${device.telemetryBase}#`, (messageArg) => this.receive(device, messageArg), signalArg);
82
+ }
83
+ await this.refresh(this.config.devices, signalArg);
84
+ }, abortSignalArg);
85
+ this.initializing = false;
58
86
  }
59
87
 
60
88
  public isHealthy(): boolean {
61
- return !this.destroyed && this.client.isHealthy() && this.config.devices.every((deviceArg) => this.ready(deviceArg));
89
+ return !this.destroyed && this.client?.isHealthy() === true && this.config.devices.every((deviceArg) => this.ready(deviceArg));
62
90
  }
63
91
 
64
92
  public async devices(abortSignalArg?: AbortSignal): Promise<plugins.shxInterfaces.data.IDeviceDefinition[]> {
65
93
  this.checkActive(abortSignalArg);
66
- return this.config.devices.map((deviceArg) => TasmotaMapper.device(deviceArg, this.state(deviceArg), this.client.connected));
94
+ return this.config.devices.map((deviceArg) => TasmotaMapper.device(deviceArg, this.state(deviceArg), this.client?.connected === true, this.options.enableControls === true));
67
95
  }
68
96
 
69
97
  public async entities(): Promise<IIntegrationEntity[]> {
70
- return (await this.devices()).flatMap((deviceArg) => TasmotaMapper.entities(deviceArg));
98
+ return (await this.devices()).flatMap((deviceArg) => TasmotaMapper.entities(deviceArg, this.homeId));
71
99
  }
72
100
 
73
101
  public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void, abortSignalArg?: AbortSignal): Promise<() => Promise<void>> {
@@ -86,10 +114,12 @@ export class TasmotaRuntime implements IIntegrationRuntime {
86
114
 
87
115
  public async writeFeature(requestArg: IDeviceFeatureWriteRequest, abortSignalArg?: AbortSignal): Promise<IServiceCallResult> {
88
116
  this.checkActive(abortSignalArg);
117
+ if (this.options.enableControls !== true) return { success: false, error: 'Tasmota controls require explicit embedding-owner enablement.' };
89
118
  if (requestArg.device.integrationDomain !== this.domain) return { success: false, error: 'Tasmota device is not selected.' };
90
119
  const device = this.config.devices.find((deviceArg) => deviceArg.id === requestArg.device.id);
91
120
  if (!device) return { success: false, error: 'Tasmota device is not selected.' };
92
- if (!this.client.isHealthy() || !this.ready(device)) return { success: false, error: 'Tasmota device has no fresh online state.' };
121
+ const client = this.client;
122
+ if (!client?.isHealthy() || !this.ready(device)) return { success: false, error: 'Tasmota device has no fresh online state.' };
93
123
  const featureId = requestArg.feature.id;
94
124
  let command: string;
95
125
  let payload: string;
@@ -103,22 +133,60 @@ export class TasmotaRuntime implements IIntegrationRuntime {
103
133
  } else {
104
134
  return { success: false, error: 'Tasmota write requires a selected relay boolean or brightness integer from 0 through 100.' };
105
135
  }
106
- await this.client.publish({ topic: `${device.commandBase}${command}`, payload, qos: 0, retain: false }, abortSignalArg);
136
+ const operation = this.owner.runOperation(this.lease.context, this.homeId, async (operationArg) => {
137
+ const signal = AbortSignal.any([operationArg.signal, this.lease.signal, this.lifetime.signal, ...(abortSignalArg ? [abortSignalArg] : [])]);
138
+ this.checkActive(signal);
139
+ await client.publish({ topic: `${device.commandBase}${command}`, payload, qos: 0, retain: false }, signal);
140
+ });
141
+ await this.retain(operation);
107
142
  return { success: true, data: { accepted: true } };
108
143
  }
109
144
 
110
145
  public destroy(): Promise<void> {
111
- if (this.destroyPromise) return this.destroyPromise;
112
- this.destroyed = true;
113
- this.stopConnectionListener();
114
- this.lifetime.abort(new DeviceCommunicationError('Tasmota runtime destroyed.'));
115
- for (const cleanup of [...this.subscriptionCleanups]) cleanup();
116
- this.listeners.clear();
117
- this.changed.clear();
118
- this.destroyPromise = this.client.destroy();
146
+ if (!this.destroyPromise) {
147
+ this.destroyed = true;
148
+ this.destroyPromise = Promise.resolve().then(() => this.lease.close());
149
+ this.lifetime.abort(new DeviceCommunicationError('Tasmota runtime destroyed.'));
150
+ }
119
151
  return this.destroyPromise;
120
152
  }
121
153
 
154
+ /** Resource drain registered on the native lease, separate from its public closure. */
155
+ public dispose(): Promise<void> {
156
+ if (!this.disposePromise || this.cleanupFailed) {
157
+ const retry = this.cleanupFailed;
158
+ this.cleanupFailed = false;
159
+ this.destroyed = true;
160
+ this.disposePromise = Promise.resolve().then(async () => {
161
+ const cleanup = Promise.allSettled([
162
+ Promise.resolve().then(() => this.stopConnectionListener?.()),
163
+ ...[...this.subscriptionCleanups].map((cleanupArg) => Promise.resolve().then(cleanupArg)),
164
+ ...(this.session ? [Promise.resolve().then(() => retry ? this.session!.retryCleanup() : this.session!.close())] : []),
165
+ ]);
166
+ const [results] = await Promise.all([cleanup, Promise.allSettled([...this.operations])]);
167
+ this.listeners.clear();
168
+ this.changed.clear();
169
+ const failures = results.filter((resultArg): resultArg is PromiseRejectedResult => resultArg.status === 'rejected')
170
+ .map((resultArg) => resultArg.reason as unknown);
171
+ if (failures.length) { this.cleanupFailed = true; throw new AggregateError(failures, 'Tasmota runtime cleanup failed.'); }
172
+ });
173
+ this.lifetime.abort(new DeviceCommunicationError('Tasmota runtime destroyed.'));
174
+ }
175
+ return this.disposePromise;
176
+ }
177
+
178
+ private track<TValue>(workArg: () => Promise<TValue>): Promise<TValue> {
179
+ this.checkActive();
180
+ const operation = Promise.resolve().then(() => { this.checkActive(); return workArg(); });
181
+ return this.retain(operation);
182
+ }
183
+
184
+ private retain<TValue>(operationArg: Promise<TValue>): Promise<TValue> {
185
+ this.operations.add(operationArg);
186
+ void operationArg.then(() => this.operations.delete(operationArg), () => this.operations.delete(operationArg));
187
+ return operationArg;
188
+ }
189
+
122
190
  private state(deviceArg: ITasmotaSelectedDevice): ITasmotaDeviceState {
123
191
  const state = this.states.get(deviceArg.id);
124
192
  if (!state) throw new DeviceCommunicationError('Selected Tasmota device state is missing.');
@@ -133,12 +201,13 @@ export class TasmotaRuntime implements IIntegrationRuntime {
133
201
  private checkActive(abortSignalArg?: AbortSignal): void {
134
202
  abortSignalArg?.throwIfAborted();
135
203
  this.lifetime.signal.throwIfAborted();
204
+ this.lease.assertOpen();
136
205
  }
137
206
 
138
207
  private async withDeadline<TValue>(operationArg: (signalArg: AbortSignal) => Promise<TValue>, abortSignalArg?: AbortSignal): Promise<TValue> {
139
208
  const deadline = new AbortController();
140
209
  const timer = setTimeout(() => deadline.abort(new DeviceCommunicationError('Tasmota fresh state deadline exceeded.')), this.config.initialSnapshotTimeoutMs);
141
- const signal = AbortSignal.any([this.lifetime.signal, deadline.signal, ...(abortSignalArg ? [abortSignalArg] : [])]);
210
+ const signal = AbortSignal.any([this.lifetime.signal, this.lease.signal, deadline.signal, ...(abortSignalArg ? [abortSignalArg] : [])]);
142
211
  try {
143
212
  signal.throwIfAborted();
144
213
  return await operationArg(signal);
@@ -160,8 +229,8 @@ export class TasmotaRuntime implements IIntegrationRuntime {
160
229
  const fresh = this.waitForFresh(devicesArg, generationsArg, signalArg);
161
230
  const queries = async (): Promise<void> => {
162
231
  for (const device of devicesArg) {
163
- await this.client.publish({ topic: `${device.commandBase}State`, payload: '', qos: 0, retain: false }, signalArg);
164
- await this.client.publish({ topic: `${device.commandBase}Status`, payload: '10', qos: 0, retain: false }, signalArg);
232
+ await this.client!.publish({ topic: `${device.commandBase}State`, payload: '', qos: 0, retain: false }, signalArg);
233
+ await this.client!.publish({ topic: `${device.commandBase}Status`, payload: '10', qos: 0, retain: false }, signalArg);
165
234
  }
166
235
  };
167
236
  await Promise.all([fresh, queries()]);
@@ -169,7 +238,7 @@ export class TasmotaRuntime implements IIntegrationRuntime {
169
238
 
170
239
  private refreshInBackground(devicesArg: ITasmotaSelectedDevice[]): void {
171
240
  const generations = new Map<string, number>();
172
- void this.withDeadline((signalArg) => this.refresh(devicesArg, signalArg, generations)).catch((errorArg: unknown) => {
241
+ void this.track(() => this.withDeadline((signalArg) => this.refresh(devicesArg, signalArg, generations))).catch((errorArg: unknown) => {
173
242
  if (this.destroyed) return;
174
243
  const currentDevices = devicesArg.filter((deviceArg) => this.state(deviceArg).generation === generations.get(deviceArg.id));
175
244
  if (!currentDevices.length) return;
@@ -190,7 +259,7 @@ export class TasmotaRuntime implements IIntegrationRuntime {
190
259
  reject(new DeviceCommunicationError('Tasmota state refresh superseded by a connection or availability change.'));
191
260
  return;
192
261
  }
193
- if (this.client.connected && devicesArg.every((deviceArg) => this.ready(deviceArg))) {
262
+ if (this.client?.connected && devicesArg.every((deviceArg) => this.ready(deviceArg))) {
194
263
  cleanup();
195
264
  resolve();
196
265
  }
@@ -1,5 +1,6 @@
1
1
  import { IntegrationError } from '../../core/errors.js';
2
2
  import type { IHomeAssistantTasmotaConfig, ITasmotaConfig, ITasmotaDeviceConfig, ITasmotaSelectedDevice } from './tasmota.types.js';
3
+ import { tasmotaCopy, tasmotaData, tasmotaText } from './tasmota.credentials.js';
3
4
 
4
5
  const fail = (messageArg: string): never => {
5
6
  throw new IntegrationError(messageArg, 'INVALID_TASMOTA_CONFIG');
@@ -51,6 +52,20 @@ const selectedDevice = (deviceArg: ITasmotaDeviceConfig): ITasmotaSelectedDevice
51
52
  };
52
53
 
53
54
  export const normalizeTasmotaConfig = (configArg: IHomeAssistantTasmotaConfig): ITasmotaConfig => {
55
+ const input = tasmotaData(tasmotaCopy(configArg), 'Tasmota configuration');
56
+ const broker = tasmotaData(input.broker, 'Tasmota broker');
57
+ if (['username', 'password', 'token', 'ca', 'cert', 'key'].some((fieldArg) => broker[fieldArg] !== undefined)) {
58
+ return fail('Legacy plaintext Tasmota broker credentials require explicit reconfiguration with an encrypted credential reference.');
59
+ }
60
+ const homeId = tasmotaText(input.homeId, 'Tasmota configuration home');
61
+ const authentication = tasmotaData(input.authentication, 'Tasmota authentication');
62
+ if (authentication.mode === 'anonymous') {
63
+ if (Object.keys(authentication).some((keyArg) => keyArg !== 'mode')) return fail('Anonymous Tasmota authentication must be selected explicitly without credentials.');
64
+ } else if (authentication.mode === 'credential-reference') {
65
+ if (Object.keys(authentication).some((keyArg) => !['mode', 'credentialRef'].includes(keyArg))) return fail('Tasmota authentication accepts only a credential reference.');
66
+ tasmotaText(authentication.credentialRef, 'Tasmota credential reference', 512);
67
+ } else return fail('Select explicit anonymous authentication or a broker credential reference.');
68
+ configArg = structuredClone(input) as unknown as IHomeAssistantTasmotaConfig;
54
69
  if (!configArg || typeof configArg !== 'object' || !configArg.broker || typeof configArg.broker.host !== 'string' || !configArg.broker.host.trim() || configArg.broker.host !== configArg.broker.host.trim()) return fail('Tasmota requires a real MQTT broker host without surrounding whitespace.');
55
70
  if (configArg.broker.port !== undefined && (!Number.isInteger(configArg.broker.port) || configArg.broker.port < 1 || configArg.broker.port > 65_535)) return fail('Tasmota broker port must be an integer from 1 through 65535.');
56
71
  if (configArg.broker.protocol !== undefined && configArg.broker.protocol !== 'mqtt' && configArg.broker.protocol !== 'mqtts') return fail('Tasmota broker protocol must be mqtt or mqtts.');
@@ -62,6 +77,8 @@ export const normalizeTasmotaConfig = (configArg: IHomeAssistantTasmotaConfig):
62
77
  const initialSnapshotTimeoutMs = configArg.initialSnapshotTimeoutMs ?? 10_000;
63
78
  if (!Number.isInteger(initialSnapshotTimeoutMs) || initialSnapshotTimeoutMs < 1 || initialSnapshotTimeoutMs > 2_147_483_647) return fail('initialSnapshotTimeoutMs must be a timer-safe positive integer.');
64
79
  return {
80
+ homeId,
81
+ authentication: Object.freeze({ ...configArg.authentication }),
65
82
  devices,
66
83
  initialSnapshotTimeoutMs,
67
84
  broker: { ...configArg.broker, discovery: false, birthMessage: false, willMessage: false, subscriptions: [], entities: [], discoveryMessages: [], retainedMessages: [] },
@@ -0,0 +1,92 @@
1
+ import type { IMqttTransportCredentials } from '../mqtt/mqtt.types.js';
2
+ import type { ITasmotaBrokerIdentity, ITasmotaConfig, ITasmotaCredential, ITasmotaIntegrationOptions } from './tasmota.types.js';
3
+
4
+ export const tasmotaData = (valueArg: unknown, nameArg: string): Record<string, unknown> => {
5
+ if (!valueArg || typeof valueArg !== 'object'
6
+ || (Object.getPrototypeOf(valueArg) !== Object.prototype && Object.getPrototypeOf(valueArg) !== null)) {
7
+ throw new TypeError(`${nameArg} must be a plain data object.`);
8
+ }
9
+ const result: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
10
+ for (const key of Reflect.ownKeys(valueArg)) {
11
+ const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
12
+ if (typeof key !== 'string' || !descriptor?.enumerable || !('value' in descriptor)) {
13
+ throw new TypeError(`${nameArg} must not contain accessors or hidden fields.`);
14
+ }
15
+ result[key] = descriptor.value as unknown;
16
+ }
17
+ return result;
18
+ };
19
+
20
+ /** Capture configuration data without invoking nested user accessors. */
21
+ export const tasmotaCopy = (valueArg: unknown, ancestorsArg = new Set<object>(), depthArg = 0): unknown => {
22
+ if (valueArg === null || valueArg === undefined || typeof valueArg === 'string' || typeof valueArg === 'boolean'
23
+ || (typeof valueArg === 'number' && Number.isFinite(valueArg))) return valueArg;
24
+ if (!valueArg || typeof valueArg !== 'object' || depthArg > 32 || ancestorsArg.has(valueArg)) throw new TypeError('Tasmota configuration must contain bounded plain data.');
25
+ ancestorsArg.add(valueArg);
26
+ try {
27
+ if (Array.isArray(valueArg)) {
28
+ const items: unknown[] = [];
29
+ for (let index = 0; index < valueArg.length; index += 1) {
30
+ const descriptor = Object.getOwnPropertyDescriptor(valueArg, String(index));
31
+ if (!descriptor?.enumerable || !('value' in descriptor)) throw new TypeError('Tasmota configuration arrays must contain data values.');
32
+ items.push(tasmotaCopy(descriptor.value as unknown, ancestorsArg, depthArg + 1));
33
+ }
34
+ if (Reflect.ownKeys(valueArg).length !== items.length + 1) throw new TypeError('Tasmota configuration arrays contain unsupported fields.');
35
+ return items;
36
+ }
37
+ const data = tasmotaData(valueArg, 'Tasmota configuration');
38
+ return Object.fromEntries(Object.entries(data).map(([keyArg, itemArg]) => [keyArg, tasmotaCopy(itemArg, ancestorsArg, depthArg + 1)]));
39
+ } finally { ancestorsArg.delete(valueArg); }
40
+ };
41
+
42
+ export const tasmotaText = (valueArg: unknown, nameArg: string, maximumArg = 256): string => {
43
+ if (typeof valueArg !== 'string' || !valueArg || valueArg.length > maximumArg
44
+ || valueArg.trim() !== valueArg || /[\p{Cc}\p{Cs}]/u.test(valueArg)) {
45
+ throw new TypeError(`${nameArg} must be a bounded nonempty identifier.`);
46
+ }
47
+ return valueArg;
48
+ };
49
+
50
+ export const tasmotaOptions = (valueArg: ITasmotaIntegrationOptions): ITasmotaIntegrationOptions => {
51
+ const options = tasmotaData(valueArg, 'Tasmota owner options');
52
+ if (Object.keys(options).some((keyArg) => !['homeId', 'capacity', 'enableControls', 'credentialResolver'].includes(keyArg))) {
53
+ throw new TypeError('Tasmota owner options contain unsupported fields.');
54
+ }
55
+ if (options.enableControls !== undefined && typeof options.enableControls !== 'boolean') throw new TypeError('Tasmota enableControls must be boolean.');
56
+ if (options.credentialResolver !== undefined && typeof options.credentialResolver !== 'function') throw new TypeError('Tasmota credential resolver must be a function.');
57
+ return Object.freeze({ ...options }) as ITasmotaIntegrationOptions;
58
+ };
59
+
60
+ export const tasmotaBrokerIdentity = (configArg: ITasmotaConfig): ITasmotaBrokerIdentity => Object.freeze({
61
+ host: configArg.broker.host, port: configArg.broker.port ?? (configArg.broker.protocol === 'mqtts' ? 8883 : 1883),
62
+ protocol: configArg.broker.protocol ?? 'mqtt',
63
+ });
64
+
65
+ export const tasmotaCredentialKey = (referenceArg: string): string => `tasmota.credential.${encodeURIComponent(tasmotaText(referenceArg, 'Credential reference', 512))}`;
66
+
67
+ export const tasmotaCredential = (valueArg: unknown, brokerArg: ITasmotaBrokerIdentity): IMqttTransportCredentials => {
68
+ const value = tasmotaData(valueArg, 'Tasmota credential');
69
+ if (Object.keys(value).some((keyArg) => !['version', 'host', 'port', 'protocol', 'username', 'password', 'ca', 'cert', 'key', 'servername'].includes(keyArg))
70
+ || value.version !== 1 || value.host !== brokerArg.host || value.port !== brokerArg.port || value.protocol !== brokerArg.protocol) {
71
+ throw new TypeError('Tasmota credential does not match the selected broker.');
72
+ }
73
+ const credentials: IMqttTransportCredentials = {};
74
+ for (const field of ['username', 'password', 'ca', 'cert', 'key', 'servername'] as const) {
75
+ const item = value[field];
76
+ if (item !== undefined) {
77
+ if (typeof item !== 'string' || item.length > 1_048_576 || item.includes('\u0000')) throw new TypeError('Tasmota credential fields are invalid.');
78
+ credentials[field] = item;
79
+ }
80
+ }
81
+ if (!credentials.username && !credentials.password && !credentials.cert) throw new TypeError('Tasmota credential reference must resolve authenticated broker credentials.');
82
+ if ((credentials.cert === undefined) !== (credentials.key === undefined)) throw new TypeError('Tasmota client certificate and private key must be supplied together.');
83
+ if ((credentials.ca || credentials.cert || credentials.key || credentials.servername) && brokerArg.protocol !== 'mqtts') throw new TypeError('Tasmota TLS credentials require mqtts.');
84
+ if (credentials.servername !== undefined) tasmotaText(credentials.servername, 'TLS server name', 253);
85
+ return Object.freeze(credentials);
86
+ };
87
+
88
+ export const createTasmotaCredential = (brokerArg: ITasmotaBrokerIdentity, usernameArg?: string, passwordArg?: string): ITasmotaCredential => {
89
+ const credential: ITasmotaCredential = { version: 1, ...brokerArg, username: usernameArg, password: passwordArg };
90
+ tasmotaCredential(credential, brokerArg);
91
+ return credential;
92
+ };
@@ -1,7 +1,10 @@
1
1
  import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
2
+ import * as plugins from '../../plugins.js';
3
+ import { NativeIntegrationOwner, type NativeIntegrationScope } from '../../core/classes.nativeintegrationowner.js';
2
4
  import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
3
5
  import { normalizeTasmotaConfig } from './tasmota.config.js';
4
- import type { IHomeAssistantTasmotaConfig } from './tasmota.types.js';
6
+ import type { IHomeAssistantTasmotaConfig, ITasmotaIntegrationOptions } from './tasmota.types.js';
7
+ import { createTasmotaCredential, tasmotaBrokerIdentity, tasmotaCopy, tasmotaCredentialKey, tasmotaData, tasmotaOptions, tasmotaText } from './tasmota.credentials.js';
5
8
 
6
9
  class TasmotaManualMatcher implements IDiscoveryMatcher<IDiscoveryCandidate> {
7
10
  public readonly id = 'tasmota-selected-device';
@@ -35,8 +38,28 @@ export const createTasmotaDiscoveryDescriptor = (): DiscoveryDescriptor => {
35
38
  };
36
39
 
37
40
  export class TasmotaConfigFlow implements IConfigFlow<IHomeAssistantTasmotaConfig> {
41
+ private readonly scope: NativeIntegrationScope;
42
+ private readonly hasResolver: boolean;
43
+
44
+ constructor(optionsArg: ITasmotaIntegrationOptions = {}, ownerArg?: NativeIntegrationOwner) {
45
+ const options = tasmotaOptions(optionsArg);
46
+ const owner = ownerArg ?? new NativeIntegrationOwner('tasmota', { homeId: options.homeId, capacity: options.capacity });
47
+ this.scope = owner.createFlowScope();
48
+ this.hasResolver = options.credentialResolver !== undefined;
49
+ }
50
+
51
+ public destroy(): Promise<void> { return this.scope.close(); }
52
+ public retryCleanup(): Promise<void> { return this.scope.retryCleanup(); }
53
+
38
54
  public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IHomeAssistantTasmotaConfig>> {
39
- void contextArg;
55
+ const candidate = tasmotaData(tasmotaCopy(candidateArg), 'Tasmota candidate');
56
+ const metadata = candidate.metadata === undefined ? undefined : tasmotaData(candidate.metadata, 'Tasmota candidate metadata');
57
+ const context = this.scope.admit(contextArg);
58
+ const candidateHost = candidate.host;
59
+ const candidatePort = candidate.port;
60
+ const candidateTopic = metadata?.topic;
61
+ let pending = false;
62
+ let submitted = false;
40
63
  return {
41
64
  kind: 'form',
42
65
  title: 'Connect a selected Tasmota device',
@@ -45,6 +68,8 @@ export class TasmotaConfigFlow implements IConfigFlow<IHomeAssistantTasmotaConfi
45
68
  { name: 'host', label: 'MQTT broker host', type: 'text', required: true },
46
69
  { name: 'port', label: 'MQTT broker port', type: 'number' },
47
70
  { name: 'useTls', label: 'Use MQTT TLS', type: 'boolean' },
71
+ { name: 'anonymous', label: 'Use an anonymous broker', type: 'boolean' },
72
+ { name: 'credentialRef', label: 'Existing broker credential reference', type: 'text' },
48
73
  { name: 'username', label: 'Broker username', type: 'text' },
49
74
  { name: 'password', label: 'Broker password', type: 'password' },
50
75
  { name: 'topic', label: 'Device Topic', type: 'text', required: true },
@@ -53,31 +78,69 @@ export class TasmotaConfigFlow implements IConfigFlow<IHomeAssistantTasmotaConfi
53
78
  { name: 'brightness', label: 'Single-channel Dimmer', type: 'boolean' },
54
79
  ],
55
80
  submit: async (valuesArg) => {
81
+ if (pending || submitted) return { kind: 'error', error: 'This Tasmota form is already submitting or completed.' };
82
+ pending = true;
56
83
  try {
57
- for (const field of ['username', 'password', 'fullTopic']) if (valuesArg[field] !== undefined && typeof valuesArg[field] !== 'string') throw new Error(`${field} must be a string.`);
58
- for (const field of ['useTls', 'brightness']) if (valuesArg[field] !== undefined && typeof valuesArg[field] !== 'boolean') throw new Error(`${field} must be boolean.`);
59
- const relayCount = valuesArg.relayCount ?? 1;
60
- if (typeof relayCount !== 'number' || !Number.isInteger(relayCount) || relayCount < 0 || relayCount > 32) throw new Error('relayCount must be an integer from 0 through 32.');
61
- const host = valuesArg.host ?? candidateArg.host;
62
- const topic = valuesArg.topic ?? candidateArg.metadata?.topic;
63
- if (typeof host !== 'string' || typeof topic !== 'string') throw new Error('Broker host and device topic are required.');
64
- const port = valuesArg.port ?? candidateArg.port;
65
- if (port !== undefined && (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65_535)) throw new Error('Broker port must be an integer from 1 through 65535.');
66
- const config: IHomeAssistantTasmotaConfig = {
67
- broker: {
68
- host,
69
- port,
70
- protocol: valuesArg.useTls === true ? 'mqtts' : 'mqtt',
71
- username: typeof valuesArg.username === 'string' ? valuesArg.username : undefined,
72
- password: typeof valuesArg.password === 'string' ? valuesArg.password : undefined,
73
- },
74
- devices: [{ topic, fullTopic: typeof valuesArg.fullTopic === 'string' && valuesArg.fullTopic ? valuesArg.fullTopic : undefined, relays: Array.from({ length: relayCount }, (_, indexArg) => indexArg + 1), brightness: valuesArg.brightness === true }],
75
- };
76
- normalizeTasmotaConfig(config);
77
- return { kind: 'done', title: 'Selected Tasmota device configured', config };
84
+ const values = tasmotaData(tasmotaCopy(valuesArg), 'Tasmota form');
85
+ return await this.scope.run(context, context.homeId, async (leaseArg) => {
86
+ if (Object.keys(values).some((keyArg) => !['homeId', 'host', 'port', 'useTls', 'anonymous', 'credentialRef', 'username', 'password', 'topic', 'fullTopic', 'relayCount', 'brightness'].includes(keyArg))) throw new Error('Tasmota form contains unsupported fields.');
87
+ if (values.homeId !== undefined && values.homeId !== context.homeId) throw new Error('Tasmota form home conflicts with the admitted host home.');
88
+ for (const field of ['username', 'password', 'fullTopic', 'credentialRef']) if (values[field] !== undefined && typeof values[field] !== 'string') throw new Error(`${field} must be a string.`);
89
+ for (const field of ['useTls', 'brightness', 'anonymous']) if (values[field] !== undefined && typeof values[field] !== 'boolean') throw new Error(`${field} must be boolean.`);
90
+ const relayCount = values.relayCount ?? 1;
91
+ if (typeof relayCount !== 'number' || !Number.isInteger(relayCount) || relayCount < 0 || relayCount > 32) throw new Error('relayCount must be an integer from 0 through 32.');
92
+ const host = values.host ?? candidateHost;
93
+ const topic = values.topic ?? candidateTopic;
94
+ if (typeof host !== 'string' || typeof topic !== 'string') throw new Error('Broker host and device topic are required.');
95
+ const port = values.port ?? candidatePort;
96
+ if (port !== undefined && (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65_535)) throw new Error('Broker port must be an integer from 1 through 65535.');
97
+ const secretInput = values.username !== undefined || values.password !== undefined;
98
+ const anonymous = values.anonymous === true;
99
+ if (anonymous && (secretInput || values.credentialRef !== undefined)) throw new Error('Anonymous Tasmota mode cannot contain credentials.');
100
+ if (!anonymous && !secretInput && values.credentialRef === undefined) throw new Error('Select anonymous mode explicitly or provide broker credentials.');
101
+ if (secretInput && values.credentialRef !== undefined) throw new Error('Provide transient credentials or an existing reference, not both.');
102
+ const reference = anonymous ? undefined : secretInput ? plugins.crypto.randomUUID() : tasmotaText(values.credentialRef, 'Credential reference', 512);
103
+ const config: IHomeAssistantTasmotaConfig = {
104
+ homeId: context.homeId,
105
+ authentication: reference === undefined ? { mode: 'anonymous' } : { mode: 'credential-reference', credentialRef: reference },
106
+ broker: {
107
+ host,
108
+ port,
109
+ protocol: values.useTls === true ? 'mqtts' : 'mqtt',
110
+ },
111
+ devices: [{ topic, fullTopic: typeof values.fullTopic === 'string' && values.fullTopic ? values.fullTopic : undefined, relays: Array.from({ length: relayCount }, (_, indexArg) => indexArg + 1), brightness: values.brightness === true }],
112
+ };
113
+ const normalized = normalizeTasmotaConfig(config);
114
+ if (!anonymous) {
115
+ const store = leaseArg.context.configStore;
116
+ if (secretInput) {
117
+ if (!store) throw new Error('Transient Tasmota broker credentials require an admitted encrypted config store.');
118
+ const credential = createTasmotaCredential(tasmotaBrokerIdentity(normalized),
119
+ typeof values.username === 'string' ? values.username : undefined,
120
+ typeof values.password === 'string' ? values.password : undefined);
121
+ const key = tasmotaCredentialKey(reference!);
122
+ let committed = false;
123
+ let write: Promise<void> = Promise.resolve();
124
+ leaseArg.own(async () => {
125
+ const result = await Promise.allSettled([write]);
126
+ if (result[0].status === 'fulfilled' && !committed) {
127
+ try { await store.delete(key); }
128
+ catch { throw new Error('Tasmota cancelled credential cleanup failed.'); }
129
+ }
130
+ });
131
+ write = Promise.resolve().then(() => store.create(key, credential));
132
+ try { await write; }
133
+ catch { throw new Error('Tasmota encrypted credential persistence failed.'); }
134
+ leaseArg.assertOpen();
135
+ committed = true;
136
+ } else if (!store && !this.hasResolver) throw new Error('Tasmota credential reference requires an explicit credential source.');
137
+ }
138
+ submitted = true;
139
+ return { kind: 'done', title: 'Selected Tasmota device configured', config };
140
+ });
78
141
  } catch (errorArg) {
79
142
  return { kind: 'error', error: errorArg instanceof Error ? errorArg.message : 'Invalid Tasmota configuration.' };
80
- }
143
+ } finally { pending = false; }
81
144
  },
82
145
  };
83
146
  }
@@ -50,9 +50,9 @@ export class TasmotaMapper {
50
50
  return sensors;
51
51
  }
52
52
 
53
- public static device(deviceArg: ITasmotaSelectedDevice, stateArg: ITasmotaDeviceState, connectedArg: boolean): plugins.shxInterfaces.data.IDeviceDefinition {
54
- const features: plugins.shxInterfaces.data.IDeviceFeature[] = deviceArg.relays.map((relayArg) => ({ id: `power_${relayArg}`, capability: 'switch', name: `Relay ${relayArg}`, readable: true, writable: true }));
55
- if (deviceArg.brightness) features.push({ id: 'brightness', capability: 'light', name: 'Brightness', readable: true, writable: true, unit: '%' });
53
+ public static device(deviceArg: ITasmotaSelectedDevice, stateArg: ITasmotaDeviceState, connectedArg: boolean, enableControlsArg = false): plugins.shxInterfaces.data.IDeviceDefinition {
54
+ const features: plugins.shxInterfaces.data.IDeviceFeature[] = deviceArg.relays.map((relayArg) => ({ id: `power_${relayArg}`, capability: 'switch', name: `Relay ${relayArg}`, readable: true, writable: enableControlsArg }));
55
+ if (deviceArg.brightness) features.push({ id: 'brightness', capability: 'light', name: 'Brightness', readable: true, writable: enableControlsArg, unit: '%' });
56
56
  for (const id of Object.keys(stateArg.sensors).sort()) features.push({ id, capability: id.startsWith('sensor_ENERGY/') ? 'energy' : 'sensor', name: id.slice('sensor_'.length).split('/').map(decodeURIComponent).join(' '), readable: true, writable: false });
57
57
  const values = { ...stateArg.controls, ...stateArg.sensors };
58
58
  return {
@@ -69,12 +69,12 @@ export class TasmotaMapper {
69
69
  };
70
70
  }
71
71
 
72
- public static entities(deviceArg: plugins.shxInterfaces.data.IDeviceDefinition): IIntegrationEntity[] {
72
+ public static entities(deviceArg: plugins.shxInterfaces.data.IDeviceDefinition, homeIdArg: string): IIntegrationEntity[] {
73
73
  return deviceArg.features.map((featureArg) => {
74
74
  const platform = featureArg.capability === 'switch' ? 'switch' : featureArg.capability === 'light' ? 'light' : 'sensor';
75
75
  return {
76
- id: `${platform}.${encodeURIComponent(deviceArg.id)}.${encodeURIComponent(featureArg.id)}`,
77
- uniqueId: `${deviceArg.id}:${featureArg.id}`,
76
+ id: `${encodeURIComponent(homeIdArg)}:${platform}.${encodeURIComponent(JSON.stringify([deviceArg.id, featureArg.id]))}`,
77
+ uniqueId: JSON.stringify([homeIdArg, deviceArg.id, featureArg.id]),
78
78
  integrationDomain: 'tasmota',
79
79
  deviceId: deviceArg.id,
80
80
  platform,