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.
- package/CHANGELOG.md +11 -0
- package/README.md +2 -0
- package/examples/HA Translator - Payload Conversion.json +18 -8
- package/examples/KNX Utility - All functions.json +389 -0
- package/nodes/commonFunctions.js +5 -4
- package/nodes/knxUltimateAlerter.html +15 -4
- package/nodes/knxUltimateAutoResponder.html +15 -4
- package/nodes/knxUltimateDateTime.html +16 -5
- package/nodes/knxUltimateDateTime.js +2 -26
- package/nodes/knxUltimateGarage.html +15 -4
- package/nodes/knxUltimateGlobalContext.html +15 -4
- package/nodes/knxUltimateHATranslator.html +15 -4
- package/nodes/knxUltimateHueController.html +3 -4
- package/nodes/knxUltimateLoadControl.html +15 -4
- package/nodes/knxUltimateLogger.html +15 -4
- package/nodes/knxUltimateSceneController.html +15 -4
- package/nodes/knxUltimateStaircase.html +15 -4
- package/nodes/knxUltimateUtility.html +325 -0
- package/nodes/knxUltimateUtility.js +24 -0
- package/nodes/knxUltimateViewer.html +16 -3
- package/nodes/knxUltimateViewer.js +106 -27
- package/nodes/knxUltimateWatchDog.html +15 -4
- package/nodes/locales/de/knxUltimateAlerter.html +2 -0
- package/nodes/locales/de/knxUltimateAlerter.json +5 -3
- package/nodes/locales/de/knxUltimateAutoResponder.html +2 -0
- package/nodes/locales/de/knxUltimateAutoResponder.json +4 -2
- package/nodes/locales/de/knxUltimateDateTime.html +2 -0
- package/nodes/locales/de/knxUltimateDateTime.json +5 -3
- package/nodes/locales/de/knxUltimateGarage.html +2 -0
- package/nodes/locales/de/knxUltimateGarage.json +5 -2
- package/nodes/locales/de/knxUltimateGlobalContext.html +2 -0
- package/nodes/locales/de/knxUltimateGlobalContext.json +21 -18
- package/nodes/locales/de/knxUltimateHATranslator.html +2 -0
- package/nodes/locales/de/knxUltimateHATranslator.json +4 -2
- package/nodes/locales/de/knxUltimateHueController.html +2 -1
- package/nodes/locales/de/knxUltimateHueController.json +4 -3
- package/nodes/locales/de/knxUltimateLoadControl.html +2 -0
- package/nodes/locales/de/knxUltimateLoadControl.json +5 -2
- package/nodes/locales/de/knxUltimateLogger.html +2 -0
- package/nodes/locales/de/knxUltimateLogger.json +6 -3
- package/nodes/locales/de/knxUltimateSceneController.html +2 -0
- package/nodes/locales/de/knxUltimateSceneController.json +8 -6
- package/nodes/locales/de/knxUltimateStaircase.html +2 -0
- package/nodes/locales/de/knxUltimateStaircase.json +5 -2
- package/nodes/locales/de/knxUltimateUtility.html +922 -0
- package/nodes/locales/de/knxUltimateUtility.json +56 -0
- package/nodes/locales/de/knxUltimateViewer.html +19 -453
- package/nodes/locales/de/knxUltimateViewer.json +9 -1
- package/nodes/locales/de/knxUltimateWatchDog.html +2 -0
- package/nodes/locales/de/knxUltimateWatchDog.json +5 -2
- package/nodes/locales/en/knxUltimateAlerter.html +2 -0
- package/nodes/locales/en/knxUltimateAlerter.json +5 -3
- package/nodes/locales/en/knxUltimateAutoResponder.html +2 -0
- package/nodes/locales/en/knxUltimateAutoResponder.json +4 -2
- package/nodes/locales/en/knxUltimateDateTime.html +2 -0
- package/nodes/locales/en/knxUltimateDateTime.json +5 -3
- package/nodes/locales/en/knxUltimateGarage.html +2 -0
- package/nodes/locales/en/knxUltimateGarage.json +5 -2
- package/nodes/locales/en/knxUltimateGlobalContext.html +2 -0
- package/nodes/locales/en/knxUltimateGlobalContext.json +21 -18
- package/nodes/locales/en/knxUltimateHATranslator.html +2 -0
- package/nodes/locales/en/knxUltimateHATranslator.json +4 -2
- package/nodes/locales/en/knxUltimateHueController.html +2 -1
- package/nodes/locales/en/knxUltimateHueController.json +4 -3
- package/nodes/locales/en/knxUltimateLoadControl.html +2 -0
- package/nodes/locales/en/knxUltimateLoadControl.json +5 -2
- package/nodes/locales/en/knxUltimateLogger.html +2 -0
- package/nodes/locales/en/knxUltimateLogger.json +6 -3
- package/nodes/locales/en/knxUltimateSceneController.html +2 -0
- package/nodes/locales/en/knxUltimateSceneController.json +15 -14
- package/nodes/locales/en/knxUltimateStaircase.html +2 -0
- package/nodes/locales/en/knxUltimateStaircase.json +5 -2
- package/nodes/locales/en/knxUltimateUtility.html +1100 -0
- package/nodes/locales/en/knxUltimateUtility.json +56 -0
- package/nodes/locales/en/knxUltimateViewer.html +18 -457
- package/nodes/locales/en/knxUltimateViewer.json +9 -1
- package/nodes/locales/en/knxUltimateWatchDog.html +2 -0
- package/nodes/locales/en/knxUltimateWatchDog.json +5 -3
- package/nodes/locales/es/knxUltimateAlerter.html +2 -0
- package/nodes/locales/es/knxUltimateAlerter.json +5 -2
- package/nodes/locales/es/knxUltimateAutoResponder.html +2 -0
- package/nodes/locales/es/knxUltimateAutoResponder.json +4 -1
- package/nodes/locales/es/knxUltimateDateTime.html +2 -0
- package/nodes/locales/es/knxUltimateDateTime.json +5 -3
- package/nodes/locales/es/knxUltimateGarage.html +2 -0
- package/nodes/locales/es/knxUltimateGarage.json +5 -2
- package/nodes/locales/es/knxUltimateGlobalContext.html +2 -0
- package/nodes/locales/es/knxUltimateGlobalContext.json +5 -2
- package/nodes/locales/es/knxUltimateHATranslator.html +2 -0
- package/nodes/locales/es/knxUltimateHATranslator.json +4 -1
- package/nodes/locales/es/knxUltimateHueController.html +2 -1
- package/nodes/locales/es/knxUltimateHueController.json +4 -3
- package/nodes/locales/es/knxUltimateLoadControl.html +2 -0
- package/nodes/locales/es/knxUltimateLoadControl.json +5 -2
- package/nodes/locales/es/knxUltimateLogger.html +2 -0
- package/nodes/locales/es/knxUltimateLogger.json +5 -2
- package/nodes/locales/es/knxUltimateSceneController.html +2 -0
- package/nodes/locales/es/knxUltimateSceneController.json +5 -2
- package/nodes/locales/es/knxUltimateStaircase.html +2 -0
- package/nodes/locales/es/knxUltimateStaircase.json +5 -2
- package/nodes/locales/es/knxUltimateUtility.html +1080 -0
- package/nodes/locales/es/knxUltimateUtility.json +56 -0
- package/nodes/locales/es/knxUltimateViewer.html +18 -450
- package/nodes/locales/es/knxUltimateViewer.json +16 -8
- package/nodes/locales/es/knxUltimateWatchDog.html +2 -0
- package/nodes/locales/es/knxUltimateWatchDog.json +5 -2
- package/nodes/locales/fr/knxUltimateAlerter.html +2 -0
- package/nodes/locales/fr/knxUltimateAlerter.json +5 -2
- package/nodes/locales/fr/knxUltimateAutoResponder.html +2 -0
- package/nodes/locales/fr/knxUltimateAutoResponder.json +4 -1
- package/nodes/locales/fr/knxUltimateDateTime.html +2 -0
- package/nodes/locales/fr/knxUltimateDateTime.json +5 -3
- package/nodes/locales/fr/knxUltimateGarage.html +2 -0
- package/nodes/locales/fr/knxUltimateGarage.json +5 -2
- package/nodes/locales/fr/knxUltimateGlobalContext.html +2 -0
- package/nodes/locales/fr/knxUltimateGlobalContext.json +5 -2
- package/nodes/locales/fr/knxUltimateHATranslator.html +2 -0
- package/nodes/locales/fr/knxUltimateHATranslator.json +4 -1
- package/nodes/locales/fr/knxUltimateHueController.html +2 -1
- package/nodes/locales/fr/knxUltimateHueController.json +4 -3
- package/nodes/locales/fr/knxUltimateLoadControl.html +2 -0
- package/nodes/locales/fr/knxUltimateLoadControl.json +5 -2
- package/nodes/locales/fr/knxUltimateLogger.html +2 -0
- package/nodes/locales/fr/knxUltimateLogger.json +5 -2
- package/nodes/locales/fr/knxUltimateSceneController.html +2 -0
- package/nodes/locales/fr/knxUltimateSceneController.json +5 -2
- package/nodes/locales/fr/knxUltimateStaircase.html +2 -0
- package/nodes/locales/fr/knxUltimateStaircase.json +5 -2
- package/nodes/locales/fr/knxUltimateUtility.html +1081 -0
- package/nodes/locales/fr/knxUltimateUtility.json +56 -0
- package/nodes/locales/fr/knxUltimateViewer.html +18 -450
- package/nodes/locales/fr/knxUltimateViewer.json +16 -8
- package/nodes/locales/fr/knxUltimateWatchDog.html +2 -0
- package/nodes/locales/fr/knxUltimateWatchDog.json +5 -2
- package/nodes/locales/it/knxUltimateAlerter.html +2 -0
- package/nodes/locales/it/knxUltimateAlerter.json +5 -3
- package/nodes/locales/it/knxUltimateAutoResponder.html +2 -0
- package/nodes/locales/it/knxUltimateAutoResponder.json +4 -2
- package/nodes/locales/it/knxUltimateDateTime.html +2 -0
- package/nodes/locales/it/knxUltimateDateTime.json +5 -3
- package/nodes/locales/it/knxUltimateGarage.html +2 -0
- package/nodes/locales/it/knxUltimateGarage.json +5 -2
- package/nodes/locales/it/knxUltimateGlobalContext.html +2 -0
- package/nodes/locales/it/knxUltimateGlobalContext.json +21 -18
- package/nodes/locales/it/knxUltimateHATranslator.html +2 -0
- package/nodes/locales/it/knxUltimateHATranslator.json +4 -2
- package/nodes/locales/it/knxUltimateHueController.html +2 -1
- package/nodes/locales/it/knxUltimateHueController.json +4 -3
- package/nodes/locales/it/knxUltimateLoadControl.html +2 -0
- package/nodes/locales/it/knxUltimateLoadControl.json +5 -2
- package/nodes/locales/it/knxUltimateLogger.html +2 -0
- package/nodes/locales/it/knxUltimateLogger.json +6 -3
- package/nodes/locales/it/knxUltimateSceneController.html +2 -0
- package/nodes/locales/it/knxUltimateSceneController.json +8 -7
- package/nodes/locales/it/knxUltimateStaircase.html +2 -0
- package/nodes/locales/it/knxUltimateStaircase.json +5 -2
- package/nodes/locales/it/knxUltimateUtility.html +959 -0
- package/nodes/locales/it/knxUltimateUtility.json +56 -0
- package/nodes/locales/it/knxUltimateViewer.html +19 -453
- package/nodes/locales/it/knxUltimateViewer.json +9 -1
- package/nodes/locales/it/knxUltimateWatchDog.html +2 -0
- package/nodes/locales/it/knxUltimateWatchDog.json +6 -6
- package/nodes/locales/zh-CN/knxUltimateAlerter.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateAlerter.json +5 -3
- package/nodes/locales/zh-CN/knxUltimateAutoResponder.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateAutoResponder.json +4 -2
- package/nodes/locales/zh-CN/knxUltimateDateTime.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateDateTime.json +5 -3
- package/nodes/locales/zh-CN/knxUltimateGarage.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateGarage.json +5 -2
- package/nodes/locales/zh-CN/knxUltimateGlobalContext.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateGlobalContext.json +21 -18
- package/nodes/locales/zh-CN/knxUltimateHATranslator.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateHATranslator.json +4 -2
- package/nodes/locales/zh-CN/knxUltimateHueController.html +2 -1
- package/nodes/locales/zh-CN/knxUltimateHueController.json +4 -3
- package/nodes/locales/zh-CN/knxUltimateLoadControl.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateLoadControl.json +5 -2
- package/nodes/locales/zh-CN/knxUltimateLogger.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateLogger.json +6 -3
- package/nodes/locales/zh-CN/knxUltimateSceneController.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateSceneController.json +12 -10
- package/nodes/locales/zh-CN/knxUltimateStaircase.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateStaircase.json +5 -2
- package/nodes/locales/zh-CN/knxUltimateUtility.html +930 -0
- package/nodes/locales/zh-CN/knxUltimateUtility.json +56 -0
- package/nodes/locales/zh-CN/knxUltimateViewer.html +19 -452
- package/nodes/locales/zh-CN/knxUltimateViewer.json +9 -1
- package/nodes/locales/zh-CN/knxUltimateWatchDog.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateWatchDog.json +5 -3
- package/nodes/plugins/knxUltimateViewer-vue/assets/app.css +1 -1
- package/nodes/plugins/knxUltimateViewer-vue/assets/app.js +2 -2
- package/nodes/utils/knxDateTimeSendNow.js +30 -0
- package/nodes/utils/knxUtilityProfileAdapter.js +66 -0
- package/nodes/utils/knxUtilityProfiles/runtime/alerter.js +305 -0
- package/nodes/utils/knxUtilityProfiles/runtime/autoresponder.js +266 -0
- package/nodes/utils/knxUtilityProfiles/runtime/datetime.js +279 -0
- package/nodes/utils/knxUtilityProfiles/runtime/garage.js +513 -0
- package/nodes/utils/knxUtilityProfiles/runtime/globalcontext.js +218 -0
- package/nodes/utils/knxUtilityProfiles/runtime/hatranslator.js +95 -0
- package/nodes/utils/knxUtilityProfiles/runtime/loadcontrol.js +448 -0
- package/nodes/utils/knxUtilityProfiles/runtime/logger.js +233 -0
- package/nodes/utils/knxUtilityProfiles/runtime/profileTimers.js +35 -0
- package/nodes/utils/knxUtilityProfiles/runtime/scenecontroller.js +487 -0
- package/nodes/utils/knxUtilityProfiles/runtime/staircase.js +472 -0
- package/nodes/utils/knxUtilityProfiles/runtime/watchdog.js +240 -0
- package/nodes/utils/knxViewerHistory.js +424 -0
- package/package.json +9 -6
- package/resources/flowMigrationBackup.js +81 -0
- package/resources/hueControllerMigrationDialog.js +15 -4
- package/resources/knxUtilityMigration.js +276 -0
- 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>
|
|
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"
|
|
132
|
-
<i class="fa fa-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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: ' +
|
|
305
|
-
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: ' +
|
|
313
|
-
gaEntry.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:
|
|
6
|
-
color: '#
|
|
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> <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.
|