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.
- package/CHANGELOG.md +1271 -1863
- package/README.md +4 -10
- package/nodes/hue-config.html +6 -0
- package/nodes/knxUltimate-config.html +14 -0
- package/nodes/knxUltimate.html +6 -0
- package/nodes/knxUltimateAI.html +6 -0
- package/nodes/knxUltimateAIHomeAssistant.html +14 -0
- package/nodes/knxUltimateAlerter.html +6 -0
- package/nodes/knxUltimateAutoResponder.html +6 -0
- package/nodes/knxUltimateDateTime.html +7 -1
- package/nodes/knxUltimateGarage.html +15 -1
- package/nodes/knxUltimateGlobalContext.html +6 -0
- package/nodes/knxUltimateHATranslator.html +6 -1
- package/nodes/knxUltimateHueAreaMotion.html +14 -0
- package/nodes/knxUltimateHueBattery.html +14 -0
- package/nodes/knxUltimateHueButton.html +14 -0
- package/nodes/knxUltimateHueCameraMotion.html +14 -0
- package/nodes/knxUltimateHueContactSensor.html +14 -0
- package/nodes/knxUltimateHueController.html +14 -0
- package/nodes/knxUltimateHueHumiditySensor.html +14 -0
- package/nodes/knxUltimateHueLight.html +14 -0
- package/nodes/knxUltimateHueLightSensor.html +14 -0
- package/nodes/knxUltimateHueMotion.html +14 -0
- package/nodes/knxUltimateHuePlug.html +14 -0
- package/nodes/knxUltimateHueScene.html +14 -0
- package/nodes/knxUltimateHueTapDial.html +14 -0
- package/nodes/knxUltimateHueTemperatureSensor.html +14 -0
- package/nodes/knxUltimateHueZigbeeConnectivity.html +14 -0
- package/nodes/knxUltimateHuedevice_software_update.html +14 -0
- package/nodes/knxUltimateIoTBridge.html +14 -0
- package/nodes/knxUltimateLoadControl.html +6 -0
- package/nodes/knxUltimateLogger.html +6 -0
- package/nodes/knxUltimateMatterBridge.html +15 -1
- package/nodes/knxUltimateMatterControllerDevice.html +15 -1
- package/nodes/knxUltimateMultiRouting.html +7 -1
- package/nodes/knxUltimateRouterFilter.html +7 -1
- package/nodes/knxUltimateSceneController.html +6 -0
- package/nodes/knxUltimateStaircase.html +7 -1
- package/nodes/knxUltimateUtility.html +6 -0
- package/nodes/knxUltimateViewer.html +6 -0
- package/nodes/knxUltimateWatchDog.html +16 -1
- package/nodes/locales/de/hue-config.html +8 -0
- package/nodes/locales/de/knxUltimate-config.html +8 -0
- package/nodes/locales/de/knxUltimate.html +8 -0
- package/nodes/locales/de/knxUltimateAI.html +8 -0
- package/nodes/locales/de/knxUltimateAIHomeAssistant.html +11 -0
- package/nodes/locales/de/knxUltimateAlerter.html +9 -1
- package/nodes/locales/de/knxUltimateAutoResponder.html +8 -0
- package/nodes/locales/de/knxUltimateDateTime.html +8 -1
- package/nodes/locales/de/knxUltimateGarage.html +8 -0
- package/nodes/locales/de/knxUltimateGlobalContext.html +9 -1
- package/nodes/locales/de/knxUltimateHATranslator.html +8 -0
- package/nodes/locales/de/knxUltimateHueAreaMotion.html +8 -0
- package/nodes/locales/de/knxUltimateHueBattery.html +8 -0
- package/nodes/locales/de/knxUltimateHueButton.html +8 -0
- package/nodes/locales/de/knxUltimateHueCameraMotion.html +8 -0
- package/nodes/locales/de/knxUltimateHueContactSensor.html +8 -0
- package/nodes/locales/de/knxUltimateHueController.html +8 -0
- package/nodes/locales/de/knxUltimateHueHumiditySensor.html +8 -0
- package/nodes/locales/de/knxUltimateHueLight.html +8 -0
- package/nodes/locales/de/knxUltimateHueLightSensor.html +8 -0
- package/nodes/locales/de/knxUltimateHueMotion.html +8 -0
- package/nodes/locales/de/knxUltimateHuePlug.html +8 -0
- package/nodes/locales/de/knxUltimateHueScene.html +8 -0
- package/nodes/locales/de/knxUltimateHueTapDial.html +8 -0
- package/nodes/locales/de/knxUltimateHueTemperatureSensor.html +8 -0
- package/nodes/locales/de/knxUltimateHueZigbeeConnectivity.html +8 -0
- package/nodes/locales/de/knxUltimateHuedevice_software_update.html +8 -0
- package/nodes/locales/de/knxUltimateIoTBridge.html +8 -0
- package/nodes/locales/de/knxUltimateLoadControl.html +8 -0
- package/nodes/locales/de/knxUltimateLogger.html +8 -0
- package/nodes/locales/de/knxUltimateMatterBridge.html +8 -0
- package/nodes/locales/de/knxUltimateMatterControllerDevice.html +8 -0
- package/nodes/locales/de/knxUltimateMultiRouting.html +8 -0
- package/nodes/locales/de/knxUltimateRouterFilter.html +8 -0
- package/nodes/locales/de/knxUltimateSceneController.html +9 -1
- package/nodes/locales/de/knxUltimateStaircase.html +8 -0
- package/nodes/locales/de/knxUltimateUtility.html +8 -0
- package/nodes/locales/de/knxUltimateUtility.json +34 -0
- package/nodes/locales/de/knxUltimateViewer.html +8 -0
- package/nodes/locales/de/knxUltimateWatchDog.html +8 -0
- package/nodes/locales/de/matter-config.html +8 -0
- package/nodes/locales/de/matterbridge-config.html +8 -0
- package/nodes/locales/en/hue-config.html +8 -0
- package/nodes/locales/en/knxUltimate-config.html +8 -0
- package/nodes/locales/en/knxUltimate.html +8 -0
- package/nodes/locales/en/knxUltimateAI.html +8 -0
- package/nodes/locales/en/knxUltimateAIHomeAssistant.html +11 -0
- package/nodes/locales/en/knxUltimateAlerter.html +8 -0
- package/nodes/locales/en/knxUltimateAutoResponder.html +8 -0
- package/nodes/locales/en/knxUltimateDateTime.html +8 -0
- package/nodes/locales/en/knxUltimateGarage.html +8 -0
- package/nodes/locales/en/knxUltimateGlobalContext.html +8 -0
- package/nodes/locales/en/knxUltimateHATranslator.html +8 -0
- package/nodes/locales/en/knxUltimateHueAreaMotion.html +8 -0
- package/nodes/locales/en/knxUltimateHueBattery.html +8 -0
- package/nodes/locales/en/knxUltimateHueButton.html +8 -0
- package/nodes/locales/en/knxUltimateHueCameraMotion.html +8 -0
- package/nodes/locales/en/knxUltimateHueContactSensor.html +8 -0
- package/nodes/locales/en/knxUltimateHueController.html +8 -0
- package/nodes/locales/en/knxUltimateHueHumiditySensor.html +8 -0
- package/nodes/locales/en/knxUltimateHueLight.html +8 -0
- package/nodes/locales/en/knxUltimateHueLightSensor.html +8 -0
- package/nodes/locales/en/knxUltimateHueMotion.html +8 -0
- package/nodes/locales/en/knxUltimateHuePlug.html +8 -0
- package/nodes/locales/en/knxUltimateHueScene.html +8 -0
- package/nodes/locales/en/knxUltimateHueTapDial.html +8 -0
- package/nodes/locales/en/knxUltimateHueTemperatureSensor.html +8 -0
- package/nodes/locales/en/knxUltimateHueZigbeeConnectivity.html +8 -0
- package/nodes/locales/en/knxUltimateHuedevice_software_update.html +8 -0
- package/nodes/locales/en/knxUltimateIoTBridge.html +8 -0
- package/nodes/locales/en/knxUltimateLoadControl.html +8 -0
- package/nodes/locales/en/knxUltimateLogger.html +8 -0
- package/nodes/locales/en/knxUltimateMatterBridge.html +8 -0
- package/nodes/locales/en/knxUltimateMatterControllerDevice.html +8 -0
- package/nodes/locales/en/knxUltimateMultiRouting.html +8 -0
- package/nodes/locales/en/knxUltimateRouterFilter.html +8 -0
- package/nodes/locales/en/knxUltimateSceneController.html +8 -0
- package/nodes/locales/en/knxUltimateStaircase.html +8 -0
- package/nodes/locales/en/knxUltimateUtility.html +8 -0
- package/nodes/locales/en/knxUltimateUtility.json +34 -0
- package/nodes/locales/en/knxUltimateViewer.html +8 -0
- package/nodes/locales/en/knxUltimateWatchDog.html +8 -0
- package/nodes/locales/en/matter-config.html +8 -0
- package/nodes/locales/en/matterbridge-config.html +8 -0
- package/nodes/locales/es/hue-config.html +8 -0
- package/nodes/locales/es/knxUltimate-config.html +8 -0
- package/nodes/locales/es/knxUltimate.html +8 -0
- package/nodes/locales/es/knxUltimateAI.html +8 -0
- package/nodes/locales/es/knxUltimateAIHomeAssistant.html +11 -0
- package/nodes/locales/es/knxUltimateAlerter.html +8 -0
- package/nodes/locales/es/knxUltimateAutoResponder.html +8 -0
- package/nodes/locales/es/knxUltimateDateTime.html +8 -1
- package/nodes/locales/es/knxUltimateGarage.html +8 -0
- package/nodes/locales/es/knxUltimateGlobalContext.html +8 -0
- package/nodes/locales/es/knxUltimateHATranslator.html +8 -0
- package/nodes/locales/es/knxUltimateHueAreaMotion.html +8 -0
- package/nodes/locales/es/knxUltimateHueBattery.html +8 -0
- package/nodes/locales/es/knxUltimateHueButton.html +8 -0
- package/nodes/locales/es/knxUltimateHueCameraMotion.html +8 -0
- package/nodes/locales/es/knxUltimateHueContactSensor.html +8 -0
- package/nodes/locales/es/knxUltimateHueController.html +8 -0
- package/nodes/locales/es/knxUltimateHueHumiditySensor.html +8 -0
- package/nodes/locales/es/knxUltimateHueLight.html +8 -0
- package/nodes/locales/es/knxUltimateHueLightSensor.html +8 -0
- package/nodes/locales/es/knxUltimateHueMotion.html +8 -0
- package/nodes/locales/es/knxUltimateHuePlug.html +8 -0
- package/nodes/locales/es/knxUltimateHueScene.html +8 -0
- package/nodes/locales/es/knxUltimateHueTapDial.html +8 -0
- package/nodes/locales/es/knxUltimateHueTemperatureSensor.html +8 -0
- package/nodes/locales/es/knxUltimateHueZigbeeConnectivity.html +8 -0
- package/nodes/locales/es/knxUltimateHuedevice_software_update.html +8 -0
- package/nodes/locales/es/knxUltimateIoTBridge.html +8 -0
- package/nodes/locales/es/knxUltimateLoadControl.html +8 -0
- package/nodes/locales/es/knxUltimateLogger.html +8 -0
- package/nodes/locales/es/knxUltimateMatterBridge.html +8 -0
- package/nodes/locales/es/knxUltimateMatterControllerDevice.html +8 -0
- package/nodes/locales/es/knxUltimateMultiRouting.html +8 -0
- package/nodes/locales/es/knxUltimateRouterFilter.html +8 -0
- package/nodes/locales/es/knxUltimateSceneController.html +8 -0
- package/nodes/locales/es/knxUltimateStaircase.html +8 -0
- package/nodes/locales/es/knxUltimateUtility.html +8 -0
- package/nodes/locales/es/knxUltimateUtility.json +35 -1
- package/nodes/locales/es/knxUltimateViewer.html +8 -0
- package/nodes/locales/es/knxUltimateWatchDog.html +8 -0
- package/nodes/locales/es/matter-config.html +8 -0
- package/nodes/locales/es/matterbridge-config.html +8 -0
- package/nodes/locales/fr/hue-config.html +8 -0
- package/nodes/locales/fr/knxUltimate-config.html +8 -0
- package/nodes/locales/fr/knxUltimate.html +8 -0
- package/nodes/locales/fr/knxUltimateAI.html +8 -0
- package/nodes/locales/fr/knxUltimateAIHomeAssistant.html +11 -0
- package/nodes/locales/fr/knxUltimateAlerter.html +8 -0
- package/nodes/locales/fr/knxUltimateAutoResponder.html +8 -0
- package/nodes/locales/fr/knxUltimateDateTime.html +8 -1
- package/nodes/locales/fr/knxUltimateGarage.html +8 -0
- package/nodes/locales/fr/knxUltimateGlobalContext.html +8 -0
- package/nodes/locales/fr/knxUltimateHATranslator.html +8 -0
- package/nodes/locales/fr/knxUltimateHueAreaMotion.html +8 -0
- package/nodes/locales/fr/knxUltimateHueBattery.html +8 -0
- package/nodes/locales/fr/knxUltimateHueButton.html +8 -0
- package/nodes/locales/fr/knxUltimateHueCameraMotion.html +8 -0
- package/nodes/locales/fr/knxUltimateHueContactSensor.html +8 -0
- package/nodes/locales/fr/knxUltimateHueController.html +8 -0
- package/nodes/locales/fr/knxUltimateHueHumiditySensor.html +8 -0
- package/nodes/locales/fr/knxUltimateHueLight.html +8 -0
- package/nodes/locales/fr/knxUltimateHueLightSensor.html +8 -0
- package/nodes/locales/fr/knxUltimateHueMotion.html +8 -0
- package/nodes/locales/fr/knxUltimateHuePlug.html +8 -0
- package/nodes/locales/fr/knxUltimateHueScene.html +8 -0
- package/nodes/locales/fr/knxUltimateHueTapDial.html +8 -0
- package/nodes/locales/fr/knxUltimateHueTemperatureSensor.html +8 -0
- package/nodes/locales/fr/knxUltimateHueZigbeeConnectivity.html +8 -0
- package/nodes/locales/fr/knxUltimateHuedevice_software_update.html +8 -0
- package/nodes/locales/fr/knxUltimateIoTBridge.html +8 -0
- package/nodes/locales/fr/knxUltimateLoadControl.html +8 -0
- package/nodes/locales/fr/knxUltimateLogger.html +8 -0
- package/nodes/locales/fr/knxUltimateMatterBridge.html +8 -0
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +8 -0
- package/nodes/locales/fr/knxUltimateMultiRouting.html +8 -0
- package/nodes/locales/fr/knxUltimateRouterFilter.html +8 -0
- package/nodes/locales/fr/knxUltimateSceneController.html +8 -0
- package/nodes/locales/fr/knxUltimateStaircase.html +8 -0
- package/nodes/locales/fr/knxUltimateUtility.html +8 -0
- package/nodes/locales/fr/knxUltimateUtility.json +34 -0
- package/nodes/locales/fr/knxUltimateViewer.html +8 -0
- package/nodes/locales/fr/knxUltimateWatchDog.html +8 -0
- package/nodes/locales/fr/matter-config.html +8 -0
- package/nodes/locales/fr/matterbridge-config.html +8 -0
- package/nodes/locales/it/hue-config.html +8 -0
- package/nodes/locales/it/knxUltimate-config.html +8 -0
- package/nodes/locales/it/knxUltimate.html +8 -0
- package/nodes/locales/it/knxUltimateAI.html +8 -0
- package/nodes/locales/it/knxUltimateAIHomeAssistant.html +11 -0
- package/nodes/locales/it/knxUltimateAlerter.html +8 -0
- package/nodes/locales/it/knxUltimateAutoResponder.html +8 -0
- package/nodes/locales/it/knxUltimateDateTime.html +8 -0
- package/nodes/locales/it/knxUltimateGarage.html +8 -0
- package/nodes/locales/it/knxUltimateGlobalContext.html +8 -0
- package/nodes/locales/it/knxUltimateHATranslator.html +8 -0
- package/nodes/locales/it/knxUltimateHueAreaMotion.html +8 -0
- package/nodes/locales/it/knxUltimateHueBattery.html +8 -0
- package/nodes/locales/it/knxUltimateHueButton.html +8 -0
- package/nodes/locales/it/knxUltimateHueCameraMotion.html +8 -0
- package/nodes/locales/it/knxUltimateHueContactSensor.html +8 -0
- package/nodes/locales/it/knxUltimateHueController.html +8 -0
- package/nodes/locales/it/knxUltimateHueHumiditySensor.html +8 -0
- package/nodes/locales/it/knxUltimateHueLight.html +8 -0
- package/nodes/locales/it/knxUltimateHueLightSensor.html +8 -0
- package/nodes/locales/it/knxUltimateHueMotion.html +8 -0
- package/nodes/locales/it/knxUltimateHuePlug.html +8 -0
- package/nodes/locales/it/knxUltimateHueScene.html +8 -0
- package/nodes/locales/it/knxUltimateHueTapDial.html +8 -0
- package/nodes/locales/it/knxUltimateHueTemperatureSensor.html +8 -0
- package/nodes/locales/it/knxUltimateHueZigbeeConnectivity.html +8 -0
- package/nodes/locales/it/knxUltimateHuedevice_software_update.html +8 -0
- package/nodes/locales/it/knxUltimateIoTBridge.html +8 -0
- package/nodes/locales/it/knxUltimateLoadControl.html +8 -0
- package/nodes/locales/it/knxUltimateLogger.html +8 -0
- package/nodes/locales/it/knxUltimateMatterBridge.html +8 -0
- package/nodes/locales/it/knxUltimateMatterControllerDevice.html +8 -0
- package/nodes/locales/it/knxUltimateMultiRouting.html +8 -0
- package/nodes/locales/it/knxUltimateRouterFilter.html +8 -0
- package/nodes/locales/it/knxUltimateSceneController.html +8 -0
- package/nodes/locales/it/knxUltimateStaircase.html +8 -0
- package/nodes/locales/it/knxUltimateUtility.html +8 -0
- package/nodes/locales/it/knxUltimateUtility.json +34 -0
- package/nodes/locales/it/knxUltimateViewer.html +8 -0
- package/nodes/locales/it/knxUltimateWatchDog.html +8 -0
- package/nodes/locales/it/matter-config.html +8 -0
- package/nodes/locales/it/matterbridge-config.html +8 -0
- package/nodes/locales/zh-CN/hue-config.html +8 -0
- package/nodes/locales/zh-CN/knxUltimate-config.html +8 -0
- package/nodes/locales/zh-CN/knxUltimate.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateAI.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateAIHomeAssistant.html +11 -0
- package/nodes/locales/zh-CN/knxUltimateAlerter.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateAutoResponder.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateDateTime.html +8 -1
- package/nodes/locales/zh-CN/knxUltimateGarage.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateGlobalContext.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHATranslator.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueAreaMotion.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueBattery.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueButton.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueCameraMotion.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueContactSensor.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueController.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueHumiditySensor.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueLight.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueLightSensor.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueMotion.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHuePlug.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueScene.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueTapDial.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueTemperatureSensor.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHueZigbeeConnectivity.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateHuedevice_software_update.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateLoadControl.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateLogger.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateMultiRouting.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateRouterFilter.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateSceneController.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateStaircase.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateUtility.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateUtility.json +34 -0
- package/nodes/locales/zh-CN/knxUltimateViewer.html +8 -0
- package/nodes/locales/zh-CN/knxUltimateWatchDog.html +8 -0
- package/nodes/locales/zh-CN/matter-config.html +8 -0
- package/nodes/locales/zh-CN/matterbridge-config.html +8 -0
- package/nodes/matter-config.html +6 -0
- package/nodes/matterbridge-config.html +6 -0
- package/nodes/plugins/knxUltimate-migration-notice-plugin.html +25 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/app.js +10 -10
- package/nodes/plugins/knxUltimateAI-vue/assets/chunk-purify.es.js +3 -3
- package/nodes/plugins/knxUltimateViewer-vue/assets/app.js +2 -2
- package/package.json +5 -1
- package/resources/flowMigrationBackup.js +1 -1
- package/resources/legacyMigrationNotice.js +8 -12
- 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
|
+
}))
|