node-red-contrib-knx-ultimate 7.0.3 → 7.1.0-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 (212) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +2 -0
  3. package/examples/HA Translator - Payload Conversion.json +18 -8
  4. package/examples/KNX Utility - All functions.json +389 -0
  5. package/nodes/commonFunctions.js +5 -4
  6. package/nodes/knxUltimateAlerter.html +15 -4
  7. package/nodes/knxUltimateAutoResponder.html +15 -4
  8. package/nodes/knxUltimateDateTime.html +16 -5
  9. package/nodes/knxUltimateDateTime.js +2 -26
  10. package/nodes/knxUltimateGarage.html +15 -4
  11. package/nodes/knxUltimateGlobalContext.html +15 -4
  12. package/nodes/knxUltimateHATranslator.html +15 -4
  13. package/nodes/knxUltimateHueController.html +3 -4
  14. package/nodes/knxUltimateLoadControl.html +15 -4
  15. package/nodes/knxUltimateLogger.html +15 -4
  16. package/nodes/knxUltimateSceneController.html +15 -4
  17. package/nodes/knxUltimateStaircase.html +15 -4
  18. package/nodes/knxUltimateUtility.html +325 -0
  19. package/nodes/knxUltimateUtility.js +24 -0
  20. package/nodes/knxUltimateViewer.html +16 -3
  21. package/nodes/knxUltimateViewer.js +106 -27
  22. package/nodes/knxUltimateWatchDog.html +15 -4
  23. package/nodes/locales/de/knxUltimateAlerter.html +2 -0
  24. package/nodes/locales/de/knxUltimateAlerter.json +5 -3
  25. package/nodes/locales/de/knxUltimateAutoResponder.html +2 -0
  26. package/nodes/locales/de/knxUltimateAutoResponder.json +4 -2
  27. package/nodes/locales/de/knxUltimateDateTime.html +2 -0
  28. package/nodes/locales/de/knxUltimateDateTime.json +5 -3
  29. package/nodes/locales/de/knxUltimateGarage.html +2 -0
  30. package/nodes/locales/de/knxUltimateGarage.json +5 -2
  31. package/nodes/locales/de/knxUltimateGlobalContext.html +2 -0
  32. package/nodes/locales/de/knxUltimateGlobalContext.json +21 -18
  33. package/nodes/locales/de/knxUltimateHATranslator.html +2 -0
  34. package/nodes/locales/de/knxUltimateHATranslator.json +4 -2
  35. package/nodes/locales/de/knxUltimateHueController.html +2 -1
  36. package/nodes/locales/de/knxUltimateHueController.json +4 -3
  37. package/nodes/locales/de/knxUltimateLoadControl.html +2 -0
  38. package/nodes/locales/de/knxUltimateLoadControl.json +5 -2
  39. package/nodes/locales/de/knxUltimateLogger.html +2 -0
  40. package/nodes/locales/de/knxUltimateLogger.json +6 -3
  41. package/nodes/locales/de/knxUltimateSceneController.html +2 -0
  42. package/nodes/locales/de/knxUltimateSceneController.json +8 -6
  43. package/nodes/locales/de/knxUltimateStaircase.html +2 -0
  44. package/nodes/locales/de/knxUltimateStaircase.json +5 -2
  45. package/nodes/locales/de/knxUltimateUtility.html +922 -0
  46. package/nodes/locales/de/knxUltimateUtility.json +56 -0
  47. package/nodes/locales/de/knxUltimateViewer.html +19 -453
  48. package/nodes/locales/de/knxUltimateViewer.json +9 -1
  49. package/nodes/locales/de/knxUltimateWatchDog.html +2 -0
  50. package/nodes/locales/de/knxUltimateWatchDog.json +5 -2
  51. package/nodes/locales/en/knxUltimateAlerter.html +2 -0
  52. package/nodes/locales/en/knxUltimateAlerter.json +5 -3
  53. package/nodes/locales/en/knxUltimateAutoResponder.html +2 -0
  54. package/nodes/locales/en/knxUltimateAutoResponder.json +4 -2
  55. package/nodes/locales/en/knxUltimateDateTime.html +2 -0
  56. package/nodes/locales/en/knxUltimateDateTime.json +5 -3
  57. package/nodes/locales/en/knxUltimateGarage.html +2 -0
  58. package/nodes/locales/en/knxUltimateGarage.json +5 -2
  59. package/nodes/locales/en/knxUltimateGlobalContext.html +2 -0
  60. package/nodes/locales/en/knxUltimateGlobalContext.json +21 -18
  61. package/nodes/locales/en/knxUltimateHATranslator.html +2 -0
  62. package/nodes/locales/en/knxUltimateHATranslator.json +4 -2
  63. package/nodes/locales/en/knxUltimateHueController.html +2 -1
  64. package/nodes/locales/en/knxUltimateHueController.json +4 -3
  65. package/nodes/locales/en/knxUltimateLoadControl.html +2 -0
  66. package/nodes/locales/en/knxUltimateLoadControl.json +5 -2
  67. package/nodes/locales/en/knxUltimateLogger.html +2 -0
  68. package/nodes/locales/en/knxUltimateLogger.json +6 -3
  69. package/nodes/locales/en/knxUltimateSceneController.html +2 -0
  70. package/nodes/locales/en/knxUltimateSceneController.json +15 -14
  71. package/nodes/locales/en/knxUltimateStaircase.html +2 -0
  72. package/nodes/locales/en/knxUltimateStaircase.json +5 -2
  73. package/nodes/locales/en/knxUltimateUtility.html +1100 -0
  74. package/nodes/locales/en/knxUltimateUtility.json +56 -0
  75. package/nodes/locales/en/knxUltimateViewer.html +18 -457
  76. package/nodes/locales/en/knxUltimateViewer.json +9 -1
  77. package/nodes/locales/en/knxUltimateWatchDog.html +2 -0
  78. package/nodes/locales/en/knxUltimateWatchDog.json +5 -3
  79. package/nodes/locales/es/knxUltimateAlerter.html +2 -0
  80. package/nodes/locales/es/knxUltimateAlerter.json +5 -2
  81. package/nodes/locales/es/knxUltimateAutoResponder.html +2 -0
  82. package/nodes/locales/es/knxUltimateAutoResponder.json +4 -1
  83. package/nodes/locales/es/knxUltimateDateTime.html +2 -0
  84. package/nodes/locales/es/knxUltimateDateTime.json +5 -3
  85. package/nodes/locales/es/knxUltimateGarage.html +2 -0
  86. package/nodes/locales/es/knxUltimateGarage.json +5 -2
  87. package/nodes/locales/es/knxUltimateGlobalContext.html +2 -0
  88. package/nodes/locales/es/knxUltimateGlobalContext.json +5 -2
  89. package/nodes/locales/es/knxUltimateHATranslator.html +2 -0
  90. package/nodes/locales/es/knxUltimateHATranslator.json +4 -1
  91. package/nodes/locales/es/knxUltimateHueController.html +2 -1
  92. package/nodes/locales/es/knxUltimateHueController.json +4 -3
  93. package/nodes/locales/es/knxUltimateLoadControl.html +2 -0
  94. package/nodes/locales/es/knxUltimateLoadControl.json +5 -2
  95. package/nodes/locales/es/knxUltimateLogger.html +2 -0
  96. package/nodes/locales/es/knxUltimateLogger.json +5 -2
  97. package/nodes/locales/es/knxUltimateSceneController.html +2 -0
  98. package/nodes/locales/es/knxUltimateSceneController.json +5 -2
  99. package/nodes/locales/es/knxUltimateStaircase.html +2 -0
  100. package/nodes/locales/es/knxUltimateStaircase.json +5 -2
  101. package/nodes/locales/es/knxUltimateUtility.html +1080 -0
  102. package/nodes/locales/es/knxUltimateUtility.json +56 -0
  103. package/nodes/locales/es/knxUltimateViewer.html +18 -450
  104. package/nodes/locales/es/knxUltimateViewer.json +16 -8
  105. package/nodes/locales/es/knxUltimateWatchDog.html +2 -0
  106. package/nodes/locales/es/knxUltimateWatchDog.json +5 -2
  107. package/nodes/locales/fr/knxUltimateAlerter.html +2 -0
  108. package/nodes/locales/fr/knxUltimateAlerter.json +5 -2
  109. package/nodes/locales/fr/knxUltimateAutoResponder.html +2 -0
  110. package/nodes/locales/fr/knxUltimateAutoResponder.json +4 -1
  111. package/nodes/locales/fr/knxUltimateDateTime.html +2 -0
  112. package/nodes/locales/fr/knxUltimateDateTime.json +5 -3
  113. package/nodes/locales/fr/knxUltimateGarage.html +2 -0
  114. package/nodes/locales/fr/knxUltimateGarage.json +5 -2
  115. package/nodes/locales/fr/knxUltimateGlobalContext.html +2 -0
  116. package/nodes/locales/fr/knxUltimateGlobalContext.json +5 -2
  117. package/nodes/locales/fr/knxUltimateHATranslator.html +2 -0
  118. package/nodes/locales/fr/knxUltimateHATranslator.json +4 -1
  119. package/nodes/locales/fr/knxUltimateHueController.html +2 -1
  120. package/nodes/locales/fr/knxUltimateHueController.json +4 -3
  121. package/nodes/locales/fr/knxUltimateLoadControl.html +2 -0
  122. package/nodes/locales/fr/knxUltimateLoadControl.json +5 -2
  123. package/nodes/locales/fr/knxUltimateLogger.html +2 -0
  124. package/nodes/locales/fr/knxUltimateLogger.json +5 -2
  125. package/nodes/locales/fr/knxUltimateSceneController.html +2 -0
  126. package/nodes/locales/fr/knxUltimateSceneController.json +5 -2
  127. package/nodes/locales/fr/knxUltimateStaircase.html +2 -0
  128. package/nodes/locales/fr/knxUltimateStaircase.json +5 -2
  129. package/nodes/locales/fr/knxUltimateUtility.html +1081 -0
  130. package/nodes/locales/fr/knxUltimateUtility.json +56 -0
  131. package/nodes/locales/fr/knxUltimateViewer.html +18 -450
  132. package/nodes/locales/fr/knxUltimateViewer.json +16 -8
  133. package/nodes/locales/fr/knxUltimateWatchDog.html +2 -0
  134. package/nodes/locales/fr/knxUltimateWatchDog.json +5 -2
  135. package/nodes/locales/it/knxUltimateAlerter.html +2 -0
  136. package/nodes/locales/it/knxUltimateAlerter.json +5 -3
  137. package/nodes/locales/it/knxUltimateAutoResponder.html +2 -0
  138. package/nodes/locales/it/knxUltimateAutoResponder.json +4 -2
  139. package/nodes/locales/it/knxUltimateDateTime.html +2 -0
  140. package/nodes/locales/it/knxUltimateDateTime.json +5 -3
  141. package/nodes/locales/it/knxUltimateGarage.html +2 -0
  142. package/nodes/locales/it/knxUltimateGarage.json +5 -2
  143. package/nodes/locales/it/knxUltimateGlobalContext.html +2 -0
  144. package/nodes/locales/it/knxUltimateGlobalContext.json +21 -18
  145. package/nodes/locales/it/knxUltimateHATranslator.html +2 -0
  146. package/nodes/locales/it/knxUltimateHATranslator.json +4 -2
  147. package/nodes/locales/it/knxUltimateHueController.html +2 -1
  148. package/nodes/locales/it/knxUltimateHueController.json +4 -3
  149. package/nodes/locales/it/knxUltimateLoadControl.html +2 -0
  150. package/nodes/locales/it/knxUltimateLoadControl.json +5 -2
  151. package/nodes/locales/it/knxUltimateLogger.html +2 -0
  152. package/nodes/locales/it/knxUltimateLogger.json +6 -3
  153. package/nodes/locales/it/knxUltimateSceneController.html +2 -0
  154. package/nodes/locales/it/knxUltimateSceneController.json +8 -7
  155. package/nodes/locales/it/knxUltimateStaircase.html +2 -0
  156. package/nodes/locales/it/knxUltimateStaircase.json +5 -2
  157. package/nodes/locales/it/knxUltimateUtility.html +959 -0
  158. package/nodes/locales/it/knxUltimateUtility.json +56 -0
  159. package/nodes/locales/it/knxUltimateViewer.html +19 -453
  160. package/nodes/locales/it/knxUltimateViewer.json +9 -1
  161. package/nodes/locales/it/knxUltimateWatchDog.html +2 -0
  162. package/nodes/locales/it/knxUltimateWatchDog.json +6 -6
  163. package/nodes/locales/zh-CN/knxUltimateAlerter.html +2 -0
  164. package/nodes/locales/zh-CN/knxUltimateAlerter.json +5 -3
  165. package/nodes/locales/zh-CN/knxUltimateAutoResponder.html +2 -0
  166. package/nodes/locales/zh-CN/knxUltimateAutoResponder.json +4 -2
  167. package/nodes/locales/zh-CN/knxUltimateDateTime.html +2 -0
  168. package/nodes/locales/zh-CN/knxUltimateDateTime.json +5 -3
  169. package/nodes/locales/zh-CN/knxUltimateGarage.html +2 -0
  170. package/nodes/locales/zh-CN/knxUltimateGarage.json +5 -2
  171. package/nodes/locales/zh-CN/knxUltimateGlobalContext.html +2 -0
  172. package/nodes/locales/zh-CN/knxUltimateGlobalContext.json +21 -18
  173. package/nodes/locales/zh-CN/knxUltimateHATranslator.html +2 -0
  174. package/nodes/locales/zh-CN/knxUltimateHATranslator.json +4 -2
  175. package/nodes/locales/zh-CN/knxUltimateHueController.html +2 -1
  176. package/nodes/locales/zh-CN/knxUltimateHueController.json +4 -3
  177. package/nodes/locales/zh-CN/knxUltimateLoadControl.html +2 -0
  178. package/nodes/locales/zh-CN/knxUltimateLoadControl.json +5 -2
  179. package/nodes/locales/zh-CN/knxUltimateLogger.html +2 -0
  180. package/nodes/locales/zh-CN/knxUltimateLogger.json +6 -3
  181. package/nodes/locales/zh-CN/knxUltimateSceneController.html +2 -0
  182. package/nodes/locales/zh-CN/knxUltimateSceneController.json +12 -10
  183. package/nodes/locales/zh-CN/knxUltimateStaircase.html +2 -0
  184. package/nodes/locales/zh-CN/knxUltimateStaircase.json +5 -2
  185. package/nodes/locales/zh-CN/knxUltimateUtility.html +930 -0
  186. package/nodes/locales/zh-CN/knxUltimateUtility.json +56 -0
  187. package/nodes/locales/zh-CN/knxUltimateViewer.html +19 -452
  188. package/nodes/locales/zh-CN/knxUltimateViewer.json +9 -1
  189. package/nodes/locales/zh-CN/knxUltimateWatchDog.html +2 -0
  190. package/nodes/locales/zh-CN/knxUltimateWatchDog.json +5 -3
  191. package/nodes/plugins/knxUltimateViewer-vue/assets/app.css +1 -1
  192. package/nodes/plugins/knxUltimateViewer-vue/assets/app.js +2 -2
  193. package/nodes/utils/knxDateTimeSendNow.js +30 -0
  194. package/nodes/utils/knxUtilityProfileAdapter.js +66 -0
  195. package/nodes/utils/knxUtilityProfiles/runtime/alerter.js +305 -0
  196. package/nodes/utils/knxUtilityProfiles/runtime/autoresponder.js +266 -0
  197. package/nodes/utils/knxUtilityProfiles/runtime/datetime.js +279 -0
  198. package/nodes/utils/knxUtilityProfiles/runtime/garage.js +513 -0
  199. package/nodes/utils/knxUtilityProfiles/runtime/globalcontext.js +218 -0
  200. package/nodes/utils/knxUtilityProfiles/runtime/hatranslator.js +95 -0
  201. package/nodes/utils/knxUtilityProfiles/runtime/loadcontrol.js +448 -0
  202. package/nodes/utils/knxUtilityProfiles/runtime/logger.js +233 -0
  203. package/nodes/utils/knxUtilityProfiles/runtime/profileTimers.js +35 -0
  204. package/nodes/utils/knxUtilityProfiles/runtime/scenecontroller.js +487 -0
  205. package/nodes/utils/knxUtilityProfiles/runtime/staircase.js +472 -0
  206. package/nodes/utils/knxUtilityProfiles/runtime/watchdog.js +240 -0
  207. package/nodes/utils/knxViewerHistory.js +424 -0
  208. package/package.json +9 -6
  209. package/resources/flowMigrationBackup.js +81 -0
  210. package/resources/hueControllerMigrationDialog.js +15 -4
  211. package/resources/knxUtilityMigration.js +276 -0
  212. package/resources/knxUtilityProfiles.js +2488 -0
@@ -0,0 +1,325 @@
1
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
2
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/knxUtilityProfiles.js"></script>
3
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/flowMigrationBackup.js"></script>
4
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/knxUtilityMigration.js"></script>
5
+
6
+ <script type="text/javascript">
7
+ (function () {
8
+ const profiles = window.KNXUltimateUtilityProfiles;
9
+ const types = Object.keys(profiles.PROFILE_TYPES);
10
+ const clone = (value) => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
11
+ const definition = (type) => types.includes(type) ? profiles.getDefinition(type, RED) : undefined;
12
+ const text = (key) => RED._('node-red-contrib-knx-ultimate/knxUltimateUtility:knxUltimateUtility.' + key);
13
+ const activeType = (node) => node._utilityEditor ? node._utilityEditor.type() : node.utilityType;
14
+ const validationContext = (node) => typeof node._utilityEditor?.values === 'function' ? node._utilityEditor.values() : node;
15
+
16
+ function buildDefaults() {
17
+ // Node-RED must know every persistable field and the real config-node
18
+ // reference before opening the editor. Only the active profile validates.
19
+ const defaults = {
20
+ server: {
21
+ value: '',
22
+ type: 'knxUltimate-config',
23
+ validate: function (value) {
24
+ const descriptor = definition(activeType(this))?.defaults.server;
25
+ if (!descriptor) return true;
26
+ if (!value || value === '_ADD_') return false;
27
+ const gateway = RED.nodes.node(value);
28
+ return Boolean(gateway && gateway.type === 'knxUltimate-config' && gateway.valid !== false);
29
+ }
30
+ },
31
+ name: {
32
+ value: '',
33
+ validate: function (value) {
34
+ const descriptor = definition(activeType(this))?.defaults.name;
35
+ return !descriptor || typeof descriptor.validate !== 'function' || descriptor.validate.call(validationContext(this), value);
36
+ }
37
+ },
38
+ // The bus address is shared by WatchDog, Logger, SceneController and
39
+ // LoadControl. Imports that omit it must not inherit WatchDog's sample GA.
40
+ topic: {
41
+ value: '',
42
+ validate: function (value) {
43
+ const descriptor = definition(activeType(this))?.defaults.topic;
44
+ if (!descriptor) return true;
45
+ if (descriptor.required && (value === '' || value === undefined || value === null)) return false;
46
+ return typeof descriptor.validate !== 'function' || descriptor.validate.call(validationContext(this), value);
47
+ }
48
+ },
49
+ utilityType: { value: 'alerter', required: true, validate: (value) => types.includes(value) },
50
+ inputs: { value: 1 },
51
+ outputs: { value: 3 }
52
+ };
53
+ types.forEach((type) => {
54
+ Object.entries(definition(type).defaults).forEach(([key, property]) => {
55
+ if (Object.prototype.hasOwnProperty.call(defaults, key)) return;
56
+ const descriptor = { ...property };
57
+ delete descriptor.required;
58
+ defaults[key] = {
59
+ ...descriptor,
60
+ value: clone(property.value),
61
+ validate: function (value) {
62
+ const selected = definition(activeType(this));
63
+ const descriptor = selected && selected.defaults[key];
64
+ if (!descriptor) return true;
65
+ if (descriptor.required && (value === '' || value === undefined || value === null)) return false;
66
+ return typeof descriptor.validate !== 'function' || descriptor.validate.call(validationContext(this), value);
67
+ }
68
+ };
69
+ });
70
+ });
71
+ return defaults;
72
+ }
73
+
74
+ function setValue(field, value) {
75
+ if (!field.length) return;
76
+ if (field.is(':checkbox')) field.prop('checked', value === true || value === 'true');
77
+ else if (value !== undefined && value !== null) field.val(typeof value === 'object' ? JSON.stringify(value) : value);
78
+ }
79
+
80
+ function readValue(field, fallback) {
81
+ if (!field.length) return fallback;
82
+ if (field.is(':checkbox')) return field.prop('checked');
83
+ const value = field.val();
84
+ return value === undefined || value === null ? fallback : value;
85
+ }
86
+
87
+ function translateFragment(container, type) {
88
+ container.find('[data-i18n]').each(function () {
89
+ const element = $(this);
90
+ const spec = element.attr('data-i18n');
91
+ const match = spec.match(/^\[([^\]]+)\](.+)$/);
92
+ const key = match ? match[2] : spec;
93
+ const value = profiles.translate(type, key, RED);
94
+ if (value && value !== key) {
95
+ if (match) element.attr(match[1], value);
96
+ else element.text(value);
97
+ element.removeAttr('data-i18n');
98
+ }
99
+ });
100
+ }
101
+
102
+ RED.nodes.registerType('knxUltimateUtility', {
103
+ category: 'KNX Ultimate',
104
+ color: '#C7E9C0',
105
+ defaults: buildDefaults(),
106
+ catalogDefaults: {
107
+ server: { type: 'knxUltimate-config' }, name: {}, utilityType: {}, inputs: {}, outputs: {}
108
+ },
109
+ inputs: 1,
110
+ outputs: 3,
111
+ icon: function () { return definition(this.utilityType)?.icon || 'node-knx-icon.svg'; },
112
+ paletteLabel: 'KNX Utility',
113
+ label: function () { return this.name || 'KNX Utility · ' + text('types.' + this.utilityType); },
114
+ outputLabels: function (index) {
115
+ if (this.utilityType === 'alerter') return text('outputs.' + index);
116
+ const selected = definition(this.utilityType);
117
+ if (this.utilityType === 'logger') return text('logger_outputs.' + index);
118
+ const labels = selected?.outputLabels;
119
+ if (typeof labels === 'function') return labels.call(this, index);
120
+ return Array.isArray(labels) ? labels[index] : '';
121
+ },
122
+ button: {
123
+ visible: function () { return this.utilityType === 'datetime'; },
124
+ enabled: function () { return !this.changed && this.utilityType === 'datetime'; },
125
+ onclick: function () {
126
+ const selected = definition(this.utilityType);
127
+ if (selected?.button?.onclick) selected.button.onclick.call(this);
128
+ }
129
+ },
130
+ oneditprepare: function () {
131
+ const node = this;
132
+ const drafts = {};
133
+ let selectedType;
134
+ let context;
135
+ let mounted;
136
+ let closed = false;
137
+ let helpTimer;
138
+ const container = $('#knx-utility-profile-editor');
139
+
140
+ // Edit a separate context: profile hooks (including asynchronous ETS
141
+ // suggestions) must never mutate the saved node before Done is pressed.
142
+ function makeContext(type, useSaved) {
143
+ const selected = definition(type);
144
+ const result = { id: node.id, z: node.z, type: node.type, changed: node.changed };
145
+ Object.entries(selected.defaults).forEach(([key, property]) => {
146
+ result[key] = clone(useSaved && node[key] !== undefined ? node[key] : property.value);
147
+ });
148
+ result.utilityType = type;
149
+ result._ = (key, values) => profiles.translate(type, key, RED, values);
150
+ return result;
151
+ }
152
+
153
+ function collect() {
154
+ if (!mounted || !context) return;
155
+ Object.keys(mounted.defaults).forEach((key) => {
156
+ context[key] = readValue($('#node-input-' + key), context[key]);
157
+ });
158
+ if (typeof mounted.oneditsave === 'function') mounted.oneditsave.call(context);
159
+ drafts[selectedType] = context;
160
+ }
161
+
162
+ function dispose() {
163
+ clearTimeout(helpTimer);
164
+ if (mounted && context && typeof mounted.oneditcancel === 'function') mounted.oneditcancel.call(context);
165
+ $('#node-input-server').off('.knxUtilityProfile');
166
+ // Widget destruction also releases autocomplete menus appended to body.
167
+ container.find('.ui-autocomplete-input').each(function () {
168
+ try { $(this).autocomplete('destroy'); } catch (error) { /* already disposed */ }
169
+ });
170
+ container.empty();
171
+ }
172
+
173
+ function render(type, first) {
174
+ if (closed) return;
175
+ if (!first && type === selectedType) return;
176
+ if (!first) collect();
177
+ dispose();
178
+ selectedType = type;
179
+ mounted = definition(type);
180
+ $('#knx-utility-invalid-function').toggle(!mounted);
181
+ if (!mounted) { context = undefined; return; }
182
+ context = drafts[type] || makeContext(type, first);
183
+ // Shared fields retain the user's current selections across profiles.
184
+ context.server = readValue($('#node-input-server'), node.server);
185
+ context.name = readValue($('#node-input-name'), node.name);
186
+ $('#knx-utility-gateway-row').toggle(Boolean(mounted.defaults.server));
187
+ const isGlobalContext = type === 'globalcontext';
188
+ if (isGlobalContext && !context.name) {
189
+ context.name = mounted.defaults.name.value;
190
+ $('#node-input-name').val(context.name);
191
+ }
192
+ $('#knx-utility-name-label').text(text(isGlobalContext ? 'variable_name' : 'name')).removeAttr('data-i18n');
193
+ $('#knx-utility-name-description').toggle(isGlobalContext);
194
+ container.html(profiles.getTemplate(type));
195
+ translateFragment(container, type);
196
+ Object.keys(mounted.defaults).forEach((key) => {
197
+ if (key !== 'name' && key !== 'server') setValue($('#node-input-' + key), context[key]);
198
+ });
199
+ $('#node-input-inputs').val(mounted.inputs);
200
+ $('#node-input-outputs').val(mounted.outputs);
201
+ $('#knx-utility-pin-description').text(text('pins.' + type));
202
+ $('#node-input-name').attr('placeholder', isGlobalContext ? mounted.defaults.name.value : 'KNX Utility · ' + text('types.' + type));
203
+ if (typeof mounted.oneditprepare === 'function') mounted.oneditprepare.call(context);
204
+ // These controls are mounted after Node-RED attaches its initial
205
+ // validation handlers. Trigger a shared field's standard handler so
206
+ // the complete editor validates again while the user edits a profile.
207
+ container.find('input, select, textarea').on('change.knxUtility keyup.knxUtility paste.knxUtility', function () {
208
+ if (!$(this).attr('skipValidation')) $('#node-input-name').trigger('change');
209
+ });
210
+ $('#node-input-name').trigger('change');
211
+ try { RED.sidebar.show('help'); } catch (error) { /* sidebar unavailable */ }
212
+ // Node-RED can replace help after the tray opens even when the previous
213
+ // node's markers still exist. Follow the selected function through
214
+ // that short transition and cancel retries on close.
215
+ let attempts = 0;
216
+ const focusHelp = () => {
217
+ if (closed || selectedType !== type || typeof document === 'undefined') return;
218
+ const marker = document.getElementById('knx-utility-help-' + type);
219
+ if (marker) marker.scrollIntoView({ block: 'start' });
220
+ if (++attempts < 4) helpTimer = setTimeout(focusHelp, 100);
221
+ };
222
+ helpTimer = setTimeout(focusHelp, 100);
223
+ }
224
+
225
+ node._utilityEditor = {
226
+ type: () => selectedType,
227
+ values: () => {
228
+ const values = { ...context, _utilityEditor: node._utilityEditor, _def: node._def };
229
+ Object.keys(mounted?.defaults || {}).forEach((key) => {
230
+ values[key] = readValue($('#node-input-' + key), values[key]);
231
+ });
232
+ return values;
233
+ },
234
+ save: () => {
235
+ collect();
236
+ if (!mounted || !context) return;
237
+ // The gateway selector belongs to Node-RED's normal save path,
238
+ // which also updates config-node users when its reference changes.
239
+ Object.keys(mounted.defaults).forEach((key) => {
240
+ if (key !== 'server') node[key] = clone(context[key]);
241
+ });
242
+ node.utilityType = selectedType;
243
+ node.inputs = mounted.inputs;
244
+ node.outputs = mounted.outputs;
245
+ },
246
+ resize: (size) => {
247
+ if (mounted && typeof mounted.oneditresize === 'function') mounted.oneditresize.call(context, size);
248
+ },
249
+ close: () => {
250
+ closed = true;
251
+ dispose();
252
+ $('#node-input-utilityType').off('.knxUtility');
253
+ }
254
+ };
255
+ const initialType = node.utilityType || 'alerter';
256
+ $('#node-input-utilityType').val(initialType).on('change.knxUtility', function () { render($(this).val(), false); });
257
+ const migration = window.KNXUltimateUtilityMigration;
258
+ $('#knx-utility-migration-row').toggle(Boolean(migration && migration.collectLegacyUtilityNodes(RED).length));
259
+ render(initialType, true);
260
+ },
261
+ oneditsave: function () {
262
+ if (this._utilityEditor) {
263
+ this._utilityEditor.save();
264
+ this._utilityEditor.close();
265
+ delete this._utilityEditor;
266
+ }
267
+ },
268
+ oneditcancel: function () {
269
+ if (this._utilityEditor) {
270
+ this._utilityEditor.close();
271
+ delete this._utilityEditor;
272
+ }
273
+ },
274
+ oneditresize: function (size) {
275
+ if (this._utilityEditor) this._utilityEditor.resize(size);
276
+ }
277
+ });
278
+ }());
279
+ </script>
280
+
281
+ <style>
282
+ #node-input-utilityType { max-width: calc(100% - 160px); }
283
+ #dialog-form:has(#knx-utility-profile-editor) > .form-row > label { width: 150px; }
284
+ #knx-utility-profile-editor .form-row > label { width: 180px; }
285
+ #knx-utility-profile-editor .form-row { box-sizing: border-box; }
286
+ #knx-utility-profile-editor input { min-width: 0; }
287
+ #knx-utility-migration-row .red-ui-button { background: #d97706; border-color: #b45309; color: white; }
288
+ </style>
289
+
290
+ <script type="text/html" data-template-name="knxUltimateUtility">
291
+ <div class="form-row" id="knx-utility-migration-row" style="display:none">
292
+ <button type="button" class="red-ui-button knx-utility-migrate-flow" data-i18n="[title]knxUltimateUtility.migration_button_title"><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateUtility.migration_button"></span></button>
293
+ </div>
294
+ <div class="form-row">
295
+ <label for="node-input-utilityType"><i class="fa fa-cogs"></i> <span data-i18n="knxUltimateUtility.function"></span></label>
296
+ <select id="node-input-utilityType" style="width:70%">
297
+ <option value="alerter" data-i18n="knxUltimateUtility.types.alerter"></option>
298
+ <option value="autoresponder" data-i18n="knxUltimateUtility.types.autoresponder"></option>
299
+ <option value="datetime" data-i18n="knxUltimateUtility.types.datetime"></option>
300
+ <option value="watchdog" data-i18n="knxUltimateUtility.types.watchdog"></option>
301
+ <option value="globalcontext" data-i18n="knxUltimateUtility.types.globalcontext"></option>
302
+ <option value="logger" data-i18n="knxUltimateUtility.types.logger"></option>
303
+ <option value="staircase" data-i18n="knxUltimateUtility.types.staircase"></option>
304
+ <option value="garage" data-i18n="knxUltimateUtility.types.garage"></option>
305
+ <option value="scenecontroller" data-i18n="knxUltimateUtility.types.scenecontroller"></option>
306
+ <option value="loadcontrol" data-i18n="knxUltimateUtility.types.loadcontrol"></option>
307
+ <option value="hatranslator" data-i18n="knxUltimateUtility.types.hatranslator"></option>
308
+ </select>
309
+ </div>
310
+ <div class="form-row" id="knx-utility-gateway-row">
311
+ <label for="node-input-server"><i class="fa fa-circle-o"></i> <span data-i18n="knxUltimateUtility.gateway"></span></label>
312
+ <input type="text" id="node-input-server">
313
+ </div>
314
+ <div class="form-row">
315
+ <label for="node-input-name"><i class="fa fa-tag"></i> <span id="knx-utility-name-label" data-i18n="knxUltimateUtility.name"></span></label>
316
+ <input type="text" id="node-input-name">
317
+ </div>
318
+ <div class="form-row" id="knx-utility-name-description" style="display:none" data-i18n="knxUltimateUtility.variable_name_description"></div>
319
+ <div class="form-row" id="knx-utility-pin-description" style="opacity:.8"></div>
320
+ <div class="form-tips" id="knx-utility-invalid-function" style="display:none" data-i18n="knxUltimateUtility.invalid_function"></div>
321
+ <input type="hidden" id="node-input-inputs">
322
+ <input type="hidden" id="node-input-outputs">
323
+ <hr>
324
+ <div id="knx-utility-profile-editor"></div>
325
+ </script>
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+ const { setupKnxUtilityRuntime } = require('./utils/knxUtilityProfileAdapter')
4
+ const registerSendNowEndpoint = require('./utils/knxDateTimeSendNow')
5
+
6
+ module.exports = function (RED) {
7
+ // Register against the real RED instance; private profile constructors use a
8
+ // facade that deliberately cannot register public Node-RED node types.
9
+ registerSendNowEndpoint(RED, 'utility')
10
+
11
+ function knxUltimateUtility (config) {
12
+ try {
13
+ setupKnxUtilityRuntime(RED, this, config)
14
+ } catch (error) {
15
+ if (!this.id) RED.nodes.createNode(this, config)
16
+ const message = error && error.message ? error.message : String(error)
17
+ RED.log.error(`knxUltimateUtility: ${message}`)
18
+ this.status({ fill: 'red', shape: 'dot', text: message })
19
+ this.error(message)
20
+ }
21
+ }
22
+
23
+ RED.nodes.registerType('knxUltimateUtility', knxUltimateUtility)
24
+ }
@@ -66,12 +66,24 @@
66
66
  return "";
67
67
  }
68
68
  };
69
+ const resolveLanguage = () => {
70
+ let language;
71
+ try {
72
+ const translated = RED._("knxUltimateViewer.locale");
73
+ if (translated && translated !== "knxUltimateViewer.locale") language = translated;
74
+ } catch (error) { }
75
+ const raw = String(language || (RED.settings && RED.settings.lang) || (window.navigator && window.navigator.language) || "en").replace(/_/g, "-").toLowerCase();
76
+ if (raw.startsWith("zh")) return "zh-CN";
77
+ const base = raw.split("-")[0];
78
+ return ["en", "it", "de", "fr", "es"].includes(base) ? base : "en";
79
+ };
69
80
  $("#knx-viewer-open-web-page").on("click", function (evt) {
70
81
  evt.preventDefault();
71
82
  const adminRoot = resolveAdminRoot();
72
83
  const targetBase = adminRoot + "/knxUltimateViewer/page";
73
84
  const params = new URLSearchParams();
74
85
  if (nodeId) params.set("nodeId", nodeId);
86
+ params.set("lang", resolveLanguage());
75
87
  const accessToken = resolveAccessToken();
76
88
  if (accessToken) params.set("access_token", accessToken);
77
89
  const target = targetBase + (params.toString() ? ("?" + params.toString()) : "");
@@ -126,13 +138,14 @@
126
138
  </div>
127
139
 
128
140
  <div class="form-row">
129
- <label><i class="fa fa-external-link"></i> Web UI</label>
141
+ <label><i class="fa fa-external-link"></i> <span data-i18n="knxUltimateViewer.web-monitor"></span></label>
130
142
  <div style="display:flex; gap:8px; flex-wrap:wrap;">
131
- <button type="button" class="red-ui-button" id="knx-viewer-open-web-page" style="background-color:#2bb673; border-color:#2bb673; color:#ffffff !important; -webkit-text-fill-color:#ffffff;">
132
- <i class="fa fa-leaf" style="color:#10341f !important;"></i> <span style="color:#10341f !important;">Open KNX Viewer Web Page</span>
143
+ <button type="button" class="red-ui-button" id="knx-viewer-open-web-page" data-i18n="[title]knxUltimateViewer.open-monitor-tooltip">
144
+ <i class="fa fa-list-alt"></i> <span data-i18n="knxUltimateViewer.open-monitor"></span>
133
145
  </button>
134
146
  </div>
135
147
  </div>
148
+ <div class="form-tips" data-i18n="knxUltimateViewer.monitor-description"></div>
136
149
 
137
150
 
138
151
 
@@ -3,9 +3,9 @@ const path = require('path')
3
3
  const KNXAddress = require('knxultimate').KNXAddress
4
4
  const _ = require('lodash')
5
5
  const { getRequestAccessToken, normalizeAuthFromAccessTokenQuery } = require('./utils/httpAdminAccessToken')
6
+ const createViewerHistory = require('./utils/knxViewerHistory')
6
7
 
7
- let viewerAdminEndpointsRegistered = false
8
- const viewerRuntimeNodes = new Map()
8
+ const viewerRuntimeContexts = new WeakMap()
9
9
  const knxUltimateViewerVueDistDir = path.join(__dirname, 'plugins', 'knxUltimateViewer-vue')
10
10
 
11
11
  const sendKnxUltimateViewerVueIndex = (req, res) => {
@@ -163,7 +163,20 @@ const buildViewerWebState = (node) => {
163
163
  }
164
164
 
165
165
  module.exports = function (RED) {
166
- if (!viewerAdminEndpointsRegistered) {
166
+ let context = viewerRuntimeContexts.get(RED)
167
+ if (!context) {
168
+ context = { nodes: new Map(), endpointsRegistered: false }
169
+ viewerRuntimeContexts.set(RED, context)
170
+ }
171
+ const viewerRuntimeNodes = context.nodes
172
+ const findViewer = nodeId => nodeId ? viewerRuntimeNodes.get(nodeId) : viewerRuntimeNodes.values().next().value
173
+ const viewerDetails = node => ({
174
+ id: node.id,
175
+ name: node.name || 'KNXViewer',
176
+ gatewayId: node.serverKNX ? node.serverKNX.id : '',
177
+ gatewayName: node.serverKNX ? node.serverKNX.name || '' : ''
178
+ })
179
+ if (!context.endpointsRegistered) {
167
180
  RED.httpAdmin.use('/knxUltimateViewer', normalizeAuthFromAccessTokenQuery)
168
181
 
169
182
  RED.httpAdmin.get('/knxUltimateViewer/page', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
@@ -191,12 +204,7 @@ module.exports = function (RED) {
191
204
  RED.httpAdmin.get('/knxUltimateViewer/nodes', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
192
205
  try {
193
206
  const nodes = Array.from(viewerRuntimeNodes.values())
194
- .map((node) => ({
195
- id: node.id,
196
- name: node.name || 'KNXViewer',
197
- gatewayId: node.serverKNX ? node.serverKNX.id : '',
198
- gatewayName: (node.serverKNX && node.serverKNX.name) ? node.serverKNX.name : ''
199
- }))
207
+ .map(viewerDetails)
200
208
  .sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')))
201
209
  res.json({ nodes })
202
210
  } catch (error) {
@@ -207,10 +215,7 @@ module.exports = function (RED) {
207
215
  RED.httpAdmin.get('/knxUltimateViewer/state', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
208
216
  try {
209
217
  const nodeId = String(req.query.nodeId || '').trim()
210
- let viewerNode = nodeId ? viewerRuntimeNodes.get(nodeId) : null
211
- if (!viewerNode) {
212
- viewerNode = Array.from(viewerRuntimeNodes.values())[0]
213
- }
218
+ const viewerNode = findViewer(nodeId)
214
219
  if (!viewerNode) {
215
220
  res.status(404).json({ error: 'KNX Viewer node not found' })
216
221
  return
@@ -221,12 +226,42 @@ module.exports = function (RED) {
221
226
  }
222
227
  })
223
228
 
224
- viewerAdminEndpointsRegistered = true
229
+ RED.httpAdmin.get('/knxUltimateViewer/history', RED.auth.needsPermission('knxUltimate-config.read'), async (req, res) => {
230
+ try {
231
+ const nodeId = String(req.query.nodeId || '').trim()
232
+ const viewerNode = findViewer(nodeId)
233
+ if (!viewerNode) {
234
+ res.status(404).json({ error: 'KNX Viewer node not found' })
235
+ return
236
+ }
237
+ if (!viewerNode.viewerHistory) {
238
+ res.status(503).json({ error: viewerNode.viewerHistoryError || 'KNX Viewer history is unavailable' })
239
+ return
240
+ }
241
+ const limit = req.query.limit === undefined ? 200 : Number(req.query.limit)
242
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
243
+ res.status(400).json({ error: 'History limit must be between 1 and 1000' })
244
+ return
245
+ }
246
+ const result = await viewerNode.viewerHistory.query({
247
+ limit,
248
+ before: String(req.query.before || ''),
249
+ search: String(req.query.search || '').slice(0, 200)
250
+ })
251
+ res.set('Cache-Control', 'no-store')
252
+ res.json({ ...result, node: viewerDetails(viewerNode), retentionHours: 24 })
253
+ } catch (error) {
254
+ res.status(error.code === 'INVALID_CURSOR' ? 400 : 500).json({ error: error.message || String(error) })
255
+ }
256
+ })
257
+
258
+ context.endpointsRegistered = true
225
259
  }
226
260
 
227
261
  function knxUltimateViewer (config) {
228
262
  RED.nodes.createNode(this, config)
229
263
  const node = this
264
+ let closed = false
230
265
  node.serverKNX = RED.nodes.getNode(config.server) || undefined
231
266
  const pushStatus = (status) => {
232
267
  if (!status) return
@@ -251,7 +286,7 @@ module.exports = function (RED) {
251
286
  node.name = config.name === undefined ? 'KNXViewer' : config.name
252
287
  node.outputtopic = node.name
253
288
  node.dpt = ''
254
- node.notifyreadrequest = false
289
+ node.notifyreadrequest = true
255
290
  node.notifyreadrequestalsorespondtobus = 'false'
256
291
  node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
257
292
  node.notifyresponse = true
@@ -269,6 +304,23 @@ module.exports = function (RED) {
269
304
  node.timerPIN3 = null
270
305
  node.exposedGAs = []
271
306
 
307
+ const reportHistoryError = error => {
308
+ const message = error && error.message ? error.message : String(error)
309
+ if (node.viewerHistoryError !== message) node.warn('KNX Viewer history: ' + message)
310
+ node.viewerHistoryError = message
311
+ }
312
+ try {
313
+ node.viewerHistory = createViewerHistory({
314
+ userDir: RED.settings.userDir,
315
+ nodeId: node.id,
316
+ gatewayId: node.serverKNX.id,
317
+ onError: reportHistoryError
318
+ })
319
+ node.viewerHistory.ready.catch(reportHistoryError)
320
+ } catch (error) {
321
+ reportHistoryError(error)
322
+ }
323
+
272
324
  viewerRuntimeNodes.set(node.id, node)
273
325
 
274
326
  node.setNodeStatus = ({ fill, shape, text, payload, GA }) => {
@@ -286,31 +338,54 @@ module.exports = function (RED) {
286
338
  }
287
339
 
288
340
  node.handleSend = (msg) => {
289
- let gaEntry
341
+ if (closed || !msg || !msg.knx) return
342
+ const isRead = msg.knx.event === 'GroupValue_Read'
343
+ const address = String(msg.knx.destination || '').trim()
344
+ let addressRAW
290
345
  try {
291
- gaEntry = node.exposedGAs.find(ga => ga.address === msg.knx.destination)
346
+ addressRAW = KNXAddress.createFromString(address, KNXAddress.TYPE_GROUP).get()
292
347
  } catch (error) {
348
+ return
293
349
  }
350
+ const timestampMs = Date.now()
294
351
  const deviceName = msg.devicename === node.name ? 'Import ETS file to view the group address name' : msg.devicename
295
- const addressRAW = KNXAddress.createFromString(msg.knx.destination, KNXAddress.TYPE_GROUP).get()
352
+ const rawPayload = Buffer.isBuffer(msg.knx.rawValue) ? msg.knx.rawValue.toString('hex') : normalizePayloadText(msg.knx.rawValue)
353
+ const unit = msg.payloadmeasureunit && msg.payloadmeasureunit !== 'unknown' ? String(msg.payloadmeasureunit).trim() : ''
354
+ if (node.viewerHistory) {
355
+ node.viewerHistory.record({
356
+ timestampMs,
357
+ address,
358
+ source: String(msg.knx.source || ''),
359
+ dpt: String(msg.knx.dpt || ''),
360
+ devicename: msg.devicename === node.name ? '' : String(msg.devicename || ''),
361
+ payload: msg.payload,
362
+ payloadText: normalizePayloadText(msg.payload),
363
+ payloadmeasureunit: unit,
364
+ rawPayload,
365
+ event: String(msg.knx.event || '')
366
+ }).catch(reportHistoryError)
367
+ }
368
+ // A read request has no state value and must not change the legacy outputs.
369
+ if (isRead) return
370
+ const gaEntry = node.exposedGAs.find(ga => ga.address === address)
296
371
  if (gaEntry === undefined) {
297
372
  node.exposedGAs.push({
298
- address: msg.knx.destination,
373
+ address,
299
374
  addressRAW,
300
375
  dpt: msg.knx.dpt,
301
376
  payload: msg.payload,
302
377
  devicename: deviceName,
303
- lastupdate: new Date(),
304
- rawPayload: 'HEX Raw: ' + msg.knx.rawValue.toString('hex') || '?',
305
- payloadmeasureunit: (msg.payloadmeasureunit !== 'unknown' ? ' ' + msg.payloadmeasureunit : '')
378
+ lastupdate: new Date(timestampMs),
379
+ rawPayload: 'HEX Raw: ' + (rawPayload || '?'),
380
+ payloadmeasureunit: unit ? ' ' + unit : ''
306
381
  })
307
382
  } else {
308
383
  gaEntry.dpt = msg.knx.dpt
309
384
  gaEntry.payload = msg.payload
310
385
  gaEntry.devicename = deviceName
311
- gaEntry.lastupdate = new Date()
312
- gaEntry.rawPayload = 'HEX Raw: ' + msg.knx.rawValue.toString('hex') || '?'
313
- gaEntry.payloadmeasureunit = (msg.payloadmeasureunit !== 'unknown' ? ' ' + msg.payloadmeasureunit : '')
386
+ gaEntry.lastupdate = new Date(timestampMs)
387
+ gaEntry.rawPayload = 'HEX Raw: ' + (rawPayload || '?')
388
+ gaEntry.payloadmeasureunit = unit ? ' ' + unit : ''
314
389
  }
315
390
 
316
391
  const pin1 = node.createPayloadPIN1()
@@ -416,12 +491,16 @@ module.exports = function (RED) {
416
491
  })
417
492
 
418
493
  node.on('close', function (done) {
494
+ closed = true
419
495
  if (node.timerPIN3 !== null) clearInterval(node.timerPIN3)
420
- viewerRuntimeNodes.delete(node.id)
496
+ if (viewerRuntimeNodes.get(node.id) === node) viewerRuntimeNodes.delete(node.id)
421
497
  if (node.serverKNX) {
422
498
  node.serverKNX.removeClient(node)
423
499
  }
424
- done()
500
+ Promise.resolve().then(() => node.viewerHistory && node.viewerHistory.close()).then(() => done(), error => {
501
+ reportHistoryError(error)
502
+ done()
503
+ })
425
504
  })
426
505
 
427
506
  if (node.serverKNX) {
@@ -1,9 +1,11 @@
1
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/flowMigrationBackup.js"></script>
2
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/knxUtilityMigration.js"></script>
1
3
  <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
2
4
 
3
5
  <script type="text/javascript">
4
6
  RED.nodes.registerType('knxUltimateWatchDog', {
5
- category: "KNX Ultimate",
6
- color: '#C7E9C0',
7
+ category: 'deprecated',
8
+ color: '#E5F0E2',
7
9
  defaults: {
8
10
  server: { type: "knxUltimate-config", required: true },
9
11
  topic: { value: "12/0/0" },
@@ -19,9 +21,9 @@
19
21
  outputLabels: ["Output"],
20
22
  icon: "node-watchdog-icon.svg",
21
23
  label: function () {
22
- return ((this.name || "KNX Watchdog") + " " + (this.checkLevel == "Ethernet" ? "Gateway IP" : this.topic));
24
+ return (((this.name || "KNX Watchdog") + " " + (this.checkLevel == "Ethernet" ? "Gateway IP" : this.topic))) + " (deprecated)";
23
25
  },
24
- paletteLabel: "KNX WatchDog",
26
+ paletteLabel: "KNX WatchDog (deprecated)",
25
27
  oneditprepare: function () {
26
28
  const node = this;
27
29
  const $knxServerInput = $("#node-input-server");
@@ -163,6 +165,15 @@
163
165
  <script type="text/markdown" data-help-name="knxUltimateWatchDog"></script>
164
166
 
165
167
  <script type="text/html" data-template-name="knxUltimateWatchDog">
168
+ <div class="form-row knx-utility-legacy-notice" role="note" style="box-sizing:border-box; padding:10px 12px; margin-bottom:12px; border-left:4px solid #d79b00; background:#fff8df; color:#4d3a00;">
169
+ <i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
170
+ <span data-i18n="knxUltimateWatchDog.legacy_notice"></span>
171
+ </div>
172
+ <div class="form-row knx-utility-legacy-migration-action" style="margin-bottom:14px;">
173
+ <button type="button" class="red-ui-button knx-utility-migrate-flow" style="background:#d97706; border-color:#b45309; color:#fff !important; font-weight:700;" data-i18n="[title]knxUltimateWatchDog.migration_button_title">
174
+ <i class="fa fa-exchange" style="color:#fff !important;"></i> <span style="color:#fff !important;" data-i18n="knxUltimateWatchDog.migration_button"></span>
175
+ </button>
176
+ </div>
166
177
  <div class="form-row" style="margin-bottom:10px;">
167
178
  <span style="color:#ff0000"><i class="fa fa-youtube"></i></span>&nbsp;<a target="_blank" href="https://www.youtube.com/playlist?list=PL9Yh1bjbLAYrU8PsVhW4xzEug2WtVFv3E"><b>KNX-Ultimate video tutorials (YouTube playlist)</b></a>
168
179
  </div>
@@ -1,4 +1,6 @@
1
1
  <script type="text/markdown" data-help-name="knxUltimateAlerter">
2
+ **Veraltet:** Dieser eigenständige Knoten wird für bestehende Flows weiterhin unterstützt. Verwenden Sie für neue Flows **KNX Utility** und wählen Sie die entsprechende Funktion. Der bisherige Knoten ist in der Palette ausgeblendet und im Flow mit `(deprecated)` gekennzeichnet. Sein Editor bietet eine Schaltfläche, um alle kompatiblen bisherigen KNX-Hilfsknoten in sämtlichen Flows und Subflows nach Bestätigung zu konvertieren. Die Konvertierung erfolgt lokal im Browser, erhält IDs, Einstellungen, Verbindungen und Gruppen und unterstützt Rückgängig. Die Bestätigung schließt den aktuellen Knoten-Editor und verwirft nicht gespeicherte Änderungen. Prüfen Sie das Ergebnis vor Deploy.
3
+
2
4
  # Alerter - Node-Konfiguration
3
5
 
4
6
  Mit dem Node "Alerter" kannst du auf einem Display oder über den Node node-red-contrib-tts-ultimate (Sprachausgabe) melden, welche ausgewählten Geräte einen Alarmzustand haben, also `payload` **true** liefern.