node-red-contrib-freya-nodes 0.0.20

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.txt ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Sanne 'SpuQ' Santens
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ ![Freya Banner](https://raw.githubusercontent.com/Freya-Vivariums/.github/refs/heads/main/brand/Freya_banner.png)
2
+
3
+ <img src="https://nodered.org/about/resources/media/node-red-icon.png" align="right" width="10%"/>
4
+
5
+ **[Node-RED](https://nodered.org/)** is a visual programming tool that lets you wire together hardware, APIs, and online services by connecting blocks in a flow-based editor. The **Freya Node-RED nodes** lorem ipsum...
6
+
7
+ <br clear="right"/>
8
+
9
+ [![Node-RED](https://img.shields.io/badge/Node--RED-Freya-%23A2CA6F?logo=nodered)](https://flows.nodered.org/node/node-red-contrib-freya)
10
+
11
+ ## Installation
12
+ #### Node-RED flow editor
13
+ Navigate to `Settings > Manage Palette`, then in the `Install` tab, search for `node-red-contrib-freya` and click the `Install` button.
14
+
15
+ #### Manually using NPM
16
+ On your device, navigate to the `nodered` folder, and run
17
+ ```
18
+ npm install node-red-contrib-freya
19
+ ```
20
+
21
+ ## License & Collaboration
22
+ **Copyright© 2025 Sanne 'SpuQ' Santens**. The Freya NodeRED nodes project is licensed under the **[MIT License](LICENSE.txt)**. The [Rules & Guidelines](https://github.com/Freya-Vivariums/.github/blob/main/brand/Freya_Trademark_Rules_and_Guidelines.md) apply to the usage of the Freya Vivariums™ brand.
23
+
24
+ ### Collaboration
25
+
26
+ If you'd like to contribute to this project, please follow these guidelines:
27
+ 1. Fork the repository and create your branch from `main`.
28
+ 2. Make your changes and ensure they adhere to the project's coding style and conventions.
29
+ 3. Test your changes thoroughly.
30
+ 4. Ensure your commits are descriptive and well-documented.
31
+ 5. Open a pull request, describing the changes you've made and the problem or feature they address.
Binary file
@@ -0,0 +1,104 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('circadian core',{
3
+ category: 'Freya Vivariums',
4
+ color: "#A2CA6F",
5
+ defaults: {
6
+ name: {value:""}
7
+ },
8
+ inputs:1,
9
+ outputs:2,
10
+ outputLabels:["control", "status"],
11
+ icon: "font-awesome/fa-globe",
12
+ label: function() {
13
+ return this.name || "Circadian Core";
14
+ }
15
+ });
16
+ </script>
17
+
18
+
19
+ <script type="text/html" data-template-name="circadian core">
20
+ <div class="form-row">
21
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
22
+ <input type="text" id="node-input-name" placeholder="Circadian Core">
23
+ </div>
24
+ <hr/>
25
+ <div class="form-row">
26
+ <h2><i class="fa fa-map-marker"></i> Location Settings</h2>
27
+ </div>
28
+ <div class="form-row">
29
+ <label for="node-input-latitude">Latitude (°)</label>
30
+ <input type="number" id="node-input-latitude" placeholder="50.98">
31
+ <i class="fa fa-info-circle" title="The latitude of your simulated environment. Controls day length variation across the year. (e.g. 0° = Equator, 50° = Central Europe)"></i>
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-timezone">Timezone</label>
35
+ <input type="text" id="node-input-timezone" placeholder="UTC+2 Copenhagen, Brussels, Kampenhout">
36
+ <i class="fa fa-info-circle" title="Timezone offset in hours. Aligns the simulated solar noon with your local wall clock."></i>
37
+ </div>
38
+ <hr/>
39
+ <div class="form-row">
40
+ <a href="#" id="toggle-advanced" style="text-decoration:none; color:#C0504D;">
41
+ <i class="fa fa-caret-right"></i> Advanced settings
42
+ </a>
43
+ </div>
44
+ <!-- Advanced settings -->
45
+ <div id="advanced-settings" style="display:none;">
46
+ <div class="form-row">
47
+ <h2><i class="fa fa-globe"></i> Planetary settings</h2>
48
+ </div>
49
+ <div class="form-row">
50
+ <label for="node-input-axialTilt">Axial Tilt (°)</label>
51
+ <input type="number" id="node-input-axialTilt" placeholder="23.44">
52
+ <i class="fa fa-info-circle" title="Angle between the rotation axis and orbital plane. Controls seasonal variation. Earth = 23.44°"></i>
53
+ </div>
54
+ <div class="form-row">
55
+ <label for="node-input-orbitalPeriod">Orbital Period (days)</label>
56
+ <input type="number" id="node-input-orbitalPeriod" placeholder="365.25">
57
+ <i class="fa fa-info-circle" title="Length of a full year (orbit around the sun). Earth = 365.25 days."></i>
58
+ </div>
59
+ <div class="form-row">
60
+ <label for="node-input-rotationalPeriod">Rotational Period (hours)</label>
61
+ <input type="number" id="node-input-rotationalPeriod" placeholder="24">
62
+ <i class="fa fa-info-circle" title="Length of a full day-night cycle. Earth = 24 hours."></i>
63
+ </div>
64
+ <hr/>
65
+ <div class="form-row">
66
+ <h2><i class="fa fa-clock-o"></i> Time</h2>
67
+ </div>
68
+ <div class="form-row">
69
+ <label for="node-input-timeScale">Time Scale</label>
70
+ <input type="number" id="node-input-timeScale" placeholder="1.0">
71
+ <i class="fa fa-info-circle" title="Controls simulation speed. 1.0 = real time, 2.0 = twice as fast, 0.5 = half speed."></i>
72
+ </div>
73
+
74
+ </div>
75
+ <!--- END Advanced settings -->
76
+ <script>
77
+ $(function() {
78
+ $('#toggle-advanced').on('click', function(e) {
79
+ e.preventDefault();
80
+ var adv = $('#advanced-settings');
81
+ var icon = $(this).find('i');
82
+ if (adv.is(':visible')) {
83
+ adv.hide();
84
+ icon.removeClass('fa-caret-down').addClass('fa-caret-right');
85
+ $(this).contents().last()[0].textContent = ' Advanced settings';
86
+ } else {
87
+ adv.show();
88
+ icon.removeClass('fa-caret-right').addClass('fa-caret-down');
89
+ $(this).contents().last()[0].textContent = ' Advanced settings';
90
+ }
91
+ });
92
+ });
93
+ </script>
94
+ </script>
95
+
96
+
97
+
98
+ <script type="text/html" data-help-name="circadian core">
99
+ <p>
100
+ The <strong>Circadian Core</strong> dynamically computes target values for key vivarium variables
101
+ (temperature, humidity, light), creating natural circadian and seasonal rhythms —
102
+ while always enforcing explicit absolute safety constraints.
103
+ </p>
104
+ </script>
@@ -0,0 +1,23 @@
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
+ });
10
+ };
11
+ const circadianCore = (RED) => {
12
+ function CircadianCoreNode(config) {
13
+ RED.nodes.createNode(this, config);
14
+ 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);
18
+ done === null || done === void 0 ? void 0 : done();
19
+ }));
20
+ }
21
+ RED.nodes.registerType('circadian core', CircadianCoreNode);
22
+ };
23
+ module.exports = circadianCore;
@@ -0,0 +1,50 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('environment sensor',{
3
+ category: 'Freya Vivariums',
4
+ color: "#70B62D",
5
+ defaults: {
6
+ name: {value:""},
7
+ variable: {value:"all"},
8
+ sampleinterval: {value:""}
9
+ },
10
+ inputs:0,
11
+ outputs:2,
12
+ outputLabels:["control", "status"],
13
+ icon: "font-awesome/fa-microchip",
14
+ label: function() {
15
+ return this.name || "Environment Sensor";
16
+ }
17
+ });
18
+ </script>
19
+
20
+ <script type="text/html" data-template-name="environment sensor">
21
+ <div class="form-row">
22
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
23
+ <input type="text" id="node-input-name" placeholder="Name">
24
+ </div>
25
+ <hr/>
26
+ <div class="form-row">
27
+ <label for="node-input-variable">Environment Variable</label>
28
+ <select id="node-input-variable">
29
+ <option value="all" selected>All measurements</option>
30
+ <option value="temperature">Temperature [&deg;C]</option>
31
+ <option value="humidity">Relative Humidity [%]</option>
32
+ <option value="airquality" disabled>Relative Air Quality [%]</option>
33
+ <option value="pressure">Barometric Pressure [hPa]</option>
34
+ <option value="light">Light Intensity [Lux]</option>
35
+ <option value="uva" disabled>UVA (320-400 nm) [µW/cm²]</option>
36
+ <option value="uvb" disabled>UVB (280-320 nm) [µW/cm²]</option>
37
+ <option value="uvc" disabled>UVC (200-280 nm) [µW/cm²]</option>
38
+ </select>
39
+ <i class="fa fa-info-circle" title="The environment variable which we want to use"></i>
40
+ </div>
41
+ <div class="form-row">
42
+ <label for="node-input-sampleinterval">Sample Interval (s)</label>
43
+ <input type="number" id="node-input-sampleinterval" placeholder="5">
44
+ <i class="fa fa-info-circle" title="The sensor sample rate (seconds)"></i>
45
+ </div>
46
+ </script>
47
+
48
+ <script type="text/html" data-help-name="environment sensor">
49
+ <p>The <strong>Freya Environment Sensor</strong> lorem ipsum ...</p>
50
+ </script>
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ const environment_sensor_1 = require("./environment-sensor");
3
+ const environmentSensor = (RED) => {
4
+ function EnvironmentSensorNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+ const variable = config.variable;
8
+ const sampleinterval = parseFloat(config.sampleinterval);
9
+ const sensorDriver = new environment_sensor_1.SensorDriver();
10
+ sensorDriver.on('status', (status) => {
11
+ switch (status.level) {
12
+ case 'ok':
13
+ node.status({ fill: 'green', shape: 'dot', text: status.message });
14
+ break;
15
+ case 'warning':
16
+ node.status({ fill: 'yellow', shape: 'dot', text: status.message });
17
+ break;
18
+ default:
19
+ node.status({ fill: 'red', shape: 'dot', text: status.message });
20
+ break;
21
+ }
22
+ });
23
+ sensorDriver.on('measurement', (measurement) => {
24
+ if (Object.prototype.hasOwnProperty.call(measurement, variable) || variable === 'all') {
25
+ const msg = {
26
+ _msgid: '',
27
+ topic: "measurement",
28
+ payload: measurement
29
+ };
30
+ this.send(msg);
31
+ }
32
+ });
33
+ }
34
+ RED.nodes.registerType('environment sensor', EnvironmentSensorNode);
35
+ };
36
+ module.exports = environmentSensor;
@@ -0,0 +1,72 @@
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
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SensorDriver = void 0;
13
+ const dbus = require('dbus-native');
14
+ const events_1 = require("events");
15
+ class SensorDriver extends events_1.EventEmitter {
16
+ constructor() {
17
+ super();
18
+ this.bus = dbus.systemBus();
19
+ this.init();
20
+ }
21
+ init() {
22
+ return __awaiter(this, void 0, void 0, function* () {
23
+ try {
24
+ yield this.initDriverConnection();
25
+ console.log('Connected to sensor driver');
26
+ this.emit('status', { level: "ok", message: "Connected to sensor driver" });
27
+ }
28
+ catch (err) {
29
+ console.error('Error connecting to sensor driver:', err);
30
+ this.emit('status', { level: "error", message: "No connection to driver" });
31
+ setTimeout(() => { this.init(); }, 5 * 1000);
32
+ }
33
+ this.iface.on('measurement', (type, value) => {
34
+ const parsedValue = isNaN(Number(value)) ? value : Number(value);
35
+ this.emit('measurement', { [type]: parsedValue });
36
+ });
37
+ });
38
+ }
39
+ initDriverConnection() {
40
+ return __awaiter(this, void 0, void 0, function* () {
41
+ const service = this.bus.getService('io.freya.EnvironmentSensorDriver');
42
+ this.iface = yield new Promise((resolve, reject) => {
43
+ service.getInterface('/io/freya/EnvironmentSensorDriver', 'io.freya.EnvironmentSensorDriver', (err, iface) => {
44
+ if (err) {
45
+ reject(err);
46
+ }
47
+ else {
48
+ resolve(iface);
49
+ }
50
+ });
51
+ });
52
+ });
53
+ }
54
+ setSampleInterval(interval) {
55
+ return __awaiter(this, void 0, void 0, function* () {
56
+ if (!this.iface) {
57
+ throw new Error('Driver not initialized. Call init() first.');
58
+ }
59
+ return new Promise((resolve, reject) => {
60
+ this.iface.setDigitalOutput(interval, (err, result) => {
61
+ if (err) {
62
+ reject(err);
63
+ }
64
+ else {
65
+ resolve(result);
66
+ }
67
+ });
68
+ });
69
+ });
70
+ }
71
+ }
72
+ exports.SensorDriver = SensorDriver;
@@ -0,0 +1,27 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('humidity controller',{
3
+ category: 'Freya Vivariums',
4
+ color: "#A2CA6F",
5
+ defaults: {
6
+ name: {value:""}
7
+ },
8
+ inputs:1,
9
+ outputs:2,
10
+ outputLabels:["control", "status"],
11
+ icon: "font-awesome/fa-tint",
12
+ label: function() {
13
+ return this.name || "Humidity Controller";
14
+ }
15
+ });
16
+ </script>
17
+
18
+ <script type="text/html" data-template-name="humidity controller">
19
+ <div class="form-row">
20
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
21
+ <input type="text" id="node-input-name" placeholder="Name">
22
+ </div>
23
+ </script>
24
+
25
+ <script type="text/html" data-help-name="humidity controller">
26
+ <p>The humidity controller lorem ipsum ...</p>
27
+ </script>
@@ -0,0 +1,23 @@
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
+ });
10
+ };
11
+ const humidityController = (RED) => {
12
+ function HumidityControllerNode(config) {
13
+ RED.nodes.createNode(this, config);
14
+ 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);
18
+ done === null || done === void 0 ? void 0 : done();
19
+ }));
20
+ }
21
+ RED.nodes.registerType('humidity controller', HumidityControllerNode);
22
+ };
23
+ module.exports = humidityController;
@@ -0,0 +1,27 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('lighting controller',{
3
+ category: 'Freya Vivariums',
4
+ color: "#A2CA6F",
5
+ defaults: {
6
+ name: {value:""}
7
+ },
8
+ inputs:1,
9
+ outputs:2,
10
+ outputLabels:["control", "status"],
11
+ icon: "font-awesome/fa-lightbulb-o",
12
+ label: function() {
13
+ return this.name || "Lighting Controller";
14
+ }
15
+ });
16
+ </script>
17
+
18
+ <script type="text/html" data-template-name="lighting controller">
19
+ <div class="form-row">
20
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
21
+ <input type="text" id="node-input-name" placeholder="Name">
22
+ </div>
23
+ </script>
24
+
25
+ <script type="text/html" data-help-name="lighting controller">
26
+ <p>The lighting controller lorem ipsum ...</p>
27
+ </script>
@@ -0,0 +1,23 @@
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
+ });
10
+ };
11
+ const lightingController = (RED) => {
12
+ function LightingControllerNode(config) {
13
+ RED.nodes.createNode(this, config);
14
+ 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);
18
+ done === null || done === void 0 ? void 0 : done();
19
+ }));
20
+ }
21
+ RED.nodes.registerType('lighting controller', LightingControllerNode);
22
+ };
23
+ module.exports = lightingController;
@@ -0,0 +1,27 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('precipitation controller',{
3
+ category: 'Freya Vivariums',
4
+ color: "#A2CA6F",
5
+ defaults: {
6
+ name: {value:""}
7
+ },
8
+ inputs:1,
9
+ outputs:2,
10
+ outputLabels:["control", "status"],
11
+ icon: "font-awesome/fa-cloud",
12
+ label: function() {
13
+ return this.name || "Precipitation Controller";
14
+ }
15
+ });
16
+ </script>
17
+
18
+ <script type="text/html" data-template-name="precipitation controller">
19
+ <div class="form-row">
20
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
21
+ <input type="text" id="node-input-name" placeholder="Name">
22
+ </div>
23
+ </script>
24
+
25
+ <script type="text/html" data-help-name="precipitation controller">
26
+ <p>The precipitation controller lorem ipsum ...</p>
27
+ </script>
@@ -0,0 +1,23 @@
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
+ });
10
+ };
11
+ const precipitationController = (RED) => {
12
+ function PrecipitationControllerNode(config) {
13
+ RED.nodes.createNode(this, config);
14
+ 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);
18
+ done === null || done === void 0 ? void 0 : done();
19
+ }));
20
+ }
21
+ RED.nodes.registerType('precipitation controller', PrecipitationControllerNode);
22
+ };
23
+ module.exports = precipitationController;
@@ -0,0 +1,29 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('status aggregator',{
3
+ category: 'Freya Vivariums',
4
+ color: "#00A29A",
5
+ defaults: {
6
+ name: {value:""}
7
+ },
8
+ inputs:1,
9
+ outputs:2,
10
+ outputLabels:["control", "status"],
11
+ icon: "font-awesome/fa-stethoscope",
12
+ label: function() {
13
+ return this.name || "Status Aggregator";
14
+ }
15
+ });
16
+ </script>
17
+
18
+ <script type="text/html" data-template-name="status aggregator">
19
+ <div class="form-row">
20
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
21
+ <input type="text" id="node-input-name" placeholder="Name">
22
+ </div>
23
+ </script>
24
+
25
+ <script type="text/html" data-help-name="status aggregator">
26
+ <p>
27
+ The <strong>Status Aggregator</strong> collects, organizes, and evaluates health and diagnostic signals from the connected components.
28
+ </p>
29
+ </script>
@@ -0,0 +1,23 @@
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
+ });
10
+ };
11
+ const StatusAggregator = (RED) => {
12
+ function StatusAggregatorNode(config) {
13
+ RED.nodes.createNode(this, config);
14
+ 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);
18
+ done === null || done === void 0 ? void 0 : done();
19
+ }));
20
+ }
21
+ RED.nodes.registerType('status aggregator', StatusAggregatorNode);
22
+ };
23
+ module.exports = StatusAggregator;
@@ -0,0 +1,59 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('system actuators',{
3
+ category: 'Freya Vivariums',
4
+ color: "#70B62D",
5
+ defaults: {
6
+ name: {value:""},
7
+ actuator: {value:""},
8
+ channel: {value:""},
9
+ mode: {value:""}
10
+ },
11
+ inputs:1,
12
+ outputs:1,
13
+ outputLabels:["status"],
14
+ icon: "font-awesome/fa-microchip",
15
+ label: function() {
16
+ return this.name || "System Actuator";
17
+ }
18
+ });
19
+ </script>
20
+
21
+ <script type="text/html" data-template-name="system actuators">
22
+ <div class="form-row">
23
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
24
+ <input type="text" id="node-input-name" placeholder="Name">
25
+ </div>
26
+ <hr/>
27
+ <div class="form-row">
28
+ <label for="node-input-actuator"><i class="icon-tag"></i> Actuator</label>
29
+ <input type="text" id="node-input-actuator" placeholder="e.g. heater">
30
+ <i class="fa fa-info-circle" title="The name of te actuator. This node will filter messages based on this name"></i>
31
+ </div>
32
+ <div class="form-row">
33
+ <label for="node-input-channel"><i class="icon-cog"></i> Channel</label>
34
+ <select id="node-input-channel">
35
+ <option value="0" disabled selected>Select output...</option>
36
+ <option value="1">Digital Output 1</option>
37
+ <option value="2">Digital Output 2</option>
38
+ <option value="3">Digital Output 3</option>
39
+ <option value="4">Digital Output 4</option>
40
+ <option value="5">Digital Output 5</option>
41
+ <option value="6">Digital Output 6</option>
42
+ <option value="0">Disabled</option>
43
+ </select>
44
+ <i class="fa fa-info-circle" title="The physical digital output where the actuator is connected to"></i>
45
+ </div>
46
+ <div class="form-row">
47
+ <label for="node-input-mode"><i class="icon-exchange"></i> Mode</label>
48
+ <select id="node-input-mode">
49
+ <option value="" disabled selected>Select mode...</option>
50
+ <option value="digital">on/off</option>
51
+ <option value="pwm" disabled>PWM</option>
52
+ </select>
53
+ <i class="fa fa-info-circle" title="The output mode"></i>
54
+ </div>
55
+ </script>
56
+
57
+ <script type="text/html" data-help-name="system actuators">
58
+ <p>The <strong>Freya System Actuators</strong> lorem ipsum ...</p>
59
+ </script>
@@ -0,0 +1,66 @@
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
+ });
10
+ };
11
+ const system_actuators_1 = require("./system-actuators");
12
+ const systemActuators = (RED) => {
13
+ function SystemActuatorsNode(config) {
14
+ RED.nodes.createNode(this, config);
15
+ const node = this;
16
+ const actuator = config.actuator;
17
+ const channel = parseInt(config.channel);
18
+ const mode = config.mode;
19
+ const actuatorsDriver = new system_actuators_1.ActuatorsDriver();
20
+ actuatorsDriver.on('status', (status) => {
21
+ switch (status.level) {
22
+ case 'ok':
23
+ node.status({ fill: 'green', shape: 'dot', text: status.message });
24
+ break;
25
+ case 'warning':
26
+ node.status({ fill: 'yellow', shape: 'dot', text: status.message });
27
+ break;
28
+ default:
29
+ node.status({ fill: 'red', shape: 'dot', text: status.message });
30
+ break;
31
+ }
32
+ });
33
+ node.on('input', (msg, send, done) => __awaiter(this, void 0, void 0, function* () {
34
+ try {
35
+ if (msg.payload != null && typeof msg.payload === 'object') {
36
+ const rawValue = msg.payload[actuator];
37
+ if (typeof rawValue !== 'undefined') {
38
+ if (mode === 'digital') {
39
+ const state = (rawValue === true || rawValue === "true" || rawValue === 1 || rawValue === "1" || rawValue === 'on') ? true : false;
40
+ actuatorsDriver.setDigitalOutput(channel, state)
41
+ .then((res) => {
42
+ if (res)
43
+ node.status({ fill: 'green', shape: 'dot', text: `D${channel} turned ${state ? 'on' : 'off'}` });
44
+ else
45
+ node.status({ fill: 'yellow', shape: 'dot', text: 'Invalid request' });
46
+ })
47
+ .catch((err) => {
48
+ node.status({ fill: 'yellow', shape: 'dot', text: err });
49
+ });
50
+ }
51
+ else if (mode === 'pwm') {
52
+ }
53
+ }
54
+ }
55
+ }
56
+ catch (err) {
57
+ node.status({ fill: 'red', shape: 'dot', text: "Failed to set output" });
58
+ done(err);
59
+ return;
60
+ }
61
+ done();
62
+ }));
63
+ }
64
+ RED.nodes.registerType('system actuators', SystemActuatorsNode);
65
+ };
66
+ module.exports = systemActuators;
@@ -0,0 +1,68 @@
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
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ActuatorsDriver = void 0;
13
+ const dbus = require('dbus-native');
14
+ const events_1 = require("events");
15
+ class ActuatorsDriver extends events_1.EventEmitter {
16
+ constructor() {
17
+ super();
18
+ this.bus = dbus.systemBus();
19
+ this.init();
20
+ }
21
+ init() {
22
+ return __awaiter(this, void 0, void 0, function* () {
23
+ try {
24
+ yield this.initDriverConnection();
25
+ console.log('Connected to actuators driver');
26
+ this.emit('status', { level: "ok", message: "Connected to actuators driver" });
27
+ }
28
+ catch (err) {
29
+ console.error('Error connecting to actuators driver:', err);
30
+ this.emit('status', { level: "error", message: "No connection to driver" });
31
+ setTimeout(() => { this.init(); }, 5 * 1000);
32
+ }
33
+ });
34
+ }
35
+ initDriverConnection() {
36
+ return __awaiter(this, void 0, void 0, function* () {
37
+ const service = this.bus.getService('io.freya.SystemActuatorsDriver');
38
+ this.iface = yield new Promise((resolve, reject) => {
39
+ service.getInterface('/io/freya/SystemActuatorsDriver', 'io.freya.SystemActuatorsDriver', (err, iface) => {
40
+ if (err) {
41
+ reject(err);
42
+ }
43
+ else {
44
+ resolve(iface);
45
+ }
46
+ });
47
+ });
48
+ });
49
+ }
50
+ setDigitalOutput(channel, state) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ if (!this.iface) {
53
+ throw new Error('Driver not initialized. Call init() first.');
54
+ }
55
+ return new Promise((resolve, reject) => {
56
+ this.iface.setDigitalOutput(channel, state, (err, result) => {
57
+ if (err) {
58
+ reject(err);
59
+ }
60
+ else {
61
+ resolve(result);
62
+ }
63
+ });
64
+ });
65
+ });
66
+ }
67
+ }
68
+ exports.ActuatorsDriver = ActuatorsDriver;
@@ -0,0 +1,60 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('temperature controller',{
3
+ category: 'Freya Vivariums',
4
+ color: "#A2CA6F",
5
+ defaults: {
6
+ name: {value:""},
7
+ minimumTemperature: {value:"25", required:true, validate:RED.validators.number()},
8
+ maximumTemperature: {value:"30", required:true, validate:RED.validators.number()},
9
+ kp: {value:"10", required:false, validate:RED.validators.number()}
10
+ },
11
+ inputs:1,
12
+ outputs:2,
13
+ outputLabels:["control","status"],
14
+ icon: "font-awesome/fa-thermometer-half",
15
+ label: function() {
16
+ return this.name||"Temperature Controller";
17
+ }
18
+ });
19
+ </script>
20
+
21
+ <script type="text/html" data-template-name="temperature controller">
22
+ <div class="form-row">
23
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
24
+ <input type="text" id="node-input-name" placeholder="Name">
25
+ </div>
26
+ <hr/>
27
+ <h2><i class="fa fa-thermometer-full"></i> Extremes</h2>
28
+ <p>The absolute maximum ratings</p>
29
+ <div class="form-row">
30
+ <label for="node-input-minimumTemperature"><i class="icon-cog"></i> Minimum (°C)</label>
31
+ <input type="number" id="node-input-minimumTemperature" placeholder="25">
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-maximumTemperature"><i class="icon-cog"></i> Maximum (°C)</label>
35
+ <input type="number" id="node-input-maximumTemperature" placeholder="30">
36
+ </div>
37
+ <hr/>
38
+ <h2><i class="fa fa-sliders"></i> P-controller</h2>
39
+ <div class="form-row">
40
+ <label for="node-input-kp"><i class="icon-cogs"></i> Proportional Gain (Kp)</label>
41
+ <input type="number" id="node-input-kp" placeholder="10">
42
+ </div>
43
+ </script>
44
+
45
+ <script type="text/html" data-help-name="temperature controller">
46
+ <p>The <b>Temperature Controller</b> node implements a proportional (P) controller.</p>
47
+ <p><b>Inputs:</b>
48
+ <ul>
49
+ <li><code>msg.topic == 'temp'</code> (payload: number) &rarr; sensor reading</li>
50
+ <li><code>msg.topic == 'config'</code> (payload: { setpoint, kp }) &rarr; runtime configuration</li>
51
+ </ul>
52
+ </p>
53
+ <p><b>Outputs:</b>
54
+ <ol>
55
+ <li><code>control</code>: <code>msg.payload.effort</code> (-100 to +100)</li>
56
+ <li><code>status</code>: <code>msg.payload</code> object with <code>level</code>, <code>message</code>, and optional <code>details</code></li>
57
+ </ol>
58
+ </p>
59
+ <p>Configure the <i>Setpoint</i> (°C) and <i>Proportional Gain</i> (Kp) in the node's edit dialog.</p>
60
+ </script>
@@ -0,0 +1,44 @@
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
+ });
10
+ };
11
+ const temperature_controller_1 = require("./temperature-controller");
12
+ const temperatureController = (RED) => {
13
+ function TemperatureControllerNode(config) {
14
+ RED.nodes.createNode(this, config);
15
+ const node = this;
16
+ const controller = new temperature_controller_1.TemperatureController();
17
+ controller.on('status', (status) => {
18
+ node.status({ fill: status.level === 'error' ? 'red' : 'green', shape: 'ring', text: status.message });
19
+ node.send({ topic: 'status', payload: status });
20
+ });
21
+ controller.on('controlOutput', (effort) => {
22
+ node.send({ topic: 'control', payload: { effort } });
23
+ });
24
+ node.status({ fill: 'green', shape: 'dot', text: 'running' });
25
+ node.on('input', (msg, send, done) => __awaiter(this, void 0, void 0, function* () {
26
+ if (msg.topic === 'temp' && typeof msg.payload === 'number') {
27
+ controller.updateTemperature(msg.payload);
28
+ }
29
+ else if (msg.topic === 'config' && typeof msg.payload === 'object') {
30
+ const { setpoint, kp } = msg.payload;
31
+ controller.configure(parseFloat(setpoint), parseFloat(kp));
32
+ }
33
+ else {
34
+ node.warn('Unsupported message topic or payload');
35
+ }
36
+ }));
37
+ node.on('close', (done) => {
38
+ controller.clear();
39
+ done();
40
+ });
41
+ }
42
+ RED.nodes.registerType('temperature controller', TemperatureControllerNode);
43
+ };
44
+ module.exports = temperatureController;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TemperatureController = void 0;
4
+ const EventEmitter = require('events');
5
+ class TemperatureController extends EventEmitter {
6
+ constructor() {
7
+ super();
8
+ this.targetTemperature = 0;
9
+ this.maximumTemperature = 0;
10
+ this.maximumTemperature = 0;
11
+ this.currentTemperature = 0;
12
+ this.proportionalGain = 10;
13
+ this.controlEffort = 0;
14
+ this.isOpenLoop = true;
15
+ this.controlLoopTimer = null;
16
+ this.watchdogTimer = null;
17
+ this.watchdogTimeout = 60;
18
+ this._startWatchdog();
19
+ this.emitStatus('error', 'Open-loop mode', 'No sensor feedback — controller running without environmental feedback');
20
+ }
21
+ _startWatchdog() {
22
+ if (this.watchdogTimer)
23
+ clearTimeout(this.watchdogTimer);
24
+ this.setOpenLoopEnabled(false);
25
+ this.watchdogTimer = setTimeout(() => {
26
+ this.setOpenLoopEnabled(true);
27
+ }, this.watchdogTimeout * 1000);
28
+ }
29
+ clear() {
30
+ if (this.controlLoopTimer)
31
+ clearInterval(this.controlLoopTimer);
32
+ if (this.watchdogTimer)
33
+ clearTimeout(this.watchdogTimer);
34
+ this.controlEffort = 0;
35
+ this.emitStatus('error', 'Open-loop mode', 'Controller inactive — running without environmental feedback');
36
+ }
37
+ updateTemperature(temperature) {
38
+ this.currentTemperature = temperature;
39
+ this._startWatchdog();
40
+ }
41
+ getCurrentReading() {
42
+ return {
43
+ targetTemperature: this.targetTemperature,
44
+ currentTemperature: this.currentTemperature,
45
+ timestamp: Math.floor(Date.now() / 1000)
46
+ };
47
+ }
48
+ configure(minimumTemperature, maximumTemperature, proportionalGain = 10) {
49
+ this.proportionalGain = proportionalGain;
50
+ this.minimumTemperature = minimumTemperature;
51
+ this.maximumTemperature = maximumTemperature;
52
+ this.emitStatus('ok', 'Controller configured', `Setpoint=${this.targetTemperature}°C, Kp=${this.proportionalGain}`);
53
+ }
54
+ setSetpoint(targetTemperature) {
55
+ this.targetTemperature = targetTemperature;
56
+ this.emitStatus('ok', 'Setpoint Updated', `Setpoint=${this.targetTemperature}°C)`);
57
+ }
58
+ setOpenLoopEnabled(enabled) {
59
+ this.isOpenLoop = enabled;
60
+ }
61
+ emitStatus(level, message, details) {
62
+ this.emit('status', { level, message, details });
63
+ }
64
+ _executeControlLoop() {
65
+ if (this.isOpenLoop) {
66
+ this.controlEffort = 0;
67
+ this.emitStatus('error', 'Open-loop mode', 'No sensor feedback — controller running without environmental feedback');
68
+ this.emit('controlOutput', this.controlEffort);
69
+ return;
70
+ }
71
+ const error = this.targetTemperature - this.currentTemperature;
72
+ this.controlEffort = this.proportionalGain * error;
73
+ this.controlEffort = Math.max(-100, Math.min(100, this.controlEffort));
74
+ let statusMessage;
75
+ if (this.controlEffort > 0) {
76
+ statusMessage = `Heating at ${Math.round(this.controlEffort)}% effort`;
77
+ }
78
+ else if (this.controlEffort < 0) {
79
+ statusMessage = `Cooling at ${Math.round(-this.controlEffort)}% effort`;
80
+ }
81
+ else {
82
+ statusMessage = 'Idle (on target)';
83
+ }
84
+ this.emitStatus('ok', statusMessage);
85
+ this.emit('controlOutput', this.controlEffort);
86
+ }
87
+ }
88
+ exports.TemperatureController = TemperatureController;
89
+ module.exports = { TemperatureController };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "node-red-contrib-freya-nodes",
3
+ "version": "0.0.20",
4
+ "description": "Custom nodes for Freya Vivarium Control System",
5
+ "author": "Sanne 'SpuQ' Santens",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/Freya-Vivariums",
8
+ "bugs": {
9
+ "url": "https://www.reddit.com/r/FreyaVivariums"
10
+ },
11
+ "private": false,
12
+ "keywords": [
13
+ "node-red",
14
+ "node-red-node",
15
+ "freya",
16
+ "vivarium",
17
+ "control system",
18
+ "paludarium",
19
+ "terrarium",
20
+ "greenhouse"
21
+ ],
22
+ "scripts": {
23
+ "test": "echo \"Error: no test specified\" && exit 1",
24
+ "build": "bash ./scripts/build.sh",
25
+ "deploy": "bash ./scripts/deploy.sh && rm -rf build/"
26
+ },
27
+ "node-red": {
28
+ "nodes": {
29
+ "environment-sensor": "nodes/environment-sensor/environment-sensor-node.js",
30
+ "system-actuator": "nodes/system-actuators/system-actuators-node.js",
31
+ "circadian-core": "nodes/circadian-core/circadian-core-node.js",
32
+ "humidity-controller": "nodes/humidity-controller/humidity-controller-node.js",
33
+ "lighting-controller": "nodes/lighting-controller/lighting-controller-node.js",
34
+ "precipitation-controller": "nodes/precipitation-controller/precipitation-controller-node.js",
35
+ "temperature-controller": "nodes/temperature-controller/temperature-controller-node.js",
36
+ "status-aggregator": "nodes/status-aggregator/status-aggregator-node.js"
37
+ }
38
+ },
39
+ "files": [
40
+ "icons",
41
+ "nodes"
42
+ ],
43
+ "devDependencies": {
44
+ "@types/node-red": "^1.3.5",
45
+ "nodemon": "^3.1.4",
46
+ "ts-node": "^10.9.2"
47
+ },
48
+ "dependencies": {
49
+ "dbus-native": "^0.4.0"
50
+ }
51
+ }