node-red-contrib-knx-ultimate 7.1.2 → 7.1.4

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 (303) hide show
  1. package/CHANGELOG.md +1271 -1863
  2. package/README.md +4 -10
  3. package/nodes/hue-config.html +6 -0
  4. package/nodes/knxUltimate-config.html +14 -0
  5. package/nodes/knxUltimate.html +6 -0
  6. package/nodes/knxUltimateAI.html +6 -0
  7. package/nodes/knxUltimateAIHomeAssistant.html +14 -0
  8. package/nodes/knxUltimateAlerter.html +6 -0
  9. package/nodes/knxUltimateAutoResponder.html +6 -0
  10. package/nodes/knxUltimateDateTime.html +7 -1
  11. package/nodes/knxUltimateGarage.html +15 -1
  12. package/nodes/knxUltimateGlobalContext.html +6 -0
  13. package/nodes/knxUltimateHATranslator.html +6 -1
  14. package/nodes/knxUltimateHueAreaMotion.html +14 -0
  15. package/nodes/knxUltimateHueBattery.html +14 -0
  16. package/nodes/knxUltimateHueButton.html +14 -0
  17. package/nodes/knxUltimateHueCameraMotion.html +14 -0
  18. package/nodes/knxUltimateHueContactSensor.html +14 -0
  19. package/nodes/knxUltimateHueController.html +14 -0
  20. package/nodes/knxUltimateHueHumiditySensor.html +14 -0
  21. package/nodes/knxUltimateHueLight.html +14 -0
  22. package/nodes/knxUltimateHueLightSensor.html +14 -0
  23. package/nodes/knxUltimateHueMotion.html +14 -0
  24. package/nodes/knxUltimateHuePlug.html +14 -0
  25. package/nodes/knxUltimateHueScene.html +14 -0
  26. package/nodes/knxUltimateHueTapDial.html +14 -0
  27. package/nodes/knxUltimateHueTemperatureSensor.html +14 -0
  28. package/nodes/knxUltimateHueZigbeeConnectivity.html +14 -0
  29. package/nodes/knxUltimateHuedevice_software_update.html +14 -0
  30. package/nodes/knxUltimateIoTBridge.html +14 -0
  31. package/nodes/knxUltimateLoadControl.html +6 -0
  32. package/nodes/knxUltimateLogger.html +6 -0
  33. package/nodes/knxUltimateMatterBridge.html +15 -1
  34. package/nodes/knxUltimateMatterControllerDevice.html +15 -1
  35. package/nodes/knxUltimateMultiRouting.html +7 -1
  36. package/nodes/knxUltimateRouterFilter.html +7 -1
  37. package/nodes/knxUltimateSceneController.html +6 -0
  38. package/nodes/knxUltimateStaircase.html +7 -1
  39. package/nodes/knxUltimateUtility.html +6 -0
  40. package/nodes/knxUltimateViewer.html +6 -0
  41. package/nodes/knxUltimateWatchDog.html +16 -1
  42. package/nodes/locales/de/hue-config.html +8 -0
  43. package/nodes/locales/de/knxUltimate-config.html +8 -0
  44. package/nodes/locales/de/knxUltimate.html +8 -0
  45. package/nodes/locales/de/knxUltimateAI.html +8 -0
  46. package/nodes/locales/de/knxUltimateAIHomeAssistant.html +11 -0
  47. package/nodes/locales/de/knxUltimateAlerter.html +9 -1
  48. package/nodes/locales/de/knxUltimateAutoResponder.html +8 -0
  49. package/nodes/locales/de/knxUltimateDateTime.html +8 -1
  50. package/nodes/locales/de/knxUltimateGarage.html +8 -0
  51. package/nodes/locales/de/knxUltimateGlobalContext.html +9 -1
  52. package/nodes/locales/de/knxUltimateHATranslator.html +8 -0
  53. package/nodes/locales/de/knxUltimateHueAreaMotion.html +8 -0
  54. package/nodes/locales/de/knxUltimateHueBattery.html +8 -0
  55. package/nodes/locales/de/knxUltimateHueButton.html +8 -0
  56. package/nodes/locales/de/knxUltimateHueCameraMotion.html +8 -0
  57. package/nodes/locales/de/knxUltimateHueContactSensor.html +8 -0
  58. package/nodes/locales/de/knxUltimateHueController.html +8 -0
  59. package/nodes/locales/de/knxUltimateHueHumiditySensor.html +8 -0
  60. package/nodes/locales/de/knxUltimateHueLight.html +8 -0
  61. package/nodes/locales/de/knxUltimateHueLightSensor.html +8 -0
  62. package/nodes/locales/de/knxUltimateHueMotion.html +8 -0
  63. package/nodes/locales/de/knxUltimateHuePlug.html +8 -0
  64. package/nodes/locales/de/knxUltimateHueScene.html +8 -0
  65. package/nodes/locales/de/knxUltimateHueTapDial.html +8 -0
  66. package/nodes/locales/de/knxUltimateHueTemperatureSensor.html +8 -0
  67. package/nodes/locales/de/knxUltimateHueZigbeeConnectivity.html +8 -0
  68. package/nodes/locales/de/knxUltimateHuedevice_software_update.html +8 -0
  69. package/nodes/locales/de/knxUltimateIoTBridge.html +8 -0
  70. package/nodes/locales/de/knxUltimateLoadControl.html +8 -0
  71. package/nodes/locales/de/knxUltimateLogger.html +8 -0
  72. package/nodes/locales/de/knxUltimateMatterBridge.html +8 -0
  73. package/nodes/locales/de/knxUltimateMatterControllerDevice.html +8 -0
  74. package/nodes/locales/de/knxUltimateMultiRouting.html +8 -0
  75. package/nodes/locales/de/knxUltimateRouterFilter.html +8 -0
  76. package/nodes/locales/de/knxUltimateSceneController.html +9 -1
  77. package/nodes/locales/de/knxUltimateStaircase.html +8 -0
  78. package/nodes/locales/de/knxUltimateUtility.html +8 -0
  79. package/nodes/locales/de/knxUltimateUtility.json +34 -0
  80. package/nodes/locales/de/knxUltimateViewer.html +8 -0
  81. package/nodes/locales/de/knxUltimateWatchDog.html +8 -0
  82. package/nodes/locales/de/matter-config.html +8 -0
  83. package/nodes/locales/de/matterbridge-config.html +8 -0
  84. package/nodes/locales/en/hue-config.html +8 -0
  85. package/nodes/locales/en/knxUltimate-config.html +8 -0
  86. package/nodes/locales/en/knxUltimate.html +8 -0
  87. package/nodes/locales/en/knxUltimateAI.html +8 -0
  88. package/nodes/locales/en/knxUltimateAIHomeAssistant.html +11 -0
  89. package/nodes/locales/en/knxUltimateAlerter.html +8 -0
  90. package/nodes/locales/en/knxUltimateAutoResponder.html +8 -0
  91. package/nodes/locales/en/knxUltimateDateTime.html +8 -0
  92. package/nodes/locales/en/knxUltimateGarage.html +8 -0
  93. package/nodes/locales/en/knxUltimateGlobalContext.html +8 -0
  94. package/nodes/locales/en/knxUltimateHATranslator.html +8 -0
  95. package/nodes/locales/en/knxUltimateHueAreaMotion.html +8 -0
  96. package/nodes/locales/en/knxUltimateHueBattery.html +8 -0
  97. package/nodes/locales/en/knxUltimateHueButton.html +8 -0
  98. package/nodes/locales/en/knxUltimateHueCameraMotion.html +8 -0
  99. package/nodes/locales/en/knxUltimateHueContactSensor.html +8 -0
  100. package/nodes/locales/en/knxUltimateHueController.html +8 -0
  101. package/nodes/locales/en/knxUltimateHueHumiditySensor.html +8 -0
  102. package/nodes/locales/en/knxUltimateHueLight.html +8 -0
  103. package/nodes/locales/en/knxUltimateHueLightSensor.html +8 -0
  104. package/nodes/locales/en/knxUltimateHueMotion.html +8 -0
  105. package/nodes/locales/en/knxUltimateHuePlug.html +8 -0
  106. package/nodes/locales/en/knxUltimateHueScene.html +8 -0
  107. package/nodes/locales/en/knxUltimateHueTapDial.html +8 -0
  108. package/nodes/locales/en/knxUltimateHueTemperatureSensor.html +8 -0
  109. package/nodes/locales/en/knxUltimateHueZigbeeConnectivity.html +8 -0
  110. package/nodes/locales/en/knxUltimateHuedevice_software_update.html +8 -0
  111. package/nodes/locales/en/knxUltimateIoTBridge.html +8 -0
  112. package/nodes/locales/en/knxUltimateLoadControl.html +8 -0
  113. package/nodes/locales/en/knxUltimateLogger.html +8 -0
  114. package/nodes/locales/en/knxUltimateMatterBridge.html +8 -0
  115. package/nodes/locales/en/knxUltimateMatterControllerDevice.html +8 -0
  116. package/nodes/locales/en/knxUltimateMultiRouting.html +8 -0
  117. package/nodes/locales/en/knxUltimateRouterFilter.html +8 -0
  118. package/nodes/locales/en/knxUltimateSceneController.html +8 -0
  119. package/nodes/locales/en/knxUltimateStaircase.html +8 -0
  120. package/nodes/locales/en/knxUltimateUtility.html +8 -0
  121. package/nodes/locales/en/knxUltimateUtility.json +34 -0
  122. package/nodes/locales/en/knxUltimateViewer.html +8 -0
  123. package/nodes/locales/en/knxUltimateWatchDog.html +8 -0
  124. package/nodes/locales/en/matter-config.html +8 -0
  125. package/nodes/locales/en/matterbridge-config.html +8 -0
  126. package/nodes/locales/es/hue-config.html +8 -0
  127. package/nodes/locales/es/knxUltimate-config.html +8 -0
  128. package/nodes/locales/es/knxUltimate.html +8 -0
  129. package/nodes/locales/es/knxUltimateAI.html +8 -0
  130. package/nodes/locales/es/knxUltimateAIHomeAssistant.html +11 -0
  131. package/nodes/locales/es/knxUltimateAlerter.html +8 -0
  132. package/nodes/locales/es/knxUltimateAutoResponder.html +8 -0
  133. package/nodes/locales/es/knxUltimateDateTime.html +8 -1
  134. package/nodes/locales/es/knxUltimateGarage.html +8 -0
  135. package/nodes/locales/es/knxUltimateGlobalContext.html +8 -0
  136. package/nodes/locales/es/knxUltimateHATranslator.html +8 -0
  137. package/nodes/locales/es/knxUltimateHueAreaMotion.html +8 -0
  138. package/nodes/locales/es/knxUltimateHueBattery.html +8 -0
  139. package/nodes/locales/es/knxUltimateHueButton.html +8 -0
  140. package/nodes/locales/es/knxUltimateHueCameraMotion.html +8 -0
  141. package/nodes/locales/es/knxUltimateHueContactSensor.html +8 -0
  142. package/nodes/locales/es/knxUltimateHueController.html +8 -0
  143. package/nodes/locales/es/knxUltimateHueHumiditySensor.html +8 -0
  144. package/nodes/locales/es/knxUltimateHueLight.html +8 -0
  145. package/nodes/locales/es/knxUltimateHueLightSensor.html +8 -0
  146. package/nodes/locales/es/knxUltimateHueMotion.html +8 -0
  147. package/nodes/locales/es/knxUltimateHuePlug.html +8 -0
  148. package/nodes/locales/es/knxUltimateHueScene.html +8 -0
  149. package/nodes/locales/es/knxUltimateHueTapDial.html +8 -0
  150. package/nodes/locales/es/knxUltimateHueTemperatureSensor.html +8 -0
  151. package/nodes/locales/es/knxUltimateHueZigbeeConnectivity.html +8 -0
  152. package/nodes/locales/es/knxUltimateHuedevice_software_update.html +8 -0
  153. package/nodes/locales/es/knxUltimateIoTBridge.html +8 -0
  154. package/nodes/locales/es/knxUltimateLoadControl.html +8 -0
  155. package/nodes/locales/es/knxUltimateLogger.html +8 -0
  156. package/nodes/locales/es/knxUltimateMatterBridge.html +8 -0
  157. package/nodes/locales/es/knxUltimateMatterControllerDevice.html +8 -0
  158. package/nodes/locales/es/knxUltimateMultiRouting.html +8 -0
  159. package/nodes/locales/es/knxUltimateRouterFilter.html +8 -0
  160. package/nodes/locales/es/knxUltimateSceneController.html +8 -0
  161. package/nodes/locales/es/knxUltimateStaircase.html +8 -0
  162. package/nodes/locales/es/knxUltimateUtility.html +8 -0
  163. package/nodes/locales/es/knxUltimateUtility.json +35 -1
  164. package/nodes/locales/es/knxUltimateViewer.html +8 -0
  165. package/nodes/locales/es/knxUltimateWatchDog.html +8 -0
  166. package/nodes/locales/es/matter-config.html +8 -0
  167. package/nodes/locales/es/matterbridge-config.html +8 -0
  168. package/nodes/locales/fr/hue-config.html +8 -0
  169. package/nodes/locales/fr/knxUltimate-config.html +8 -0
  170. package/nodes/locales/fr/knxUltimate.html +8 -0
  171. package/nodes/locales/fr/knxUltimateAI.html +8 -0
  172. package/nodes/locales/fr/knxUltimateAIHomeAssistant.html +11 -0
  173. package/nodes/locales/fr/knxUltimateAlerter.html +8 -0
  174. package/nodes/locales/fr/knxUltimateAutoResponder.html +8 -0
  175. package/nodes/locales/fr/knxUltimateDateTime.html +8 -1
  176. package/nodes/locales/fr/knxUltimateGarage.html +8 -0
  177. package/nodes/locales/fr/knxUltimateGlobalContext.html +8 -0
  178. package/nodes/locales/fr/knxUltimateHATranslator.html +8 -0
  179. package/nodes/locales/fr/knxUltimateHueAreaMotion.html +8 -0
  180. package/nodes/locales/fr/knxUltimateHueBattery.html +8 -0
  181. package/nodes/locales/fr/knxUltimateHueButton.html +8 -0
  182. package/nodes/locales/fr/knxUltimateHueCameraMotion.html +8 -0
  183. package/nodes/locales/fr/knxUltimateHueContactSensor.html +8 -0
  184. package/nodes/locales/fr/knxUltimateHueController.html +8 -0
  185. package/nodes/locales/fr/knxUltimateHueHumiditySensor.html +8 -0
  186. package/nodes/locales/fr/knxUltimateHueLight.html +8 -0
  187. package/nodes/locales/fr/knxUltimateHueLightSensor.html +8 -0
  188. package/nodes/locales/fr/knxUltimateHueMotion.html +8 -0
  189. package/nodes/locales/fr/knxUltimateHuePlug.html +8 -0
  190. package/nodes/locales/fr/knxUltimateHueScene.html +8 -0
  191. package/nodes/locales/fr/knxUltimateHueTapDial.html +8 -0
  192. package/nodes/locales/fr/knxUltimateHueTemperatureSensor.html +8 -0
  193. package/nodes/locales/fr/knxUltimateHueZigbeeConnectivity.html +8 -0
  194. package/nodes/locales/fr/knxUltimateHuedevice_software_update.html +8 -0
  195. package/nodes/locales/fr/knxUltimateIoTBridge.html +8 -0
  196. package/nodes/locales/fr/knxUltimateLoadControl.html +8 -0
  197. package/nodes/locales/fr/knxUltimateLogger.html +8 -0
  198. package/nodes/locales/fr/knxUltimateMatterBridge.html +8 -0
  199. package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +8 -0
  200. package/nodes/locales/fr/knxUltimateMultiRouting.html +8 -0
  201. package/nodes/locales/fr/knxUltimateRouterFilter.html +8 -0
  202. package/nodes/locales/fr/knxUltimateSceneController.html +8 -0
  203. package/nodes/locales/fr/knxUltimateStaircase.html +8 -0
  204. package/nodes/locales/fr/knxUltimateUtility.html +8 -0
  205. package/nodes/locales/fr/knxUltimateUtility.json +34 -0
  206. package/nodes/locales/fr/knxUltimateViewer.html +8 -0
  207. package/nodes/locales/fr/knxUltimateWatchDog.html +8 -0
  208. package/nodes/locales/fr/matter-config.html +8 -0
  209. package/nodes/locales/fr/matterbridge-config.html +8 -0
  210. package/nodes/locales/it/hue-config.html +8 -0
  211. package/nodes/locales/it/knxUltimate-config.html +8 -0
  212. package/nodes/locales/it/knxUltimate.html +8 -0
  213. package/nodes/locales/it/knxUltimateAI.html +8 -0
  214. package/nodes/locales/it/knxUltimateAIHomeAssistant.html +11 -0
  215. package/nodes/locales/it/knxUltimateAlerter.html +8 -0
  216. package/nodes/locales/it/knxUltimateAutoResponder.html +8 -0
  217. package/nodes/locales/it/knxUltimateDateTime.html +8 -0
  218. package/nodes/locales/it/knxUltimateGarage.html +8 -0
  219. package/nodes/locales/it/knxUltimateGlobalContext.html +8 -0
  220. package/nodes/locales/it/knxUltimateHATranslator.html +8 -0
  221. package/nodes/locales/it/knxUltimateHueAreaMotion.html +8 -0
  222. package/nodes/locales/it/knxUltimateHueBattery.html +8 -0
  223. package/nodes/locales/it/knxUltimateHueButton.html +8 -0
  224. package/nodes/locales/it/knxUltimateHueCameraMotion.html +8 -0
  225. package/nodes/locales/it/knxUltimateHueContactSensor.html +8 -0
  226. package/nodes/locales/it/knxUltimateHueController.html +8 -0
  227. package/nodes/locales/it/knxUltimateHueHumiditySensor.html +8 -0
  228. package/nodes/locales/it/knxUltimateHueLight.html +8 -0
  229. package/nodes/locales/it/knxUltimateHueLightSensor.html +8 -0
  230. package/nodes/locales/it/knxUltimateHueMotion.html +8 -0
  231. package/nodes/locales/it/knxUltimateHuePlug.html +8 -0
  232. package/nodes/locales/it/knxUltimateHueScene.html +8 -0
  233. package/nodes/locales/it/knxUltimateHueTapDial.html +8 -0
  234. package/nodes/locales/it/knxUltimateHueTemperatureSensor.html +8 -0
  235. package/nodes/locales/it/knxUltimateHueZigbeeConnectivity.html +8 -0
  236. package/nodes/locales/it/knxUltimateHuedevice_software_update.html +8 -0
  237. package/nodes/locales/it/knxUltimateIoTBridge.html +8 -0
  238. package/nodes/locales/it/knxUltimateLoadControl.html +8 -0
  239. package/nodes/locales/it/knxUltimateLogger.html +8 -0
  240. package/nodes/locales/it/knxUltimateMatterBridge.html +8 -0
  241. package/nodes/locales/it/knxUltimateMatterControllerDevice.html +8 -0
  242. package/nodes/locales/it/knxUltimateMultiRouting.html +8 -0
  243. package/nodes/locales/it/knxUltimateRouterFilter.html +8 -0
  244. package/nodes/locales/it/knxUltimateSceneController.html +8 -0
  245. package/nodes/locales/it/knxUltimateStaircase.html +8 -0
  246. package/nodes/locales/it/knxUltimateUtility.html +8 -0
  247. package/nodes/locales/it/knxUltimateUtility.json +34 -0
  248. package/nodes/locales/it/knxUltimateViewer.html +8 -0
  249. package/nodes/locales/it/knxUltimateWatchDog.html +8 -0
  250. package/nodes/locales/it/matter-config.html +8 -0
  251. package/nodes/locales/it/matterbridge-config.html +8 -0
  252. package/nodes/locales/zh-CN/hue-config.html +8 -0
  253. package/nodes/locales/zh-CN/knxUltimate-config.html +8 -0
  254. package/nodes/locales/zh-CN/knxUltimate.html +8 -0
  255. package/nodes/locales/zh-CN/knxUltimateAI.html +8 -0
  256. package/nodes/locales/zh-CN/knxUltimateAIHomeAssistant.html +11 -0
  257. package/nodes/locales/zh-CN/knxUltimateAlerter.html +8 -0
  258. package/nodes/locales/zh-CN/knxUltimateAutoResponder.html +8 -0
  259. package/nodes/locales/zh-CN/knxUltimateDateTime.html +8 -1
  260. package/nodes/locales/zh-CN/knxUltimateGarage.html +8 -0
  261. package/nodes/locales/zh-CN/knxUltimateGlobalContext.html +8 -0
  262. package/nodes/locales/zh-CN/knxUltimateHATranslator.html +8 -0
  263. package/nodes/locales/zh-CN/knxUltimateHueAreaMotion.html +8 -0
  264. package/nodes/locales/zh-CN/knxUltimateHueBattery.html +8 -0
  265. package/nodes/locales/zh-CN/knxUltimateHueButton.html +8 -0
  266. package/nodes/locales/zh-CN/knxUltimateHueCameraMotion.html +8 -0
  267. package/nodes/locales/zh-CN/knxUltimateHueContactSensor.html +8 -0
  268. package/nodes/locales/zh-CN/knxUltimateHueController.html +8 -0
  269. package/nodes/locales/zh-CN/knxUltimateHueHumiditySensor.html +8 -0
  270. package/nodes/locales/zh-CN/knxUltimateHueLight.html +8 -0
  271. package/nodes/locales/zh-CN/knxUltimateHueLightSensor.html +8 -0
  272. package/nodes/locales/zh-CN/knxUltimateHueMotion.html +8 -0
  273. package/nodes/locales/zh-CN/knxUltimateHuePlug.html +8 -0
  274. package/nodes/locales/zh-CN/knxUltimateHueScene.html +8 -0
  275. package/nodes/locales/zh-CN/knxUltimateHueTapDial.html +8 -0
  276. package/nodes/locales/zh-CN/knxUltimateHueTemperatureSensor.html +8 -0
  277. package/nodes/locales/zh-CN/knxUltimateHueZigbeeConnectivity.html +8 -0
  278. package/nodes/locales/zh-CN/knxUltimateHuedevice_software_update.html +8 -0
  279. package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +8 -0
  280. package/nodes/locales/zh-CN/knxUltimateLoadControl.html +8 -0
  281. package/nodes/locales/zh-CN/knxUltimateLogger.html +8 -0
  282. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +8 -0
  283. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +8 -0
  284. package/nodes/locales/zh-CN/knxUltimateMultiRouting.html +8 -0
  285. package/nodes/locales/zh-CN/knxUltimateRouterFilter.html +8 -0
  286. package/nodes/locales/zh-CN/knxUltimateSceneController.html +8 -0
  287. package/nodes/locales/zh-CN/knxUltimateStaircase.html +8 -0
  288. package/nodes/locales/zh-CN/knxUltimateUtility.html +8 -0
  289. package/nodes/locales/zh-CN/knxUltimateUtility.json +34 -0
  290. package/nodes/locales/zh-CN/knxUltimateViewer.html +8 -0
  291. package/nodes/locales/zh-CN/knxUltimateWatchDog.html +8 -0
  292. package/nodes/locales/zh-CN/matter-config.html +8 -0
  293. package/nodes/locales/zh-CN/matterbridge-config.html +8 -0
  294. package/nodes/matter-config.html +6 -0
  295. package/nodes/matterbridge-config.html +6 -0
  296. package/nodes/plugins/knxUltimate-migration-notice-plugin.html +25 -0
  297. package/nodes/plugins/knxUltimateAI-vue/assets/app.js +10 -10
  298. package/nodes/plugins/knxUltimateAI-vue/assets/chunk-purify.es.js +3 -3
  299. package/nodes/plugins/knxUltimateViewer-vue/assets/app.js +2 -2
  300. package/package.json +5 -1
  301. package/resources/flowMigrationBackup.js +1 -1
  302. package/resources/legacyMigrationNotice.js +8 -12
  303. package/resources/upgradeToV8.js +1193 -0
@@ -0,0 +1,1193 @@
1
+ (function (root, factory) {
2
+ const api = factory(root)
3
+ if (typeof module === 'object' && module.exports) module.exports = api
4
+ if (root) root.KNXUltimateUpgradeToV8 = api
5
+ }(typeof globalThis !== 'undefined' ? globalThis : this, function (root) {
6
+ 'use strict'
7
+
8
+ const PACKAGE_NAME = 'node-red-contrib-knx-ultimate'
9
+ const TARGET_VERSION = '8.0.1-beta.0'
10
+ const HUE_PACKAGE = Object.freeze({
11
+ name: 'node-red-contrib-hue-ultimate',
12
+ version: '1.0.2',
13
+ types: Object.freeze(['hueUltimateController', 'hue-ultimate-config'])
14
+ })
15
+ const MATTER_PACKAGE = Object.freeze({
16
+ name: 'node-red-contrib-matter-ultimate',
17
+ version: '1.0.3',
18
+ types: Object.freeze(['matterUltimateController', 'matterUltimateBridge', 'matter-ultimate-config', 'matter-ultimate-bridge-config'])
19
+ })
20
+ const HUE_TYPE_MAP = Object.freeze({
21
+ 'hue-config': 'hue-ultimate-config',
22
+ knxUltimateHueController: 'hueUltimateController',
23
+ knxUltimateHueLight: 'hueUltimateController',
24
+ knxUltimateHuePlug: 'hueUltimateController',
25
+ knxUltimateHueButton: 'hueUltimateController',
26
+ knxUltimateHueTapDial: 'hueUltimateController',
27
+ knxUltimateHueMotion: 'hueUltimateController',
28
+ knxUltimateHueAreaMotion: 'hueUltimateController',
29
+ knxUltimateHueCameraMotion: 'hueUltimateController',
30
+ knxUltimateHueContactSensor: 'hueUltimateController',
31
+ knxUltimateHueLightSensor: 'hueUltimateController',
32
+ knxUltimateHueTemperatureSensor: 'hueUltimateController',
33
+ knxUltimateHueHumiditySensor: 'hueUltimateController',
34
+ knxUltimateHueScene: 'hueUltimateController',
35
+ knxUltimateHueBattery: 'hueUltimateController',
36
+ knxUltimateHueZigbeeConnectivity: 'hueUltimateController',
37
+ knxUltimateHuedevice_software_update: 'hueUltimateController'
38
+ })
39
+ const MATTER_TYPE_MAP = Object.freeze({
40
+ 'matter-config': 'matter-ultimate-config',
41
+ 'matterbridge-config': 'matter-ultimate-bridge-config',
42
+ knxUltimateMatterControllerDevice: 'matterUltimateController',
43
+ knxUltimateMatterBridge: 'matterUltimateBridge',
44
+ // This short-lived beta type is still blocked by the version 8 install
45
+ // guard. Its saved contract is compatible with the standalone Controller.
46
+ knxUltimateMatterLight: 'matterUltimateController'
47
+ })
48
+ const AI_TYPES = Object.freeze(new Set(['knxUltimateAI', 'knxUltimateAIHomeAssistant']))
49
+ const HUE_TARGET_TYPES = Object.freeze(new Set(HUE_PACKAGE.types))
50
+ const MATTER_TARGET_TYPES = Object.freeze(new Set(MATTER_PACKAGE.types))
51
+ const I18N_PREFIX = 'node-red-contrib-knx-ultimate/knxUltimateUtility:knxUltimateUtility.v8_upgrade.'
52
+ const EVENT_NAMESPACE = '.knxUltimateV8Upgrade'
53
+ const installations = new WeakMap()
54
+ const blockedUpgrades = new WeakSet()
55
+
56
+ function translate (RED, key, fallback, values = {}) {
57
+ const qualifiedKey = I18N_PREFIX + key
58
+ let result
59
+ try { result = RED._(qualifiedKey, values) } catch (error) { /* use fallback */ }
60
+ result = result && result !== qualifiedKey ? result : fallback
61
+ return String(result).replace(/{{\s*(\w+)\s*}}/g, (match, name) => (
62
+ values[name] === undefined ? match : String(values[name])
63
+ ))
64
+ }
65
+
66
+ function collectAllNodes (RED) {
67
+ const nodes = new Map()
68
+ const visit = node => {
69
+ if (node && typeof node.id === 'string') nodes.set(node.id, node)
70
+ }
71
+ if (RED && RED.nodes) {
72
+ if (typeof RED.nodes.eachNode === 'function') RED.nodes.eachNode(visit)
73
+ if (typeof RED.nodes.eachConfig === 'function') RED.nodes.eachConfig(visit)
74
+ }
75
+ return Array.from(nodes.values())
76
+ }
77
+
78
+ function assertUnlocked (RED, node) {
79
+ if (!node || !node.z) return
80
+ const workspace = typeof RED.nodes.workspace === 'function' ? RED.nodes.workspace(node.z) : null
81
+ const subflow = typeof RED.nodes.subflow === 'function' ? RED.nodes.subflow(node.z) : null
82
+ if ((workspace && workspace.locked) || (subflow && subflow.locked) ||
83
+ (RED.workspaces && typeof RED.workspaces.isLocked === 'function' && RED.workspaces.isLocked(node.z))) {
84
+ throw new Error('A flow containing nodes that need conversion is locked: ' + node.z)
85
+ }
86
+ }
87
+
88
+ function unknownOriginalValues (node) {
89
+ if (!node || node.type !== 'unknown' || !node._orig) return {}
90
+ const protectedKeys = new Set([
91
+ 'id', 'type', 'z', 'g', 'x', 'y', 'wires', '_def', '_', '_orig',
92
+ 'changed', 'dirty', 'resize', 'valid', 'validationErrors'
93
+ ])
94
+ return Object.keys(node._orig).reduce((values, key) => {
95
+ if (!protectedKeys.has(key)) values[key] = node._orig[key]
96
+ return values
97
+ }, {})
98
+ }
99
+
100
+ function migrationSource (node, sourceType) {
101
+ if (!node || node.type !== 'unknown' || !node._orig) return { ...node, type: sourceType }
102
+ return {
103
+ ...node._orig,
104
+ id: node.id,
105
+ type: sourceType,
106
+ z: node.z,
107
+ g: node.g,
108
+ x: node.x,
109
+ y: node.y,
110
+ wires: node.wires
111
+ }
112
+ }
113
+
114
+ function createMigrationPlan (RED, options = {}) {
115
+ if (!RED || !RED.nodes) throw new Error('The Node-RED editor API is unavailable')
116
+ const environment = options.environment || root || {}
117
+ const utilityApi = options.utilityApi || environment.KNXUltimateUtilityMigration
118
+ const hueApi = options.hueApi || environment.KNXUltimateHueControllerMigration
119
+ if (!utilityApi || !utilityApi.LEGACY_NODE_PROFILES || typeof utilityApi.createLocalMigrationPatches !== 'function') {
120
+ throw new Error('The KNX Utility migration component is unavailable')
121
+ }
122
+ if (!hueApi || !hueApi.LEGACY_NODE_PROFILES || typeof hueApi.createLocalMigrationPatches !== 'function') {
123
+ throw new Error('The HUE migration component is unavailable')
124
+ }
125
+
126
+ const entries = []
127
+ const ai = []
128
+ const missingStandalone = { hue: [], matter: [] }
129
+ const counts = { utility: 0, hue: 0, matter: 0, total: 0 }
130
+ collectAllNodes(RED).forEach(node => {
131
+ const originalType = node.type === 'unknown' && node._orig && node._orig.type
132
+ const savedType = node.type === 'unknown'
133
+ ? (originalType || node.name)
134
+ : node.type
135
+ if (node.type === 'unknown' && HUE_TARGET_TYPES.has(savedType)) {
136
+ missingStandalone.hue.push(node)
137
+ return
138
+ }
139
+ if (node.type === 'unknown' && MATTER_TARGET_TYPES.has(savedType)) {
140
+ missingStandalone.matter.push(node)
141
+ return
142
+ }
143
+ const sourceType = node.type === 'unknown' && originalType
144
+ ? originalType
145
+ : node.type
146
+ const fromUnknown = node.type === 'unknown' && Boolean(originalType)
147
+ if (AI_TYPES.has(savedType)) {
148
+ ai.push(node)
149
+ return
150
+ }
151
+
152
+ if (Object.prototype.hasOwnProperty.call(utilityApi.LEGACY_NODE_PROFILES, sourceType)) {
153
+ assertUnlocked(RED, node)
154
+ const patch = utilityApi.createLocalMigrationPatches([migrationSource(node, sourceType)])[0]
155
+ const values = {
156
+ ...unknownOriginalValues(node),
157
+ utilityType: patch.utilityType,
158
+ inputs: patch.inputs,
159
+ outputs: patch.outputs
160
+ }
161
+ if (patch.haTranslationTable !== undefined) values.haTranslationTable = patch.haTranslationTable
162
+ entries.push({ node, sourceType, targetType: 'knxUltimateUtility', family: 'utility', values, removeOriginal: fromUnknown })
163
+ counts.utility += 1
164
+ return
165
+ }
166
+
167
+ if (Object.prototype.hasOwnProperty.call(HUE_TYPE_MAP, sourceType)) {
168
+ assertUnlocked(RED, node)
169
+ const values = unknownOriginalValues(node)
170
+ if (Object.prototype.hasOwnProperty.call(hueApi.LEGACY_NODE_PROFILES, sourceType)) {
171
+ const patch = hueApi.createLocalMigrationPatches([migrationSource(node, sourceType)])[0]
172
+ values.hueControllerType = patch.hueControllerType
173
+ values.inputs = patch.inputs
174
+ values.outputs = patch.outputs
175
+ }
176
+ entries.push({ node, sourceType, targetType: HUE_TYPE_MAP[sourceType], family: 'hue', values, removeOriginal: fromUnknown })
177
+ counts.hue += 1
178
+ return
179
+ }
180
+
181
+ if (Object.prototype.hasOwnProperty.call(MATTER_TYPE_MAP, sourceType)) {
182
+ assertUnlocked(RED, node)
183
+ const values = unknownOriginalValues(node)
184
+ entries.push({ node, sourceType, targetType: MATTER_TYPE_MAP[sourceType], family: 'matter', values, removeOriginal: fromUnknown })
185
+ counts.matter += 1
186
+ }
187
+ })
188
+ counts.total = entries.length
189
+ return {
190
+ entries,
191
+ ai,
192
+ missingStandalone,
193
+ counts,
194
+ needsHue: counts.hue > 0 || missingStandalone.hue.length > 0,
195
+ needsMatter: counts.matter > 0 || missingStandalone.matter.length > 0
196
+ }
197
+ }
198
+
199
+ function snapshot (node, keys) {
200
+ return keys.reduce((state, key) => {
201
+ state[key] = Object.getOwnPropertyDescriptor(node, key)
202
+ return state
203
+ }, {})
204
+ }
205
+
206
+ function restore (node, state) {
207
+ Object.keys(state).forEach(key => {
208
+ const descriptor = state[key]
209
+ if (key === 'changed' || key === 'moved') {
210
+ // These assignments must pass through Node-RED's node Proxy so its
211
+ // per-workspace dirty tracking is resynchronised after a rollback.
212
+ const currentDescriptor = Object.getOwnPropertyDescriptor(node, key)
213
+ const targetValue = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
214
+ ? descriptor.value
215
+ : undefined
216
+ if (Boolean(currentDescriptor) !== Boolean(descriptor) || node[key] !== targetValue) node[key] = targetValue
217
+ }
218
+ if (descriptor) Object.defineProperty(node, key, descriptor)
219
+ else delete node[key]
220
+ })
221
+ }
222
+
223
+ function snapshotValidationState (RED) {
224
+ const objects = new Set(collectAllNodes(RED))
225
+ const collect = method => {
226
+ if (RED.nodes && typeof RED.nodes[method] === 'function') RED.nodes[method](value => objects.add(value))
227
+ }
228
+ for (const method of ['eachGroup', 'eachJunction', 'eachSubflow', 'eachWorkspace']) collect(method)
229
+ const keys = ['changed', 'moved', 'dirty', 'resize', 'valid', 'validationErrors']
230
+ return Array.from(objects).filter(Boolean).map(value => ({ value, state: snapshot(value, keys) }))
231
+ }
232
+
233
+ function restoreValidationState (states) {
234
+ states.forEach(item => restore(item.value, item.state))
235
+ }
236
+
237
+ function applyMigrationPlan (RED, plan) {
238
+ if (!plan || !Array.isArray(plan.entries)) throw new Error('The version 8 migration plan is invalid')
239
+ if (plan.ai && plan.ai.length) throw new Error('Legacy KNX AI nodes require manual migration')
240
+
241
+ const previousDirty = typeof RED.nodes.dirty === 'function' ? RED.nodes.dirty() : false
242
+ const validationState = snapshotValidationState(RED)
243
+ const prepared = plan.entries.map(entry => {
244
+ const currentType = entry.node && entry.node.type === 'unknown' && entry.node._orig
245
+ ? entry.node._orig.type
246
+ : entry.node && entry.node.type
247
+ if (!entry.node || currentType !== entry.sourceType) throw new Error('A node changed while the version 8 upgrade was running')
248
+ if (typeof RED.nodes.node === 'function' && RED.nodes.node(entry.node.id) !== entry.node) {
249
+ throw new Error('A node changed while the version 8 upgrade was running')
250
+ }
251
+ assertUnlocked(RED, entry.node)
252
+ const definition = RED.nodes.getType(entry.targetType)
253
+ if (!definition) throw new Error('Target node type is not loaded: ' + entry.targetType)
254
+ const values = {
255
+ type: entry.targetType,
256
+ _def: definition,
257
+ _: definition._ || RED._,
258
+ ...entry.values
259
+ }
260
+ const keys = [...Object.keys(values), '_orig', 'changed', 'moved', 'dirty', 'resize', 'valid', 'validationErrors', '_colorChanged', 'w']
261
+ return {
262
+ ...entry,
263
+ values,
264
+ state: snapshot(entry.node, keys)
265
+ }
266
+ })
267
+
268
+ const applied = []
269
+ try {
270
+ prepared.forEach(entry => {
271
+ applied.push(entry)
272
+ Object.assign(entry.node, entry.values, { changed: true, dirty: true, resize: true })
273
+ if (entry.removeOriginal) delete entry.node._orig
274
+ delete entry.node.w
275
+ delete entry.node._colorChanged
276
+ })
277
+ } catch (error) {
278
+ applied.reverse().forEach(entry => restore(entry.node, entry.state))
279
+ restoreValidationState(validationState)
280
+ if (typeof RED.nodes.dirty === 'function') RED.nodes.dirty(previousDirty)
281
+ try {
282
+ if (RED.view && typeof RED.view.redraw === 'function') RED.view.redraw(true)
283
+ } catch (refreshError) { /* preserve the migration error */ }
284
+ throw error
285
+ }
286
+
287
+ try {
288
+ if (typeof RED.nodes.dirty === 'function') RED.nodes.dirty(true)
289
+ } catch (error) {
290
+ prepared.slice().reverse().forEach(entry => restore(entry.node, entry.state))
291
+ restoreValidationState(validationState)
292
+ try {
293
+ if (typeof RED.nodes.dirty === 'function') RED.nodes.dirty(previousDirty)
294
+ } catch (restoreError) { /* preserve the migration error */ }
295
+ throw error
296
+ }
297
+ let active = true
298
+ const result = { ...plan.counts }
299
+ Object.defineProperties(result, {
300
+ commit: {
301
+ value: function () { active = false }
302
+ },
303
+ rollback: {
304
+ value: function () {
305
+ if (!active) return
306
+ active = false
307
+ prepared.slice().reverse().forEach(entry => restore(entry.node, entry.state))
308
+ restoreValidationState(validationState)
309
+ if (typeof RED.nodes.dirty === 'function') RED.nodes.dirty(previousDirty)
310
+ prepared.forEach(entry => {
311
+ try {
312
+ if (RED.events && typeof RED.events.emit === 'function') RED.events.emit('nodes:change', entry.node)
313
+ } catch (error) { /* best-effort editor refresh */ }
314
+ })
315
+ try {
316
+ if (RED.view && typeof RED.view.redraw === 'function') RED.view.redraw(true)
317
+ } catch (error) { /* best-effort editor refresh */ }
318
+ }
319
+ }
320
+ })
321
+ return result
322
+ }
323
+
324
+ function legacyNodes (nodes) {
325
+ const utilityTypes = new Set([
326
+ 'knxUltimateAlerter', 'knxUltimateAutoResponder', 'knxUltimateDateTime', 'knxUltimateWatchDog',
327
+ 'knxUltimateGlobalContext', 'knxUltimateLogger', 'knxUltimateStaircase', 'knxUltimateGarage',
328
+ 'knxUltimateSceneController', 'knxUltimateLoadControl', 'knxUltimateHATranslator'
329
+ ])
330
+ return nodes.filter(node => (
331
+ utilityTypes.has(node.type) || Object.prototype.hasOwnProperty.call(HUE_TYPE_MAP, node.type) ||
332
+ Object.prototype.hasOwnProperty.call(MATTER_TYPE_MAP, node.type) || AI_TYPES.has(node.type)
333
+ ))
334
+ }
335
+
336
+ function assertNoLegacyNodes (RED) {
337
+ const remaining = legacyNodes(collectAllNodes(RED))
338
+ if (remaining.length) throw new Error('Legacy node types remain after conversion: ' + remaining.map(node => node.type).join(', '))
339
+ }
340
+
341
+ function validateDeployable (RED) {
342
+ const invalid = []
343
+ const unknown = []
344
+ collectAllNodes(RED).forEach(node => {
345
+ if (node.type === 'unknown' || (node._def && node._def.category === 'unknown')) {
346
+ unknown.push(node.id)
347
+ return
348
+ }
349
+ if (RED.editor && typeof RED.editor.validateNode === 'function') RED.editor.validateNode(node)
350
+ if (node.valid === false && node.d !== true) invalid.push(node.id)
351
+ })
352
+ if (unknown.length) throw new Error('Unknown nodes must be resolved before the automatic upgrade: ' + unknown.join(', '))
353
+ if (invalid.length) throw new Error('Invalid nodes must be corrected before the automatic upgrade: ' + invalid.join(', '))
354
+ }
355
+
356
+ function validatePreflight (RED, plan) {
357
+ const migrating = new Set((plan && plan.entries ? plan.entries : []).map(entry => entry.node))
358
+ const expectedUnknown = new Set([
359
+ ...((plan && plan.missingStandalone && plan.missingStandalone.hue) || []),
360
+ ...((plan && plan.missingStandalone && plan.missingStandalone.matter) || [])
361
+ ])
362
+ const invalid = []
363
+ const unknown = []
364
+ collectAllNodes(RED).forEach(node => {
365
+ if (migrating.has(node) || expectedUnknown.has(node) || (plan && plan.ai && plan.ai.includes(node))) return
366
+ if (node.type === 'unknown' || (node._def && node._def.category === 'unknown')) {
367
+ unknown.push(node.id)
368
+ return
369
+ }
370
+ if (RED.editor && typeof RED.editor.validateNode === 'function') RED.editor.validateNode(node)
371
+ if (node.valid === false && node.d !== true) invalid.push(node.id)
372
+ })
373
+ if (unknown.length) throw new Error('Unknown nodes must be resolved before the automatic upgrade: ' + unknown.join(', '))
374
+ if (invalid.length) throw new Error('Invalid nodes must be corrected before the automatic upgrade: ' + invalid.join(', '))
375
+ }
376
+
377
+ function requestError (xhr, fallback) {
378
+ const detail = xhr && xhr.responseJSON && xhr.responseJSON.message
379
+ ? xhr.responseJSON.message
380
+ : xhr && xhr.responseText ? xhr.responseText : fallback
381
+ const error = new Error(String(detail || 'Node-RED request failed'))
382
+ if (xhr && xhr.status !== undefined) error.status = xhr.status
383
+ if (xhr && xhr.responseJSON && xhr.responseJSON.code) error.code = xhr.responseJSON.code
384
+ return error
385
+ }
386
+
387
+ function requestJson (options, settings) {
388
+ if (typeof options.request === 'function') return Promise.resolve().then(() => options.request(settings))
389
+ const $ = options.$ || (options.environment && options.environment.jQuery)
390
+ if (!$ || typeof $.ajax !== 'function') return Promise.reject(new Error('The Node-RED HTTP API is unavailable'))
391
+ return new Promise((resolve, reject) => {
392
+ $.ajax(settings)
393
+ .done((data) => resolve(data))
394
+ .fail((xhr) => reject(requestError(xhr, settings.url)))
395
+ })
396
+ }
397
+
398
+ function boundedRequestTimeout (options, key, fallback) {
399
+ const configured = options[key] === undefined ? fallback : options[key]
400
+ const cap = options.singleRequestTimeout
401
+ if (cap === undefined) return configured
402
+ return Math.max(1, Math.min(configured, cap))
403
+ }
404
+
405
+ function hasTypes (RED, types) {
406
+ return types.every(type => Boolean(RED.nodes.getType(type)))
407
+ }
408
+
409
+ function waitForTypes (RED, types, options = {}) {
410
+ if (hasTypes(RED, types)) return Promise.resolve()
411
+ const environment = options.environment || root
412
+ const timeout = options.typeLoadTimeout === undefined ? 30000 : options.typeLoadTimeout
413
+ const interval = options.typeLoadInterval === undefined ? 100 : options.typeLoadInterval
414
+ if (!environment || typeof environment.setTimeout !== 'function') {
415
+ return Promise.reject(new Error('The browser timer API is unavailable'))
416
+ }
417
+ return new Promise((resolve, reject) => {
418
+ const started = Date.now()
419
+ const check = () => {
420
+ if (hasTypes(RED, types)) return resolve()
421
+ if (Date.now() - started >= timeout) return reject(new Error('Installed node types did not load: ' + types.join(', ')))
422
+ environment.setTimeout(check, interval)
423
+ }
424
+ check()
425
+ })
426
+ }
427
+
428
+ function isTransientRequestError (error) {
429
+ return Boolean(error && (error.status === 0 || (error.status >= 500 && error.status <= 599)))
430
+ }
431
+
432
+ function isReconcileableDeployError (error) {
433
+ return isTransientRequestError(error) || Boolean(error && error.status === 409)
434
+ }
435
+
436
+ async function getModuleInfo (descriptor, options) {
437
+ return requestJson(options, {
438
+ url: 'nodes/' + encodeURIComponent(descriptor.name),
439
+ type: 'GET',
440
+ cache: false,
441
+ timeout: boundedRequestTimeout(options, 'moduleInfoRequestTimeout', 10000),
442
+ headers: { Accept: 'application/json' }
443
+ })
444
+ }
445
+
446
+ async function getOptionalModuleInfo (descriptor, options) {
447
+ try {
448
+ return await getModuleInfo(descriptor, options)
449
+ } catch (error) {
450
+ if (error && (error.status === 404 || error.code === 'not_found')) return null
451
+ throw error
452
+ }
453
+ }
454
+
455
+ function moduleIsEnabled (info) {
456
+ return Boolean(info && Array.isArray(info.nodes) && info.nodes.every(nodeSet => nodeSet.enabled !== false))
457
+ }
458
+
459
+ function parseVersion (value) {
460
+ const match = String(value || '').trim().match(/^[~^<>=\s]*v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/)
461
+ if (!match) return null
462
+ return {
463
+ major: Number(match[1]),
464
+ minor: Number(match[2]),
465
+ patch: Number(match[3]),
466
+ prerelease: match[4] || ''
467
+ }
468
+ }
469
+
470
+ function compareVersions (left, right) {
471
+ for (const key of ['major', 'minor', 'patch']) {
472
+ if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1
473
+ }
474
+ if (left.prerelease === right.prerelease) return 0
475
+ if (!left.prerelease) return 1
476
+ if (!right.prerelease) return -1
477
+ return left.prerelease.localeCompare(right.prerelease)
478
+ }
479
+
480
+ function isCompatibleVersion (value, targetValue) {
481
+ const current = parseVersion(value)
482
+ const target = parseVersion(targetValue)
483
+ return Boolean(current && target && current.major === target.major && compareVersions(current, target) >= 0)
484
+ }
485
+
486
+ function assertModuleUsable (info, descriptor) {
487
+ const failed = info && Array.isArray(info.nodes)
488
+ ? info.nodes.find(nodeSet => nodeSet.err || nodeSet.runtime === false)
489
+ : null
490
+ if (failed) throw new Error('The installed package could not start: ' + descriptor.name + (failed.err ? ' (' + failed.err + ')' : ''))
491
+ }
492
+
493
+ async function enableModule (descriptor, options) {
494
+ return requestJson(options, {
495
+ url: 'nodes/' + encodeURIComponent(descriptor.name),
496
+ type: 'PUT',
497
+ data: JSON.stringify({ enabled: true }),
498
+ timeout: boundedRequestTimeout(options, 'moduleEnableRequestTimeout', 30000),
499
+ contentType: 'application/json; charset=utf-8'
500
+ })
501
+ }
502
+
503
+ async function requestModuleInstall (descriptor, options) {
504
+ return requestJson(options, {
505
+ url: 'nodes',
506
+ type: 'POST',
507
+ data: JSON.stringify({ module: descriptor.name, version: descriptor.version }),
508
+ timeout: boundedRequestTimeout(options, 'moduleInstallRequestTimeout', 120000),
509
+ contentType: 'application/json; charset=utf-8'
510
+ })
511
+ }
512
+
513
+ async function waitForModuleVersion (descriptor, options) {
514
+ const environment = options.environment || root
515
+ const timeout = options.moduleLoadTimeout === undefined ? 30000 : options.moduleLoadTimeout
516
+ const interval = options.moduleLoadInterval === undefined ? 500 : options.moduleLoadInterval
517
+ const started = Date.now()
518
+ do {
519
+ try {
520
+ const remaining = Math.max(1, timeout - (Date.now() - started))
521
+ const info = await getOptionalModuleInfo(descriptor, { ...options, singleRequestTimeout: remaining })
522
+ if (info && (info.pending_version || isCompatibleVersion(info.version, descriptor.version))) return info
523
+ } catch (error) {
524
+ if (error && (error.status === 401 || error.status === 403)) throw error
525
+ }
526
+ if (Date.now() - started >= timeout) break
527
+ if (!environment || typeof environment.setTimeout !== 'function') break
528
+ await new Promise(resolve => environment.setTimeout(resolve, interval))
529
+ } while (Date.now() - started < timeout)
530
+ return null
531
+ }
532
+
533
+ function standaloneRestartError (descriptors) {
534
+ const names = descriptors.map(descriptor => descriptor.name).join(', ')
535
+ const error = new Error('Updated standalone package' + (descriptors.length === 1 ? '' : 's') +
536
+ ' ' + names + '. Restart the Node-RED service, reload the editor, then press the upgrade button again. No flows were changed.')
537
+ error.code = 'standalone_restart_required'
538
+ return error
539
+ }
540
+
541
+ function moduleInstallUncertainError (cause, descriptor) {
542
+ const error = new Error('The package operation for ' + descriptor.name +
543
+ ' may still be running. Restart the Node-RED service, reload the editor and press the upgrade button again. Do not deploy flows before restarting.')
544
+ error.code = 'module_install_uncertain'
545
+ error.moduleInstallUncertain = true
546
+ if (cause && cause.status !== undefined) error.status = cause.status
547
+ return error
548
+ }
549
+
550
+ async function ensureModule (RED, descriptor, options) {
551
+ let info = await getOptionalModuleInfo(descriptor, options)
552
+ if (info) {
553
+ if (info.pending_version) {
554
+ return { installed: false, updated: false, restartRequired: true, info }
555
+ }
556
+ if (!isCompatibleVersion(info.version, descriptor.version)) {
557
+ const current = parseVersion(info.version)
558
+ const target = parseVersion(descriptor.version)
559
+ if (!current || !target || compareVersions(current, target) >= 0) {
560
+ throw new Error('Package ' + descriptor.name + ' has an unsupported version: ' + String(info.version || 'unknown'))
561
+ }
562
+ try {
563
+ info = await requestModuleInstall(descriptor, options)
564
+ } catch (error) {
565
+ if (!isTransientRequestError(error)) throw error
566
+ info = await waitForModuleVersion(descriptor, options)
567
+ if (!info) throw moduleInstallUncertainError(error, descriptor)
568
+ }
569
+ return { installed: false, updated: true, restartRequired: true, info }
570
+ }
571
+ if (!moduleIsEnabled(info)) info = await enableModule(descriptor, options)
572
+ assertModuleUsable(info, descriptor)
573
+ if (!hasTypes(RED, descriptor.types)) {
574
+ try {
575
+ await waitForTypes(RED, descriptor.types, options)
576
+ } catch (error) {
577
+ throw moduleInstallUncertainError(error, descriptor)
578
+ }
579
+ }
580
+ return { installed: false, updated: false, restartRequired: false, info }
581
+ }
582
+ try {
583
+ info = await requestModuleInstall(descriptor, options)
584
+ } catch (error) {
585
+ if (error.code === 'module_already_loaded' || /already loaded/i.test(error.message)) {
586
+ info = await getOptionalModuleInfo(descriptor, options)
587
+ if (!info) throw error
588
+ return ensureModule(RED, descriptor, options)
589
+ } else if (!isTransientRequestError(error)) {
590
+ throw error
591
+ } else {
592
+ info = await waitForModuleVersion(descriptor, options)
593
+ if (!info) throw moduleInstallUncertainError(error, descriptor)
594
+ }
595
+ }
596
+ if (!info) info = await waitForModuleVersion(descriptor, options)
597
+ if (!info) throw moduleInstallUncertainError(null, descriptor)
598
+ if (info.pending_version) {
599
+ return { installed: false, updated: true, restartRequired: true, info }
600
+ }
601
+ if (info.version && !isCompatibleVersion(info.version, descriptor.version)) {
602
+ const current = parseVersion(info.version)
603
+ const target = parseVersion(descriptor.version)
604
+ if (current && target && compareVersions(current, target) < 0) {
605
+ throw moduleInstallUncertainError(null, descriptor)
606
+ }
607
+ throw new Error('Package ' + descriptor.name + ' has an unsupported version: ' + String(info.version))
608
+ }
609
+ if (Array.isArray(info.nodes) && !moduleIsEnabled(info)) info = await enableModule(descriptor, options)
610
+ assertModuleUsable(info, descriptor)
611
+ try {
612
+ await waitForTypes(RED, descriptor.types, options)
613
+ } catch (error) {
614
+ throw moduleInstallUncertainError(error, descriptor)
615
+ }
616
+ return { installed: true, updated: false, restartRequired: false, info }
617
+ }
618
+
619
+ function clearEditorStateAfterDeploy (RED, flows, response) {
620
+ if (typeof RED.nodes.dirty === 'function') RED.nodes.dirty(false)
621
+ if (typeof RED.nodes.version === 'function' && response && response.rev !== undefined) RED.nodes.version(response.rev)
622
+ if (typeof RED.nodes.originalFlow === 'function') RED.nodes.originalFlow(flows)
623
+ const flowsToLock = new Set()
624
+ const ensureUnlocked = id => {
625
+ const flow = id && ((typeof RED.nodes.workspace === 'function' && RED.nodes.workspace(id)) ||
626
+ (typeof RED.nodes.subflow === 'function' && RED.nodes.subflow(id)))
627
+ if (flow && flow.locked) {
628
+ flow.locked = false
629
+ flowsToLock.add(flow)
630
+ }
631
+ }
632
+ try {
633
+ const cleanCanvasNode = node => {
634
+ ensureUnlocked(node.z)
635
+ if (node.changed) { node.dirty = true; node.changed = false }
636
+ if (node.moved) { node.dirty = true; node.moved = false }
637
+ }
638
+ if (typeof RED.nodes.eachNode === 'function') {
639
+ RED.nodes.eachNode(node => {
640
+ cleanCanvasNode(node)
641
+ if (node.credentials) delete node.credentials
642
+ })
643
+ }
644
+ if (typeof RED.nodes.eachGroup === 'function') RED.nodes.eachGroup(cleanCanvasNode)
645
+ if (typeof RED.nodes.eachJunction === 'function') RED.nodes.eachJunction(cleanCanvasNode)
646
+ if (typeof RED.nodes.eachConfig === 'function') {
647
+ RED.nodes.eachConfig(node => {
648
+ if (node.z) ensureUnlocked(node.z)
649
+ node.changed = false
650
+ if (node.credentials) delete node.credentials
651
+ })
652
+ }
653
+ if (typeof RED.nodes.eachSubflow === 'function') RED.nodes.eachSubflow(subflow => { subflow.changed = false })
654
+ if (typeof RED.nodes.eachWorkspace === 'function') {
655
+ RED.nodes.eachWorkspace(workspace => {
656
+ if (workspace.changed || workspace.added) {
657
+ ensureUnlocked(workspace.z)
658
+ workspace.changed = false
659
+ delete workspace.added
660
+ if (RED.events && typeof RED.events.emit === 'function') RED.events.emit('flows:change', workspace)
661
+ }
662
+ })
663
+ }
664
+ } finally {
665
+ flowsToLock.forEach(flow => { flow.locked = true })
666
+ }
667
+ if (RED.history && typeof RED.history.markAllDirty === 'function') RED.history.markAllDirty()
668
+ if (RED.view && typeof RED.view.redraw === 'function') RED.view.redraw()
669
+ if (RED.events && typeof RED.events.emit === 'function') RED.events.emit('deploy')
670
+ }
671
+
672
+ function stableFlowSignature (flows) {
673
+ function normalize (value) {
674
+ if (Array.isArray(value)) return value.map(normalize)
675
+ if (!value || typeof value !== 'object') return value
676
+ const result = {}
677
+ Object.keys(value).sort().forEach(key => {
678
+ if (key !== 'credentials') result[key] = normalize(value[key])
679
+ })
680
+ return result
681
+ }
682
+ return JSON.stringify((flows || []).map(normalize))
683
+ }
684
+
685
+ function flowsContainCredentials (flows) {
686
+ return (flows || []).some(node => node && Object.prototype.hasOwnProperty.call(node, 'credentials'))
687
+ }
688
+
689
+ function uncertainPersistenceError (cause, message) {
690
+ const error = new Error(message)
691
+ error.code = 'flow_persistence_uncertain'
692
+ error.persistenceUncertain = true
693
+ if (cause && cause.status !== undefined) error.status = cause.status
694
+ return error
695
+ }
696
+
697
+ function flowConflictError (cause) {
698
+ const error = new Error('Another editor changed the flow revision during the upgrade. The automatic conversion was rolled back locally. Review or export any pre-existing local changes, then reload the editor before trying again.')
699
+ error.code = 'flow_revision_conflict'
700
+ error.flowConflict = true
701
+ if (cause && cause.status !== undefined) error.status = cause.status
702
+ return error
703
+ }
704
+
705
+ async function waitForPersistedFlows (options, plan, expectedFlows, cause) {
706
+ const conflict = Boolean(cause && cause.status === 409)
707
+ const containsCredentials = flowsContainCredentials(expectedFlows)
708
+ if (!conflict && containsCredentials) {
709
+ throw uncertainPersistenceError(cause,
710
+ 'The flow deploy connection was interrupted while credentials were included, so the saved state cannot be verified safely. Reload the editor before trying again; no automatic rollback was applied.')
711
+ }
712
+ const environment = options.environment || root
713
+ const timeout = conflict
714
+ ? 0
715
+ : (options.flowPersistTimeout === undefined ? 30000 : options.flowPersistTimeout)
716
+ const interval = options.flowPersistInterval === undefined ? 500 : options.flowPersistInterval
717
+ const started = Date.now()
718
+ do {
719
+ try {
720
+ const remaining = Math.max(1, timeout - (Date.now() - started))
721
+ const verifyOptions = timeout === 0 ? options : { ...options, singleRequestTimeout: remaining }
722
+ const savedFlows = await verifySavedFlows(verifyOptions, plan)
723
+ if (!containsCredentials && stableFlowSignature(savedFlows) === stableFlowSignature(expectedFlows)) return savedFlows
724
+ } catch (error) {
725
+ // A transient POST can complete after its client connection closes. Until
726
+ // the timeout, an old flow or a temporary GET failure is not conclusive.
727
+ }
728
+ if (Date.now() - started >= timeout) break
729
+ if (!environment || typeof environment.setTimeout !== 'function') break
730
+ await new Promise(resolve => environment.setTimeout(resolve, interval))
731
+ } while (Date.now() - started < timeout)
732
+
733
+ if (conflict) throw flowConflictError(cause)
734
+ throw uncertainPersistenceError(cause,
735
+ 'The flow deploy response was interrupted and the saved state could not be verified. Reload the editor before trying again; no automatic rollback was applied.')
736
+ }
737
+
738
+ async function deployFlows (RED, options) {
739
+ const flows = RED.nodes.createCompleteNodeSet()
740
+ if (!Array.isArray(flows)) throw new Error('The Node-RED flow export API returned invalid data')
741
+ const payload = { flows }
742
+ if (typeof RED.nodes.version === 'function') payload.rev = RED.nodes.version()
743
+ let response
744
+ if (RED.deploy && typeof RED.deploy.setDeployInflight === 'function') RED.deploy.setDeployInflight(true)
745
+ try {
746
+ try {
747
+ response = await requestJson(options, {
748
+ url: 'flows',
749
+ type: 'POST',
750
+ data: JSON.stringify(payload),
751
+ timeout: boundedRequestTimeout(options, 'deployRequestTimeout', 60000),
752
+ contentType: 'application/json; charset=utf-8',
753
+ headers: {
754
+ 'Node-RED-API-Version': 'v2',
755
+ 'Node-RED-Deployment-Type': 'full'
756
+ }
757
+ })
758
+ } catch (error) {
759
+ if (!isReconcileableDeployError(error)) throw error
760
+ const savedFlows = await waitForPersistedFlows(options, options.plan, flows, error)
761
+ response = { rev: savedFlows.revision }
762
+ }
763
+ if (typeof options.onPersisted === 'function') options.onPersisted(response, flows)
764
+ try {
765
+ clearEditorStateAfterDeploy(RED, flows, response)
766
+ } catch (error) {
767
+ const logger = options.environment && options.environment.console
768
+ if (logger && typeof logger.warn === 'function') logger.warn('KNX Ultimate v8 upgrade editor cleanup failed', error)
769
+ }
770
+ } finally {
771
+ if (RED.deploy && typeof RED.deploy.setDeployInflight === 'function') RED.deploy.setDeployInflight(false)
772
+ }
773
+ return response
774
+ }
775
+
776
+ async function verifySavedFlows (options, plan) {
777
+ const response = await requestJson(options, {
778
+ url: 'flows',
779
+ type: 'GET',
780
+ cache: false,
781
+ timeout: boundedRequestTimeout(options, 'flowRequestTimeout', 10000),
782
+ headers: { 'Node-RED-API-Version': 'v2' }
783
+ })
784
+ const flows = Array.isArray(response) ? response : response && response.flows
785
+ if (!Array.isArray(flows)) throw new Error('Unable to verify the deployed flow')
786
+ const remaining = legacyNodes(flows)
787
+ if (remaining.length) throw new Error('The deployed flow still contains legacy nodes: ' + remaining.map(node => node.type).join(', '))
788
+ if (plan && Array.isArray(plan.entries)) {
789
+ const savedById = new Map(flows.map(node => [node.id, node]))
790
+ const mismatch = plan.entries.find(entry => !savedById.has(entry.node.id) || savedById.get(entry.node.id).type !== entry.targetType)
791
+ if (mismatch) throw new Error('The deployed flow could not be verified for node: ' + mismatch.node.id)
792
+ }
793
+ Object.defineProperty(flows, 'revision', { value: response && response.rev })
794
+ return flows
795
+ }
796
+
797
+ async function installKnxV8 (options) {
798
+ const descriptor = { name: PACKAGE_NAME, version: TARGET_VERSION }
799
+ const current = await getModuleInfo(descriptor, options)
800
+ if (current && current.pending_version) {
801
+ if (isCompatibleVersion(current.pending_version, TARGET_VERSION)) return current
802
+ throw moduleInstallUncertainError(null, descriptor)
803
+ }
804
+ if (current && isCompatibleVersion(current.version, TARGET_VERSION)) return current
805
+ const currentVersion = current && parseVersion(current.version)
806
+ const targetVersion = parseVersion(TARGET_VERSION)
807
+ if (currentVersion && targetVersion && compareVersions(currentVersion, targetVersion) > 0) {
808
+ throw new Error('A newer incompatible KNX Ultimate version is already installed: ' + current.version)
809
+ }
810
+ try {
811
+ const result = await requestModuleInstall(descriptor, options)
812
+ if (result && result.pending_version) {
813
+ if (!isCompatibleVersion(result.pending_version, TARGET_VERSION)) {
814
+ throw moduleInstallUncertainError(null, descriptor)
815
+ }
816
+ } else if (result && result.version && !isCompatibleVersion(result.version, TARGET_VERSION)) {
817
+ throw moduleInstallUncertainError(null, descriptor)
818
+ }
819
+ return result
820
+ } catch (error) {
821
+ if (!isTransientRequestError(error)) throw error
822
+ // npm may keep running after a proxy timeout. Poll the registry instead
823
+ // of starting a second package update over the first one.
824
+ const info = await waitForModuleVersion(descriptor, options)
825
+ if (info && info.pending_version) {
826
+ if (isCompatibleVersion(info.pending_version, TARGET_VERSION)) return info
827
+ throw moduleInstallUncertainError(error, descriptor)
828
+ }
829
+ if (info && isCompatibleVersion(info.version, TARGET_VERSION)) return info
830
+ throw moduleInstallUncertainError(error, descriptor)
831
+ }
832
+ }
833
+
834
+ function disposeStandaloneMigrationPrompts (environment) {
835
+ for (const name of ['HueUltimateMigration', 'MatterUltimateMigration']) {
836
+ const migration = environment && environment[name]
837
+ try {
838
+ if (migration && typeof migration.dispose === 'function') migration.dispose()
839
+ } catch (error) { /* the upgrade owns the editor while it is running */ }
840
+ }
841
+ }
842
+
843
+ async function runUpgrade (RED, plan, options = {}) {
844
+ const environment = options.environment || root || {}
845
+ const progress = typeof options.onProgress === 'function' ? options.onProgress : function () {}
846
+ const replanAfterInstall = Boolean(plan.missingStandalone &&
847
+ (((plan.missingStandalone.hue || []).length) || ((plan.missingStandalone.matter || []).length)))
848
+ const restartDescriptors = []
849
+ disposeStandaloneMigrationPrompts(environment)
850
+ if (plan.needsHue) {
851
+ progress('install_hue')
852
+ const result = await ensureModule(RED, HUE_PACKAGE, { ...options, environment })
853
+ if (result.restartRequired) restartDescriptors.push(HUE_PACKAGE)
854
+ disposeStandaloneMigrationPrompts(environment)
855
+ }
856
+ if (plan.needsMatter) {
857
+ progress('install_matter')
858
+ let result
859
+ try {
860
+ result = await ensureModule(RED, MATTER_PACKAGE, { ...options, environment })
861
+ } catch (error) {
862
+ if (restartDescriptors.length) error.restartRequired = true
863
+ throw error
864
+ }
865
+ if (result.restartRequired) restartDescriptors.push(MATTER_PACKAGE)
866
+ disposeStandaloneMigrationPrompts(environment)
867
+ }
868
+ if (restartDescriptors.length) throw standaloneRestartError(restartDescriptors)
869
+ if (replanAfterInstall) {
870
+ plan = createMigrationPlan(RED, { ...options, environment })
871
+ if (plan.ai.length) throw new Error('Legacy KNX AI nodes require manual migration')
872
+ validatePreflight(RED, plan)
873
+ }
874
+ progress('convert')
875
+ const counts = applyMigrationPlan(RED, plan)
876
+ let deployed = false
877
+ try {
878
+ assertNoLegacyNodes(RED)
879
+ validateDeployable(RED)
880
+ progress('deploy')
881
+ await deployFlows(RED, {
882
+ ...options,
883
+ environment,
884
+ plan,
885
+ onPersisted: function () {
886
+ deployed = true
887
+ if (typeof counts.commit === 'function') counts.commit()
888
+ }
889
+ })
890
+ } catch (error) {
891
+ if (!deployed && !error.persistenceUncertain && typeof counts.rollback === 'function') counts.rollback()
892
+ if (error.flowConflict && typeof RED.nodes.dirty === 'function') RED.nodes.dirty(true)
893
+ throw error
894
+ }
895
+ try {
896
+ progress('verify')
897
+ await verifySavedFlows({ ...options, environment }, plan)
898
+ progress('install_v8')
899
+ await installKnxV8({ ...options, environment })
900
+ } catch (error) {
901
+ error.flowsPersisted = true
902
+ throw error
903
+ }
904
+ progress('complete')
905
+ return counts
906
+ }
907
+
908
+ function closeEditor (RED, $) {
909
+ try {
910
+ if (RED.actions && typeof RED.actions.invoke === 'function') {
911
+ RED.actions.invoke('core:cancel-edit-tray')
912
+ return
913
+ }
914
+ } catch (error) { /* use the button fallback */ }
915
+ if ($) $('#node-dialog-cancel:visible, #node-config-dialog-cancel:visible').first().trigger('click')
916
+ }
917
+
918
+ function lockEditorDeploy (RED, $) {
919
+ let active = true
920
+ let keyboardLocked = false
921
+ const buttonStates = []
922
+ let originalActionInvoke
923
+ let guardedActionInvoke
924
+ const blockDeployClick = event => {
925
+ event.preventDefault()
926
+ event.stopImmediatePropagation()
927
+ }
928
+ try {
929
+ blockedUpgrades.add(RED)
930
+ if (RED.keyboard && typeof RED.keyboard.disable === 'function') {
931
+ RED.keyboard.disable()
932
+ keyboardLocked = true
933
+ }
934
+ if (RED.actions && typeof RED.actions.invoke === 'function') {
935
+ originalActionInvoke = RED.actions.invoke
936
+ guardedActionInvoke = function (actionName, ...args) {
937
+ if (actionName === 'knxUltimate:upgrade-to-v8' ||
938
+ /^core:(?:deploy|restart-flows|start-flows|stop-flows)/.test(String(actionName))) return
939
+ return originalActionInvoke.apply(this, [actionName, ...args])
940
+ }
941
+ RED.actions.invoke = guardedActionInvoke
942
+ }
943
+ if ($) {
944
+ const deployButtons = $('#red-ui-header-button-deploy, #red-ui-header-button-deploy-options')
945
+ if (deployButtons && deployButtons.length) {
946
+ deployButtons.each(function () {
947
+ const button = $(this)
948
+ const state = {
949
+ button,
950
+ element: this,
951
+ isMain: this.id === 'red-ui-header-button-deploy',
952
+ wasDisabled: button.hasClass('disabled'),
953
+ ariaDisabled: button.attr('aria-disabled'),
954
+ pointerEvents: button.css('pointer-events')
955
+ }
956
+ buttonStates.push(state)
957
+ button.addClass('disabled').attr('aria-disabled', 'true').css('pointer-events', 'none')
958
+ if (typeof this.addEventListener === 'function') this.addEventListener('click', blockDeployClick, true)
959
+ })
960
+ }
961
+ }
962
+ } catch (error) {
963
+ buttonStates.forEach(state => {
964
+ if (state.element && typeof state.element.removeEventListener === 'function') {
965
+ state.element.removeEventListener('click', blockDeployClick, true)
966
+ }
967
+ if (state.button && state.button.length) {
968
+ state.button.css('pointer-events', state.pointerEvents || '')
969
+ if (!state.wasDisabled) state.button.removeClass('disabled')
970
+ if (state.ariaDisabled === undefined) state.button.removeAttr('aria-disabled')
971
+ else state.button.attr('aria-disabled', state.ariaDisabled)
972
+ }
973
+ })
974
+ blockedUpgrades.delete(RED)
975
+ if (guardedActionInvoke && RED.actions && RED.actions.invoke === guardedActionInvoke) RED.actions.invoke = originalActionInvoke
976
+ if (keyboardLocked && RED.keyboard && typeof RED.keyboard.enable === 'function') RED.keyboard.enable()
977
+ throw error
978
+ }
979
+ return function unlockEditorDeploy (unlockOptions = {}) {
980
+ if (!active) return
981
+ active = false
982
+ if (unlockOptions.keepDeployBlocked) return
983
+ blockedUpgrades.delete(RED)
984
+ const dirty = RED.nodes && typeof RED.nodes.dirty === 'function' ? RED.nodes.dirty() : true
985
+ buttonStates.forEach(state => {
986
+ if (state.element && typeof state.element.removeEventListener === 'function') {
987
+ state.element.removeEventListener('click', blockDeployClick, true)
988
+ }
989
+ if (state.button && state.button.length) {
990
+ state.button.css('pointer-events', state.pointerEvents || '')
991
+ const shouldDisable = state.wasDisabled || (state.isMain && dirty === false)
992
+ if (shouldDisable) {
993
+ state.button.addClass('disabled').attr('aria-disabled', 'true')
994
+ } else {
995
+ state.button.removeClass('disabled')
996
+ if (state.ariaDisabled === undefined) state.button.removeAttr('aria-disabled')
997
+ else state.button.attr('aria-disabled', state.ariaDisabled)
998
+ }
999
+ }
1000
+ })
1001
+ if (guardedActionInvoke && RED.actions && RED.actions.invoke === guardedActionInvoke) RED.actions.invoke = originalActionInvoke
1002
+ if (keyboardLocked && RED.keyboard && typeof RED.keyboard.enable === 'function') RED.keyboard.enable()
1003
+ }
1004
+ }
1005
+
1006
+ function makeMessage ($, title, paragraphs) {
1007
+ const message = $('<div></div>')
1008
+ $('<p></p>').append($('<strong></strong>').text(title)).appendTo(message)
1009
+ paragraphs.forEach(paragraph => $('<p></p>').text(paragraph).appendTo(message))
1010
+ return message
1011
+ }
1012
+
1013
+ function open (RED, options = {}) {
1014
+ const environment = options.environment || root || {}
1015
+ const $ = options.$ || environment.jQuery
1016
+ if (!$ || typeof RED.notify !== 'function') throw new Error('The Node-RED editor notification API is unavailable')
1017
+ const t = (key, fallback, values) => translate(RED, key, fallback, values)
1018
+ if (blockedUpgrades.has(RED)) {
1019
+ return RED.notify(t('upgrade_locked', 'An upgrade is already running or Node-RED must be restarted/reloaded before it can be tried again.'), { type: 'warning', fixed: true })
1020
+ }
1021
+ let plan
1022
+ try {
1023
+ plan = createMigrationPlan(RED, { ...options, environment })
1024
+ } catch (error) {
1025
+ RED.notify(t('preflight_failed', 'Upgrade preflight failed:') + ' ' + error.message, { type: 'error', fixed: true })
1026
+ return
1027
+ }
1028
+
1029
+ if (plan.ai.length) {
1030
+ const message = makeMessage($, t('title', 'Upgrade to KNX Ultimate 8'), [
1031
+ t('ai_blocked', 'Found {{count}} legacy KNX AI nodes. The automatic upgrade stopped before making changes. Replace or remove those nodes first; their configuration cannot be converted safely to Cerebrum Ultimate.', { count: plan.ai.length })
1032
+ ])
1033
+ return RED.notify(message, { type: 'error', fixed: true, modal: true })
1034
+ }
1035
+ try {
1036
+ validatePreflight(RED, plan)
1037
+ } catch (error) {
1038
+ RED.notify(t('preflight_failed', 'Upgrade preflight failed:') + ' ' + error.message, { type: 'error', fixed: true })
1039
+ return
1040
+ }
1041
+ const needsModuleInspection = plan.needsHue || plan.needsMatter
1042
+ if (RED.user && typeof RED.user.hasPermission === 'function' &&
1043
+ (!RED.user.hasPermission('flows.read') || !RED.user.hasPermission('flows.write') ||
1044
+ (needsModuleInspection && !RED.user.hasPermission('nodes.read')))) {
1045
+ return RED.notify(t('permission_denied', 'Your Node-RED account cannot install nodes or deploy flows.'), { type: 'error', fixed: true })
1046
+ }
1047
+
1048
+ const values = plan.counts
1049
+ const message = makeMessage($, t('title', 'Upgrade to KNX Ultimate 8'), [
1050
+ t('summary', 'Ready to upgrade. Nodes/configurations to convert: KNX Utility {{utility}}, HUE {{hue}}, Matter {{matter}}.', values),
1051
+ t('confirm', 'A JSON backup of all flows will download first. The required HUE/Matter packages will then be installed, all current editor changes deployed, and KNX Ultimate 8 installed. IDs, wiring, configuration references, credentials and Matter storage are preserved. The current node editor will close and discard edits not yet saved in this form.', values),
1052
+ t('restart_note', 'When the process finishes, you must restart the Node-RED service and reload the editor.', values)
1053
+ ])
1054
+ let notification
1055
+ let running = false
1056
+ notification = RED.notify(message, {
1057
+ type: 'warning',
1058
+ fixed: true,
1059
+ modal: true,
1060
+ buttons: [
1061
+ { text: t('cancel', 'Cancel'), click: () => notification.close() },
1062
+ {
1063
+ text: t('start', 'Back up and upgrade'),
1064
+ class: 'primary',
1065
+ click: function () {
1066
+ if (running) return
1067
+ running = true
1068
+ try {
1069
+ const backupApi = options.backupApi || environment.KNXUltimateFlowMigrationBackup
1070
+ if (!backupApi || typeof backupApi.download !== 'function') throw new Error('The flow backup tool is unavailable')
1071
+ backupApi.download(RED, { environment, documentObject: environment.document, kind: 'v8' })
1072
+ } catch (error) {
1073
+ running = false
1074
+ RED.notify(t('backup_failed', 'Flow backup could not be started. Nothing was changed:') + ' ' + error.message, { type: 'error', fixed: true })
1075
+ return
1076
+ }
1077
+ notification.close()
1078
+ closeEditor(RED, $)
1079
+ let unlockEditor
1080
+ let progressNotice
1081
+ let progressText
1082
+ try {
1083
+ unlockEditor = lockEditorDeploy(RED, $)
1084
+ progressText = $('<span></span>').text(t('progress_start', 'Preparing the automatic upgrade…'))
1085
+ const progressMessage = $('<p></p>').append($('<i class="fa fa-circle-notch fa-spin" style="margin-right:8px;"></i>')).append(progressText)
1086
+ progressNotice = RED.notify(progressMessage, { type: 'info', fixed: true, modal: true })
1087
+ } catch (error) {
1088
+ if (unlockEditor) unlockEditor()
1089
+ RED.notify(t('failed', 'Automatic upgrade stopped safely. KNX Ultimate 8 was not activated:') + ' ' + error.message, { type: 'error', fixed: true })
1090
+ return
1091
+ }
1092
+ const onProgress = stage => progressText.text(t('progress_' + stage, stage))
1093
+ let keepDeployBlocked = false
1094
+ Promise.resolve().then(() => runUpgrade(RED, plan, { ...options, environment, $, onProgress }))
1095
+ .then(counts => {
1096
+ keepDeployBlocked = true
1097
+ if (progressNotice && typeof progressNotice.close === 'function') progressNotice.close()
1098
+ const done = makeMessage($, t('complete_title', 'KNX Ultimate 8 installed'), [
1099
+ t('complete', 'Converted {{total}} nodes/configurations and installed KNX Ultimate 8. Restart the Node-RED service now, then reload this page. Do not edit or deploy flows before restarting.', counts)
1100
+ ])
1101
+ let doneNotice
1102
+ doneNotice = RED.notify(done, {
1103
+ type: 'success',
1104
+ fixed: true,
1105
+ modal: true,
1106
+ buttons: [{ text: t('close', 'Close'), click: () => doneNotice.close() }]
1107
+ })
1108
+ })
1109
+ .catch(error => {
1110
+ if (progressNotice && typeof progressNotice.close === 'function') progressNotice.close()
1111
+ let failureMessage
1112
+ if (error.code === 'standalone_restart_required') {
1113
+ keepDeployBlocked = true
1114
+ failureMessage = t('standalone_restart_required', 'A HUE/Matter package update is ready. Restart the Node-RED service, reload the editor, then press the upgrade button again. No flows were changed.')
1115
+ } else if (error.moduleInstallUncertain) {
1116
+ keepDeployBlocked = true
1117
+ failureMessage = t('module_install_uncertain', 'A package operation may still be running. Restart the Node-RED service, reload the editor and press the upgrade button again. Do not deploy flows before restarting.')
1118
+ } else if (error.restartRequired) {
1119
+ keepDeployBlocked = true
1120
+ failureMessage = t('standalone_restart_required', 'A HUE/Matter package update is ready. Restart the Node-RED service, reload the editor, then press the upgrade button again. No flows were changed.') + ' ' + error.message
1121
+ } else if (error.flowConflict) {
1122
+ keepDeployBlocked = true
1123
+ failureMessage = t('flow_conflict', 'Another editor changed the flow revision. The automatic conversion was rolled back locally. Review or export any pre-existing local changes, then reload the editor before trying again.')
1124
+ } else if (error.persistenceUncertain) {
1125
+ keepDeployBlocked = true
1126
+ failureMessage = t('persistence_uncertain', 'Node-RED did not confirm the flow save. No automatic rollback was applied. Reload the editor and review the saved flows before trying again.')
1127
+ } else if (error.flowsPersisted) {
1128
+ keepDeployBlocked = true
1129
+ failureMessage = t('flows_persisted', 'The converted flows were saved, but KNX Ultimate 8 was not installed. Do not deploy more changes. Resolve the reported error, reload the editor and press the upgrade button again:') + ' ' + error.message
1130
+ } else {
1131
+ failureMessage = t('failed', 'Automatic upgrade stopped safely. KNX Ultimate 8 was not activated:') + ' ' + error.message
1132
+ }
1133
+ RED.notify(failureMessage, { type: 'error', fixed: true })
1134
+ })
1135
+ .finally(() => unlockEditor({ keepDeployBlocked }))
1136
+ }
1137
+ }
1138
+ ]
1139
+ })
1140
+ return notification
1141
+ }
1142
+
1143
+ function install (RED, options = {}) {
1144
+ if (installations.has(RED)) return installations.get(RED)
1145
+ const environment = options.environment || root || {}
1146
+ const $ = options.$ || environment.jQuery
1147
+ if (!$ || !environment.document) throw new Error('The Node-RED editor DOM API is unavailable')
1148
+ const selector = '.knx-ultimate-v8-upgrade-button'
1149
+ const handler = function (event) {
1150
+ event.preventDefault()
1151
+ event.stopPropagation()
1152
+ open(RED, { ...options, environment, $ })
1153
+ }
1154
+ $(environment.document).off('click' + EVENT_NAMESPACE, selector).on('click' + EVENT_NAMESPACE, selector, handler)
1155
+ if (RED.actions && typeof RED.actions.add === 'function') RED.actions.add('knxUltimate:upgrade-to-v8', () => open(RED, { ...options, environment, $ }))
1156
+ const installation = {
1157
+ open: () => open(RED, { ...options, environment, $ }),
1158
+ dispose () {
1159
+ $(environment.document).off('click' + EVENT_NAMESPACE, selector)
1160
+ if (RED.actions && typeof RED.actions.remove === 'function') RED.actions.remove('knxUltimate:upgrade-to-v8')
1161
+ installations.delete(RED)
1162
+ }
1163
+ }
1164
+ installations.set(RED, installation)
1165
+ return installation
1166
+ }
1167
+
1168
+ return {
1169
+ AI_TYPES,
1170
+ HUE_PACKAGE,
1171
+ HUE_TYPE_MAP,
1172
+ MATTER_PACKAGE,
1173
+ MATTER_TYPE_MAP,
1174
+ PACKAGE_NAME,
1175
+ TARGET_VERSION,
1176
+ applyMigrationPlan,
1177
+ assertNoLegacyNodes,
1178
+ collectAllNodes,
1179
+ createMigrationPlan,
1180
+ deployFlows,
1181
+ ensureModule,
1182
+ install,
1183
+ installKnxV8,
1184
+ legacyNodes,
1185
+ open,
1186
+ runUpgrade,
1187
+ stableFlowSignature,
1188
+ validateDeployable,
1189
+ validatePreflight,
1190
+ verifySavedFlows,
1191
+ waitForTypes
1192
+ }
1193
+ }))