node-red-contrib-pt1 1.0.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/README.md +38 -0
- package/locales/de/pt1.json +8 -0
- package/locales/en/pt1.json +8 -0
- package/locales/es/pt1.json +8 -0
- package/package.json +46 -0
- package/pt1.html +70 -0
- package/pt1.js +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# node-red-contrib-pt1
|
|
2
|
+
|
|
3
|
+
A Node-RED PT1 first-order low-pass filter for smoothing noisy measurement values.
|
|
4
|
+
|
|
5
|
+
Formula: y(t) = y(t-1) + (dt / (T1 + dt)) * (x(t) - y(t-1))
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install node-red-contrib-pt1
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Inputs
|
|
14
|
+
|
|
15
|
+
- **payload** `number`: Raw measurement value to filter.
|
|
16
|
+
- **T1** _(optional)_ `number`: Override T1 in seconds for this message only.
|
|
17
|
+
- **reset** _(optional)_ `boolean`: Reset filter state; next input passes through directly.
|
|
18
|
+
|
|
19
|
+
## Outputs
|
|
20
|
+
|
|
21
|
+
- **payload** `number`: Filtered (smoothed) value.
|
|
22
|
+
- **pt1** `object`: { input, output, T1, dt, decimals, initialized }
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
- **Time Constant T1**: Smoothing constant with unit s/min/h. Larger = more smoothing, slower response.
|
|
27
|
+
- **Decimal Places**: Output precision 0-4 (default: 2).
|
|
28
|
+
|
|
29
|
+
## Tips
|
|
30
|
+
|
|
31
|
+
- First message always passes through unfiltered (initialization).
|
|
32
|
+
- T1 = 0 disables filtering (passthrough mode).
|
|
33
|
+
- Uses actual elapsed time between messages - no fixed interval required.
|
|
34
|
+
- Tip: use the 24h moving average as input for best predictive results.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT (c) sr.rpo
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-pt1",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "PT1 first-order low-pass filter for smoothing noisy measurement values",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Wobi848/node-red-contrib-rpo-suite.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/Wobi848/node-red-contrib-rpo-suite/issues"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"pt1.js",
|
|
14
|
+
"pt1.html",
|
|
15
|
+
"locales",
|
|
16
|
+
"examples"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"node-red",
|
|
20
|
+
"pt1",
|
|
21
|
+
"low-pass",
|
|
22
|
+
"filter",
|
|
23
|
+
"smooth",
|
|
24
|
+
"lag",
|
|
25
|
+
"measurement"
|
|
26
|
+
],
|
|
27
|
+
"author": "sr.rpo",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"node-red": {
|
|
30
|
+
"version": "1.0.0",
|
|
31
|
+
"nodes": {
|
|
32
|
+
"pt1": "pt1.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=14.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "mocha \"test/**/*_spec.js\" --exit"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"mocha": "^10.2.0",
|
|
43
|
+
"node-red": "^3.1.0",
|
|
44
|
+
"node-red-node-test-helper": "^0.3.6"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/pt1.html
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('pt1', {
|
|
3
|
+
category: 'rpo',
|
|
4
|
+
color: '#D4600A',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
T1: { value: 60 },
|
|
8
|
+
T1Unit: { value: 's' },
|
|
9
|
+
decimals: { value: 2 }
|
|
10
|
+
},
|
|
11
|
+
inputs: 1,
|
|
12
|
+
outputs: 1,
|
|
13
|
+
icon: 'font-awesome/fa-filter',
|
|
14
|
+
label: function() { return this.name || ('PT1 T1=' + this.T1 + this.T1Unit); },
|
|
15
|
+
labelStyle: function() { return this.name ? 'node_label_italic' : ''; }
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script type="text/html" data-template-name="pt1">
|
|
20
|
+
<div class="form-row">
|
|
21
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
|
|
22
|
+
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
|
|
23
|
+
</div>
|
|
24
|
+
<div class="form-row">
|
|
25
|
+
<label for="node-input-T1"><i class="fa fa-clock-o"></i> <span data-i18n="pt1.label.T1"></span></label>
|
|
26
|
+
<input type="number" id="node-input-T1" min="0" style="width:50%">
|
|
27
|
+
<select id="node-input-T1Unit" style="width:18%; margin-left:4px">
|
|
28
|
+
<option value="s">s</option>
|
|
29
|
+
<option value="min">min</option>
|
|
30
|
+
<option value="h">h</option>
|
|
31
|
+
</select>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<label for="node-input-decimals"><i class="fa fa-sort-numeric-asc"></i> <span data-i18n="pt1.label.decimals"></span></label>
|
|
35
|
+
<input type="number" id="node-input-decimals" min="0" max="4" style="width:70%">
|
|
36
|
+
</div>
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<script type="text/html" data-help-name="pt1">
|
|
40
|
+
<p>PT1 first-order low-pass filter. Smooths noisy measurement values with a configurable time constant <strong>T1</strong>.</p>
|
|
41
|
+
|
|
42
|
+
<h3>Formula</h3>
|
|
43
|
+
<pre>y(t) = y(t-1) + (dt / (T1 + dt)) × (x(t) - y(t-1))</pre>
|
|
44
|
+
|
|
45
|
+
<h3>Input</h3>
|
|
46
|
+
<dl class="message-properties">
|
|
47
|
+
<dt>payload <span class="property-type">number</span></dt>
|
|
48
|
+
<dd>Raw measurement value to filter.</dd>
|
|
49
|
+
<dt class="optional">T1 <span class="property-type">number</span></dt>
|
|
50
|
+
<dd>Override time constant (in seconds) for this message only.</dd>
|
|
51
|
+
<dt class="optional">reset <span class="property-type">boolean</span></dt>
|
|
52
|
+
<dd>Set to <code>true</code> to reset filter state — next input passes through directly.</dd>
|
|
53
|
+
</dl>
|
|
54
|
+
|
|
55
|
+
<h3>Output</h3>
|
|
56
|
+
<dl class="message-properties">
|
|
57
|
+
<dt>payload <span class="property-type">number</span></dt>
|
|
58
|
+
<dd>Filtered (smoothed) value.</dd>
|
|
59
|
+
<dt>pt1 <span class="property-type">object</span></dt>
|
|
60
|
+
<dd><code>{ input, output, T1, dt, decimals, initialized }</code></dd>
|
|
61
|
+
</dl>
|
|
62
|
+
|
|
63
|
+
<h3>Tips</h3>
|
|
64
|
+
<ul>
|
|
65
|
+
<li>The first message always passes through unfiltered (no previous state).</li>
|
|
66
|
+
<li>Set T1 = 0 to disable filtering (passthrough mode).</li>
|
|
67
|
+
<li>Larger T1 = more smoothing but slower response to changes.</li>
|
|
68
|
+
<li>The node uses actual elapsed time (dt) between messages — no fixed interval needed.</li>
|
|
69
|
+
</ul>
|
|
70
|
+
</script>
|
package/pt1.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function PT1Node(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
var node = this;
|
|
5
|
+
|
|
6
|
+
var _T1raw = parseFloat(config.T1);
|
|
7
|
+
var T1raw = isNaN(_T1raw) ? 60 : _T1raw;
|
|
8
|
+
var unit = config.T1Unit || 's';
|
|
9
|
+
node.decimals = parseInt(config.decimals, 10);
|
|
10
|
+
if (isNaN(node.decimals) || node.decimals < 0) node.decimals = 2;
|
|
11
|
+
|
|
12
|
+
// Convert T1 to seconds
|
|
13
|
+
node.T1 = toSeconds(T1raw, unit);
|
|
14
|
+
|
|
15
|
+
node.lastOutput = undefined;
|
|
16
|
+
node.lastTime = undefined;
|
|
17
|
+
|
|
18
|
+
node.status({ fill: 'grey', shape: 'ring', text: 'Initializing...' });
|
|
19
|
+
|
|
20
|
+
node.on('input', function(msg) {
|
|
21
|
+
if (msg.reset === true) {
|
|
22
|
+
node.lastOutput = undefined;
|
|
23
|
+
node.lastTime = undefined;
|
|
24
|
+
node.status({ fill: 'grey', shape: 'ring', text: 'Initializing...' });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var x = parseFloat(msg.payload);
|
|
29
|
+
if (isNaN(x)) { node.warn('Input is not a number: ' + msg.payload); return; }
|
|
30
|
+
|
|
31
|
+
var T1 = (msg.T1 !== undefined) ? parseFloat(msg.T1) : node.T1;
|
|
32
|
+
if (isNaN(T1) || T1 < 0) T1 = node.T1;
|
|
33
|
+
|
|
34
|
+
var now = Date.now();
|
|
35
|
+
var output;
|
|
36
|
+
var initialized = true;
|
|
37
|
+
|
|
38
|
+
if (node.lastOutput === undefined || node.lastTime === undefined) {
|
|
39
|
+
output = x;
|
|
40
|
+
initialized = false;
|
|
41
|
+
} else {
|
|
42
|
+
var dt = (now - node.lastTime) / 1000;
|
|
43
|
+
if (T1 === 0) {
|
|
44
|
+
output = x;
|
|
45
|
+
} else {
|
|
46
|
+
var alpha = dt / (T1 + dt);
|
|
47
|
+
output = node.lastOutput + alpha * (x - node.lastOutput);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
output = parseFloat(output.toFixed(node.decimals));
|
|
52
|
+
node.lastOutput = output;
|
|
53
|
+
node.lastTime = now;
|
|
54
|
+
|
|
55
|
+
msg.payload = output;
|
|
56
|
+
msg.pt1 = {
|
|
57
|
+
input: x,
|
|
58
|
+
output: output,
|
|
59
|
+
T1: T1,
|
|
60
|
+
dt: node.lastTime ? Math.round((now - (node.lastTime === now ? now : node.lastTime)) / 10) / 100 : 0,
|
|
61
|
+
decimals: node.decimals,
|
|
62
|
+
initialized: initialized
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
node.status({ fill: 'green', shape: 'dot', text: 'in: ' + x + ' → out: ' + output + ' (T1: ' + T1 + 's)' });
|
|
66
|
+
node.send(msg);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function toSeconds(val, unit) {
|
|
70
|
+
if (unit === 'min') return val * 60;
|
|
71
|
+
if (unit === 'h') return val * 3600;
|
|
72
|
+
return val;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
RED.nodes.registerType('pt1', PT1Node);
|
|
77
|
+
};
|