smart-nodes 0.3.15 → 0.3.26
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/LICENSE.md +1 -1
- package/README.md +51 -21
- package/central/central.js +12 -8
- package/compare/compare.html +48 -0
- package/compare/compare.js +36 -20
- package/counter/counter.html +104 -0
- package/counter/counter.js +120 -0
- package/delay/delay.js +39 -37
- package/examples/heating-curve.json +480 -0
- package/examples/heating-curve.png +0 -0
- package/examples/mixing-valve.json +622 -0
- package/examples/mixing-valve.png +0 -0
- package/forwarder/forwarder.js +28 -26
- package/heating-curve/heating-curve.html +348 -0
- package/heating-curve/heating-curve.js +134 -0
- package/hysteresis/hysteresis.js +58 -45
- package/icons/smart_counter.png +0 -0
- package/light-control/light-control.js +42 -30
- package/logic/logic.html +18 -2
- package/logic/logic.js +25 -23
- package/long-press-control/long-press-control.js +6 -4
- package/mixing-valve/mixing-valve.html +188 -0
- package/mixing-valve/mixing-valve.js +357 -0
- package/multi-press-control/multi-press-control.js +3 -1
- package/package.json +8 -1
- package/scene-control/scene-control.js +37 -27
- package/scheduler/scheduler.js +27 -25
- package/shutter-complex-control/shutter-complex-control.js +64 -62
- package/shutter-control/shutter-control.js +41 -37
- package/smart_helper.js +156 -5
- package/statistic/statistic.js +30 -28
- package/text-exec/text-exec.js +14 -6
- package/LICENSE +0 -21
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("smart_mixing-valve", {
|
|
3
|
+
category: "Smart Nodes",
|
|
4
|
+
paletteLabel: "Mixing Valve",
|
|
5
|
+
color: "#3FADB5",
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: "" },
|
|
8
|
+
enabled: { value: true },
|
|
9
|
+
setpoint: { value: 45 },
|
|
10
|
+
time_total: { value: 60 },
|
|
11
|
+
time_sampling: { value: 60 },
|
|
12
|
+
off_mode: { value: "NOTHING" }, // NOTHING | OPEN | CLOSE
|
|
13
|
+
valve_mode: { value: "HEATING" }, // HEATING | COOLING
|
|
14
|
+
},
|
|
15
|
+
inputs: 1,
|
|
16
|
+
outputs: 3,
|
|
17
|
+
outputLabels: ["Open", "Close", "Status Position"],
|
|
18
|
+
icon: "font-awesome/fa-arrows-h",
|
|
19
|
+
label: function ()
|
|
20
|
+
{
|
|
21
|
+
return this.name || "Mixing Valve";
|
|
22
|
+
},
|
|
23
|
+
oneditprepare: function ()
|
|
24
|
+
{
|
|
25
|
+
$("#node-input-setpoint")
|
|
26
|
+
.spinner({
|
|
27
|
+
min: -30,
|
|
28
|
+
max: 100,
|
|
29
|
+
change: function (event, ui)
|
|
30
|
+
{
|
|
31
|
+
var value = parseInt(this.value);
|
|
32
|
+
value = isNaN(value) ? 0 : value;
|
|
33
|
+
value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
|
|
34
|
+
value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
|
|
35
|
+
if (value !== this.value) $(this).spinner("value", value);
|
|
36
|
+
},
|
|
37
|
+
}).css("max-width", "4rem");
|
|
38
|
+
|
|
39
|
+
$("#node-input-time_total")
|
|
40
|
+
.spinner({
|
|
41
|
+
min: 0,
|
|
42
|
+
change: function (event, ui)
|
|
43
|
+
{
|
|
44
|
+
var value = parseInt(this.value);
|
|
45
|
+
value = isNaN(value) ? 0 : value;
|
|
46
|
+
value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
|
|
47
|
+
// value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
|
|
48
|
+
if (value !== this.value) $(this).spinner("value", value);
|
|
49
|
+
},
|
|
50
|
+
}).css("max-width", "4rem");
|
|
51
|
+
|
|
52
|
+
$("#node-input-time_sampling")
|
|
53
|
+
.spinner({
|
|
54
|
+
min: 0,
|
|
55
|
+
change: function (event, ui)
|
|
56
|
+
{
|
|
57
|
+
var value = parseInt(this.value);
|
|
58
|
+
value = isNaN(value) ? 0 : value;
|
|
59
|
+
value = Math.max(value, parseInt($(this).attr("aria-valuemin")));
|
|
60
|
+
// value = Math.min(value, parseInt($(this).attr("aria-valuemax")));
|
|
61
|
+
if (value !== this.value) $(this).spinner("value", value);
|
|
62
|
+
},
|
|
63
|
+
}).css("max-width", "4rem");
|
|
64
|
+
|
|
65
|
+
$("#node-input-off_mode").typedInput({
|
|
66
|
+
types: [
|
|
67
|
+
{
|
|
68
|
+
default: "NOTHING",
|
|
69
|
+
options: [
|
|
70
|
+
{ value: "NOTHING", label: "Nichts" },
|
|
71
|
+
{ value: "OPEN", label: "Öffnen (100%)" },
|
|
72
|
+
{ value: "CLOSE", label: "Schließen (0%)" }
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
$("#node-input-valve_mode").typedInput({
|
|
79
|
+
types: [
|
|
80
|
+
{
|
|
81
|
+
default: "HEATING",
|
|
82
|
+
options: [
|
|
83
|
+
{ value: "HEATING", label: "Heizen (normal)" },
|
|
84
|
+
{ value: "COOLING", label: "Kühlen (invertiert)" }
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<script type="text/html" data-template-name="smart_mixing-valve">
|
|
94
|
+
<div class="form-row">
|
|
95
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
96
|
+
<input type="text" id="node-input-name" placeholder="Name" />
|
|
97
|
+
</div>
|
|
98
|
+
<div class="form-row">
|
|
99
|
+
<input type="checkbox" id="node-input-enabled" style="width: 20px;" />
|
|
100
|
+
<span for="node-input-enabled" style="width: 200px;"> Aktiviert</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="form-row">
|
|
103
|
+
<label for="node-input-setpoint"><i class="fa fa-sliders"></i> Sollwert</label>
|
|
104
|
+
<input id="node-input-setpoint" value="0" /> °C
|
|
105
|
+
</div>
|
|
106
|
+
<div class="form-row">
|
|
107
|
+
<label for="node-input-time_total"><i class="fa fa-clock-o"></i> Laufzeit</label>
|
|
108
|
+
<input id="node-input-time_total" value="0" /> s
|
|
109
|
+
</div>
|
|
110
|
+
<div class="form-row">
|
|
111
|
+
<label for="node-input-time_sampling"><i class="fa fa-clock-o"></i> Abtastzeit</label>
|
|
112
|
+
<input id="node-input-time_sampling" value="0" /> s
|
|
113
|
+
</div>
|
|
114
|
+
<div class="form-row">
|
|
115
|
+
<label for="node-input-off_mode"><i class="fa fa-power-off"></i> Aus-Modus</label>
|
|
116
|
+
<input id="node-input-off_mode" />
|
|
117
|
+
</div>
|
|
118
|
+
<div class="form-row">
|
|
119
|
+
<label for="node-input-valve_mode"><i class="fa fa-fire"></i> Modus</label>
|
|
120
|
+
<input id="node-input-valve_mode" />
|
|
121
|
+
</div>
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<script type="text/html" data-help-name="smart_mixing-valve">
|
|
125
|
+
<p>
|
|
126
|
+
Diese Node steuert einen Heizungsmischer. Dieser kann sowohl fürs Heizen als auch fürs Kühlen verwendet werden.
|
|
127
|
+
Nach der eingestellten Abtastzeit wird geprüft, ob die Position des Mischers korrigiert werden muss.
|
|
128
|
+
</p>
|
|
129
|
+
<p>
|
|
130
|
+
Beim ersten Verwenden wird eine Kalibrierungsfahrt gestartet. D.h. der Mischer schließt für die eingestellte Laufzeit und befindet sich dann auf 0%.
|
|
131
|
+
Danach kann der Mischvorgang starten. Die zuletzt angefahrene Position wird persistent gespeichert, wodurch weitere Kalibrierungsfahrten in der Regel nicht mehr notwending sind.
|
|
132
|
+
</p>
|
|
133
|
+
<p>
|
|
134
|
+
<b>Hinweis:</b> Smart Nodes verwenden Topics im Format <code>name#nummer</code>, damit können verschiedene Smart Nodes mit dem gleichen Topic angesteuert werden.<br/>
|
|
135
|
+
Diese Node verwendet nur den Teil <code>name</code>. <code>#</code> und <code>nummer</code> sind dabei optional.
|
|
136
|
+
</p>
|
|
137
|
+
<p>
|
|
138
|
+
Folgende topics werden akzeptiert:
|
|
139
|
+
<table>
|
|
140
|
+
<thead>
|
|
141
|
+
<tr>
|
|
142
|
+
<th>Topic</th>
|
|
143
|
+
<th>Beschreibung</th>
|
|
144
|
+
</tr>
|
|
145
|
+
</thead>
|
|
146
|
+
<tbody>
|
|
147
|
+
<tr>
|
|
148
|
+
<td><code>enable</code></td>
|
|
149
|
+
<td>Aktiviert die Mischeransteuerung.</td>
|
|
150
|
+
</tr>
|
|
151
|
+
<tr>
|
|
152
|
+
<td><code>disable</code></td>
|
|
153
|
+
<td>Deaktiviert die Mischeransteuerung und fährt die Position an, die als Aus-Modus festgelegt ist.</td>
|
|
154
|
+
</tr>
|
|
155
|
+
<tr>
|
|
156
|
+
<td><code>set_state</code></td>
|
|
157
|
+
<td>Aktiviert das Weiterleiten, wenn <code>msg.payload = true</code> oder deaktiviert das Weiterleiten, wenn <code>msg.payload = false</code>.</td>
|
|
158
|
+
</tr>
|
|
159
|
+
<tr>
|
|
160
|
+
<td><code>setpoint</code></td>
|
|
161
|
+
<td>Überschreibt den Sollwert mit <code>msg.payload</code> °C.</td>
|
|
162
|
+
</tr>
|
|
163
|
+
<tr>
|
|
164
|
+
<td><code>off_mode</code></td>
|
|
165
|
+
<td>
|
|
166
|
+
Überschreibt den Aus-Modus mit <code>msg.payload</code>.
|
|
167
|
+
Gültige Werte für <code>msg.payload</code> sind <code>"NOTHING"</code>, <code>"OPEN"</code> und <code>"CLOSE"</code>.
|
|
168
|
+
</td>
|
|
169
|
+
</tr>
|
|
170
|
+
<tr>
|
|
171
|
+
<td><code>valve_mode</code></td>
|
|
172
|
+
<td>
|
|
173
|
+
Überschreibt den Modus mit <code>msg.payload</code>.
|
|
174
|
+
Gültige Werte für <code>msg.payload</code> sind <code>"HEATING"</code> und <code>"COOLING"</code>.
|
|
175
|
+
</td>
|
|
176
|
+
</tr>
|
|
177
|
+
<tr>
|
|
178
|
+
<td><code>current_temperature</code></td>
|
|
179
|
+
<td>Setzt die aktuelle Temperatur auf <code>msg.payload</code> °C.</td>
|
|
180
|
+
</tr>
|
|
181
|
+
<tr>
|
|
182
|
+
<td><code>calibrate</code></td>
|
|
183
|
+
<td>Erzwingt eine Kalibrierungsfahrt.</td>
|
|
184
|
+
</tr>
|
|
185
|
+
</tbody>
|
|
186
|
+
</table>
|
|
187
|
+
</p>
|
|
188
|
+
</script>
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
module.exports = function (RED)
|
|
2
|
+
{
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
function MixingValveNode(config)
|
|
6
|
+
{
|
|
7
|
+
const node = this;
|
|
8
|
+
RED.nodes.createNode(node, config);
|
|
9
|
+
|
|
10
|
+
const smart_context = require("../persistence.js")(RED);
|
|
11
|
+
const helper = require("../smart_helper.js");
|
|
12
|
+
|
|
13
|
+
// persistent values
|
|
14
|
+
var node_settings = Object.assign({}, {
|
|
15
|
+
enabled: config.enabled,
|
|
16
|
+
setpoint: config.setpoint,
|
|
17
|
+
off_mode: config.off_mode,
|
|
18
|
+
valve_mode: config.valve_mode,
|
|
19
|
+
last_position: null
|
|
20
|
+
}, smart_context.get(node.id));
|
|
21
|
+
|
|
22
|
+
// dynamic config
|
|
23
|
+
let time_total = config.time_total;
|
|
24
|
+
let time_sampling = config.time_sampling;
|
|
25
|
+
|
|
26
|
+
// runtime values
|
|
27
|
+
let sampling_interval = null;
|
|
28
|
+
let calibration_timeout = null;
|
|
29
|
+
let changing_timeout = null;
|
|
30
|
+
let changing_open = null;
|
|
31
|
+
let current_temperature = null;
|
|
32
|
+
let changing_start_time = null;
|
|
33
|
+
|
|
34
|
+
node.on("input", function (msg)
|
|
35
|
+
{
|
|
36
|
+
handleTopic(msg);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
node.on("close", function ()
|
|
40
|
+
{
|
|
41
|
+
stopSampling();
|
|
42
|
+
|
|
43
|
+
if (sampling_interval !== null)
|
|
44
|
+
clearInterval(sampling_interval);
|
|
45
|
+
|
|
46
|
+
if (calibration_timeout !== null)
|
|
47
|
+
clearTimeout(calibration_timeout);
|
|
48
|
+
|
|
49
|
+
if (changing_timeout !== null)
|
|
50
|
+
clearTimeout(changing_timeout);
|
|
51
|
+
|
|
52
|
+
stopChanging();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
let handleTopic = msg =>
|
|
57
|
+
{
|
|
58
|
+
let real_topic = helper.getTopicName(msg.topic);
|
|
59
|
+
switch (real_topic)
|
|
60
|
+
{
|
|
61
|
+
case "enable":
|
|
62
|
+
node_settings.enabled = true;
|
|
63
|
+
smart_context.set(node.id, node_settings);
|
|
64
|
+
|
|
65
|
+
stopChanging();
|
|
66
|
+
startSampling();
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case "disable":
|
|
70
|
+
node_settings.enabled = false;
|
|
71
|
+
smart_context.set(node.id, node_settings);
|
|
72
|
+
|
|
73
|
+
stopSampling();
|
|
74
|
+
doOffMode();
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case "set_state":
|
|
78
|
+
node_settings.enabled = !!msg.payload; // force boolean
|
|
79
|
+
smart_context.set(node.id, node_settings);
|
|
80
|
+
|
|
81
|
+
if (node_settings.enabled)
|
|
82
|
+
{
|
|
83
|
+
startSampling();
|
|
84
|
+
}
|
|
85
|
+
else
|
|
86
|
+
{
|
|
87
|
+
stopSampling();
|
|
88
|
+
doOffMode();
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
case "setpoint":
|
|
93
|
+
let new_setpoint = parseFloat(msg.payload);
|
|
94
|
+
if (isNaN(new_setpoint) && !isFinite(new_setpoint))
|
|
95
|
+
{
|
|
96
|
+
node.error("Invalid payload: " + msg.payload);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
node_settings.setpoint = new_setpoint;
|
|
100
|
+
smart_context.set(node.id, node_settings);
|
|
101
|
+
node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": New setpoint " + new_setpoint });
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case "off_mode":
|
|
105
|
+
switch (msg.payload)
|
|
106
|
+
{
|
|
107
|
+
case "NOTHING":
|
|
108
|
+
case "OPEN":
|
|
109
|
+
case "CLOSE":
|
|
110
|
+
node_settings.off_mode = msg.payload;
|
|
111
|
+
smart_context.set(node.id, node_settings);
|
|
112
|
+
|
|
113
|
+
if (!node_settings.enabled)
|
|
114
|
+
doOffMode();
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
case "valve_mode":
|
|
120
|
+
switch (msg.payload)
|
|
121
|
+
{
|
|
122
|
+
case "HEATING":
|
|
123
|
+
case "COOLING":
|
|
124
|
+
node_settings.valve_mode = msg.payload;
|
|
125
|
+
smart_context.set(node.id, node_settings);
|
|
126
|
+
setStatus();
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
node_settings.enabled = true;
|
|
130
|
+
smart_context.set(node.id, node_settings);
|
|
131
|
+
|
|
132
|
+
startSampling();
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
case "current_temperature":
|
|
136
|
+
let new_temp = parseFloat(msg.payload);
|
|
137
|
+
if (isNaN(new_temp) && !isFinite(new_temp))
|
|
138
|
+
{
|
|
139
|
+
node.error("Invalid payload: " + msg.payload);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
current_temperature = new_temp;
|
|
143
|
+
setStatus();
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "calibrate":
|
|
147
|
+
calibrate();
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
default:
|
|
151
|
+
node.error("Invalid topic: " + real_topic);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let startSampling = () =>
|
|
157
|
+
{
|
|
158
|
+
// Wait for calibration first
|
|
159
|
+
if (node_settings.last_position === null || calibration_timeout !== null || !node_settings.enabled)
|
|
160
|
+
return;
|
|
161
|
+
|
|
162
|
+
// sampling is already running, so do nothing
|
|
163
|
+
if (sampling_interval !== null)
|
|
164
|
+
return;
|
|
165
|
+
|
|
166
|
+
// start sampling
|
|
167
|
+
sampling_interval = setInterval(sample, time_sampling * 1000);
|
|
168
|
+
|
|
169
|
+
setStatus();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let stopSampling = () =>
|
|
173
|
+
{
|
|
174
|
+
// Wait for calibration first
|
|
175
|
+
if (node_settings.last_position === null || calibration_timeout !== null)
|
|
176
|
+
return;
|
|
177
|
+
|
|
178
|
+
// sampling is not running, so do nothing
|
|
179
|
+
if (sampling_interval === null)
|
|
180
|
+
return;
|
|
181
|
+
|
|
182
|
+
// stop sampling
|
|
183
|
+
clearInterval(sampling_interval);
|
|
184
|
+
sampling_interval = null;
|
|
185
|
+
|
|
186
|
+
setStatus();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let sample = () =>
|
|
190
|
+
{
|
|
191
|
+
// No current temperature available or in calibration => no action
|
|
192
|
+
if (current_temperature === null || calibration_timeout !== null || !node_settings.enabled)
|
|
193
|
+
return;
|
|
194
|
+
|
|
195
|
+
// +/- 1°C => already good enough, do nothing
|
|
196
|
+
let temp_diff = Math.abs(current_temperature - node_settings.setpoint);
|
|
197
|
+
if (temp_diff < 1)
|
|
198
|
+
return;
|
|
199
|
+
|
|
200
|
+
// Calculate change time
|
|
201
|
+
// Change time in ms for 1%
|
|
202
|
+
let moving_time = time_total * 1000 / 100;
|
|
203
|
+
if (temp_diff > 5)
|
|
204
|
+
{
|
|
205
|
+
// 0 °C diff => 0% change
|
|
206
|
+
// 20 °C diff => 5% change
|
|
207
|
+
moving_time *= helper.scale(Math.min(temp_diff, 20), 0, 20, 0, 5);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// calculate direction
|
|
211
|
+
let do_open = false;
|
|
212
|
+
if (current_temperature < node_settings.setpoint)
|
|
213
|
+
{
|
|
214
|
+
if (node_settings.valve_mode == "HEATING")
|
|
215
|
+
do_open = true;
|
|
216
|
+
}
|
|
217
|
+
else
|
|
218
|
+
{
|
|
219
|
+
if (node_settings.valve_mode == "COOLING")
|
|
220
|
+
do_open = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// start moving
|
|
224
|
+
startChanging(do_open, moving_time);
|
|
225
|
+
|
|
226
|
+
setStatus();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let startChanging = (do_open, time_ms) =>
|
|
230
|
+
{
|
|
231
|
+
stopChanging();
|
|
232
|
+
|
|
233
|
+
// Already oppened/closed
|
|
234
|
+
if (do_open && node_settings.last_position == 100)
|
|
235
|
+
return;
|
|
236
|
+
if (!do_open && node_settings.last_position == 0)
|
|
237
|
+
return;
|
|
238
|
+
|
|
239
|
+
changing_start_time = Date.now();
|
|
240
|
+
if (do_open)
|
|
241
|
+
node.send([{ payload: true }, { payload: false }, null]);
|
|
242
|
+
else
|
|
243
|
+
node.send([{ payload: false }, { payload: true }, null]);
|
|
244
|
+
|
|
245
|
+
node.status({
|
|
246
|
+
fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": Start " + (do_open ? "opening" : "closing") + " for " + helper.formatMsToStatus(time_ms)
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
changing_open = do_open;
|
|
250
|
+
changing_timeout = setTimeout(() =>
|
|
251
|
+
{
|
|
252
|
+
changing_timeout = null;
|
|
253
|
+
stopChanging();
|
|
254
|
+
}, time_ms);
|
|
255
|
+
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let stopChanging = () =>
|
|
259
|
+
{
|
|
260
|
+
if (changing_timeout !== null)
|
|
261
|
+
{
|
|
262
|
+
clearTimeout(changing_timeout);
|
|
263
|
+
changing_timeout = null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// No changing in progress
|
|
267
|
+
if (changing_start_time == null)
|
|
268
|
+
return;
|
|
269
|
+
|
|
270
|
+
// Calculate moved percentage
|
|
271
|
+
let time_passed = (Date.now() - changing_start_time) / 1000;
|
|
272
|
+
changing_start_time = null;
|
|
273
|
+
|
|
274
|
+
let changed_value = (time_passed / time_total) * 100; // calculate in % value (0-100)
|
|
275
|
+
if (changing_open)
|
|
276
|
+
node_settings.last_position += changed_value;
|
|
277
|
+
else
|
|
278
|
+
node_settings.last_position -= changed_value;
|
|
279
|
+
|
|
280
|
+
// Only values from 0 to 100 are allowed
|
|
281
|
+
node_settings.last_position = Math.min(Math.max(node_settings.last_position, 0), 100);
|
|
282
|
+
|
|
283
|
+
// Save state
|
|
284
|
+
smart_context.set(node.id, node_settings);
|
|
285
|
+
|
|
286
|
+
node.send([{ payload: false }, { payload: false }, { payload: node_settings.last_position.toFixed(1) }]);
|
|
287
|
+
|
|
288
|
+
setStatus();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let calibrate = () =>
|
|
292
|
+
{
|
|
293
|
+
// start closing
|
|
294
|
+
node.send([{ payload: false }, { payload: true }, null]);
|
|
295
|
+
|
|
296
|
+
calibration_timeout = setTimeout(() =>
|
|
297
|
+
{
|
|
298
|
+
// stop closing
|
|
299
|
+
node.send([{ payload: false }, { payload: false }, null]);
|
|
300
|
+
node_settings.last_position = 0;
|
|
301
|
+
smart_context.set(node.id, node_settings);
|
|
302
|
+
|
|
303
|
+
// Calibration finished, start sampling if enabled
|
|
304
|
+
calibration_timeout = null;
|
|
305
|
+
if (node_settings.enabled)
|
|
306
|
+
startSampling();
|
|
307
|
+
else
|
|
308
|
+
stopSampling();
|
|
309
|
+
|
|
310
|
+
setStatus();
|
|
311
|
+
}, time_total * 1000);
|
|
312
|
+
|
|
313
|
+
setStatus();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let doOffMode = () =>
|
|
317
|
+
{
|
|
318
|
+
switch (node_settings.off_mode)
|
|
319
|
+
{
|
|
320
|
+
case "OPEN":
|
|
321
|
+
startChanging(true, time_total * 1000);
|
|
322
|
+
break;
|
|
323
|
+
|
|
324
|
+
case "CLOSE":
|
|
325
|
+
startChanging(false, time_total * 1000);
|
|
326
|
+
break;
|
|
327
|
+
|
|
328
|
+
case "NOTHING":
|
|
329
|
+
default:
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let setStatus = () =>
|
|
335
|
+
{
|
|
336
|
+
if (calibration_timeout !== null)
|
|
337
|
+
node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": In calibration" });
|
|
338
|
+
else
|
|
339
|
+
node.status({ fill: node_settings.enabled ? "green" : "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": " + (node_settings.valve_mode == "HEATING" ? "🔥" : "❄️") + " Set: " + node_settings.setpoint.toFixed(1) + "°C, Cur: " + current_temperature?.toFixed(1) + "°C, Pos: " + node_settings.last_position.toFixed(1) + "%" });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (node_settings.last_position === null)
|
|
343
|
+
{
|
|
344
|
+
// Start calibration after 10s
|
|
345
|
+
setTimeout(calibrate, 10 * 1000);
|
|
346
|
+
}
|
|
347
|
+
else if (node_settings.enabled)
|
|
348
|
+
{
|
|
349
|
+
startSampling();
|
|
350
|
+
node.send([null, null, { payload: node_settings.last_position.toFixed(1) }]);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
setStatus();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
RED.nodes.registerType("smart_mixing-valve", MixingValveNode);
|
|
357
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module.exports = function (RED)
|
|
2
2
|
{
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
3
5
|
function MultiPressControlNode(config)
|
|
4
6
|
{
|
|
5
7
|
const node = this;
|
|
@@ -55,7 +57,7 @@ module.exports = function (RED)
|
|
|
55
57
|
|
|
56
58
|
let sendResult = () =>
|
|
57
59
|
{
|
|
58
|
-
node.status({ fill: "green", shape: "dot", text:
|
|
60
|
+
node.status({ fill: "green", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Last was press " + count + " time" + (count == 1 ? "" : "s") });
|
|
59
61
|
let data = Array.from({ length: config.outputs }).fill(null);
|
|
60
62
|
data[count - 1] = outs[count - 1];
|
|
61
63
|
node.send(data);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-nodes",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"description": "Smart Nodes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|
|
@@ -18,8 +18,12 @@
|
|
|
18
18
|
"compare",
|
|
19
19
|
"comparator",
|
|
20
20
|
"statistic",
|
|
21
|
+
"counter",
|
|
21
22
|
"scheduler",
|
|
22
23
|
"central",
|
|
24
|
+
"heating",
|
|
25
|
+
"mixing valve",
|
|
26
|
+
"heating curve",
|
|
23
27
|
"text execution"
|
|
24
28
|
],
|
|
25
29
|
"scripts": {
|
|
@@ -41,9 +45,12 @@
|
|
|
41
45
|
"compare": "compare/compare.js",
|
|
42
46
|
"hysteresis": "hysteresis/hysteresis.js",
|
|
43
47
|
"statistic": "statistic/statistic.js",
|
|
48
|
+
"counter": "counter/counter.js",
|
|
44
49
|
"scheduler": "scheduler/scheduler.js",
|
|
45
50
|
"delay": "delay/delay.js",
|
|
46
51
|
"central": "central/central.js",
|
|
52
|
+
"mixing-valve": "mixing-valve/mixing-valve.js",
|
|
53
|
+
"heating-curve": "heating-curve/heating-curve.js",
|
|
47
54
|
"text-exec": "text-exec/text-exec.js"
|
|
48
55
|
}
|
|
49
56
|
},
|