@smarthome.exchange/integrations 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/changelog.md +49 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/core/classes.configstore.d.ts +2 -0
  4. package/dist_ts/core/classes.configstore.js +84 -27
  5. package/dist_ts/core/classes.nativehomeadmission.d.ts +19 -0
  6. package/dist_ts/core/classes.nativehomeadmission.js +84 -0
  7. package/dist_ts/core/classes.nativeintegrationowner.d.ts +79 -0
  8. package/dist_ts/core/classes.nativeintegrationowner.js +269 -0
  9. package/dist_ts/core/index.d.ts +2 -0
  10. package/dist_ts/core/index.js +3 -1
  11. package/dist_ts/core/types.d.ts +14 -0
  12. package/dist_ts/integrations/braviatv/braviatv.classes.client.d.ts +23 -72
  13. package/dist_ts/integrations/braviatv/braviatv.classes.client.js +265 -625
  14. package/dist_ts/integrations/braviatv/braviatv.classes.configflow.d.ts +7 -7
  15. package/dist_ts/integrations/braviatv/braviatv.classes.configflow.js +51 -68
  16. package/dist_ts/integrations/braviatv/braviatv.classes.integration.d.ts +8 -11
  17. package/dist_ts/integrations/braviatv/braviatv.classes.integration.js +186 -135
  18. package/dist_ts/integrations/braviatv/braviatv.discovery.js +12 -11
  19. package/dist_ts/integrations/braviatv/braviatv.mapper.d.ts +6 -14
  20. package/dist_ts/integrations/braviatv/braviatv.mapper.js +37 -131
  21. package/dist_ts/integrations/braviatv/braviatv.types.d.ts +55 -133
  22. package/dist_ts/integrations/braviatv/braviatv.validation.d.ts +7 -0
  23. package/dist_ts/integrations/braviatv/braviatv.validation.js +72 -0
  24. package/dist_ts/integrations/broadlink/broadlink.actions.d.ts +2 -0
  25. package/dist_ts/integrations/broadlink/broadlink.actions.js +36 -0
  26. package/dist_ts/integrations/broadlink/broadlink.classes.client.d.ts +21 -19
  27. package/dist_ts/integrations/broadlink/broadlink.classes.client.js +185 -124
  28. package/dist_ts/integrations/broadlink/broadlink.classes.configflow.d.ts +5 -4
  29. package/dist_ts/integrations/broadlink/broadlink.classes.configflow.js +64 -83
  30. package/dist_ts/integrations/broadlink/broadlink.classes.integration.d.ts +16 -19
  31. package/dist_ts/integrations/broadlink/broadlink.classes.integration.js +114 -59
  32. package/dist_ts/integrations/broadlink/broadlink.mapper.d.ts +3 -2
  33. package/dist_ts/integrations/broadlink/broadlink.mapper.js +2 -1
  34. package/dist_ts/integrations/broadlink/broadlink.selected.mapper.d.ts +10 -0
  35. package/dist_ts/integrations/broadlink/broadlink.selected.mapper.js +41 -0
  36. package/dist_ts/integrations/broadlink/broadlink.types.d.ts +43 -7
  37. package/dist_ts/integrations/broadlink/broadlink.validation.d.ts +7 -0
  38. package/dist_ts/integrations/broadlink/broadlink.validation.js +63 -0
  39. package/dist_ts/integrations/daikin/daikin.classes.client.d.ts +10 -52
  40. package/dist_ts/integrations/daikin/daikin.classes.client.js +55 -614
  41. package/dist_ts/integrations/daikin/daikin.classes.configflow.d.ts +8 -12
  42. package/dist_ts/integrations/daikin/daikin.classes.configflow.js +77 -138
  43. package/dist_ts/integrations/daikin/daikin.classes.integration.d.ts +7 -24
  44. package/dist_ts/integrations/daikin/daikin.classes.integration.js +175 -67
  45. package/dist_ts/integrations/daikin/daikin.discovery.d.ts +11 -24
  46. package/dist_ts/integrations/daikin/daikin.discovery.js +98 -305
  47. package/dist_ts/integrations/daikin/daikin.mapper.d.ts +6 -7
  48. package/dist_ts/integrations/daikin/daikin.mapper.js +95 -155
  49. package/dist_ts/integrations/daikin/daikin.types.d.ts +24 -44
  50. package/dist_ts/integrations/daikin/daikin.types.js +1 -1
  51. package/dist_ts/integrations/daikin/daikin.validation.d.ts +5 -0
  52. package/dist_ts/integrations/daikin/daikin.validation.js +53 -0
  53. package/dist_ts/integrations/denonavr/denonavr.classes.client.d.ts +31 -40
  54. package/dist_ts/integrations/denonavr/denonavr.classes.client.js +149 -404
  55. package/dist_ts/integrations/denonavr/denonavr.classes.configflow.d.ts +7 -2
  56. package/dist_ts/integrations/denonavr/denonavr.classes.configflow.js +44 -36
  57. package/dist_ts/integrations/denonavr/denonavr.classes.integration.d.ts +6 -11
  58. package/dist_ts/integrations/denonavr/denonavr.classes.integration.js +192 -113
  59. package/dist_ts/integrations/denonavr/denonavr.discovery.d.ts +10 -18
  60. package/dist_ts/integrations/denonavr/denonavr.discovery.js +71 -193
  61. package/dist_ts/integrations/denonavr/denonavr.mapper.d.ts +5 -13
  62. package/dist_ts/integrations/denonavr/denonavr.mapper.js +35 -159
  63. package/dist_ts/integrations/denonavr/denonavr.types.d.ts +55 -90
  64. package/dist_ts/integrations/denonavr/denonavr.validation.d.ts +10 -0
  65. package/dist_ts/integrations/denonavr/denonavr.validation.js +86 -0
  66. package/dist_ts/integrations/kodi/kodi.classes.client.d.ts +11 -45
  67. package/dist_ts/integrations/kodi/kodi.classes.client.js +84 -310
  68. package/dist_ts/integrations/kodi/kodi.classes.configflow.d.ts +9 -8
  69. package/dist_ts/integrations/kodi/kodi.classes.configflow.js +53 -48
  70. package/dist_ts/integrations/kodi/kodi.classes.integration.d.ts +11 -11
  71. package/dist_ts/integrations/kodi/kodi.classes.integration.js +114 -171
  72. package/dist_ts/integrations/kodi/kodi.discovery.js +53 -29
  73. package/dist_ts/integrations/kodi/kodi.mapper.d.ts +7 -2
  74. package/dist_ts/integrations/kodi/kodi.mapper.js +48 -10
  75. package/dist_ts/integrations/kodi/kodi.types.d.ts +32 -28
  76. package/dist_ts/integrations/kodi/kodi.validation.d.ts +9 -0
  77. package/dist_ts/integrations/kodi/kodi.validation.js +62 -0
  78. package/dist_ts/integrations/modbus/modbus.classes.client.d.ts +15 -61
  79. package/dist_ts/integrations/modbus/modbus.classes.client.js +117 -776
  80. package/dist_ts/integrations/modbus/modbus.classes.configflow.d.ts +9 -5
  81. package/dist_ts/integrations/modbus/modbus.classes.configflow.js +70 -38
  82. package/dist_ts/integrations/modbus/modbus.classes.integration.d.ts +6 -17
  83. package/dist_ts/integrations/modbus/modbus.classes.integration.js +133 -57
  84. package/dist_ts/integrations/modbus/modbus.discovery.d.ts +5 -8
  85. package/dist_ts/integrations/modbus/modbus.discovery.js +38 -93
  86. package/dist_ts/integrations/modbus/modbus.mapper.d.ts +8 -32
  87. package/dist_ts/integrations/modbus/modbus.mapper.js +71 -431
  88. package/dist_ts/integrations/modbus/modbus.types.d.ts +82 -332
  89. package/dist_ts/integrations/modbus/modbus.validation.d.ts +10 -0
  90. package/dist_ts/integrations/modbus/modbus.validation.js +128 -0
  91. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.client.d.ts +12 -34
  92. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.client.js +143 -231
  93. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.d.ts +6 -1
  94. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.configflow.js +105 -18
  95. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.d.ts +8 -8
  96. package/dist_ts/integrations/nanoleaf/nanoleaf.classes.integration.js +141 -155
  97. package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.d.ts +9 -0
  98. package/dist_ts/integrations/nanoleaf/nanoleaf.credentials.js +35 -0
  99. package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.d.ts +6 -18
  100. package/dist_ts/integrations/nanoleaf/nanoleaf.mapper.js +45 -220
  101. package/dist_ts/integrations/nanoleaf/nanoleaf.types.d.ts +33 -147
  102. package/dist_ts/integrations/nanoleaf/nanoleaf.validation.d.ts +6 -0
  103. package/dist_ts/integrations/nanoleaf/nanoleaf.validation.js +82 -0
  104. package/dist_ts/integrations/onkyo/onkyo.classes.client.d.ts +15 -20
  105. package/dist_ts/integrations/onkyo/onkyo.classes.client.js +161 -404
  106. package/dist_ts/integrations/onkyo/onkyo.classes.configflow.d.ts +9 -4
  107. package/dist_ts/integrations/onkyo/onkyo.classes.configflow.js +51 -6
  108. package/dist_ts/integrations/onkyo/onkyo.classes.integration.d.ts +22 -6
  109. package/dist_ts/integrations/onkyo/onkyo.classes.integration.js +144 -11
  110. package/dist_ts/integrations/onkyo/onkyo.discovery.d.ts +6 -1
  111. package/dist_ts/integrations/onkyo/onkyo.discovery.js +40 -4
  112. package/dist_ts/integrations/onkyo/onkyo.mapper.d.ts +7 -8
  113. package/dist_ts/integrations/onkyo/onkyo.mapper.js +45 -12
  114. package/dist_ts/integrations/onkyo/onkyo.types.d.ts +58 -8
  115. package/dist_ts/integrations/onkyo/onkyo.types.js +13 -102
  116. package/dist_ts/integrations/onkyo/onkyo.validation.d.ts +16 -0
  117. package/dist_ts/integrations/onkyo/onkyo.validation.js +77 -0
  118. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.d.ts +25 -25
  119. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.client.js +116 -360
  120. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.d.ts +11 -3
  121. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.js +76 -6
  122. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.d.ts +26 -4
  123. package/dist_ts/integrations/panasonic_viera/panasonic_viera.classes.integration.js +142 -9
  124. package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.d.ts +2 -1
  125. package/dist_ts/integrations/panasonic_viera/panasonic_viera.discovery.js +31 -4
  126. package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.d.ts +9 -9
  127. package/dist_ts/integrations/panasonic_viera/panasonic_viera.mapper.js +31 -149
  128. package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.d.ts +51 -11
  129. package/dist_ts/integrations/panasonic_viera/panasonic_viera.types.js +22 -87
  130. package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.d.ts +6 -0
  131. package/dist_ts/integrations/panasonic_viera/panasonic_viera.validation.js +56 -0
  132. package/dist_ts/integrations/pioneer/pioneer.classes.client.d.ts +31 -16
  133. package/dist_ts/integrations/pioneer/pioneer.classes.client.js +167 -409
  134. package/dist_ts/integrations/pioneer/pioneer.classes.configflow.d.ts +10 -3
  135. package/dist_ts/integrations/pioneer/pioneer.classes.configflow.js +66 -6
  136. package/dist_ts/integrations/pioneer/pioneer.classes.integration.d.ts +13 -5
  137. package/dist_ts/integrations/pioneer/pioneer.classes.integration.js +239 -11
  138. package/dist_ts/integrations/pioneer/pioneer.discovery.d.ts +14 -1
  139. package/dist_ts/integrations/pioneer/pioneer.discovery.js +51 -4
  140. package/dist_ts/integrations/pioneer/pioneer.mapper.d.ts +14 -8
  141. package/dist_ts/integrations/pioneer/pioneer.mapper.js +54 -12
  142. package/dist_ts/integrations/pioneer/pioneer.types.d.ts +79 -12
  143. package/dist_ts/integrations/pioneer/pioneer.types.js +12 -76
  144. package/dist_ts/integrations/pioneer/pioneer.validation.d.ts +10 -0
  145. package/dist_ts/integrations/pioneer/pioneer.validation.js +85 -0
  146. package/dist_ts/integrations/soundtouch/soundtouch.classes.client.d.ts +13 -36
  147. package/dist_ts/integrations/soundtouch/soundtouch.classes.client.js +179 -502
  148. package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.d.ts +6 -1
  149. package/dist_ts/integrations/soundtouch/soundtouch.classes.configflow.js +50 -40
  150. package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.d.ts +14 -15
  151. package/dist_ts/integrations/soundtouch/soundtouch.classes.integration.js +108 -128
  152. package/dist_ts/integrations/soundtouch/soundtouch.discovery.d.ts +2 -20
  153. package/dist_ts/integrations/soundtouch/soundtouch.discovery.js +23 -113
  154. package/dist_ts/integrations/soundtouch/soundtouch.mapper.d.ts +3 -12
  155. package/dist_ts/integrations/soundtouch/soundtouch.mapper.js +35 -189
  156. package/dist_ts/integrations/soundtouch/soundtouch.types.d.ts +57 -142
  157. package/dist_ts/integrations/soundtouch/soundtouch.types.js +18 -3
  158. package/dist_ts/integrations/soundtouch/soundtouch.validation.d.ts +22 -0
  159. package/dist_ts/integrations/soundtouch/soundtouch.validation.js +65 -0
  160. package/dist_ts/integrations/wiz/index.d.ts +1 -0
  161. package/dist_ts/integrations/wiz/index.js +2 -1
  162. package/dist_ts/integrations/wiz/wiz.classes.client.d.ts +14 -28
  163. package/dist_ts/integrations/wiz/wiz.classes.client.js +93 -229
  164. package/dist_ts/integrations/wiz/wiz.classes.configflow.d.ts +6 -4
  165. package/dist_ts/integrations/wiz/wiz.classes.configflow.js +38 -67
  166. package/dist_ts/integrations/wiz/wiz.classes.integration.d.ts +10 -21
  167. package/dist_ts/integrations/wiz/wiz.classes.integration.js +204 -44
  168. package/dist_ts/integrations/wiz/wiz.discovery.d.ts +10 -31
  169. package/dist_ts/integrations/wiz/wiz.discovery.js +48 -249
  170. package/dist_ts/integrations/wiz/wiz.mapper.d.ts +7 -51
  171. package/dist_ts/integrations/wiz/wiz.mapper.js +82 -668
  172. package/dist_ts/integrations/wiz/wiz.types.d.ts +34 -208
  173. package/dist_ts/integrations/wiz/wiz.validation.d.ts +8 -0
  174. package/dist_ts/integrations/wiz/wiz.validation.js +72 -0
  175. package/dist_ts/integrations/yamaha/index.d.ts +1 -0
  176. package/dist_ts/integrations/yamaha/index.js +2 -1
  177. package/dist_ts/integrations/yamaha/yamaha.classes.client.d.ts +30 -4
  178. package/dist_ts/integrations/yamaha/yamaha.classes.client.js +162 -6
  179. package/dist_ts/integrations/yamaha/yamaha.classes.configflow.d.ts +10 -3
  180. package/dist_ts/integrations/yamaha/yamaha.classes.configflow.js +106 -6
  181. package/dist_ts/integrations/yamaha/yamaha.classes.integration.d.ts +22 -4
  182. package/dist_ts/integrations/yamaha/yamaha.classes.integration.js +243 -8
  183. package/dist_ts/integrations/yamaha/yamaha.discovery.d.ts +13 -1
  184. package/dist_ts/integrations/yamaha/yamaha.discovery.js +57 -4
  185. package/dist_ts/integrations/yamaha/yamaha.mapper.d.ts +16 -8
  186. package/dist_ts/integrations/yamaha/yamaha.mapper.js +58 -12
  187. package/dist_ts/integrations/yamaha/yamaha.types.d.ts +73 -6
  188. package/dist_ts/integrations/yamaha/yamaha.types.js +8 -94
  189. package/dist_ts/integrations/yamaha/yamaha.validation.d.ts +12 -0
  190. package/dist_ts/integrations/yamaha/yamaha.validation.js +89 -0
  191. package/package.json +3 -3
  192. package/readme.md +514 -2
  193. package/ts/00_commitinfo_data.ts +1 -1
  194. package/ts/core/classes.configstore.ts +84 -27
  195. package/ts/core/classes.nativehomeadmission.ts +96 -0
  196. package/ts/core/classes.nativeintegrationowner.ts +297 -0
  197. package/ts/core/index.ts +2 -0
  198. package/ts/core/types.ts +16 -0
  199. package/ts/integrations/braviatv/braviatv.classes.client.ts +245 -709
  200. package/ts/integrations/braviatv/braviatv.classes.configflow.ts +47 -76
  201. package/ts/integrations/braviatv/braviatv.classes.integration.ts +167 -147
  202. package/ts/integrations/braviatv/braviatv.discovery.ts +11 -10
  203. package/ts/integrations/braviatv/braviatv.mapper.ts +39 -142
  204. package/ts/integrations/braviatv/braviatv.types.ts +53 -173
  205. package/ts/integrations/braviatv/braviatv.validation.ts +68 -0
  206. package/ts/integrations/broadlink/broadlink.actions.ts +31 -0
  207. package/ts/integrations/broadlink/broadlink.classes.client.ts +170 -144
  208. package/ts/integrations/broadlink/broadlink.classes.configflow.ts +59 -89
  209. package/ts/integrations/broadlink/broadlink.classes.integration.ts +110 -64
  210. package/ts/integrations/broadlink/broadlink.mapper.ts +5 -4
  211. package/ts/integrations/broadlink/broadlink.selected.mapper.ts +46 -0
  212. package/ts/integrations/broadlink/broadlink.types.ts +48 -8
  213. package/ts/integrations/broadlink/broadlink.validation.ts +66 -0
  214. package/ts/integrations/daikin/daikin.classes.client.ts +52 -661
  215. package/ts/integrations/daikin/daikin.classes.configflow.ts +72 -152
  216. package/ts/integrations/daikin/daikin.classes.integration.ts +166 -71
  217. package/ts/integrations/daikin/daikin.discovery.ts +94 -346
  218. package/ts/integrations/daikin/daikin.mapper.ts +83 -166
  219. package/ts/integrations/daikin/daikin.types.ts +27 -48
  220. package/ts/integrations/daikin/daikin.validation.ts +51 -0
  221. package/ts/integrations/daikin/readme.md +83 -0
  222. package/ts/integrations/denonavr/denonavr.classes.client.ts +132 -421
  223. package/ts/integrations/denonavr/denonavr.classes.configflow.ts +44 -37
  224. package/ts/integrations/denonavr/denonavr.classes.integration.ts +171 -117
  225. package/ts/integrations/denonavr/denonavr.discovery.ts +67 -220
  226. package/ts/integrations/denonavr/denonavr.mapper.ts +36 -182
  227. package/ts/integrations/denonavr/denonavr.types.ts +48 -111
  228. package/ts/integrations/denonavr/denonavr.validation.ts +87 -0
  229. package/ts/integrations/kodi/kodi.classes.client.ts +72 -352
  230. package/ts/integrations/kodi/kodi.classes.configflow.ts +52 -55
  231. package/ts/integrations/kodi/kodi.classes.integration.ts +97 -178
  232. package/ts/integrations/kodi/kodi.discovery.ts +49 -28
  233. package/ts/integrations/kodi/kodi.mapper.ts +52 -10
  234. package/ts/integrations/kodi/kodi.types.ts +33 -58
  235. package/ts/integrations/kodi/kodi.validation.ts +60 -0
  236. package/ts/integrations/modbus/modbus.classes.client.ts +100 -842
  237. package/ts/integrations/modbus/modbus.classes.configflow.ts +70 -41
  238. package/ts/integrations/modbus/modbus.classes.integration.ts +115 -60
  239. package/ts/integrations/modbus/modbus.discovery.ts +38 -106
  240. package/ts/integrations/modbus/modbus.mapper.ts +64 -467
  241. package/ts/integrations/modbus/modbus.types.ts +64 -381
  242. package/ts/integrations/modbus/modbus.validation.ts +119 -0
  243. package/ts/integrations/nanoleaf/nanoleaf.classes.client.ts +115 -249
  244. package/ts/integrations/nanoleaf/nanoleaf.classes.configflow.ts +90 -19
  245. package/ts/integrations/nanoleaf/nanoleaf.classes.integration.ts +130 -181
  246. package/ts/integrations/nanoleaf/nanoleaf.credentials.ts +39 -0
  247. package/ts/integrations/nanoleaf/nanoleaf.mapper.ts +47 -260
  248. package/ts/integrations/nanoleaf/nanoleaf.types.ts +29 -147
  249. package/ts/integrations/nanoleaf/nanoleaf.validation.ts +77 -0
  250. package/ts/integrations/onkyo/onkyo.classes.client.ts +148 -457
  251. package/ts/integrations/onkyo/onkyo.classes.configflow.ts +52 -6
  252. package/ts/integrations/onkyo/onkyo.classes.integration.ts +121 -12
  253. package/ts/integrations/onkyo/onkyo.discovery.ts +41 -3
  254. package/ts/integrations/onkyo/onkyo.mapper.ts +46 -19
  255. package/ts/integrations/onkyo/onkyo.types.ts +55 -110
  256. package/ts/integrations/onkyo/onkyo.validation.ts +85 -0
  257. package/ts/integrations/onkyo/readme.md +126 -0
  258. package/ts/integrations/panasonic_viera/panasonic_viera.classes.client.ts +101 -398
  259. package/ts/integrations/panasonic_viera/panasonic_viera.classes.configflow.ts +69 -5
  260. package/ts/integrations/panasonic_viera/panasonic_viera.classes.integration.ts +141 -9
  261. package/ts/integrations/panasonic_viera/panasonic_viera.discovery.ts +32 -3
  262. package/ts/integrations/panasonic_viera/panasonic_viera.mapper.ts +34 -161
  263. package/ts/integrations/panasonic_viera/panasonic_viera.types.ts +52 -96
  264. package/ts/integrations/panasonic_viera/panasonic_viera.validation.ts +56 -0
  265. package/ts/integrations/pioneer/pioneer.classes.client.ts +132 -428
  266. package/ts/integrations/pioneer/pioneer.classes.configflow.ts +67 -5
  267. package/ts/integrations/pioneer/pioneer.classes.integration.ts +225 -11
  268. package/ts/integrations/pioneer/pioneer.discovery.ts +45 -3
  269. package/ts/integrations/pioneer/pioneer.mapper.ts +57 -13
  270. package/ts/integrations/pioneer/pioneer.types.ts +87 -85
  271. package/ts/integrations/pioneer/pioneer.validation.ts +84 -0
  272. package/ts/integrations/soundtouch/soundtouch.classes.client.ts +152 -571
  273. package/ts/integrations/soundtouch/soundtouch.classes.configflow.ts +47 -42
  274. package/ts/integrations/soundtouch/soundtouch.classes.integration.ts +95 -150
  275. package/ts/integrations/soundtouch/soundtouch.discovery.ts +21 -133
  276. package/ts/integrations/soundtouch/soundtouch.mapper.ts +36 -207
  277. package/ts/integrations/soundtouch/soundtouch.types.ts +50 -195
  278. package/ts/integrations/soundtouch/soundtouch.validation.ts +60 -0
  279. package/ts/integrations/wiz/index.ts +1 -0
  280. package/ts/integrations/wiz/wiz.classes.client.ts +84 -254
  281. package/ts/integrations/wiz/wiz.classes.configflow.ts +39 -70
  282. package/ts/integrations/wiz/wiz.classes.integration.ts +189 -45
  283. package/ts/integrations/wiz/wiz.discovery.ts +48 -291
  284. package/ts/integrations/wiz/wiz.mapper.ts +81 -754
  285. package/ts/integrations/wiz/wiz.types.ts +24 -224
  286. package/ts/integrations/wiz/wiz.validation.ts +73 -0
  287. package/ts/integrations/yamaha/index.ts +1 -0
  288. package/ts/integrations/yamaha/yamaha.classes.client.ts +150 -6
  289. package/ts/integrations/yamaha/yamaha.classes.configflow.ts +100 -5
  290. package/ts/integrations/yamaha/yamaha.classes.integration.ts +236 -8
  291. package/ts/integrations/yamaha/yamaha.discovery.ts +52 -3
  292. package/ts/integrations/yamaha/yamaha.mapper.ts +65 -13
  293. package/ts/integrations/yamaha/yamaha.types.ts +77 -97
  294. package/ts/integrations/yamaha/yamaha.validation.ts +86 -0
@@ -0,0 +1,126 @@
1
+ # Selected native Onkyo integration
2
+
3
+ `OnkyoIntegration` uses the published `OnkyoProtocol` from
4
+ `@ecobridge.xyz/devicemanager@4.5.0` for one selected Onkyo receiver main zone over
5
+ a persistent eISCP TCP connection. Device definitions use the canonical `tcp`
6
+ protocol from `@smarthome.exchange/interfaces@2.1.0`.
7
+
8
+ ```typescript
9
+ import { OnkyoIntegration } from '@smarthome.exchange/integrations';
10
+
11
+ const integration = new OnkyoIntegration();
12
+ const runtime = await integration.setup({
13
+ homeId: 'home-1',
14
+ host: '192.168.1.40',
15
+ macAddress: '0009B0D24B75',
16
+ model: 'TX-NR609',
17
+ allowUnencryptedLan: true,
18
+ volumeLimitRaw: 30,
19
+ }, { homeId: 'home-1' });
20
+ try {
21
+ const device = (await runtime.devices())[0];
22
+ const result = await runtime.callService!({
23
+ domain: 'onkyo', service: 'set_power',
24
+ target: { deviceId: device.id }, data: { power: true },
25
+ });
26
+ console.log(result);
27
+ } finally {
28
+ await integration.destroy();
29
+ }
30
+ ```
31
+
32
+ The embedding host must select `context.homeId` independently of configuration,
33
+ form values, discovery or store scope. Setup and config flow require that explicit
34
+ selection; configuration must match it. Construction without options is inert.
35
+ An optional constructor `{ homeId }` adds a consistency binding and still requires
36
+ the explicit context. One instance binds synchronously to its first validated host
37
+ home, including when later receiver setup fails. Flow and setup share that binding.
38
+ This trusted host contract is not authentication by a home-id string.
39
+
40
+ Configuration is captured plain data. It requires a matching `homeId`, one
41
+ private/link-local/loopback unicast IPv4 `host`, twelve hexadecimal MAC digits,
42
+ the exact selected `model`, `allowUnencryptedLan: true`, and an integer
43
+ `volumeLimitRaw` from 0 through 100. Optional `port` defaults to 60128;
44
+ `requestTimeoutMs` defaults to 5000 and accepts 1–60000; `maxPendingOperations`
45
+ defaults to 16 and accepts 1–128. `name` is optional. Unknown keys, accessors,
46
+ hidden properties, injected clients/executors/snapshots, hostname resolution,
47
+ and configurable source/volume conversion maps are rejected.
48
+
49
+ Setup checks live NRI identity and an enabled main zone. Every control checks
50
+ fresh identity and capabilities through the public protocol. MAC/model binding
51
+ selects the receiver; eISCP does not authenticate or encrypt the LAN peer.
52
+
53
+ Device ids are `onkyo:<encoded homeId>:<uppercase MAC>` and the media entity id
54
+ adds `:main`. Every service needs an exact owned device or entity target. If
55
+ both target ids are supplied, both must match. Feature writes validate the
56
+ owned home/device and a fresh canonical writable feature; caller-provided
57
+ `writable` flags cannot grant capability.
58
+
59
+ | Service | Exact data |
60
+ | --- | --- |
61
+ | `onkyo.status`, `onkyo.snapshot`, `onkyo.refresh` | Empty or absent data; live sequential inspection |
62
+ | `onkyo.set_power` | `{ power: boolean }` |
63
+ | `onkyo.set_mute`, `onkyo.volume_mute` | `{ muted: boolean }` |
64
+ | `onkyo.set_volume_raw` | `{ volumeRaw: integer }` |
65
+ | `onkyo.set_input`, `onkyo.select_source` | `{ inputId: '10' }`; advertised two-digit selector id |
66
+
67
+ The same control services accept the `media_player` domain. Feature ids are
68
+ `power`, `mute`, `volume_raw`, and `input`, when observed. Identity, connectivity,
69
+ and advertised input lists are read-only. Unavailable reports produce absent
70
+ state features. Power/mute need current boolean reports; input writes need
71
+ current selector availability and enabled advertised main-zone choices.
72
+
73
+ Volume values are absolute integer receiver wire units. Writes require an
74
+ advertised `volstep="1"`, a positive `volmax` at most 100, and an observed raw
75
+ level consistent with that contract. The configured ceiling and current
76
+ advertised maximum both apply. Unknown/half-step contracts stay read-only.
77
+ There is no normalized Home Assistant `volume_level`, percent conversion,
78
+ guessed model scaling, increment, or toggle control.
79
+
80
+ Success returns a matching receiver report after dispatch with
81
+ `observation: 'matching-receiver-state'`, `commandAcknowledged: false`, and
82
+ `causalityConfirmed: false`. eISCP has no request id; this report does not prove
83
+ the command caused a physical change. Failures retain `effectOutcome` from the
84
+ protocol: `not-sent` before dispatch or `indeterminate` after dispatch. A
85
+ different report never counts as success. Controls are never retried or
86
+ replayed. Inspection is sequential and `observedAt` records its completion.
87
+
88
+ `subscribe()` emits observations from explicit inspection/control calls, plus
89
+ operation errors. It opens no polling timer and provides no unsolicited push
90
+ subscription. Subscriber cancellation unregisters its listener; runtime,
91
+ integration, or caller lifetime cancellation joins active/queued operations
92
+ and terminal protocol/socket cleanup. Shutdown is idempotent. A terminal
93
+ protocol cannot reconnect; create a new authorized runtime explicitly.
94
+ Integration shutdown collects cleanup failures, waits for every admitted resource,
95
+ pending setup, config-flow submission and active client operation to settle, then
96
+ reports the failures together in an `AggregateError`. Repeated shutdown returns
97
+ the same terminal promise. Failed cleanup stays owned and consumes the shared
98
+ capacity (16 by default; constructor `capacity` is a positive integer).
99
+ `retryCleanup()` explicitly retries retained cleanup handles, never receiver effects.
100
+ Closing is terminal and cannot select a different home or reopen an old form.
101
+
102
+ Manual/custom discovery candidates must supply `integrationDomain: 'onkyo'`,
103
+ selected host, MAC and model. The descriptor does no automatic network scan.
104
+ `probeSelectedOnkyo({ host, port?, requestTimeoutMs?, signal? })` delegates one
105
+ selected-unicast UDP query to the public protocol and returns a custom candidate
106
+ with the advertised TCP port. Config flow fixes its endpoint and identity,
107
+ captures the host home and lifetime before effects, collects acknowledgement and
108
+ ceiling, then validates TCP readiness before completion. Home is not editable; a
109
+ submitted conflicting home fails before opening a connection. Standalone
110
+ `OnkyoConfigFlow` also requires explicit context home and supports `destroy()`.
111
+
112
+ A supplied `configStore` must be an actual public `NoSqlConfigStore` with scope
113
+ matching exactly this home and the `onkyo` domain. A store is optional for these
114
+ protocol paths. The caller owns the public NoSQL store, database, and persistence
115
+ lifetime. This integration does not create, close, read, write, or delete store
116
+ data and does not persist receiver observations.
117
+
118
+ Direct `OnkyoClient` construction likewise requires a separate native context as
119
+ its second argument, such as `{ homeId: 'home-1' }`. Its snapshot home comes from
120
+ that captured admission; an optional third signal carries an owning lease lifetime.
121
+
122
+ Tests use bounded real TCP/UDP fixtures. No physical receiver or firmware has
123
+ been certified. Other zones, media transport commands, HDMI routing, arbitrary
124
+ raw commands, broadcast discovery, and cryptographic LAN authentication are
125
+ outside the supported contract. `HomeAssistantOnkyoIntegration` remains a class
126
+ alias with the same native semantics.
@@ -1,426 +1,129 @@
1
1
  import * as plugins from '../../plugins.js';
2
- import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
3
- import { PanasonicVieraMapper } from './panasonic_viera.mapper.js';
4
- import type { IPanasonicVieraConfig, IPanasonicVieraRawApiData } from './panasonic_viera.types.js';
5
- import { panasonicVieraDefaultPort, panasonicVieraDefaultTimeoutMs, panasonicVieraProfile } from './panasonic_viera.types.js';
6
-
7
- interface IPanasonicVieraSession {
8
- key: Buffer;
9
- iv: Buffer;
10
- hmacKey: Buffer;
11
- id?: string;
12
- sequenceNumber?: number;
13
- }
14
-
15
- export class PanasonicVieraClient extends SimpleLocalClient<IPanasonicVieraConfig> {
16
- private cachedSnapshot?: ISimpleLocalSnapshot;
17
- private encryptedSession?: IPanasonicVieraSession;
2
+ import { NativeHomeAdmission } from '../../core/classes.nativehomeadmission.js';
3
+ import type { IPanasonicVieraClientOptions, IPanasonicVieraConfig, IPanasonicVieraSnapshot, TPanasonicVieraKey, TPanasonicVieraKeyReceipt } from './panasonic_viera.types.js';
4
+ import { drainPanasonicVieraCleanup, normalizePanasonicVieraConfig, panasonicVieraRecord } from './panasonic_viera.validation.js';
5
+
6
+ export class PanasonicVieraClient {
7
+ private readonly config: Readonly<IPanasonicVieraConfig>;
8
+ private readonly protocol: plugins.ecobridgeDeviceManager.PanasonicProtocol;
9
+ private readonly lifetime = new AbortController();
10
+ private readonly operations = new Set<Promise<unknown>>();
11
+ private readonly ownerSignal?: AbortSignal;
12
+ private readonly onDestroyed?: () => void;
13
+ private state: 'new' | 'starting' | 'ready' | 'closed' = 'new';
14
+ private closePromise?: Promise<void>;
15
+ private retryPromise?: Promise<void>;
16
+ private cleanupComplete = false;
17
+ private readonly onOwnerAbort = (): void => {
18
+ // An event listener cannot return the close result. Retain it for destroy(), while observing its rejection.
19
+ void this.destroy().catch(() => undefined);
20
+ };
18
21
 
19
- constructor(private readonly clientConfig: IPanasonicVieraConfig) {
20
- super(panasonicVieraProfile, clientConfig);
22
+ public constructor(configArg: IPanasonicVieraConfig, optionsArg: IPanasonicVieraClientOptions) {
23
+ this.config = normalizePanasonicVieraConfig(configArg);
24
+ const options = panasonicVieraRecord(optionsArg, ['homeId', 'configStore', 'logger', 'abortSignal', 'onDestroyed']);
25
+ if (options.onDestroyed !== undefined && typeof options.onDestroyed !== 'function') {
26
+ throw new TypeError('Panasonic Viera client lifetime options are invalid.');
27
+ }
28
+ const { onDestroyed: _onDestroyed, ...contextOptions } = options;
29
+ const context = new NativeHomeAdmission('panasonic_viera').admit(contextOptions, this.config.homeId);
30
+ this.protocol = new plugins.ecobridgeDeviceManager.PanasonicProtocol({
31
+ address: this.config.host, descriptionUrl: this.config.descriptionUrl,
32
+ expectedUdn: this.config.expectedUdn, expectedModelName: this.config.expectedModelName,
33
+ timeoutMs: this.config.timeoutMs, maxResponseBytes: this.config.maxResponseBytes,
34
+ });
35
+ this.ownerSignal = context.abortSignal;
36
+ this.onDestroyed = optionsArg.onDestroyed;
37
+ if (this.ownerSignal?.aborted) this.onOwnerAbort();
38
+ else this.ownerSignal?.addEventListener('abort', this.onOwnerAbort, { once: true });
21
39
  }
22
40
 
23
- public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
24
- if (!forceRefreshArg && this.cachedSnapshot) {
25
- return clone(this.cachedSnapshot);
26
- }
27
-
28
- if (!forceRefreshArg && this.clientConfig.snapshot) {
29
- this.cachedSnapshot = PanasonicVieraMapper.toSnapshot({ config: this.clientConfig, source: 'snapshot', online: this.clientConfig.snapshot.online });
30
- return clone(this.cachedSnapshot);
31
- }
41
+ public get isClosed(): boolean { return this.state === 'closed'; }
42
+ public get isHealthy(): boolean { return this.state === 'ready' && this.protocol.isStarted; }
32
43
 
33
- if (this.endpoint()) {
44
+ public start(abortSignalArg?: AbortSignal): Promise<IPanasonicVieraSnapshot> {
45
+ this.assertOpen();
46
+ if (this.state !== 'new') return Promise.reject(new Error('Panasonic Viera startup is single-use.'));
47
+ this.state = 'starting';
48
+ return this.track(async () => {
34
49
  try {
35
- const rawData = await this.fetchRawData();
36
- this.cachedSnapshot = PanasonicVieraMapper.toSnapshot({ config: this.clientConfig, rawData, online: true, source: 'http' });
50
+ const profile = await this.protocol.start({ signal: this.operationSignal(abortSignalArg) });
51
+ this.assertOpen();
52
+ this.state = 'ready';
53
+ return this.snapshot(profile);
37
54
  } catch (errorArg) {
38
- this.cachedSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
39
- }
40
- return clone(this.cachedSnapshot);
41
- }
42
-
43
- this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
44
- return clone(this.cachedSnapshot);
45
- }
46
-
47
- public async refresh(): Promise<IServiceCallResult> {
48
- this.cachedSnapshot = undefined;
49
- const snapshot = await this.getSnapshot(Boolean(this.endpoint()));
50
- const success = snapshot.online && !snapshot.error;
51
- return { success, error: success ? undefined : snapshot.error || 'Panasonic Viera refresh requires a live SOAP endpoint, injected client, snapshotProvider, snapshot, entities, or rawData.', data: snapshot };
52
- }
53
-
54
- public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
55
- if (this.clientConfig.client?.execute || this.clientConfig.commandExecutor) {
56
- return super.execute(requestArg);
57
- }
58
-
59
- if (!this.endpoint()) {
60
- return super.execute(requestArg);
61
- }
62
-
63
- try {
64
- const data = requestArg.data || {};
65
- if (requestArg.service === 'volume_set' || requestArg.service === 'set_volume') {
66
- const volume = numberValue(data.volume_level ?? data.volumeLevel ?? data.volume);
67
- if (volume === undefined) {
68
- throw new Error('Panasonic Viera volume_set requires data.volume_level or data.volume.');
69
- }
70
- await this.setVolume(volume <= 1 ? Math.round(volume * 100) : Math.round(volume));
71
- this.cachedSnapshot = undefined;
72
- return { success: true, data: { volume } };
73
- }
74
-
75
- if (requestArg.service === 'volume_mute' || requestArg.service === 'set_mute') {
76
- const mute = booleanValue(data.is_volume_muted ?? data.mute ?? data.muted);
77
- if (mute === undefined) {
78
- await this.sendKey('NRC_MUTE-ONOFF');
79
- this.cachedSnapshot = undefined;
80
- return { success: true, data: { key: 'NRC_MUTE-ONOFF' } };
81
- }
82
- await this.setMute(mute);
83
- this.cachedSnapshot = undefined;
84
- return { success: true, data: { muted: mute } };
55
+ // Close the protocol here; destroy drains this wrapper after the rejection settles.
56
+ this.state = 'closed';
57
+ this.ownerSignal?.removeEventListener('abort', this.onOwnerAbort);
58
+ this.lifetime.abort(errorArg);
59
+ try { await this.protocol.close(); }
60
+ catch (cleanupErrorArg) { throw new AggregateError([errorArg, cleanupErrorArg], 'Panasonic Viera admission and cleanup failed.'); }
61
+ throw errorArg;
85
62
  }
86
-
87
- const key = keyForService(requestArg.service, data);
88
- if (!key) {
89
- return { success: false, error: `Unsupported Panasonic Viera service: ${requestArg.service}` };
90
- }
91
- await this.sendKey(key);
92
- this.cachedSnapshot = undefined;
93
- return { success: true, data: { key } };
94
- } catch (errorArg) {
95
- return { success: false, error: errorMessage(errorArg) };
96
- }
97
- }
98
-
99
- public async destroy(): Promise<void> {
100
- await this.clientConfig.client?.destroy?.();
101
- }
102
-
103
- private async fetchRawData(): Promise<IPanasonicVieraRawApiData> {
104
- const deviceInfoXml = await this.requestText('/nrc/ddd.xml');
105
- const deviceInfo = parseDeviceInfo(deviceInfoXml);
106
- const rawData: IPanasonicVieraRawApiData = {
107
- deviceInfo,
108
- fetchedAt: new Date().toISOString(),
109
- };
110
-
111
- try {
112
- rawData.volume = await this.getVolume();
113
- } catch (errorArg) {
114
- rawData.volumeError = errorMessage(errorArg);
115
- }
116
-
117
- try {
118
- rawData.muted = await this.getMute();
119
- } catch (errorArg) {
120
- rawData.muteError = errorMessage(errorArg);
121
- }
122
-
123
- return rawData;
124
- }
125
-
126
- private async getVolume(): Promise<number> {
127
- const text = await this.soapRequest('/dmr/control_0', 'schemas-upnp-org:service:RenderingControl:1', 'GetVolume', '<InstanceID>0</InstanceID><Channel>Master</Channel>');
128
- const volume = numberValue(extractXmlValue(text, 'CurrentVolume'));
129
- if (volume === undefined) {
130
- throw new Error('Panasonic Viera GetVolume response did not include CurrentVolume.');
131
- }
132
- return volume;
133
- }
134
-
135
- private async setVolume(volumeArg: number): Promise<void> {
136
- if (volumeArg < 0 || volumeArg > 100) {
137
- throw new Error('Panasonic Viera volume must be between 0 and 100.');
138
- }
139
- await this.soapRequest('/dmr/control_0', 'schemas-upnp-org:service:RenderingControl:1', 'SetVolume', `<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredVolume>${volumeArg}</DesiredVolume>`);
140
- }
141
-
142
- private async getMute(): Promise<boolean> {
143
- const text = await this.soapRequest('/dmr/control_0', 'schemas-upnp-org:service:RenderingControl:1', 'GetMute', '<InstanceID>0</InstanceID><Channel>Master</Channel>');
144
- return extractXmlValue(text, 'CurrentMute') !== '0';
145
- }
146
-
147
- private async setMute(muteArg: boolean): Promise<void> {
148
- await this.soapRequest('/dmr/control_0', 'schemas-upnp-org:service:RenderingControl:1', 'SetMute', `<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredMute>${muteArg ? '1' : '0'}</DesiredMute>`);
149
- }
150
-
151
- private async sendKey(keyArg: string): Promise<void> {
152
- await this.soapRequest('/nrc/control_0', 'panasonic-com:service:p00NetworkControl:1', 'X_SendKey', `<X_KeyEvent>${escapeXml(keyArg)}</X_KeyEvent>`);
153
- }
154
-
155
- private async soapRequest(pathArg: string, urnArg: string, actionArg: string, paramsArg: string, bodyElementArg = 'm'): Promise<string> {
156
- let action = actionArg;
157
- let params = paramsArg;
158
- let bodyElement = bodyElementArg;
159
- let encrypted = false;
160
-
161
- if (this.shouldEncrypt(urnArg, actionArg)) {
162
- const session = await this.ensureEncryptedSession();
163
- session.sequenceNumber = (session.sequenceNumber || 1) + 1;
164
- const originalCommand = `<X_SessionId>${session.id}</X_SessionId><X_SequenceNumber>${String(session.sequenceNumber).padStart(8, '0')}</X_SequenceNumber><X_OriginalCommand><u:${actionArg} xmlns:u="urn:${urnArg}">${paramsArg}</u:${actionArg}></X_OriginalCommand>`;
165
- const encryptedCommand = this.encryptSoapPayload(originalCommand, session);
166
- action = 'X_EncryptedCommand';
167
- params = `<X_ApplicationId>${escapeXml(this.clientConfig.appId || '')}</X_ApplicationId><X_EncInfo>${encryptedCommand}</X_EncInfo>`;
168
- bodyElement = 'u';
169
- encrypted = true;
170
- }
171
-
172
- const body = `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><${bodyElement}:${action} xmlns:${bodyElement}="urn:${urnArg}">${params}</${bodyElement}:${action}></s:Body></s:Envelope>`;
173
- const text = await this.requestText(pathArg, {
174
- method: 'POST',
175
- body,
176
- headers: {
177
- 'Content-Type': 'text/xml; charset=utf-8',
178
- SOAPAction: `"urn:${urnArg}#${action}"`,
179
- },
180
63
  });
181
-
182
- if (!encrypted) {
183
- return text;
184
- }
185
-
186
- const encResult = extractXmlValue(text, 'X_EncResult');
187
- if (!encResult) {
188
- throw new Error('Panasonic Viera encrypted SOAP response did not include X_EncResult.');
189
- }
190
- return this.decryptSoapPayload(encResult, await this.ensureEncryptedSession());
191
- }
192
-
193
- private shouldEncrypt(urnArg: string, actionArg: string): boolean {
194
- return urnArg === 'panasonic-com:service:p00NetworkControl:1'
195
- && !['X_GetEncryptSessionId', 'X_DisplayPinCode', 'X_RequestAuth'].includes(actionArg)
196
- && Boolean(this.clientConfig.appId && this.clientConfig.encryptionKey);
197
- }
198
-
199
- private async ensureEncryptedSession(): Promise<IPanasonicVieraSession> {
200
- if (!this.clientConfig.appId || !this.clientConfig.encryptionKey) {
201
- throw new Error('Panasonic Viera encrypted commands require config.appId and config.encryptionKey.');
202
- }
203
- if (!this.encryptedSession) {
204
- this.encryptedSession = deriveSession(this.clientConfig.encryptionKey);
205
- }
206
- if (this.encryptedSession.id) {
207
- return this.encryptedSession;
208
- }
209
-
210
- const encInfo = this.encryptSoapPayload(`<X_ApplicationId>${escapeXml(this.clientConfig.appId)}</X_ApplicationId>`, this.encryptedSession);
211
- const text = await this.soapRequest('/nrc/control_0', 'panasonic-com:service:p00NetworkControl:1', 'X_GetEncryptSessionId', `<X_ApplicationId>${escapeXml(this.clientConfig.appId)}</X_ApplicationId><X_EncInfo>${encInfo}</X_EncInfo>`, 'u');
212
- const encResult = extractXmlValue(text, 'X_EncResult');
213
- if (!encResult) {
214
- throw new Error('Panasonic Viera session response did not include X_EncResult.');
215
- }
216
- const decrypted = this.decryptSoapPayload(encResult, this.encryptedSession);
217
- const sessionId = extractXmlValue(decrypted, 'X_SessionId');
218
- if (!sessionId) {
219
- throw new Error('Panasonic Viera encrypted session response did not include X_SessionId.');
220
- }
221
- this.encryptedSession.id = sessionId;
222
- this.encryptedSession.sequenceNumber = 1;
223
- return this.encryptedSession;
224
64
  }
225
65
 
226
- private encryptSoapPayload(dataArg: string, sessionArg: IPanasonicVieraSession): string {
227
- const header = Buffer.concat([plugins.crypto.randomBytes(12), uint32be(Buffer.byteLength(dataArg, 'latin1'))]);
228
- const payload = Buffer.concat([header, Buffer.from(dataArg, 'latin1')]);
229
- const padLength = (16 - (payload.length % 16)) % 16;
230
- const paddedPayload = padLength ? Buffer.concat([payload, Buffer.alloc(padLength)]) : payload;
231
- const cipher = plugins.crypto.createCipheriv('aes-128-cbc', sessionArg.key, sessionArg.iv);
232
- cipher.setAutoPadding(false);
233
- const ciphertext = Buffer.concat([cipher.update(paddedPayload), cipher.final()]);
234
- const signature = plugins.crypto.createHmac('sha256', sessionArg.hmacKey).update(ciphertext).digest();
235
- return Buffer.concat([ciphertext, signature]).toString('base64');
66
+ public getSnapshot(abortSignalArg?: AbortSignal): Promise<IPanasonicVieraSnapshot> {
67
+ this.assertOpen();
68
+ if (this.state === 'new') return this.start(abortSignalArg);
69
+ if (this.state !== 'ready') return Promise.reject(new Error('Panasonic Viera startup is in progress.'));
70
+ return this.track(async () => this.snapshot(await this.protocol.readProfile({ signal: this.operationSignal(abortSignalArg) })));
236
71
  }
237
72
 
238
- private decryptSoapPayload(dataArg: string, sessionArg: IPanasonicVieraSession): string {
239
- const encrypted = Buffer.from(dataArg, 'base64');
240
- const decipher = plugins.crypto.createDecipheriv('aes-128-cbc', sessionArg.key, sessionArg.iv);
241
- decipher.setAutoPadding(false);
242
- const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('latin1');
243
- return decrypted.slice(16).split('\0')[0];
73
+ public sendKey(keyArg: TPanasonicVieraKey, abortSignalArg?: AbortSignal): Promise<TPanasonicVieraKeyReceipt> {
74
+ this.assertOpen();
75
+ if (this.state !== 'ready') return Promise.reject(new Error('Panasonic Viera has not completed live admission.'));
76
+ return this.track(() => this.protocol.sendKey(keyArg, { signal: this.operationSignal(abortSignalArg) }));
244
77
  }
245
78
 
246
- private async requestText(pathArg: string, optionsArg: { method?: 'GET' | 'POST'; body?: string; headers?: Record<string, string> } = {}): Promise<string> {
247
- const endpoint = this.requireEndpoint();
248
- const url = new URL(pathArg.replace(/^\/+/, ''), `${endpoint.origin}/`);
249
- const timeoutMs = this.clientConfig.timeoutMs || panasonicVieraDefaultTimeoutMs;
250
- const controller = new AbortController();
251
- const timer = setTimeout(() => controller.abort(), timeoutMs);
252
- try {
253
- const response = await globalThis.fetch(url, {
254
- method: optionsArg.method || 'GET',
255
- headers: { Accept: 'text/xml, application/xml;q=0.9, */*;q=0.8', ...(optionsArg.headers || {}) },
256
- body: optionsArg.body,
257
- signal: controller.signal,
258
- });
259
- const text = await response.text();
260
- if (!response.ok) {
261
- throw new Error(`Panasonic Viera request ${url.pathname} failed with ${response.status}: ${extractXmlValue(text, 'errorDescription') || text.slice(0, 160)}`);
262
- }
263
- return text;
264
- } finally {
265
- clearTimeout(timer);
79
+ public destroy(): Promise<void> {
80
+ if (!this.closePromise) {
81
+ this.state = 'closed';
82
+ this.ownerSignal?.removeEventListener('abort', this.onOwnerAbort);
83
+ const operations = [...this.operations];
84
+ this.closePromise = Promise.resolve().then(() => drainPanasonicVieraCleanup([this.protocol.close()], operations,
85
+ 'Panasonic Viera client cleanup failed.')).then(() => { this.onDestroyed?.(); this.cleanupComplete = true; });
86
+ this.lifetime.abort(new Error('Panasonic Viera client is closed.'));
266
87
  }
88
+ return this.closePromise;
267
89
  }
268
90
 
269
- private requireEndpoint(): URL {
270
- const endpoint = this.endpoint();
271
- if (!endpoint) {
272
- throw new Error('Panasonic Viera local SOAP API requires config.host or config.url.');
91
+ /** Explicit cleanup retry only. The original destroy rejection remains observable; transport work is never replayed. */
92
+ public retryCleanup(): Promise<void> {
93
+ if (!this.closePromise) return Promise.reject(new Error('Panasonic Viera cleanup must first be closed.'));
94
+ if (this.cleanupComplete) return Promise.resolve();
95
+ if (!this.retryPromise) {
96
+ const closed = this.closePromise;
97
+ this.retryPromise = Promise.resolve().then(async () => {
98
+ await closed.catch(() => undefined);
99
+ await drainPanasonicVieraCleanup([this.protocol.close()], [...this.operations], 'Panasonic Viera explicit cleanup retry failed.');
100
+ this.onDestroyed?.();
101
+ this.cleanupComplete = true;
102
+ });
103
+ const settled = (): void => { this.retryPromise = undefined; };
104
+ void this.retryPromise.then(settled, settled);
273
105
  }
274
- return endpoint;
106
+ return this.retryPromise;
275
107
  }
276
108
 
277
- private endpoint(): URL | undefined {
278
- const configuredUrl = stringValue(this.clientConfig.url);
279
- if (configuredUrl) {
280
- return normalizeBaseUrl(configuredUrl, this.clientConfig.port, this.clientConfig.useTls);
281
- }
282
- const host = stringValue(this.clientConfig.host);
283
- if (!host) {
284
- return undefined;
285
- }
286
- return normalizeBaseUrl(host, this.clientConfig.port || panasonicVieraDefaultPort, this.clientConfig.useTls);
109
+ private snapshot(profileArg: plugins.ecobridgeDeviceManager.IPanasonicProfile): IPanasonicVieraSnapshot {
110
+ this.assertOpen();
111
+ return Object.freeze({ homeId: this.config.homeId, name: this.config.name ?? profileArg.identity.name, profile: profileArg });
287
112
  }
288
113
 
289
- private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
290
- return PanasonicVieraMapper.toSnapshot({ config: this.clientConfig, online: false, source: 'runtime', error: errorArg });
114
+ private operationSignal(signalArg?: AbortSignal): AbortSignal {
115
+ if (signalArg !== undefined && !(signalArg instanceof AbortSignal)) throw new TypeError('Panasonic Viera cancellation must be an AbortSignal.');
116
+ return signalArg ? AbortSignal.any([this.lifetime.signal, signalArg]) : this.lifetime.signal;
291
117
  }
292
- }
293
-
294
- const keyMap: Record<string, string> = {
295
- media_play: 'NRC_PLAY-ONOFF',
296
- media_pause: 'NRC_PAUSE-ONOFF',
297
- media_stop: 'NRC_STOP-ONOFF',
298
- media_next_track: 'NRC_FF-ONOFF',
299
- media_previous_track: 'NRC_REW-ONOFF',
300
- volume_up: 'NRC_VOLUP-ONOFF',
301
- volume_down: 'NRC_VOLDOWN-ONOFF',
302
- turn_on: 'NRC_POWER-ONOFF',
303
- turn_off: 'NRC_POWER-ONOFF',
304
- toggle: 'NRC_POWER-ONOFF',
305
- };
306
118
 
307
- const keyForService = (serviceArg: string, dataArg: Record<string, unknown>): string | undefined => {
308
- const explicit = stringValue(dataArg.command) || stringValue(dataArg.key) || stringValue(dataArg.button);
309
- if (serviceArg === 'press' && explicit) {
310
- return normalizeKey(explicit);
119
+ private assertOpen(): void {
120
+ if (this.isClosed) throw new Error('Panasonic Viera client is closed.');
311
121
  }
312
- return keyMap[serviceArg];
313
- };
314
122
 
315
- const normalizeKey = (valueArg: string): string => {
316
- if (valueArg.startsWith('NRC_')) {
317
- return valueArg;
123
+ private track<TResult>(executeArg: () => Promise<TResult>): Promise<TResult> {
124
+ if (this.operations.size !== 0) return Promise.reject(new Error('Panasonic Viera has an active operation.'));
125
+ const operation = Promise.resolve().then(executeArg).finally(() => this.operations.delete(operation));
126
+ this.operations.add(operation);
127
+ return operation;
318
128
  }
319
- const normalized = valueArg.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
320
- const aliases: Record<string, string> = {
321
- PLAY: 'NRC_PLAY-ONOFF',
322
- PAUSE: 'NRC_PAUSE-ONOFF',
323
- STOP: 'NRC_STOP-ONOFF',
324
- POWER: 'NRC_POWER-ONOFF',
325
- VOLUME_UP: 'NRC_VOLUP-ONOFF',
326
- VOLUME_DOWN: 'NRC_VOLDOWN-ONOFF',
327
- MUTE: 'NRC_MUTE-ONOFF',
328
- ENTER: 'NRC_ENTER-ONOFF',
329
- UP: 'NRC_UP-ONOFF',
330
- DOWN: 'NRC_DOWN-ONOFF',
331
- LEFT: 'NRC_LEFT-ONOFF',
332
- RIGHT: 'NRC_RIGHT-ONOFF',
333
- HOME: 'NRC_HOME-ONOFF',
334
- MENU: 'NRC_MENU-ONOFF',
335
- BACK: 'NRC_RETURN-ONOFF',
336
- };
337
- return aliases[normalized] || `NRC_${normalized}-ONOFF`;
338
- };
339
-
340
- const deriveSession = (encryptionKeyArg: string): IPanasonicVieraSession => {
341
- const iv = Buffer.from(encryptionKeyArg, 'base64').subarray(0, 16);
342
- if (iv.length !== 16) {
343
- throw new Error('Panasonic Viera encryptionKey must decode to a 16 byte AES IV.');
344
- }
345
- const key = Buffer.alloc(16);
346
- for (let index = 0; index < 16; index += 4) {
347
- key[index] = iv[index + 2];
348
- key[index + 1] = iv[index + 3];
349
- key[index + 2] = iv[index];
350
- key[index + 3] = iv[index + 1];
351
- }
352
- return { key, iv, hmacKey: Buffer.concat([iv, iv]) };
353
- };
354
-
355
- const uint32be = (valueArg: number): Buffer => {
356
- const buffer = Buffer.alloc(4);
357
- buffer.writeUInt32BE(valueArg, 0);
358
- return buffer;
359
- };
360
-
361
- const parseDeviceInfo = (xmlArg: string): Record<string, unknown> => ({
362
- UDN: extractXmlValue(xmlArg, 'UDN'),
363
- friendlyName: extractXmlValue(xmlArg, 'friendlyName'),
364
- manufacturer: extractXmlValue(xmlArg, 'manufacturer'),
365
- modelName: extractXmlValue(xmlArg, 'modelName'),
366
- modelNumber: extractXmlValue(xmlArg, 'modelNumber'),
367
- });
368
-
369
- const extractXmlValue = (xmlArg: string, tagArg: string): string | undefined => {
370
- const pattern = new RegExp(`<(?:\\w+:)?${tagArg}[^>]*>([\\s\\S]*?)</(?:\\w+:)?${tagArg}>`, 'i');
371
- const match = pattern.exec(xmlArg);
372
- return match?.[1]?.trim();
373
- };
374
-
375
- const escapeXml = (valueArg: string): string => valueArg.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
376
-
377
- const normalizeBaseUrl = (valueArg: string, portArg: number | undefined, useTlsArg?: boolean): URL | undefined => {
378
- try {
379
- const parsed = new URL(valueArg.includes('://') ? valueArg : `${useTlsArg ? 'https' : 'http'}://${valueArg}`);
380
- if (portArg) {
381
- parsed.port = String(portArg);
382
- }
383
- parsed.pathname = '';
384
- parsed.search = '';
385
- parsed.hash = '';
386
- return parsed;
387
- } catch {
388
- return undefined;
389
- }
390
- };
391
-
392
- const stringValue = (valueArg: unknown): string | undefined => {
393
- return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
394
- };
395
-
396
- const numberValue = (valueArg: unknown): number | undefined => {
397
- if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
398
- return valueArg;
399
- }
400
- if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
401
- return Number(valueArg);
402
- }
403
- return undefined;
404
- };
405
-
406
- const booleanValue = (valueArg: unknown): boolean | undefined => {
407
- if (typeof valueArg === 'boolean') {
408
- return valueArg;
409
- }
410
- if (typeof valueArg === 'number') {
411
- return valueArg !== 0;
412
- }
413
- if (typeof valueArg === 'string') {
414
- if (valueArg === '1' || valueArg.toLowerCase() === 'true') {
415
- return true;
416
- }
417
- if (valueArg === '0' || valueArg.toLowerCase() === 'false') {
418
- return false;
419
- }
420
- }
421
- return undefined;
422
- };
423
-
424
- const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;
425
-
426
- const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
129
+ }