node-red-contrib-knx-ultimate 6.2.2 → 6.2.3-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/nodes/knxUltimateAI.js +7 -1
- package/nodes/knxUltimateHueAreaMotion.html +15 -4
- package/nodes/knxUltimateHueBattery.html +15 -4
- package/nodes/knxUltimateHueButton.html +15 -4
- package/nodes/knxUltimateHueCameraMotion.html +15 -4
- package/nodes/knxUltimateHueContactSensor.html +15 -4
- package/nodes/knxUltimateHueController.html +1386 -0
- package/nodes/knxUltimateHueController.js +28 -0
- package/nodes/knxUltimateHueHumiditySensor.html +15 -4
- package/nodes/knxUltimateHueLight.html +15 -4
- package/nodes/knxUltimateHueLightSensor.html +15 -4
- package/nodes/knxUltimateHueMotion.html +15 -4
- package/nodes/knxUltimateHuePlug.html +15 -4
- package/nodes/knxUltimateHueScene.html +18 -7
- package/nodes/knxUltimateHueTapDial.html +15 -4
- package/nodes/knxUltimateHueTemperatureSensor.html +15 -4
- package/nodes/knxUltimateHueZigbeeConnectivity.html +15 -4
- package/nodes/knxUltimateHuedevice_software_update.html +15 -4
- package/nodes/locales/de/knxUltimateHueAreaMotion.html +1 -1
- package/nodes/locales/de/knxUltimateHueBattery.html +1 -0
- package/nodes/locales/de/knxUltimateHueButton.html +1 -0
- package/nodes/locales/de/knxUltimateHueCameraMotion.html +1 -0
- package/nodes/locales/de/knxUltimateHueContactSensor.html +1 -0
- package/nodes/locales/de/knxUltimateHueController.html +669 -0
- package/nodes/locales/de/knxUltimateHueController.json +48 -0
- package/nodes/locales/de/knxUltimateHueHumiditySensor.html +1 -0
- package/nodes/locales/de/knxUltimateHueLight.html +1 -0
- package/nodes/locales/de/knxUltimateHueLightSensor.html +1 -0
- package/nodes/locales/de/knxUltimateHueMotion.html +1 -0
- package/nodes/locales/de/knxUltimateHuePlug.html +1 -0
- package/nodes/locales/de/knxUltimateHueScene.html +1 -0
- package/nodes/locales/de/knxUltimateHueTapDial.html +1 -0
- package/nodes/locales/de/knxUltimateHueTemperatureSensor.html +1 -0
- package/nodes/locales/de/knxUltimateHueZigbeeConnectivity.html +1 -0
- package/nodes/locales/de/knxUltimateHuedevice_software_update.html +1 -0
- package/nodes/locales/en/knxUltimateHueAreaMotion.html +1 -1
- package/nodes/locales/en/knxUltimateHueBattery.html +1 -0
- package/nodes/locales/en/knxUltimateHueButton.html +1 -0
- package/nodes/locales/en/knxUltimateHueCameraMotion.html +1 -0
- package/nodes/locales/en/knxUltimateHueContactSensor.html +1 -0
- package/nodes/locales/en/knxUltimateHueController.html +672 -0
- package/nodes/locales/en/knxUltimateHueController.json +48 -0
- package/nodes/locales/en/knxUltimateHueHumiditySensor.html +1 -0
- package/nodes/locales/en/knxUltimateHueLight.html +1 -0
- package/nodes/locales/en/knxUltimateHueLightSensor.html +1 -0
- package/nodes/locales/en/knxUltimateHueMotion.html +1 -0
- package/nodes/locales/en/knxUltimateHuePlug.html +1 -0
- package/nodes/locales/en/knxUltimateHueScene.html +1 -0
- package/nodes/locales/en/knxUltimateHueTapDial.html +1 -0
- package/nodes/locales/en/knxUltimateHueTemperatureSensor.html +1 -0
- package/nodes/locales/en/knxUltimateHueZigbeeConnectivity.html +1 -0
- package/nodes/locales/en/knxUltimateHuedevice_software_update.html +1 -0
- package/nodes/locales/es/knxUltimateHueAreaMotion.html +1 -1
- package/nodes/locales/es/knxUltimateHueBattery.html +1 -0
- package/nodes/locales/es/knxUltimateHueButton.html +1 -0
- package/nodes/locales/es/knxUltimateHueCameraMotion.html +1 -0
- package/nodes/locales/es/knxUltimateHueContactSensor.html +1 -0
- package/nodes/locales/es/knxUltimateHueController.html +672 -0
- package/nodes/locales/es/knxUltimateHueController.json +48 -0
- package/nodes/locales/es/knxUltimateHueHumiditySensor.html +1 -0
- package/nodes/locales/es/knxUltimateHueLight.html +1 -0
- package/nodes/locales/es/knxUltimateHueLightSensor.html +1 -0
- package/nodes/locales/es/knxUltimateHueMotion.html +1 -0
- package/nodes/locales/es/knxUltimateHuePlug.html +1 -0
- package/nodes/locales/es/knxUltimateHueScene.html +1 -0
- package/nodes/locales/es/knxUltimateHueTapDial.html +1 -0
- package/nodes/locales/es/knxUltimateHueTemperatureSensor.html +1 -0
- package/nodes/locales/es/knxUltimateHueZigbeeConnectivity.html +1 -0
- package/nodes/locales/es/knxUltimateHuedevice_software_update.html +1 -0
- package/nodes/locales/fr/knxUltimateHueAreaMotion.html +1 -1
- package/nodes/locales/fr/knxUltimateHueBattery.html +1 -0
- package/nodes/locales/fr/knxUltimateHueButton.html +1 -0
- package/nodes/locales/fr/knxUltimateHueCameraMotion.html +1 -0
- package/nodes/locales/fr/knxUltimateHueContactSensor.html +1 -0
- package/nodes/locales/fr/knxUltimateHueController.html +672 -0
- package/nodes/locales/fr/knxUltimateHueController.json +48 -0
- package/nodes/locales/fr/knxUltimateHueHumiditySensor.html +1 -0
- package/nodes/locales/fr/knxUltimateHueLight.html +1 -0
- package/nodes/locales/fr/knxUltimateHueLightSensor.html +1 -0
- package/nodes/locales/fr/knxUltimateHueMotion.html +1 -0
- package/nodes/locales/fr/knxUltimateHuePlug.html +1 -0
- package/nodes/locales/fr/knxUltimateHueScene.html +1 -0
- package/nodes/locales/fr/knxUltimateHueTapDial.html +1 -0
- package/nodes/locales/fr/knxUltimateHueTemperatureSensor.html +1 -0
- package/nodes/locales/fr/knxUltimateHueZigbeeConnectivity.html +1 -0
- package/nodes/locales/fr/knxUltimateHuedevice_software_update.html +1 -0
- package/nodes/locales/it/knxUltimateHueAreaMotion.html +1 -1
- package/nodes/locales/it/knxUltimateHueBattery.html +1 -0
- package/nodes/locales/it/knxUltimateHueButton.html +1 -0
- package/nodes/locales/it/knxUltimateHueCameraMotion.html +1 -0
- package/nodes/locales/it/knxUltimateHueContactSensor.html +1 -0
- package/nodes/locales/it/knxUltimateHueController.html +672 -0
- package/nodes/locales/it/knxUltimateHueController.json +48 -0
- package/nodes/locales/it/knxUltimateHueHumiditySensor.html +1 -0
- package/nodes/locales/it/knxUltimateHueLight.html +1 -0
- package/nodes/locales/it/knxUltimateHueLightSensor.html +1 -0
- package/nodes/locales/it/knxUltimateHueMotion.html +1 -0
- package/nodes/locales/it/knxUltimateHuePlug.html +1 -0
- package/nodes/locales/it/knxUltimateHueScene.html +1 -0
- package/nodes/locales/it/knxUltimateHueTapDial.html +1 -0
- package/nodes/locales/it/knxUltimateHueTemperatureSensor.html +1 -0
- package/nodes/locales/it/knxUltimateHueZigbeeConnectivity.html +1 -0
- package/nodes/locales/it/knxUltimateHuedevice_software_update.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueAreaMotion.html +1 -1
- package/nodes/locales/zh-CN/knxUltimateHueBattery.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueButton.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueCameraMotion.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueContactSensor.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueController.html +674 -0
- package/nodes/locales/zh-CN/knxUltimateHueController.json +48 -0
- package/nodes/locales/zh-CN/knxUltimateHueHumiditySensor.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueLight.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueLightSensor.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueMotion.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHuePlug.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueScene.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueTapDial.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueTemperatureSensor.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHueZigbeeConnectivity.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateHuedevice_software_update.html +1 -0
- package/nodes/utils/hueControllerProfileAdapter.js +104 -0
- package/nodes/utils/hueControllerProfiles/runtime/area_motion.js +221 -0
- package/nodes/utils/hueControllerProfiles/runtime/button.js +295 -0
- package/nodes/utils/hueControllerProfiles/runtime/camera_motion.js +221 -0
- package/nodes/utils/hueControllerProfiles/runtime/contact.js +191 -0
- package/nodes/utils/hueControllerProfiles/runtime/device_power.js +185 -0
- package/nodes/utils/hueControllerProfiles/runtime/device_software_update.js +185 -0
- package/nodes/utils/hueControllerProfiles/runtime/humidity.js +204 -0
- package/nodes/utils/hueControllerProfiles/runtime/light.js +1583 -0
- package/nodes/utils/hueControllerProfiles/runtime/light_level.js +180 -0
- package/nodes/utils/hueControllerProfiles/runtime/motion.js +185 -0
- package/nodes/utils/hueControllerProfiles/runtime/plug.js +344 -0
- package/nodes/utils/hueControllerProfiles/runtime/relative_rotary.js +271 -0
- package/nodes/utils/hueControllerProfiles/runtime/scene.js +352 -0
- package/nodes/utils/hueControllerProfiles/runtime/temperature.js +187 -0
- package/nodes/utils/hueControllerProfiles/runtime/zigbee_connectivity.js +185 -0
- package/package.json +5 -2
- package/resources/hueControllerMigration.js +199 -0
- package/resources/hueControllerMigrationDialog.js +189 -0
- package/resources/hueControllerProfiles.js +9290 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Canonical private runtime profile for HUE Controller: zigbee_connectivity.
|
|
2
|
+
// This module is shipped with the Controller but is intentionally absent from
|
|
3
|
+
// package.json, so Node-RED never registers it as a separate palette node.
|
|
4
|
+
module.exports = function (RED) {
|
|
5
|
+
function knxUltimateHueZigbeeConnectivity (config) {
|
|
6
|
+
RED.nodes.createNode(this, config)
|
|
7
|
+
const node = this
|
|
8
|
+
node.serverKNX = RED.nodes.getNode(config.server) || undefined
|
|
9
|
+
node.serverHue = RED.nodes.getNode(config.serverHue) || undefined
|
|
10
|
+
node.topic = node.name
|
|
11
|
+
node.name = config.name === undefined ? 'Hue' : config.name
|
|
12
|
+
node.dpt = ''
|
|
13
|
+
node.notifyreadrequest = true
|
|
14
|
+
node.notifyreadrequestalsorespondtobus = 'false'
|
|
15
|
+
node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
|
|
16
|
+
node.notifyresponse = false
|
|
17
|
+
node.notifywrite = true
|
|
18
|
+
node.initialread = true
|
|
19
|
+
node.listenallga = true // Don't remove
|
|
20
|
+
node.outputtype = 'write'
|
|
21
|
+
node.outputRBE = 'false' // Apply or not RBE to the output (Messages coming from flow)
|
|
22
|
+
node.inputRBE = 'false' // Apply or not RBE to the input (Messages coming from BUS)
|
|
23
|
+
node.currentPayload = '' // Current value for the RBE input and for the .previouspayload msg
|
|
24
|
+
node.passthrough = 'no'
|
|
25
|
+
node.formatmultiplyvalue = 1
|
|
26
|
+
node.formatnegativevalue = 'leave'
|
|
27
|
+
node.formatdecimalsvalue = 2
|
|
28
|
+
node.hueDevice = config.hueDevice
|
|
29
|
+
node.initializingAtStart = (config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes')
|
|
30
|
+
node.currentDeviceValue = 0
|
|
31
|
+
const pinsSetting = (config.enableNodePINS === undefined || config.enableNodePINS === 'yes' || config.enableNodePINS === true)
|
|
32
|
+
node.enableNodePINS = pinsSetting ? 'yes' : 'no'
|
|
33
|
+
node.outputs = pinsSetting ? 1 : 0
|
|
34
|
+
if (!node.serverKNX && node.outputs === 0) {
|
|
35
|
+
node.enableNodePINS = 'yes'
|
|
36
|
+
node.outputs = 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pushStatus = (status) => {
|
|
40
|
+
if (!status) return
|
|
41
|
+
const provider = node.serverKNX
|
|
42
|
+
if (provider && typeof provider.applyStatusUpdate === 'function') {
|
|
43
|
+
provider.applyStatusUpdate(node, status)
|
|
44
|
+
} else {
|
|
45
|
+
node.status(status)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const updateStatus = (status) => {
|
|
50
|
+
if (!status) return
|
|
51
|
+
pushStatus(status)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const formatTs = (date) => {
|
|
55
|
+
const d = date instanceof Date ? date : new Date(date)
|
|
56
|
+
const provider = node.serverKNX
|
|
57
|
+
if (provider && typeof provider.formatStatusTimestamp === 'function') return provider.formatStatusTimestamp(d)
|
|
58
|
+
return `${d.getDate()}, ${d.toLocaleTimeString()}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const safeSendToKNX = (telegram, context = 'write') => {
|
|
62
|
+
try {
|
|
63
|
+
if (!node.serverKNX || typeof node.serverKNX.sendKNXTelegramToKNXEngine !== 'function') {
|
|
64
|
+
const now = new Date()
|
|
65
|
+
updateStatus({ fill: 'red', shape: 'dot', text: `KNX server missing (${context}) (${formatTs(now)})` })
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
node.serverKNX.sendKNXTelegramToKNXEngine({ ...telegram, nodecallerid: node.id })
|
|
69
|
+
} catch (error) {
|
|
70
|
+
updateStatus({ fill: 'red', shape: 'dot', text: `KNX send error ${error.message}` })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Used to call the status update from the config node.
|
|
75
|
+
node.setNodeStatus = ({
|
|
76
|
+
fill, shape, text, payload
|
|
77
|
+
}) => {
|
|
78
|
+
try {
|
|
79
|
+
if (payload === undefined) payload = ''
|
|
80
|
+
const dDate = new Date()
|
|
81
|
+
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
|
|
82
|
+
node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${formatTs(dDate)})`
|
|
83
|
+
pushStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') })
|
|
84
|
+
} catch (error) { }
|
|
85
|
+
}
|
|
86
|
+
// Used to call the status update from the HUE config node.
|
|
87
|
+
node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
|
|
88
|
+
try {
|
|
89
|
+
if (payload === undefined) payload = ''
|
|
90
|
+
const dDate = new Date()
|
|
91
|
+
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
|
|
92
|
+
node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${formatTs(dDate)})`
|
|
93
|
+
pushStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') })
|
|
94
|
+
} catch (error) { }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// This function is called by the knx-ultimate config node, to output a msg.payload.
|
|
98
|
+
node.handleSend = (msg) => {
|
|
99
|
+
// Respond to KNX read telegram, by sending the current value as response telegram.
|
|
100
|
+
if (msg.knx.event === 'GroupValue_Read') {
|
|
101
|
+
switch (msg.knx.destination) {
|
|
102
|
+
case config.GAzigbeeconnectivity:
|
|
103
|
+
// To the KNX bus wires
|
|
104
|
+
node.sendResponseToKNX(node.currentDeviceValue)
|
|
105
|
+
break
|
|
106
|
+
default:
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
node.handleSendHUE = (_event) => {
|
|
113
|
+
try {
|
|
114
|
+
// Event status: one of connected, disconnected, connectivity_issue, unidirectional_incoming
|
|
115
|
+
if (_event.id === config.hueDevice) {
|
|
116
|
+
if (!_event.hasOwnProperty('status') || _event.status === undefined) return
|
|
117
|
+
|
|
118
|
+
const knxMsgPayload = {}
|
|
119
|
+
knxMsgPayload.topic = config.GAzigbeeconnectivity
|
|
120
|
+
knxMsgPayload.dpt = config.dptzigbeeconnectivity
|
|
121
|
+
|
|
122
|
+
knxMsgPayload.payload = (_event.status === 'connected')
|
|
123
|
+
// Send to KNX bus
|
|
124
|
+
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
|
|
125
|
+
safeSendToKNX({
|
|
126
|
+
grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'write'
|
|
127
|
+
}, 'write')
|
|
128
|
+
}
|
|
129
|
+
node.currentDeviceValue = knxMsgPayload.payload
|
|
130
|
+
// Setup the output msg
|
|
131
|
+
knxMsgPayload.name = node.name
|
|
132
|
+
knxMsgPayload.status = _event.status
|
|
133
|
+
knxMsgPayload.event = 'zigbee_connectivity'
|
|
134
|
+
// Send payload
|
|
135
|
+
knxMsgPayload.rawEvent = _event
|
|
136
|
+
node.send(knxMsgPayload)
|
|
137
|
+
node.setNodeStatusHue({
|
|
138
|
+
fill: 'blue', shape: 'ring', text: 'HUE->KNX', payload: knxMsgPayload.payload
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
updateStatus({ fill: 'red', shape: 'dot', text: `HUE->KNX error ${error.message} (${formatTs(new Date())})` })
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
node.sendResponseToKNX = (_level) => {
|
|
147
|
+
const knxMsgPayload = {}
|
|
148
|
+
knxMsgPayload.topic = config.GAzigbeeconnectivity
|
|
149
|
+
knxMsgPayload.dpt = config.dptzigbeeconnectivity
|
|
150
|
+
|
|
151
|
+
knxMsgPayload.payload = _level
|
|
152
|
+
// Send to KNX bus
|
|
153
|
+
if (knxMsgPayload.topic !== '' && knxMsgPayload.topic !== undefined) {
|
|
154
|
+
safeSendToKNX({
|
|
155
|
+
grpaddr: knxMsgPayload.topic, payload: knxMsgPayload.payload, dpt: knxMsgPayload.dpt, outputtype: 'response'
|
|
156
|
+
}, 'response')
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// On each deploy, unsubscribe+resubscribe
|
|
161
|
+
if (node.serverKNX) {
|
|
162
|
+
node.serverKNX.removeClient(node)
|
|
163
|
+
node.serverKNX.addClient(node)
|
|
164
|
+
}
|
|
165
|
+
if (node.serverHue) {
|
|
166
|
+
node.serverHue.removeClient(node)
|
|
167
|
+
node.serverHue.addClient(node)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
node.on('input', (msg) => {
|
|
171
|
+
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
node.on('close', (done) => {
|
|
175
|
+
if (node.serverKNX) {
|
|
176
|
+
node.serverKNX.removeClient(node)
|
|
177
|
+
}
|
|
178
|
+
if (node.serverHue) {
|
|
179
|
+
node.serverHue.removeClient(node)
|
|
180
|
+
}
|
|
181
|
+
done()
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
RED.nodes.registerType('knxUltimateHueZigbeeConnectivity', knxUltimateHueZigbeeConnectivity)
|
|
185
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "6.2.
|
|
6
|
+
"version": "6.2.3-beta.0",
|
|
7
7
|
"description": "KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT, diagnostics with AI, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"knxUltimateWatchDog": "/nodes/knxUltimateWatchDog.js",
|
|
54
54
|
"knxUltimate-config": "/nodes/knxUltimate-config.js",
|
|
55
55
|
"hueConfig": "/nodes/hue-config.js",
|
|
56
|
+
"knxUltimateHueController": "/nodes/knxUltimateHueController.js",
|
|
56
57
|
"knxUltimateHueLight": "/nodes/knxUltimateHueLight.js",
|
|
57
58
|
"knxUltimateMatterControllerDevice": "/nodes/knxUltimateMatterControllerDevice.js",
|
|
58
59
|
"knxUltimateHueButton": "/nodes/knxUltimateHueButton.js",
|
|
@@ -108,8 +109,10 @@
|
|
|
108
109
|
"build": "npm run knx-ai:build && npm run knx-viewer:build",
|
|
109
110
|
"prepack": "npm run knx-ai:build && npm run knx-viewer:build",
|
|
110
111
|
"prepublishOnly": "npm run build",
|
|
111
|
-
"test": "DEBUG_KNX_HUE_TEST=0 npm run test:unit && node scripts/check-node-load.js",
|
|
112
|
+
"test": "npm run hue-controller:check && DEBUG_KNX_HUE_TEST=0 npm run test:unit && node scripts/check-node-load.js",
|
|
112
113
|
"test:unit": "mocha \"test/**/*.test.js\"",
|
|
114
|
+
"hue-controller:generate": "node scripts/generate-hue-controller-profiles.js",
|
|
115
|
+
"hue-controller:check": "node scripts/generate-hue-controller-profiles.js --check",
|
|
113
116
|
"avviaquesto-itest:light": "node test/integration/hueLiveTest.mjs",
|
|
114
117
|
"avviaquesto-itest:matterlight": "node test/integration/matterLiveTest.mjs",
|
|
115
118
|
"avviaquesto-itest:nodered-knx": "node test/integration/nodeRedLiveTest.mjs",
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory()
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api
|
|
4
|
+
if (root) root.KNXUltimateHueControllerMigration = api
|
|
5
|
+
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const LEGACY_NODE_PROFILES = Object.freeze({
|
|
9
|
+
knxUltimateHueLight: { controllerType: 'light', pinMode: 'dual', pinsDefault: false },
|
|
10
|
+
knxUltimateHuePlug: { controllerType: 'plug', pinMode: 'dual', pinsDefault: false },
|
|
11
|
+
knxUltimateHueButton: { controllerType: 'button', pinMode: 'output', pinsDefault: true },
|
|
12
|
+
knxUltimateHueTapDial: { controllerType: 'relative_rotary', pinMode: 'output', pinsDefault: true },
|
|
13
|
+
knxUltimateHueMotion: { controllerType: 'motion', pinMode: 'output', pinsDefault: true },
|
|
14
|
+
knxUltimateHueAreaMotion: { controllerType: 'area_motion', pinMode: 'output', pinsDefault: true },
|
|
15
|
+
knxUltimateHueCameraMotion: { controllerType: 'camera_motion', pinMode: 'output', pinsDefault: true },
|
|
16
|
+
knxUltimateHueContactSensor: { controllerType: 'contact', pinMode: 'output', pinsDefault: true },
|
|
17
|
+
knxUltimateHueLightSensor: { controllerType: 'light_level', pinMode: 'output', pinsDefault: true },
|
|
18
|
+
knxUltimateHueTemperatureSensor: { controllerType: 'temperature', pinMode: 'output', pinsDefault: true },
|
|
19
|
+
knxUltimateHueHumiditySensor: { controllerType: 'humidity', pinMode: 'output', pinsDefault: true },
|
|
20
|
+
knxUltimateHueScene: { controllerType: 'scene', pinMode: 'dual', pinsDefault: false },
|
|
21
|
+
knxUltimateHueBattery: { controllerType: 'device_power', pinMode: 'output', pinsDefault: true },
|
|
22
|
+
knxUltimateHueZigbeeConnectivity: { controllerType: 'zigbee_connectivity', pinMode: 'output', pinsDefault: true },
|
|
23
|
+
knxUltimateHuedevice_software_update: { controllerType: 'device_software_update', pinMode: 'output', pinsDefault: true }
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const CONFIG_NODE_REFERENCE_FIELDS = Object.freeze({
|
|
27
|
+
'knxUltimate-config': 'server',
|
|
28
|
+
'hue-config': 'serverHue'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Kept as a tiny public predicate so the editor can decide whether the
|
|
32
|
+
// migration entry point is relevant without duplicating the canonical type
|
|
33
|
+
// catalog. It only classifies an object; it never reads or changes a flow.
|
|
34
|
+
function isLegacyHueNode (node) {
|
|
35
|
+
return Boolean(node && typeof node === 'object' && LEGACY_NODE_PROFILES[node.type])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasEnabledPins (node, profile) {
|
|
39
|
+
const rawValue = node.enableNodePINS
|
|
40
|
+
if (rawValue === true) return true
|
|
41
|
+
if (rawValue === false) return false
|
|
42
|
+
if (typeof rawValue === 'string' && rawValue.trim() !== '') {
|
|
43
|
+
return rawValue.trim().toLowerCase() === 'yes'
|
|
44
|
+
}
|
|
45
|
+
return profile.pinsDefault === true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readPinCount (value) {
|
|
49
|
+
if (value === undefined || value === null || value === '') return undefined
|
|
50
|
+
const numericValue = Number(value)
|
|
51
|
+
if (!Number.isInteger(numericValue) || numericValue < 0) return undefined
|
|
52
|
+
return numericValue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function derivePinShape (node, profile) {
|
|
56
|
+
const enabled = hasEnabledPins(node, profile)
|
|
57
|
+
const defaults = profile.pinMode === 'dual'
|
|
58
|
+
? { inputs: enabled ? 1 : 0, outputs: enabled ? 1 : 0 }
|
|
59
|
+
: { inputs: 0, outputs: enabled ? 1 : 0 }
|
|
60
|
+
const exportedInputs = readPinCount(node.inputs)
|
|
61
|
+
const exportedOutputs = readPinCount(node.outputs)
|
|
62
|
+
const wiredOutputs = Array.isArray(node.wires) ? node.wires.length : undefined
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
inputs: exportedInputs === undefined ? defaults.inputs : exportedInputs,
|
|
66
|
+
outputs: exportedOutputs === undefined
|
|
67
|
+
? (wiredOutputs === undefined ? defaults.outputs : wiredOutputs)
|
|
68
|
+
: exportedOutputs
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cloneFlowNode (node, index) {
|
|
73
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
74
|
+
throw new TypeError(`Flow entry ${index} is not a Node-RED node object`)
|
|
75
|
+
}
|
|
76
|
+
return JSON.parse(JSON.stringify(node))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function copyTextWithTemporaryTextarea (text, documentObject) {
|
|
80
|
+
if (!documentObject || !documentObject.body || typeof documentObject.createElement !== 'function') {
|
|
81
|
+
throw new Error('Clipboard fallback is unavailable')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const temporary = documentObject.createElement('textarea')
|
|
85
|
+
temporary.value = String(text)
|
|
86
|
+
temporary.readOnly = true
|
|
87
|
+
temporary.setAttribute('aria-hidden', 'true')
|
|
88
|
+
temporary.style.position = 'fixed'
|
|
89
|
+
temporary.style.left = '-9999px'
|
|
90
|
+
temporary.style.top = '0'
|
|
91
|
+
documentObject.body.appendChild(temporary)
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
temporary.focus()
|
|
95
|
+
temporary.select()
|
|
96
|
+
if (typeof temporary.setSelectionRange === 'function') {
|
|
97
|
+
temporary.setSelectionRange(0, temporary.value.length)
|
|
98
|
+
}
|
|
99
|
+
if (typeof documentObject.execCommand !== 'function' || documentObject.execCommand('copy') !== true) {
|
|
100
|
+
throw new Error('The browser refused clipboard access')
|
|
101
|
+
}
|
|
102
|
+
return true
|
|
103
|
+
} finally {
|
|
104
|
+
if (temporary.parentNode) temporary.parentNode.removeChild(temporary)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function copyTextToClipboard (text, environment = {}) {
|
|
109
|
+
const value = String(text)
|
|
110
|
+
const navigatorObject = environment.navigator
|
|
111
|
+
const documentObject = environment.document
|
|
112
|
+
const clipboard = navigatorObject && navigatorObject.clipboard
|
|
113
|
+
const fallback = () => copyTextWithTemporaryTextarea(value, documentObject)
|
|
114
|
+
|
|
115
|
+
if (clipboard && typeof clipboard.writeText === 'function') {
|
|
116
|
+
try {
|
|
117
|
+
return Promise.resolve(clipboard.writeText(value))
|
|
118
|
+
.then(() => true)
|
|
119
|
+
.catch(fallback)
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return Promise.resolve().then(fallback)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return Promise.resolve().then(fallback)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function convertLegacyHueFlow (sourceFlow, options = {}) {
|
|
128
|
+
if (!Array.isArray(sourceFlow)) {
|
|
129
|
+
throw new TypeError('The pasted Node-RED flow must be a JSON array')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let convertedCount = 0
|
|
133
|
+
const convertedByType = {}
|
|
134
|
+
const referencedConfigIds = new Set()
|
|
135
|
+
const flow = sourceFlow.map(function (sourceNode, index) {
|
|
136
|
+
const node = cloneFlowNode(sourceNode, index)
|
|
137
|
+
const profile = LEGACY_NODE_PROFILES[node.type]
|
|
138
|
+
if (!profile) return node
|
|
139
|
+
|
|
140
|
+
const oldType = node.type
|
|
141
|
+
const pinShape = derivePinShape(node, profile)
|
|
142
|
+
node.type = 'knxUltimateHueController'
|
|
143
|
+
node.hueControllerType = profile.controllerType
|
|
144
|
+
node.inputs = pinShape.inputs
|
|
145
|
+
node.outputs = pinShape.outputs
|
|
146
|
+
Object.values(CONFIG_NODE_REFERENCE_FIELDS).forEach(function (field) {
|
|
147
|
+
if (typeof node[field] === 'string' && node[field].trim() !== '') {
|
|
148
|
+
referencedConfigIds.add(node[field])
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
convertedCount += 1
|
|
152
|
+
convertedByType[oldType] = (convertedByType[oldType] || 0) + 1
|
|
153
|
+
return node
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const reuseConfigNodes = options.reuseConfigNodes !== false
|
|
157
|
+
const omittedConfigByType = {}
|
|
158
|
+
const convertedFlow = reuseConfigNodes && convertedCount > 0
|
|
159
|
+
? flow.filter(function (node) {
|
|
160
|
+
const referenceField = CONFIG_NODE_REFERENCE_FIELDS[node.type]
|
|
161
|
+
const omitNode = Boolean(referenceField && referencedConfigIds.has(node.id))
|
|
162
|
+
if (omitNode) omittedConfigByType[node.type] = (omittedConfigByType[node.type] || 0) + 1
|
|
163
|
+
return !omitNode
|
|
164
|
+
})
|
|
165
|
+
: flow
|
|
166
|
+
const omittedConfigCount = Object.values(omittedConfigByType)
|
|
167
|
+
.reduce((total, count) => total + count, 0)
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
flow: convertedFlow,
|
|
171
|
+
convertedCount,
|
|
172
|
+
convertedByType,
|
|
173
|
+
omittedConfigCount,
|
|
174
|
+
omittedConfigByType
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function convertLegacyHueFlowJson (rawJson, options = {}) {
|
|
179
|
+
if (typeof rawJson !== 'string' || rawJson.trim() === '') {
|
|
180
|
+
throw new TypeError('Paste a Node-RED flow before starting the conversion')
|
|
181
|
+
}
|
|
182
|
+
const sourceFlow = JSON.parse(rawJson)
|
|
183
|
+
const result = convertLegacyHueFlow(sourceFlow, options)
|
|
184
|
+
return {
|
|
185
|
+
...result,
|
|
186
|
+
json: JSON.stringify(result.flow, null, 2)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
CONFIG_NODE_REFERENCE_FIELDS,
|
|
192
|
+
LEGACY_NODE_PROFILES,
|
|
193
|
+
copyTextToClipboard,
|
|
194
|
+
convertLegacyHueFlow,
|
|
195
|
+
convertLegacyHueFlowJson,
|
|
196
|
+
derivePinShape,
|
|
197
|
+
isLegacyHueNode
|
|
198
|
+
}
|
|
199
|
+
}))
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory()
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api
|
|
4
|
+
if (root) {
|
|
5
|
+
root.KNXUltimateHueControllerMigrationDialog = api
|
|
6
|
+
if (root.RED && root.jQuery && root.document) {
|
|
7
|
+
api.installLegacyButton(root.RED, root.jQuery, root, root.document)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
11
|
+
'use strict'
|
|
12
|
+
|
|
13
|
+
const EVENT_NAMESPACE = '.knxUltimateHueControllerMigrationDialog'
|
|
14
|
+
const BUTTON_SELECTOR = '.hue-legacy-migrate-flow'
|
|
15
|
+
const I18N_PREFIX = 'node-red-contrib-knx-ultimate/knxUltimateHueController:knxUltimateHueController.'
|
|
16
|
+
let closeActiveDialog = function () {}
|
|
17
|
+
|
|
18
|
+
function defaultTranslate (RED, key, fallback, values) {
|
|
19
|
+
try {
|
|
20
|
+
const qualifiedKey = `${I18N_PREFIX}${key}`
|
|
21
|
+
const translated = RED._(qualifiedKey, values)
|
|
22
|
+
if (translated && translated !== qualifiedKey) return translated
|
|
23
|
+
} catch (error) { /* use fallback */ }
|
|
24
|
+
return fallback
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function open (options = {}) {
|
|
28
|
+
const RED = options.RED
|
|
29
|
+
const $ = options.$
|
|
30
|
+
const windowObject = options.windowObject
|
|
31
|
+
const documentObject = options.documentObject
|
|
32
|
+
const migrationApi = options.migrationApi || (windowObject && windowObject.KNXUltimateHueControllerMigration)
|
|
33
|
+
const translate = typeof options.translate === 'function'
|
|
34
|
+
? options.translate
|
|
35
|
+
: (key, fallback, values) => defaultTranslate(RED, key, fallback, values)
|
|
36
|
+
|
|
37
|
+
if (!RED || !$ || !windowObject || !documentObject || !migrationApi || typeof migrationApi.convertLegacyHueFlowJson !== 'function') {
|
|
38
|
+
if (RED && typeof RED.notify === 'function') {
|
|
39
|
+
RED.notify(translate('migration_unavailable', 'The HUE migration tool is unavailable. Restart Node-RED after updating the package.'), 'error')
|
|
40
|
+
}
|
|
41
|
+
return function () {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const $dialog = $('<div class="hue-controller-migration-dialog"></div>').appendTo('body')
|
|
45
|
+
$('<div class="hue-controller-migration-note"></div>')
|
|
46
|
+
.text(translate('migration_privacy', 'Paste an exported Node-RED flow. Conversion happens locally and does not modify the open flow.'))
|
|
47
|
+
.css({ marginBottom: '8px' })
|
|
48
|
+
.appendTo($dialog)
|
|
49
|
+
$('<div class="hue-controller-migration-credentials"></div>')
|
|
50
|
+
.text(translate('migration_credentials_notice', 'Node-RED deliberately excludes keys and passwords from exported flows. The converter cannot read or recreate them.'))
|
|
51
|
+
.css({ marginBottom: '12px', padding: '8px 10px', borderLeft: '4px solid #d79b00', background: '#fff8df' })
|
|
52
|
+
.appendTo($dialog)
|
|
53
|
+
$('<label for="hue-controller-migration-input"></label>')
|
|
54
|
+
.text(translate('migration_input', 'Original flow JSON'))
|
|
55
|
+
.css({ display: 'block', fontWeight: 'bold', marginBottom: '4px' })
|
|
56
|
+
.appendTo($dialog)
|
|
57
|
+
const $input = $('<textarea id="hue-controller-migration-input"></textarea>')
|
|
58
|
+
.attr('placeholder', translate('migration_input_placeholder', 'Paste the complete Node-RED flow export here'))
|
|
59
|
+
.css({ width: '100%', height: '190px', boxSizing: 'border-box', fontFamily: 'monospace', resize: 'vertical' })
|
|
60
|
+
.appendTo($dialog)
|
|
61
|
+
const $reuseConfigsRow = $('<div></div>')
|
|
62
|
+
.css({ margin: '10px 0' })
|
|
63
|
+
.appendTo($dialog)
|
|
64
|
+
const $reuseConfigs = $('<input type="checkbox" id="hue-controller-migration-reuse-configs">')
|
|
65
|
+
.prop('checked', true)
|
|
66
|
+
.appendTo($reuseConfigsRow)
|
|
67
|
+
$('<label for="hue-controller-migration-reuse-configs"></label>')
|
|
68
|
+
.text(` ${translate('migration_reuse_configs', 'Reuse existing Hue and KNX config nodes (recommended)')}`)
|
|
69
|
+
.css({ display: 'inline', fontWeight: 'bold' })
|
|
70
|
+
.appendTo($reuseConfigsRow)
|
|
71
|
+
$('<div></div>')
|
|
72
|
+
.text(translate('migration_reuse_configs_help', 'Keep this selected when importing back into the same Node-RED installation. Referenced config nodes are omitted from the result, so their stored credentials remain untouched.'))
|
|
73
|
+
.css({ margin: '4px 0 0 22px', color: '#666' })
|
|
74
|
+
.appendTo($reuseConfigsRow)
|
|
75
|
+
const $toolbar = $('<div></div>')
|
|
76
|
+
.css({ display: 'flex', gap: '8px', alignItems: 'center', margin: '10px 0' })
|
|
77
|
+
.appendTo($dialog)
|
|
78
|
+
const $convert = $('<button type="button" class="red-ui-button"></button>')
|
|
79
|
+
.append($('<i class="fa fa-exchange"></i>'))
|
|
80
|
+
.append(documentObject.createTextNode(` ${translate('migration_convert', 'Convert')}`))
|
|
81
|
+
.appendTo($toolbar)
|
|
82
|
+
const $copy = $('<button type="button" class="red-ui-button"></button>')
|
|
83
|
+
.append($('<i class="fa fa-clipboard"></i>'))
|
|
84
|
+
.append(documentObject.createTextNode(` ${translate('migration_copy', 'Copy converted flow')}`))
|
|
85
|
+
.prop('disabled', true)
|
|
86
|
+
.appendTo($toolbar)
|
|
87
|
+
const $status = $('<span class="hue-controller-migration-status"></span>')
|
|
88
|
+
.css({ marginLeft: '4px' })
|
|
89
|
+
.appendTo($toolbar)
|
|
90
|
+
$('<label for="hue-controller-migration-output"></label>')
|
|
91
|
+
.text(translate('migration_output', 'Converted flow JSON'))
|
|
92
|
+
.css({ display: 'block', fontWeight: 'bold', marginBottom: '4px' })
|
|
93
|
+
.appendTo($dialog)
|
|
94
|
+
const $output = $('<textarea id="hue-controller-migration-output" readonly></textarea>')
|
|
95
|
+
.css({ width: '100%', height: '230px', boxSizing: 'border-box', fontFamily: 'monospace', resize: 'vertical' })
|
|
96
|
+
.appendTo($dialog)
|
|
97
|
+
|
|
98
|
+
$convert.on(`click${EVENT_NAMESPACE}`, () => {
|
|
99
|
+
try {
|
|
100
|
+
const result = migrationApi.convertLegacyHueFlowJson($input.val(), {
|
|
101
|
+
reuseConfigNodes: $reuseConfigs.prop('checked')
|
|
102
|
+
})
|
|
103
|
+
$output.val(result.json).scrollTop(0)
|
|
104
|
+
$copy.prop('disabled', false)
|
|
105
|
+
if (result.convertedCount === 0) {
|
|
106
|
+
$status.text(translate('migration_none', 'No legacy HUE nodes were found.')).css('color', '#a15c00')
|
|
107
|
+
} else {
|
|
108
|
+
let summary = translate('migration_summary', '{{count}} legacy HUE nodes converted.')
|
|
109
|
+
.replace('{{count}}', String(result.convertedCount))
|
|
110
|
+
let statusColor = '#1b7d33'
|
|
111
|
+
if (result.omittedConfigCount > 0) {
|
|
112
|
+
summary += ` ${translate('migration_summary_reused', '{{count}} referenced config nodes will be reused with their existing credentials.')
|
|
113
|
+
.replace('{{count}}', String(result.omittedConfigCount))}`
|
|
114
|
+
} else if (!$reuseConfigs.prop('checked')) {
|
|
115
|
+
summary += ` ${translate('migration_configs_included_warning', 'Config nodes are included without exported credentials; enter their keys and passwords after import.')}`
|
|
116
|
+
statusColor = '#a15c00'
|
|
117
|
+
}
|
|
118
|
+
$status.text(summary).css('color', statusColor)
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
$output.val('')
|
|
122
|
+
$copy.prop('disabled', true)
|
|
123
|
+
const message = error && error.message ? error.message : String(error)
|
|
124
|
+
$status.text(`${translate('migration_invalid', 'Invalid flow JSON:')} ${message}`).css('color', '#b00020')
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
$copy.on(`click${EVENT_NAMESPACE}`, () => {
|
|
129
|
+
const convertedJson = $output.val()
|
|
130
|
+
if (!convertedJson) return
|
|
131
|
+
$copy.prop('disabled', true)
|
|
132
|
+
migrationApi.copyTextToClipboard(convertedJson, {
|
|
133
|
+
navigator: windowObject.navigator,
|
|
134
|
+
document: documentObject
|
|
135
|
+
}).then(() => {
|
|
136
|
+
RED.notify(translate('migration_copy_success', 'Converted flow copied.'), 'success')
|
|
137
|
+
}).catch(() => {
|
|
138
|
+
const outputElement = $output.get(0)
|
|
139
|
+
if (outputElement) {
|
|
140
|
+
outputElement.focus()
|
|
141
|
+
outputElement.select()
|
|
142
|
+
if (typeof outputElement.setSelectionRange === 'function') {
|
|
143
|
+
outputElement.setSelectionRange(0, convertedJson.length)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
RED.notify(translate('migration_copy_failed', 'Clipboard access was blocked. The converted JSON has been selected; copy it manually.'), 'error')
|
|
147
|
+
}).finally(() => {
|
|
148
|
+
$copy.prop('disabled', false)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
let closed = false
|
|
153
|
+
const closeDialog = () => {
|
|
154
|
+
if (closed) return
|
|
155
|
+
closed = true
|
|
156
|
+
$convert.off(EVENT_NAMESPACE)
|
|
157
|
+
$copy.off(EVENT_NAMESPACE)
|
|
158
|
+
try { $dialog.dialog('destroy') } catch (error) { /* already detached */ }
|
|
159
|
+
$dialog.remove()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
$dialog.dialog({
|
|
163
|
+
modal: true,
|
|
164
|
+
width: Math.min(920, Math.max(620, $(windowObject).width() - 80)),
|
|
165
|
+
height: Math.min(760, Math.max(600, $(windowObject).height() - 60)),
|
|
166
|
+
title: translate('migration_title', 'Convert legacy HUE flow'),
|
|
167
|
+
close: closeDialog,
|
|
168
|
+
buttons: [{
|
|
169
|
+
text: translate('migration_close', 'Close'),
|
|
170
|
+
click () { $(this).dialog('close') }
|
|
171
|
+
}]
|
|
172
|
+
})
|
|
173
|
+
setTimeout(() => $input.trigger('focus'), 0)
|
|
174
|
+
return closeDialog
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function installLegacyButton (RED, $, windowObject, documentObject) {
|
|
178
|
+
if (!RED || !$ || !documentObject) return
|
|
179
|
+
const $document = $(documentObject)
|
|
180
|
+
$document.off(`click${EVENT_NAMESPACE}`, BUTTON_SELECTOR)
|
|
181
|
+
$document.on(`click${EVENT_NAMESPACE}`, BUTTON_SELECTOR, (event) => {
|
|
182
|
+
event.preventDefault()
|
|
183
|
+
closeActiveDialog()
|
|
184
|
+
closeActiveDialog = open({ RED, $, windowObject, documentObject })
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { installLegacyButton, open }
|
|
189
|
+
}))
|