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.
Files changed (141) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/nodes/knxUltimateAI.js +7 -1
  3. package/nodes/knxUltimateHueAreaMotion.html +15 -4
  4. package/nodes/knxUltimateHueBattery.html +15 -4
  5. package/nodes/knxUltimateHueButton.html +15 -4
  6. package/nodes/knxUltimateHueCameraMotion.html +15 -4
  7. package/nodes/knxUltimateHueContactSensor.html +15 -4
  8. package/nodes/knxUltimateHueController.html +1386 -0
  9. package/nodes/knxUltimateHueController.js +28 -0
  10. package/nodes/knxUltimateHueHumiditySensor.html +15 -4
  11. package/nodes/knxUltimateHueLight.html +15 -4
  12. package/nodes/knxUltimateHueLightSensor.html +15 -4
  13. package/nodes/knxUltimateHueMotion.html +15 -4
  14. package/nodes/knxUltimateHuePlug.html +15 -4
  15. package/nodes/knxUltimateHueScene.html +18 -7
  16. package/nodes/knxUltimateHueTapDial.html +15 -4
  17. package/nodes/knxUltimateHueTemperatureSensor.html +15 -4
  18. package/nodes/knxUltimateHueZigbeeConnectivity.html +15 -4
  19. package/nodes/knxUltimateHuedevice_software_update.html +15 -4
  20. package/nodes/locales/de/knxUltimateHueAreaMotion.html +1 -1
  21. package/nodes/locales/de/knxUltimateHueBattery.html +1 -0
  22. package/nodes/locales/de/knxUltimateHueButton.html +1 -0
  23. package/nodes/locales/de/knxUltimateHueCameraMotion.html +1 -0
  24. package/nodes/locales/de/knxUltimateHueContactSensor.html +1 -0
  25. package/nodes/locales/de/knxUltimateHueController.html +669 -0
  26. package/nodes/locales/de/knxUltimateHueController.json +48 -0
  27. package/nodes/locales/de/knxUltimateHueHumiditySensor.html +1 -0
  28. package/nodes/locales/de/knxUltimateHueLight.html +1 -0
  29. package/nodes/locales/de/knxUltimateHueLightSensor.html +1 -0
  30. package/nodes/locales/de/knxUltimateHueMotion.html +1 -0
  31. package/nodes/locales/de/knxUltimateHuePlug.html +1 -0
  32. package/nodes/locales/de/knxUltimateHueScene.html +1 -0
  33. package/nodes/locales/de/knxUltimateHueTapDial.html +1 -0
  34. package/nodes/locales/de/knxUltimateHueTemperatureSensor.html +1 -0
  35. package/nodes/locales/de/knxUltimateHueZigbeeConnectivity.html +1 -0
  36. package/nodes/locales/de/knxUltimateHuedevice_software_update.html +1 -0
  37. package/nodes/locales/en/knxUltimateHueAreaMotion.html +1 -1
  38. package/nodes/locales/en/knxUltimateHueBattery.html +1 -0
  39. package/nodes/locales/en/knxUltimateHueButton.html +1 -0
  40. package/nodes/locales/en/knxUltimateHueCameraMotion.html +1 -0
  41. package/nodes/locales/en/knxUltimateHueContactSensor.html +1 -0
  42. package/nodes/locales/en/knxUltimateHueController.html +672 -0
  43. package/nodes/locales/en/knxUltimateHueController.json +48 -0
  44. package/nodes/locales/en/knxUltimateHueHumiditySensor.html +1 -0
  45. package/nodes/locales/en/knxUltimateHueLight.html +1 -0
  46. package/nodes/locales/en/knxUltimateHueLightSensor.html +1 -0
  47. package/nodes/locales/en/knxUltimateHueMotion.html +1 -0
  48. package/nodes/locales/en/knxUltimateHuePlug.html +1 -0
  49. package/nodes/locales/en/knxUltimateHueScene.html +1 -0
  50. package/nodes/locales/en/knxUltimateHueTapDial.html +1 -0
  51. package/nodes/locales/en/knxUltimateHueTemperatureSensor.html +1 -0
  52. package/nodes/locales/en/knxUltimateHueZigbeeConnectivity.html +1 -0
  53. package/nodes/locales/en/knxUltimateHuedevice_software_update.html +1 -0
  54. package/nodes/locales/es/knxUltimateHueAreaMotion.html +1 -1
  55. package/nodes/locales/es/knxUltimateHueBattery.html +1 -0
  56. package/nodes/locales/es/knxUltimateHueButton.html +1 -0
  57. package/nodes/locales/es/knxUltimateHueCameraMotion.html +1 -0
  58. package/nodes/locales/es/knxUltimateHueContactSensor.html +1 -0
  59. package/nodes/locales/es/knxUltimateHueController.html +672 -0
  60. package/nodes/locales/es/knxUltimateHueController.json +48 -0
  61. package/nodes/locales/es/knxUltimateHueHumiditySensor.html +1 -0
  62. package/nodes/locales/es/knxUltimateHueLight.html +1 -0
  63. package/nodes/locales/es/knxUltimateHueLightSensor.html +1 -0
  64. package/nodes/locales/es/knxUltimateHueMotion.html +1 -0
  65. package/nodes/locales/es/knxUltimateHuePlug.html +1 -0
  66. package/nodes/locales/es/knxUltimateHueScene.html +1 -0
  67. package/nodes/locales/es/knxUltimateHueTapDial.html +1 -0
  68. package/nodes/locales/es/knxUltimateHueTemperatureSensor.html +1 -0
  69. package/nodes/locales/es/knxUltimateHueZigbeeConnectivity.html +1 -0
  70. package/nodes/locales/es/knxUltimateHuedevice_software_update.html +1 -0
  71. package/nodes/locales/fr/knxUltimateHueAreaMotion.html +1 -1
  72. package/nodes/locales/fr/knxUltimateHueBattery.html +1 -0
  73. package/nodes/locales/fr/knxUltimateHueButton.html +1 -0
  74. package/nodes/locales/fr/knxUltimateHueCameraMotion.html +1 -0
  75. package/nodes/locales/fr/knxUltimateHueContactSensor.html +1 -0
  76. package/nodes/locales/fr/knxUltimateHueController.html +672 -0
  77. package/nodes/locales/fr/knxUltimateHueController.json +48 -0
  78. package/nodes/locales/fr/knxUltimateHueHumiditySensor.html +1 -0
  79. package/nodes/locales/fr/knxUltimateHueLight.html +1 -0
  80. package/nodes/locales/fr/knxUltimateHueLightSensor.html +1 -0
  81. package/nodes/locales/fr/knxUltimateHueMotion.html +1 -0
  82. package/nodes/locales/fr/knxUltimateHuePlug.html +1 -0
  83. package/nodes/locales/fr/knxUltimateHueScene.html +1 -0
  84. package/nodes/locales/fr/knxUltimateHueTapDial.html +1 -0
  85. package/nodes/locales/fr/knxUltimateHueTemperatureSensor.html +1 -0
  86. package/nodes/locales/fr/knxUltimateHueZigbeeConnectivity.html +1 -0
  87. package/nodes/locales/fr/knxUltimateHuedevice_software_update.html +1 -0
  88. package/nodes/locales/it/knxUltimateHueAreaMotion.html +1 -1
  89. package/nodes/locales/it/knxUltimateHueBattery.html +1 -0
  90. package/nodes/locales/it/knxUltimateHueButton.html +1 -0
  91. package/nodes/locales/it/knxUltimateHueCameraMotion.html +1 -0
  92. package/nodes/locales/it/knxUltimateHueContactSensor.html +1 -0
  93. package/nodes/locales/it/knxUltimateHueController.html +672 -0
  94. package/nodes/locales/it/knxUltimateHueController.json +48 -0
  95. package/nodes/locales/it/knxUltimateHueHumiditySensor.html +1 -0
  96. package/nodes/locales/it/knxUltimateHueLight.html +1 -0
  97. package/nodes/locales/it/knxUltimateHueLightSensor.html +1 -0
  98. package/nodes/locales/it/knxUltimateHueMotion.html +1 -0
  99. package/nodes/locales/it/knxUltimateHuePlug.html +1 -0
  100. package/nodes/locales/it/knxUltimateHueScene.html +1 -0
  101. package/nodes/locales/it/knxUltimateHueTapDial.html +1 -0
  102. package/nodes/locales/it/knxUltimateHueTemperatureSensor.html +1 -0
  103. package/nodes/locales/it/knxUltimateHueZigbeeConnectivity.html +1 -0
  104. package/nodes/locales/it/knxUltimateHuedevice_software_update.html +1 -0
  105. package/nodes/locales/zh-CN/knxUltimateHueAreaMotion.html +1 -1
  106. package/nodes/locales/zh-CN/knxUltimateHueBattery.html +1 -0
  107. package/nodes/locales/zh-CN/knxUltimateHueButton.html +1 -0
  108. package/nodes/locales/zh-CN/knxUltimateHueCameraMotion.html +1 -0
  109. package/nodes/locales/zh-CN/knxUltimateHueContactSensor.html +1 -0
  110. package/nodes/locales/zh-CN/knxUltimateHueController.html +674 -0
  111. package/nodes/locales/zh-CN/knxUltimateHueController.json +48 -0
  112. package/nodes/locales/zh-CN/knxUltimateHueHumiditySensor.html +1 -0
  113. package/nodes/locales/zh-CN/knxUltimateHueLight.html +1 -0
  114. package/nodes/locales/zh-CN/knxUltimateHueLightSensor.html +1 -0
  115. package/nodes/locales/zh-CN/knxUltimateHueMotion.html +1 -0
  116. package/nodes/locales/zh-CN/knxUltimateHuePlug.html +1 -0
  117. package/nodes/locales/zh-CN/knxUltimateHueScene.html +1 -0
  118. package/nodes/locales/zh-CN/knxUltimateHueTapDial.html +1 -0
  119. package/nodes/locales/zh-CN/knxUltimateHueTemperatureSensor.html +1 -0
  120. package/nodes/locales/zh-CN/knxUltimateHueZigbeeConnectivity.html +1 -0
  121. package/nodes/locales/zh-CN/knxUltimateHuedevice_software_update.html +1 -0
  122. package/nodes/utils/hueControllerProfileAdapter.js +104 -0
  123. package/nodes/utils/hueControllerProfiles/runtime/area_motion.js +221 -0
  124. package/nodes/utils/hueControllerProfiles/runtime/button.js +295 -0
  125. package/nodes/utils/hueControllerProfiles/runtime/camera_motion.js +221 -0
  126. package/nodes/utils/hueControllerProfiles/runtime/contact.js +191 -0
  127. package/nodes/utils/hueControllerProfiles/runtime/device_power.js +185 -0
  128. package/nodes/utils/hueControllerProfiles/runtime/device_software_update.js +185 -0
  129. package/nodes/utils/hueControllerProfiles/runtime/humidity.js +204 -0
  130. package/nodes/utils/hueControllerProfiles/runtime/light.js +1583 -0
  131. package/nodes/utils/hueControllerProfiles/runtime/light_level.js +180 -0
  132. package/nodes/utils/hueControllerProfiles/runtime/motion.js +185 -0
  133. package/nodes/utils/hueControllerProfiles/runtime/plug.js +344 -0
  134. package/nodes/utils/hueControllerProfiles/runtime/relative_rotary.js +271 -0
  135. package/nodes/utils/hueControllerProfiles/runtime/scene.js +352 -0
  136. package/nodes/utils/hueControllerProfiles/runtime/temperature.js +187 -0
  137. package/nodes/utils/hueControllerProfiles/runtime/zigbee_connectivity.js +185 -0
  138. package/package.json +5 -2
  139. package/resources/hueControllerMigration.js +199 -0
  140. package/resources/hueControllerMigrationDialog.js +189 -0
  141. 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.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
+ }))