node-red-contrib-freya-nodes 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,12 +20,16 @@
20
20
  category: 'Freya Vivariums',
21
21
  color: "#A2CA6F",
22
22
  defaults: {
23
- name: {value:""}
23
+ name: {value:""},
24
+ interval: {value: 60, required: true, validate: RED.validators.number()},
25
+ duration: {value: 30, required: true, validate: RED.validators.number()},
26
+ tickInterval: {value: 60, required: true, validate: function(v) { return RED.validators.number()(v) && parseInt(v) >= 1; }}
24
27
  },
25
28
  inputs:1,
26
- outputs:2,
27
- outputLabels:["control", "status"],
28
- icon: "font-awesome/fa-cloud",
29
+ outputs:1,
30
+ inputLabels:["parameter update"],
31
+ outputLabels:["actuator command"],
32
+ icon: "font-awesome/fa-tint",
29
33
  label: function() {
30
34
  return this.name || "Precipitation Controller";
31
35
  }
@@ -34,11 +38,80 @@
34
38
 
35
39
  <script type="text/html" data-template-name="precipitation controller">
36
40
  <div class="form-row">
37
- <label for="node-input-name"><i class="icon-tag"></i> Name</label>
38
- <input type="text" id="node-input-name" placeholder="Name">
41
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
42
+ <input type="text" id="node-input-name" placeholder="Precipitation Controller">
43
+ </div>
44
+ <hr/>
45
+ <div class="form-row">
46
+ <label for="node-input-interval"><i class="fa fa-clock-o"></i> Interval (minutes)</label>
47
+ <input type="number" id="node-input-interval" placeholder="60" step="1" min="1">
48
+ </div>
49
+ <div class="form-row">
50
+ <label for="node-input-duration"><i class="fa fa-tint"></i> Duration (seconds)</label>
51
+ <input type="number" id="node-input-duration" placeholder="30" step="1" min="1">
52
+ </div>
53
+ <hr/>
54
+ <div class="form-row">
55
+ <label for="node-input-tickInterval"><i class="fa fa-clock-o"></i> Tick Interval (seconds)</label>
56
+ <input type="number" id="node-input-tickInterval" placeholder="60" step="1" min="1">
39
57
  </div>
40
58
  </script>
41
59
 
42
60
  <script type="text/html" data-help-name="precipitation controller">
43
- <p>The precipitation controller lorem ipsum ...</p>
61
+ <p>The <b>Precipitation Controller</b> drives a pump relay on an interval/duration cycle
62
+ for vivarium misting or rain simulation.</p>
63
+
64
+ <h3>Input</h3>
65
+ <dl class="message-properties">
66
+ <dt>msg.topic = "interval" <span class="property-type">number</span></dt>
67
+ <dd><code>msg.payload</code> sets the precipitation interval in minutes, overriding the configured default</dd>
68
+
69
+ <dt>msg.topic = "duration" <span class="property-type">number</span></dt>
70
+ <dd><code>msg.payload</code> sets the pump duration in seconds, overriding the configured default</dd>
71
+
72
+ <dt>msg.topic = "night" <span class="property-type">boolean</span></dt>
73
+ <dd><code>msg.payload</code> signals nighttime (<code>true</code>) or daytime (<code>false</code>).
74
+ During nighttime no new precipitation events are started. If the pump is already active
75
+ when night begins, the current cycle finishes normally.</dd>
76
+ </dl>
77
+ <p>Messages with unrecognised topics are ignored.</p>
78
+
79
+ <h3>Output</h3>
80
+ <dl class="message-properties">
81
+ <dt>msg.payload <span class="property-type">object</span></dt>
82
+ <dd><code>{ pump: "on" }</code> when the pump turns on, <code>{ pump: "off" }</code> when it turns off.
83
+ <code>msg.topic</code> is set to <code>"actuators"</code>.
84
+ Two messages are sent per precipitation event.</dd>
85
+ </dl>
86
+
87
+ <h3>Details</h3>
88
+ <p>The node runs an autonomous internal tick timer. On each tick it evaluates whether
89
+ precipitation is due:</p>
90
+ <ul>
91
+ <li>If the pump is already running the tick is skipped (no overlapping cycles).</li>
92
+ <li>If nighttime is signalled, the tick is skipped.</li>
93
+ <li>If this is the first tick after deploy, or the configured interval has elapsed
94
+ since the last event, the pump is turned on for the configured duration and then
95
+ automatically turned off.</li>
96
+ <li>Otherwise the node updates its status to show the time remaining
97
+ until the next event.</li>
98
+ </ul>
99
+ <p>On redeploy or shutdown the node clears all timers and forces the pump off
100
+ to prevent it from getting stuck on.</p>
101
+
102
+ <h3>Configuration</h3>
103
+ <ul>
104
+ <li><strong>Interval:</strong> Time between precipitation events in minutes (default: 60)</li>
105
+ <li><strong>Duration:</strong> How long the pump runs per event in seconds (default: 30)</li>
106
+ <li><strong>Tick Interval:</strong> How often to evaluate whether precipitation is due in seconds (default: 60)</li>
107
+ </ul>
108
+
109
+ <h3>Status</h3>
110
+ <ul>
111
+ <li><strong>idle</strong> (grey/ring) — initial state before first tick</li>
112
+ <li><strong>pump on</strong> (blue/dot) — pump is running, shows duration</li>
113
+ <li><strong>next in …</strong> (green/ring) — waiting, shows time until next event</li>
114
+ <li><strong>last: HH:MM:SS</strong> (grey/ring) — after pump off, shows timestamp</li>
115
+ <li><strong>paused (night)</strong> (grey/ring) — precipitation disabled during nighttime</li>
116
+ </ul>
44
117
  </script>
@@ -1,22 +1,98 @@
1
1
  "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
2
+ const formatDuration = (ms) => {
3
+ if (ms >= 3600000) {
4
+ return (ms / 3600000).toFixed(1) + 'h';
5
+ }
6
+ else if (ms >= 60000) {
7
+ return (ms / 60000).toFixed(1) + 'm';
8
+ }
9
+ else {
10
+ return (ms / 1000).toFixed(0) + 's';
11
+ }
10
12
  };
11
13
  const precipitationController = (RED) => {
12
14
  function PrecipitationControllerNode(config) {
13
15
  RED.nodes.createNode(this, config);
14
16
  const node = this;
15
- node.status({ fill: 'green', shape: 'dot', text: 'running' });
16
- node.on('input', (msg, send, done) => __awaiter(this, void 0, void 0, function* () {
17
- send(msg);
17
+ this.interval = parseFloat(config.interval) || 60;
18
+ this.duration = parseFloat(config.duration) || 30;
19
+ this.tickInterval = parseInt(config.tickInterval) || 60;
20
+ const state = {
21
+ lastPrecipitation: null,
22
+ pumpActive: false,
23
+ intervalMs: (node.interval || 60) * 60000,
24
+ durationMs: (node.duration || 30) * 1000,
25
+ night: false
26
+ };
27
+ const evaluatePrecipitation = () => {
28
+ if (state.pumpActive) {
29
+ return;
30
+ }
31
+ if (state.night) {
32
+ node.status({ fill: 'grey', shape: 'ring', text: 'paused (night)' });
33
+ return;
34
+ }
35
+ const now = Date.now();
36
+ if (state.lastPrecipitation === null || (now - state.lastPrecipitation) >= state.intervalMs) {
37
+ state.pumpActive = true;
38
+ state.lastPrecipitation = now;
39
+ node.send({ payload: { pump: 'on' }, topic: 'actuators' });
40
+ node.status({ fill: 'blue', shape: 'dot', text: `pump on (${formatDuration(state.durationMs)})` });
41
+ state.offTimer = setTimeout(() => {
42
+ state.pumpActive = false;
43
+ state.offTimer = undefined;
44
+ node.send({ payload: { pump: 'off' }, topic: 'actuators' });
45
+ const timeStr = new Date().toLocaleTimeString('en-GB', { hour12: false });
46
+ node.status({ fill: 'grey', shape: 'ring', text: `last: ${timeStr}` });
47
+ }, state.durationMs);
48
+ }
49
+ else {
50
+ const elapsed = now - state.lastPrecipitation;
51
+ const remaining = state.intervalMs - elapsed;
52
+ node.status({ fill: 'green', shape: 'ring', text: `next in ${formatDuration(remaining)}` });
53
+ }
54
+ };
55
+ const startTickInterval = () => {
56
+ if (state.tickTimerId) {
57
+ clearInterval(state.tickTimerId);
58
+ }
59
+ const intervalMs = (node.tickInterval || 60) * 1000;
60
+ state.tickTimerId = setInterval(() => {
61
+ evaluatePrecipitation();
62
+ }, intervalMs);
63
+ };
64
+ node.on('input', (msg, send, done) => {
65
+ if (msg.topic === 'interval' && typeof msg.payload === 'number' && isFinite(msg.payload)) {
66
+ state.intervalMs = msg.payload * 60000;
67
+ node.log(`Interval updated: ${msg.payload} minutes`);
68
+ }
69
+ else if (msg.topic === 'duration' && typeof msg.payload === 'number' && isFinite(msg.payload)) {
70
+ state.durationMs = msg.payload * 1000;
71
+ node.log(`Duration updated: ${msg.payload} seconds`);
72
+ }
73
+ else if (msg.topic === 'night' && typeof msg.payload === 'boolean') {
74
+ state.night = msg.payload;
75
+ node.log(`Night state updated: ${msg.payload}`);
76
+ }
18
77
  done === null || done === void 0 ? void 0 : done();
19
- }));
78
+ });
79
+ node.on('close', (done) => {
80
+ if (state.tickTimerId) {
81
+ clearInterval(state.tickTimerId);
82
+ state.tickTimerId = undefined;
83
+ }
84
+ if (state.offTimer) {
85
+ clearTimeout(state.offTimer);
86
+ state.offTimer = undefined;
87
+ }
88
+ if (state.pumpActive) {
89
+ state.pumpActive = false;
90
+ node.send({ payload: { pump: 'off' }, topic: 'actuators' });
91
+ }
92
+ done();
93
+ });
94
+ node.status({ fill: 'grey', shape: 'ring', text: 'idle' });
95
+ startTickInterval();
20
96
  }
21
97
  RED.nodes.registerType('precipitation controller', PrecipitationControllerNode);
22
98
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-freya-nodes",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Custom nodes for Freya Vivarium Control System",
5
5
  "author": "Sanne 'SpuQ' Santens",
6
6
  "license": "GPL-3.0",