@nightspark77/node-red-charlie-home-assistant 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/ask.html +133 -0
- package/ask.js +35 -0
- package/device.html +150 -0
- package/device.js +94 -0
- package/icons/device.png +0 -0
- package/icons/provider.png +0 -0
- package/package.json +23 -0
- package/provider.html +76 -0
- package/provider.js +39 -0
- package/shared.js +25 -0
package/ask.html
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('charlie-ask', {
|
|
3
|
+
category: 'Charlie',
|
|
4
|
+
paletteLabel: 'Ask',
|
|
5
|
+
color: '#fff',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
},
|
|
9
|
+
inputs: 1,
|
|
10
|
+
outputs: 1,
|
|
11
|
+
label: function () {
|
|
12
|
+
return this.name || 'Ask';
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script type="text/x-red" data-template-name="charlie-ask">
|
|
18
|
+
<form id="dialog-form" class="form-horizontal" autocomplete="off">
|
|
19
|
+
<div class="form-row">
|
|
20
|
+
<label for="node-input-name">Name</label>
|
|
21
|
+
<input id="node-input-name" />
|
|
22
|
+
</div>
|
|
23
|
+
</form>
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<script type="text/x-red" data-help-name="charlie-ask">
|
|
27
|
+
<p>
|
|
28
|
+
The <b>Charlie Ask</b> node sends a natural‑language prompt to the Charlie
|
|
29
|
+
assistant and returns the generated response. It acts as a simple interface
|
|
30
|
+
to the Charlie LLM backend and is designed for conversational flows,
|
|
31
|
+
automation queries, and contextual reasoning.
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<h3>Inputs</h3>
|
|
35
|
+
<p>
|
|
36
|
+
This node accepts a single input message. The input must contain a
|
|
37
|
+
<code>payload</code> object with a <code>message</code> field:
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<pre>
|
|
41
|
+
{
|
|
42
|
+
"message": "Your question or instruction"
|
|
43
|
+
}
|
|
44
|
+
</pre>
|
|
45
|
+
|
|
46
|
+
<ul>
|
|
47
|
+
<li><b>message</b> – Required. A string representing the prompt to send to
|
|
48
|
+
the assistant.</li>
|
|
49
|
+
</ul>
|
|
50
|
+
|
|
51
|
+
<p>Examples:</p>
|
|
52
|
+
|
|
53
|
+
<pre>
|
|
54
|
+
// Ask a question
|
|
55
|
+
{ "message": "Quels appareils sont éteints ?" }
|
|
56
|
+
|
|
57
|
+
// Request an explanation
|
|
58
|
+
{ "message": "Explique-moi comment fonctionne l'arrosage automatique." }
|
|
59
|
+
|
|
60
|
+
// Ask for help with automation
|
|
61
|
+
{ "message": "Ouvre le volet du salon à 50%." }
|
|
62
|
+
</pre>
|
|
63
|
+
|
|
64
|
+
<h3>Outputs</h3>
|
|
65
|
+
<p>
|
|
66
|
+
The node outputs a single message containing the assistant's full response
|
|
67
|
+
in <code>msg.payload</code>. The structure includes:
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
<ul>
|
|
71
|
+
<li><b>input_entries</b> – The messages sent to the assistant.</li>
|
|
72
|
+
<li><b>output_entries</b> – The assistant’s reasoning steps, tool calls,
|
|
73
|
+
and final answer.</li>
|
|
74
|
+
<li><b>conversation_id</b> – Identifier for the ongoing conversation.</li>
|
|
75
|
+
<li><b>files</b> – Optional file attachments.</li>
|
|
76
|
+
</ul>
|
|
77
|
+
|
|
78
|
+
<p>
|
|
79
|
+
The final assistant answer is always found in the last
|
|
80
|
+
<code>output_entries</code> item with <code>type: "message.output"</code>.
|
|
81
|
+
</p>
|
|
82
|
+
|
|
83
|
+
<h4>Example Output</h4>
|
|
84
|
+
|
|
85
|
+
<pre>
|
|
86
|
+
{
|
|
87
|
+
"input_entries": [
|
|
88
|
+
{
|
|
89
|
+
"role": "user",
|
|
90
|
+
"content": "Quel appareils sont eteints ?",
|
|
91
|
+
"type": "message.input"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"conversation_id": "conv_019bece990a773288d23949e97df190d",
|
|
95
|
+
"output_entries": [
|
|
96
|
+
{
|
|
97
|
+
"type": "function.call",
|
|
98
|
+
"name": "fetch-devices-state",
|
|
99
|
+
"arguments": "{}"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"type": "function.result",
|
|
103
|
+
"result": "[ ... device state list ... ]"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"type": "message.output",
|
|
107
|
+
"content": "Les appareils éteints sont l'ampoule du salon et la lumière cabannon2."
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
</pre>
|
|
112
|
+
|
|
113
|
+
<h3>Details</h3>
|
|
114
|
+
<p>
|
|
115
|
+
The Charlie assistant may call internal tools (for example,
|
|
116
|
+
<code>fetch-devices-state</code>) to gather information before generating
|
|
117
|
+
the final answer. These intermediate steps are included in
|
|
118
|
+
<code>output_entries</code> for transparency and debugging.
|
|
119
|
+
</p>
|
|
120
|
+
|
|
121
|
+
<p>
|
|
122
|
+
The last <code>message.output</code> entry contains the assistant’s final
|
|
123
|
+
natural‑language response, which is typically what you will use in your
|
|
124
|
+
flow.
|
|
125
|
+
</p>
|
|
126
|
+
|
|
127
|
+
<h3>Usage</h3>
|
|
128
|
+
<p>
|
|
129
|
+
Connect inject, function, or automation nodes to generate the
|
|
130
|
+
<code>msg.payload.message</code>. The output can be routed to debug nodes,
|
|
131
|
+
dashboards, notification systems, or other Charlie nodes.
|
|
132
|
+
</p>
|
|
133
|
+
</script>
|
package/ask.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { conf, isReady, apiUrl, mqttHost } = require('./shared');
|
|
2
|
+
|
|
3
|
+
module.exports = function (RED) {
|
|
4
|
+
function MyNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
|
|
8
|
+
node.on('input', async function (msg, send) {
|
|
9
|
+
await isReady();
|
|
10
|
+
|
|
11
|
+
const payload = msg.payload;
|
|
12
|
+
|
|
13
|
+
let json = undefined;
|
|
14
|
+
if (payload?.message) {
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`${apiUrl()}/api/assistant/chat`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({ message: payload.message }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
json = await res.json();
|
|
25
|
+
} catch (e) {
|
|
26
|
+
node.warn('Error cannot ask assistant please retry later.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
node.send([{ ...msg, payload: json }]);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
RED.nodes.registerType('charlie-ask', MyNode);
|
|
35
|
+
};
|
package/device.html
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('charlie-device', {
|
|
3
|
+
category: 'Charlie',
|
|
4
|
+
paletteLabel: 'Device',
|
|
5
|
+
color: '#fff',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '', required: true },
|
|
8
|
+
id: { value: '', required: true },
|
|
9
|
+
deviceType: { value: 'light', required: true },
|
|
10
|
+
externalId: { value: '', required: true },
|
|
11
|
+
outputs: { value: 2 },
|
|
12
|
+
},
|
|
13
|
+
inputs: 1,
|
|
14
|
+
outputs: function () {
|
|
15
|
+
const n = Number(this.outputs);
|
|
16
|
+
return isNaN(n) ? 2 : n;
|
|
17
|
+
},
|
|
18
|
+
outputLabels: function (index) {
|
|
19
|
+
return index === 0 ? 'On' : index === 1 ? 'Off' : 'Pause';
|
|
20
|
+
},
|
|
21
|
+
icon: 'device.png',
|
|
22
|
+
label: function () {
|
|
23
|
+
return this.name || 'Device';
|
|
24
|
+
},
|
|
25
|
+
oneditsave: function () {
|
|
26
|
+
const type = $('#node-input-deviceType').val();
|
|
27
|
+
this.outputs = type === 'shutter' ? 3 : 2;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<script type="text/x-red" data-template-name="charlie-device">
|
|
33
|
+
<form id="dialog-form" class="form-horizontal" autocomplete="off">
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<label for="node-input-id">Id</label>
|
|
36
|
+
<input id="node-input-id" />
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="form-row">
|
|
40
|
+
<label for="node-input-name">Name</label>
|
|
41
|
+
<input id="node-input-name" />
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="form-row">
|
|
45
|
+
<label for="node-input-externalId">External id</label>
|
|
46
|
+
<input id="node-input-externalId" />
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<label for="node-input-deviceType">Type</label>
|
|
51
|
+
<select id="node-input-deviceType">
|
|
52
|
+
<option value="light">Lum</option>
|
|
53
|
+
<option value="switch">Interupteur</option>
|
|
54
|
+
<option value="shutter">Volet</option>
|
|
55
|
+
<option value="sprinkler">Arrosage</option>
|
|
56
|
+
</select>
|
|
57
|
+
</div>
|
|
58
|
+
</form>
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<script type="text/x-red" data-help-name="charlie-device">
|
|
62
|
+
<p>
|
|
63
|
+
The <b>Charlie Device</b> node represents a controllable device managed by the
|
|
64
|
+
Charlie provider. It reacts to incoming messages and triggers the appropriate
|
|
65
|
+
action on the Charlie backend based on the content of <code>msg.payload</code>.
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
<h3>Inputs</h3>
|
|
69
|
+
<p>
|
|
70
|
+
This node accepts a single input message. The input must contain a
|
|
71
|
+
<code>payload</code> object describing the desired action:
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
<pre>
|
|
75
|
+
{
|
|
76
|
+
"state": "on" | "off" | "pause",
|
|
77
|
+
"level": <number> // optional
|
|
78
|
+
}
|
|
79
|
+
</pre>
|
|
80
|
+
|
|
81
|
+
<ul>
|
|
82
|
+
<li><b>state</b> – Required. Defines the action to perform.</li>
|
|
83
|
+
<li><b>level</b> – Optional. A numeric value used for devices that support
|
|
84
|
+
intensity or position (e.g., dimmable lights, shutters).</li>
|
|
85
|
+
</ul>
|
|
86
|
+
|
|
87
|
+
<p>
|
|
88
|
+
Examples:
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<pre>
|
|
92
|
+
// Turn on a light
|
|
93
|
+
{ "state": "on" }
|
|
94
|
+
|
|
95
|
+
// Turn off a device
|
|
96
|
+
{ "state": "off" }
|
|
97
|
+
|
|
98
|
+
// Set a shutter to 50%
|
|
99
|
+
{ "state": "on", "level": 50 }
|
|
100
|
+
|
|
101
|
+
// Pause a shutter
|
|
102
|
+
{ "state": "pause" }
|
|
103
|
+
</pre>
|
|
104
|
+
|
|
105
|
+
<h3>Outputs</h3>
|
|
106
|
+
<ul>
|
|
107
|
+
<li><b>Light / Switch / Sprinkler</b>: 2 outputs (On / Off)</li>
|
|
108
|
+
<li><b>Shutter</b>: 3 outputs (On / Off / Pause)</li>
|
|
109
|
+
</ul>
|
|
110
|
+
|
|
111
|
+
<p>
|
|
112
|
+
The number of outputs is automatically adjusted based on the selected
|
|
113
|
+
device type. Output labels are:
|
|
114
|
+
</p>
|
|
115
|
+
|
|
116
|
+
<ul>
|
|
117
|
+
<li><b>Output 1</b>: On</li>
|
|
118
|
+
<li><b>Output 2</b>: Off</li>
|
|
119
|
+
<li><b>Output 3</b> (shutter only): Pause</li>
|
|
120
|
+
</ul>
|
|
121
|
+
|
|
122
|
+
<h3>Properties</h3>
|
|
123
|
+
<ul>
|
|
124
|
+
<li><b>ID</b> – Unique identifier for this device within Charlie.</li>
|
|
125
|
+
<li><b>Name</b> – Display name shown in the Node‑RED editor.</li>
|
|
126
|
+
<li><b>External ID</b> – Identifier used by the Charlie backend.</li>
|
|
127
|
+
<li><b>Type</b> – Device category:
|
|
128
|
+
<ul>
|
|
129
|
+
<li><b>Lum</b> – Light</li>
|
|
130
|
+
<li><b>Interrupteur</b> – Switch</li>
|
|
131
|
+
<li><b>Volet</b> – Shutter</li>
|
|
132
|
+
<li><b>Arrosage</b> – Sprinkler</li>
|
|
133
|
+
</ul>
|
|
134
|
+
</li>
|
|
135
|
+
</ul>
|
|
136
|
+
|
|
137
|
+
<h3>Details</h3>
|
|
138
|
+
<p>
|
|
139
|
+
When a valid payload is received, the node forwards the corresponding
|
|
140
|
+
command to the Charlie backend. For shutters, the optional
|
|
141
|
+
<code>level</code> attribute can be used to set a specific position.
|
|
142
|
+
</p>
|
|
143
|
+
|
|
144
|
+
<h3>Usage</h3>
|
|
145
|
+
<p>
|
|
146
|
+
Connect inject, function, or automation logic nodes to generate the
|
|
147
|
+
appropriate <code>msg.payload</code>. Route the outputs to the rest of
|
|
148
|
+
your flow depending on the action you want to perform.
|
|
149
|
+
</p>
|
|
150
|
+
</script>
|
package/device.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const { conf, isReady, apiUrl, mqttHost } = require('./shared');
|
|
2
|
+
const mqtt = require('mqtt');
|
|
3
|
+
|
|
4
|
+
module.exports = function (RED) {
|
|
5
|
+
function MyNode(config) {
|
|
6
|
+
RED.nodes.createNode(this, config);
|
|
7
|
+
const node = this;
|
|
8
|
+
let ready = false;
|
|
9
|
+
let client = null;
|
|
10
|
+
|
|
11
|
+
(async () => {
|
|
12
|
+
if (
|
|
13
|
+
!config.id ||
|
|
14
|
+
!config.name ||
|
|
15
|
+
!config.externalId ||
|
|
16
|
+
!config.deviceType
|
|
17
|
+
)
|
|
18
|
+
return;
|
|
19
|
+
|
|
20
|
+
await isReady();
|
|
21
|
+
|
|
22
|
+
// register device
|
|
23
|
+
await fetch(`${apiUrl()}/api/devices`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
_id: config.id,
|
|
30
|
+
name: config.name,
|
|
31
|
+
externalId: config.externalId,
|
|
32
|
+
provider: conf.provider.id,
|
|
33
|
+
type: config.deviceType,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
client = mqtt.connect(mqttHost());
|
|
38
|
+
|
|
39
|
+
client.on('connect', () => {
|
|
40
|
+
client.subscribe(`device/${config.id}/state`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
client.on('message', (topic, payload) => {
|
|
44
|
+
const res = JSON.parse(payload.toString());
|
|
45
|
+
const oi =
|
|
46
|
+
res.power === 'on'
|
|
47
|
+
? 0
|
|
48
|
+
: res.power === 'off' || config.deviceType === 'shutter'
|
|
49
|
+
? 1
|
|
50
|
+
: 2;
|
|
51
|
+
|
|
52
|
+
const outputs = [null, null, null];
|
|
53
|
+
outputs[oi] = {
|
|
54
|
+
topic: config.id,
|
|
55
|
+
payload: { state: res.power, level: res.level },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
node.send(outputs);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
ready = true;
|
|
62
|
+
})();
|
|
63
|
+
|
|
64
|
+
node.on('input', async function (msg, send) {
|
|
65
|
+
if (!ready) return;
|
|
66
|
+
|
|
67
|
+
const payload = msg.payload;
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
payload.state === 'on' ||
|
|
71
|
+
payload.state === 'off' ||
|
|
72
|
+
payload.state === 'pause'
|
|
73
|
+
) {
|
|
74
|
+
try {
|
|
75
|
+
client.publish(
|
|
76
|
+
`device/state`,
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
id: config.id,
|
|
79
|
+
power: payload.state,
|
|
80
|
+
level: payload.level ?? 100,
|
|
81
|
+
}),
|
|
82
|
+
{
|
|
83
|
+
qos: 0,
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
node.warn('Error cannot send state to home assistant.');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
RED.nodes.registerType('charlie-device', MyNode);
|
|
94
|
+
};
|
package/icons/device.png
ADDED
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nightspark77/node-red-charlie-home-assistant",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create and control your own device with Charlie within nodered",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"node-red"
|
|
8
|
+
],
|
|
9
|
+
"author": "Quentin Vanhauteghem",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"node-red": {
|
|
14
|
+
"nodes": {
|
|
15
|
+
"provider": "provider.js",
|
|
16
|
+
"device": "device.js",
|
|
17
|
+
"ask": "ask.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"mqtt": "^5.14.1"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/provider.html
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('charlie-provider', {
|
|
3
|
+
category: 'Charlie',
|
|
4
|
+
paletteLabel: 'Provider',
|
|
5
|
+
color: '#fff',
|
|
6
|
+
defaults: {
|
|
7
|
+
id: { value: '', required: true },
|
|
8
|
+
name: { value: '', required: true },
|
|
9
|
+
charlieHost: { value: 'localhost' },
|
|
10
|
+
apiPort: { value: 9300 },
|
|
11
|
+
mqttPort: { value: 9304 },
|
|
12
|
+
},
|
|
13
|
+
inputs: 0,
|
|
14
|
+
outputs: 0,
|
|
15
|
+
icon: 'provider.png',
|
|
16
|
+
label: function () {
|
|
17
|
+
return this.name || 'Charlie provider';
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script type="text/x-red" data-template-name="charlie-provider">
|
|
23
|
+
<form id="dialog-form" class="form-horizontal" autocomplete="off">
|
|
24
|
+
<div class="form-row">
|
|
25
|
+
<label for="node-input-id"><i class="fa fa-tag"></i> ID</label>
|
|
26
|
+
<input type="text" id="node-input-id" />
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
31
|
+
<input type="text" id="node-input-name" />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<label for="node-input-charlieHost"
|
|
36
|
+
><i class="fa fa-tag"></i> Charlie host</label
|
|
37
|
+
>
|
|
38
|
+
<input type="text" id="node-input-charlieHost" placeholder="localhost" />
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="form-row">
|
|
42
|
+
<label for="node-input-apiPort"><i class="fa fa-tag"></i> API port</label>
|
|
43
|
+
<input type="number" id="node-input-apiPort" placeholder="9300" />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="form-row">
|
|
47
|
+
<label for="node-input-mqttPort"><i class="fa fa-tag"></i> MQTT port</label>
|
|
48
|
+
<input type="number" id="node-input-mqttPort" placeholder="9304" />
|
|
49
|
+
</div>
|
|
50
|
+
</form>
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<script type="text/x-red" data-help-name="charlie-provider">
|
|
54
|
+
<p>This node connects to a Charlie provider server.</p>
|
|
55
|
+
|
|
56
|
+
<h3>Inputs</h3>
|
|
57
|
+
<p>No inputs. This is a configuration node.</p>
|
|
58
|
+
|
|
59
|
+
<h3>Outputs</h3>
|
|
60
|
+
<p>No outputs.</p>
|
|
61
|
+
|
|
62
|
+
<h3>Properties</h3>
|
|
63
|
+
<ul>
|
|
64
|
+
<li><b>ID</b> – Unique identifier for this provider.</li>
|
|
65
|
+
<li><b>Name</b> – Display name.</li>
|
|
66
|
+
<li><b>Charlie host</b> – Hostname or IP of the Charlie server.</li>
|
|
67
|
+
<li><b>API port</b> – Port for the API.</li>
|
|
68
|
+
<li><b>MQTT port</b> – Port for MQTT communication.</li>
|
|
69
|
+
</ul>
|
|
70
|
+
|
|
71
|
+
<h3>Details</h3>
|
|
72
|
+
<p>
|
|
73
|
+
This node registers a provider with the Charlie backend and makes it
|
|
74
|
+
available to other Charlie nodes in the flow.
|
|
75
|
+
</p>
|
|
76
|
+
</script>
|
package/provider.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { conf, apiUrl } = require('./shared');
|
|
2
|
+
|
|
3
|
+
module.exports = function (RED) {
|
|
4
|
+
function MyNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
|
|
8
|
+
conf.provider.charlieHost = config.charlieHost;
|
|
9
|
+
conf.provider.apiPort = config.apiPort;
|
|
10
|
+
conf.provider.mqttPort = config.mqttPort;
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
if (!conf?.provider?.charlieHost) return;
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`${apiUrl()}/api/providers`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
_id: config.id,
|
|
22
|
+
name: config.name,
|
|
23
|
+
codesource: 'default_custom',
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const json = await res.json();
|
|
28
|
+
|
|
29
|
+
conf.provider.id = json.uuid;
|
|
30
|
+
|
|
31
|
+
conf.ready = true;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
node.warn('Charlie provider server host not configured');
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
RED.nodes.registerType('charlie-provider', MyNode);
|
|
39
|
+
};
|
package/shared.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const conf = {
|
|
2
|
+
provider: {
|
|
3
|
+
id: undefined,
|
|
4
|
+
host: undefined,
|
|
5
|
+
},
|
|
6
|
+
ready: false,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
conf,
|
|
11
|
+
apiUrl: () =>
|
|
12
|
+
`http://${conf.provider.charlieHost ?? 'localhost'}:${conf.provider.apiPort ?? '9300'}`,
|
|
13
|
+
mqttHost: () =>
|
|
14
|
+
`mqtt://${conf.provider.charlieHost}:${conf.provider.mqttHost ?? '9304'}`,
|
|
15
|
+
isReady: () => {
|
|
16
|
+
return new Promise((res) => {
|
|
17
|
+
const inter = setInterval(() => {
|
|
18
|
+
if (conf.ready) {
|
|
19
|
+
res();
|
|
20
|
+
}
|
|
21
|
+
clearInterval(inter);
|
|
22
|
+
}, 500);
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
};
|