node-red-contrib-knx-ultimate 6.2.2 → 6.2.3-beta.1

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 +16 -4
  4. package/nodes/knxUltimateHueBattery.html +16 -4
  5. package/nodes/knxUltimateHueButton.html +16 -4
  6. package/nodes/knxUltimateHueCameraMotion.html +16 -4
  7. package/nodes/knxUltimateHueContactSensor.html +16 -4
  8. package/nodes/knxUltimateHueController.html +1259 -0
  9. package/nodes/knxUltimateHueController.js +28 -0
  10. package/nodes/knxUltimateHueHumiditySensor.html +16 -4
  11. package/nodes/knxUltimateHueLight.html +16 -4
  12. package/nodes/knxUltimateHueLightSensor.html +16 -4
  13. package/nodes/knxUltimateHueMotion.html +16 -4
  14. package/nodes/knxUltimateHuePlug.html +16 -4
  15. package/nodes/knxUltimateHueScene.html +19 -7
  16. package/nodes/knxUltimateHueTapDial.html +16 -4
  17. package/nodes/knxUltimateHueTemperatureSensor.html +16 -4
  18. package/nodes/knxUltimateHueZigbeeConnectivity.html +16 -4
  19. package/nodes/knxUltimateHuedevice_software_update.html +16 -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 +668 -0
  26. package/nodes/locales/de/knxUltimateHueController.json +47 -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 +671 -0
  43. package/nodes/locales/en/knxUltimateHueController.json +47 -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 +671 -0
  60. package/nodes/locales/es/knxUltimateHueController.json +47 -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 +671 -0
  77. package/nodes/locales/fr/knxUltimateHueController.json +47 -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 +671 -0
  94. package/nodes/locales/it/knxUltimateHueController.json +47 -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 +673 -0
  111. package/nodes/locales/zh-CN/knxUltimateHueController.json +47 -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 +338 -0
  140. package/resources/hueControllerMigrationDialog.js +235 -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.1",
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,338 @@
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
+ const MIGRATION_USAGE_EMAIL = 'maxsupergiovane@icloud.com'
32
+ const MIGRATION_DONATION_URL = 'https://www.paypal.com/donate/?hosted_button_id=S8SKPUBSPK758'
33
+
34
+ // Kept as a tiny public predicate so the editor can decide whether the
35
+ // migration entry point is relevant without duplicating the canonical type
36
+ // catalog. It only classifies an object; it never reads or changes a flow.
37
+ function isLegacyHueNode (node) {
38
+ return Boolean(node && typeof node === 'object' && LEGACY_NODE_PROFILES[node.type])
39
+ }
40
+
41
+ function hasEnabledPins (node, profile) {
42
+ const rawValue = node.enableNodePINS
43
+ if (rawValue === true) return true
44
+ if (rawValue === false) return false
45
+ if (typeof rawValue === 'string' && rawValue.trim() !== '') {
46
+ return rawValue.trim().toLowerCase() === 'yes'
47
+ }
48
+ return profile.pinsDefault === true
49
+ }
50
+
51
+ function readPinCount (value) {
52
+ if (value === undefined || value === null || value === '') return undefined
53
+ const numericValue = Number(value)
54
+ if (!Number.isInteger(numericValue) || numericValue < 0) return undefined
55
+ return numericValue
56
+ }
57
+
58
+ function derivePinShape (node, profile) {
59
+ const enabled = hasEnabledPins(node, profile)
60
+ const defaults = profile.pinMode === 'dual'
61
+ ? { inputs: enabled ? 1 : 0, outputs: enabled ? 1 : 0 }
62
+ : { inputs: 0, outputs: enabled ? 1 : 0 }
63
+ const exportedInputs = readPinCount(node.inputs)
64
+ const exportedOutputs = readPinCount(node.outputs)
65
+ const wiredOutputs = Array.isArray(node.wires) ? node.wires.length : undefined
66
+
67
+ return {
68
+ inputs: exportedInputs === undefined ? defaults.inputs : exportedInputs,
69
+ outputs: exportedOutputs === undefined
70
+ ? (wiredOutputs === undefined ? defaults.outputs : wiredOutputs)
71
+ : exportedOutputs
72
+ }
73
+ }
74
+
75
+ function collectLegacyHueNodes (RED) {
76
+ const nodes = []
77
+ if (!RED || !RED.nodes || typeof RED.nodes.eachNode !== 'function') return nodes
78
+ RED.nodes.eachNode(function (node) {
79
+ if (isLegacyHueNode(node)) nodes.push(node)
80
+ })
81
+ return nodes
82
+ }
83
+
84
+ function createLocalMigrationPatches (legacyNodes) {
85
+ if (!Array.isArray(legacyNodes)) throw new TypeError('Legacy HUE nodes must be an array')
86
+ return legacyNodes.map(function (node, index) {
87
+ if (!isLegacyHueNode(node)) throw new TypeError(`Entry ${index} is not a supported legacy HUE node`)
88
+ const profile = LEGACY_NODE_PROFILES[node.type]
89
+ const pinShape = derivePinShape(node, profile)
90
+ return {
91
+ index,
92
+ type: 'knxUltimateHueController',
93
+ hueControllerType: profile.controllerType,
94
+ inputs: pinShape.inputs,
95
+ outputs: pinShape.outputs
96
+ }
97
+ })
98
+ }
99
+
100
+ function createUsageMailto (convertedCount, options = {}) {
101
+ if (!Number.isInteger(convertedCount) || convertedCount < 1) {
102
+ throw new TypeError('Converted HUE node count must be a positive integer')
103
+ }
104
+ const subject = String(options.subject || 'KNX Ultimate - legacy HUE conversion used')
105
+ const bodyTemplate = String(options.body || 'Hello Massimo,\n\nI used the legacy HUE node conversion button.\nConverted legacy HUE nodes: {{count}}.\n\nOptional notes:\n')
106
+ const body = bodyTemplate.replace(/{{\s*count\s*}}/g, String(convertedCount))
107
+ return `mailto:${MIGRATION_USAGE_EMAIL}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`
108
+ }
109
+
110
+ function openUsageMailto (mailto, environment = {}) {
111
+ if (typeof mailto !== 'string' || !mailto.startsWith('mailto:')) {
112
+ throw new TypeError('A valid mailto link is required')
113
+ }
114
+ const documentObject = environment.document
115
+ if (!documentObject || !documentObject.body || typeof documentObject.createElement !== 'function') {
116
+ throw new Error('The browser cannot open the email draft')
117
+ }
118
+ const mailFrame = documentObject.createElement('iframe')
119
+ mailFrame.setAttribute('aria-hidden', 'true')
120
+ mailFrame.style.display = 'none'
121
+ mailFrame.src = mailto
122
+ documentObject.body.appendChild(mailFrame)
123
+ const removeMailFrame = () => {
124
+ try {
125
+ if (mailFrame.parentNode) mailFrame.parentNode.removeChild(mailFrame)
126
+ } catch (error) { /* best-effort cleanup */ }
127
+ }
128
+ if (typeof environment.setTimeout === 'function') environment.setTimeout(removeMailFrame, 1000)
129
+ return mailFrame
130
+ }
131
+
132
+ function applyLocalMigration (RED, legacyNodes) {
133
+ if (!RED || !RED.nodes || typeof RED.nodes.getType !== 'function') throw new Error('The Node-RED editor API is unavailable')
134
+ const patches = createLocalMigrationPatches(legacyNodes)
135
+ const controllerDefinition = RED.nodes.getType('knxUltimateHueController')
136
+ if (!controllerDefinition) throw new Error('The HUE Controller node is not registered')
137
+
138
+ const previousDirty = typeof RED.nodes.dirty === 'function' ? RED.nodes.dirty() : false
139
+ const prepared = patches.map(function (patch, index) {
140
+ const node = legacyNodes[index]
141
+ if (!node || !isLegacyHueNode(node) || patch.index !== index) {
142
+ throw new Error('A legacy HUE node changed before the local conversion')
143
+ }
144
+ if (RED.workspaces && typeof RED.workspaces.isLocked === 'function' && RED.workspaces.isLocked(node.z)) {
145
+ throw new Error('A flow containing a legacy HUE node is locked')
146
+ }
147
+ return {
148
+ node,
149
+ oldValues: {
150
+ type: node.type,
151
+ _def: node._def,
152
+ _: node._,
153
+ hueControllerType: node.hueControllerType,
154
+ inputs: node.inputs,
155
+ outputs: node.outputs
156
+ },
157
+ oldChanged: node.changed,
158
+ patch
159
+ }
160
+ })
161
+
162
+ const applied = []
163
+ try {
164
+ prepared.forEach(function (entry) {
165
+ const { node, patch } = entry
166
+ node.type = patch.type
167
+ node._def = controllerDefinition
168
+ node._ = controllerDefinition._ || RED._
169
+ node.hueControllerType = patch.hueControllerType
170
+ node.inputs = patch.inputs
171
+ node.outputs = patch.outputs
172
+ node.changed = true
173
+ node.dirty = true
174
+ if (RED.editor && typeof RED.editor.validateNode === 'function') RED.editor.validateNode(node)
175
+ if (RED.events && typeof RED.events.emit === 'function') RED.events.emit('nodes:change', node)
176
+ applied.push(entry)
177
+ })
178
+ } catch (error) {
179
+ applied.reverse().forEach(function (entry) {
180
+ Object.entries(entry.oldValues).forEach(([key, value]) => { entry.node[key] = value })
181
+ entry.node.changed = entry.oldChanged
182
+ entry.node.dirty = true
183
+ })
184
+ if (typeof RED.nodes.dirty === 'function') RED.nodes.dirty(previousDirty)
185
+ if (RED.view && typeof RED.view.redraw === 'function') RED.view.redraw(true)
186
+ throw error
187
+ }
188
+
189
+ if (RED.history && typeof RED.history.push === 'function' && prepared.length > 0) {
190
+ const events = prepared.map((entry) => ({
191
+ t: 'edit',
192
+ node: entry.node,
193
+ changes: entry.oldValues,
194
+ changed: entry.oldChanged,
195
+ dirty: previousDirty
196
+ }))
197
+ RED.history.push(events.length === 1 ? events[0] : { t: 'multi', events })
198
+ }
199
+ if (typeof RED.nodes.dirty === 'function') RED.nodes.dirty(true)
200
+ if (RED.view && typeof RED.view.redraw === 'function') RED.view.redraw(true)
201
+ return prepared.length
202
+ }
203
+
204
+ function cloneFlowNode (node, index) {
205
+ if (!node || typeof node !== 'object' || Array.isArray(node)) {
206
+ throw new TypeError(`Flow entry ${index} is not a Node-RED node object`)
207
+ }
208
+ return JSON.parse(JSON.stringify(node))
209
+ }
210
+
211
+ function copyTextWithTemporaryTextarea (text, documentObject) {
212
+ if (!documentObject || !documentObject.body || typeof documentObject.createElement !== 'function') {
213
+ throw new Error('Clipboard fallback is unavailable')
214
+ }
215
+
216
+ const temporary = documentObject.createElement('textarea')
217
+ temporary.value = String(text)
218
+ temporary.readOnly = true
219
+ temporary.setAttribute('aria-hidden', 'true')
220
+ temporary.style.position = 'fixed'
221
+ temporary.style.left = '-9999px'
222
+ temporary.style.top = '0'
223
+ documentObject.body.appendChild(temporary)
224
+
225
+ try {
226
+ temporary.focus()
227
+ temporary.select()
228
+ if (typeof temporary.setSelectionRange === 'function') {
229
+ temporary.setSelectionRange(0, temporary.value.length)
230
+ }
231
+ if (typeof documentObject.execCommand !== 'function' || documentObject.execCommand('copy') !== true) {
232
+ throw new Error('The browser refused clipboard access')
233
+ }
234
+ return true
235
+ } finally {
236
+ if (temporary.parentNode) temporary.parentNode.removeChild(temporary)
237
+ }
238
+ }
239
+
240
+ function copyTextToClipboard (text, environment = {}) {
241
+ const value = String(text)
242
+ const navigatorObject = environment.navigator
243
+ const documentObject = environment.document
244
+ const clipboard = navigatorObject && navigatorObject.clipboard
245
+ const fallback = () => copyTextWithTemporaryTextarea(value, documentObject)
246
+
247
+ if (clipboard && typeof clipboard.writeText === 'function') {
248
+ try {
249
+ return Promise.resolve(clipboard.writeText(value))
250
+ .then(() => true)
251
+ .catch(fallback)
252
+ } catch (error) {
253
+ return Promise.resolve().then(fallback)
254
+ }
255
+ }
256
+ return Promise.resolve().then(fallback)
257
+ }
258
+
259
+ function convertLegacyHueFlow (sourceFlow, options = {}) {
260
+ if (!Array.isArray(sourceFlow)) {
261
+ throw new TypeError('The pasted Node-RED flow must be a JSON array')
262
+ }
263
+
264
+ let convertedCount = 0
265
+ const convertedByType = {}
266
+ const referencedConfigIds = new Set()
267
+ const flow = sourceFlow.map(function (sourceNode, index) {
268
+ const node = cloneFlowNode(sourceNode, index)
269
+ const profile = LEGACY_NODE_PROFILES[node.type]
270
+ if (!profile) return node
271
+
272
+ const oldType = node.type
273
+ const pinShape = derivePinShape(node, profile)
274
+ node.type = 'knxUltimateHueController'
275
+ node.hueControllerType = profile.controllerType
276
+ node.inputs = pinShape.inputs
277
+ node.outputs = pinShape.outputs
278
+ Object.values(CONFIG_NODE_REFERENCE_FIELDS).forEach(function (field) {
279
+ if (typeof node[field] === 'string' && node[field].trim() !== '') {
280
+ referencedConfigIds.add(node[field])
281
+ }
282
+ })
283
+ convertedCount += 1
284
+ convertedByType[oldType] = (convertedByType[oldType] || 0) + 1
285
+ return node
286
+ })
287
+
288
+ const reuseConfigNodes = options.reuseConfigNodes !== false
289
+ const omittedConfigByType = {}
290
+ const convertedFlow = reuseConfigNodes && convertedCount > 0
291
+ ? flow.filter(function (node) {
292
+ const referenceField = CONFIG_NODE_REFERENCE_FIELDS[node.type]
293
+ const omitNode = Boolean(referenceField && referencedConfigIds.has(node.id))
294
+ if (omitNode) omittedConfigByType[node.type] = (omittedConfigByType[node.type] || 0) + 1
295
+ return !omitNode
296
+ })
297
+ : flow
298
+ const omittedConfigCount = Object.values(omittedConfigByType)
299
+ .reduce((total, count) => total + count, 0)
300
+
301
+ return {
302
+ flow: convertedFlow,
303
+ convertedCount,
304
+ convertedByType,
305
+ omittedConfigCount,
306
+ omittedConfigByType
307
+ }
308
+ }
309
+
310
+ function convertLegacyHueFlowJson (rawJson, options = {}) {
311
+ if (typeof rawJson !== 'string' || rawJson.trim() === '') {
312
+ throw new TypeError('Paste a Node-RED flow before starting the conversion')
313
+ }
314
+ const sourceFlow = JSON.parse(rawJson)
315
+ const result = convertLegacyHueFlow(sourceFlow, options)
316
+ return {
317
+ ...result,
318
+ json: JSON.stringify(result.flow, null, 2)
319
+ }
320
+ }
321
+
322
+ return {
323
+ CONFIG_NODE_REFERENCE_FIELDS,
324
+ LEGACY_NODE_PROFILES,
325
+ MIGRATION_DONATION_URL,
326
+ MIGRATION_USAGE_EMAIL,
327
+ applyLocalMigration,
328
+ collectLegacyHueNodes,
329
+ copyTextToClipboard,
330
+ convertLegacyHueFlow,
331
+ convertLegacyHueFlowJson,
332
+ createLocalMigrationPatches,
333
+ createUsageMailto,
334
+ derivePinShape,
335
+ isLegacyHueNode,
336
+ openUsageMailto
337
+ }
338
+ }))