@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 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": &lt;number&gt; // 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
+ };
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
+ };