├── .gitignore
├── LICENSE
├── README.md
├── images
└── red-subscribe-publish.gif
├── nodes
├── api
│ ├── api.html
│ ├── api.js
│ └── icons
│ │ ├── particle.png
│ │ └── particle@2x.png
├── config
│ ├── config.html
│ ├── config.js
│ └── helpers.js
├── function
│ ├── function.html
│ ├── function.js
│ └── icons
│ │ ├── particle.png
│ │ └── particle@2x.png
├── publish
│ ├── icons
│ │ ├── particle.png
│ │ └── particle@2x.png
│ ├── publish.html
│ └── publish.js
├── subscribe
│ ├── icons
│ │ ├── particle.png
│ │ └── particle@2x.png
│ ├── subscribe.html
│ └── subscribe.js
└── variable
│ ├── icons
│ ├── particle.png
│ └── particle@2x.png
│ ├── variable.html
│ └── variable.js
├── package-lock.json
├── package.json
└── src
├── api.js
├── helpers.js
└── particle-base-node.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Particle Industries
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node-RED Particle nodes
2 |
3 | 
4 |
5 | ## Installation
6 |
7 | ```bash
8 | $ cd ~/.node-red
9 | $ npm install @particle/node-red-contrib-particle-official
10 | ```
11 |
12 | ## Version History
13 |
14 | #### 0.1.7 (2021-05-13)
15 |
16 | - Upgrade lodash from 4.17.19 to 4.17.21 for security vulnerability fix
17 |
18 | #### 0.1.6 (2020-11-30)
19 |
20 | - Fixed a bug where a subscribe node would keep using the same value it received on the first event, even if the value changed.
21 | "lodash": "^4.17.19",
22 | "lodash": "^4.17.21",
--------------------------------------------------------------------------------
/images/red-subscribe-publish.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/images/red-subscribe-publish.gif
--------------------------------------------------------------------------------
/nodes/api/api.html:
--------------------------------------------------------------------------------
1 |
2 |
312 |
313 |
314 |
--------------------------------------------------------------------------------
/nodes/api/api.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ParticleBaseNode = require('../../src/particle-base-node');
4 |
5 | module.exports = (RED) => {
6 | function ParticleAPINode(node) {
7 | RED.nodes.createNode(this, node);
8 |
9 | new ParticleBaseNode({
10 | self: this,
11 | node: node,
12 | RED: RED,
13 | properties: ['name', 'method', 'url', 'payload'],
14 | inputProperties: ['method', 'url', 'payload'],
15 | functionName: 'request',
16 | functionArguments: [
17 | { key: 'uri', value: 'url', prefix: 'https://api.particle.io/', customValue: ({ url }) => {
18 | if (url[0] === '/') {
19 | url = url.substring(1);
20 | }
21 |
22 | return url;
23 | } },
24 | { key: 'method', value: 'method' },
25 | { key: 'data', value: 'payload', format: 'object' }
26 | ],
27 | mandatoryArguments: ['uri', 'method'],
28 | success: {
29 | fields: [
30 | { key: 'payload', path: 'body' },
31 | { key: 'statusCode', path: 'statusCode' }
32 | ],
33 | status: 'got response'
34 | },
35 | info: {
36 | status: 'requesting'
37 | },
38 | error: {
39 | status: 'failed',
40 | onRuntime: 'Failed to call endpoint',
41 | onConfig: 'Configuration error'
42 | }
43 | });
44 | }
45 |
46 | RED.nodes.registerType('particle-api', ParticleAPINode);
47 |
48 | };
49 |
--------------------------------------------------------------------------------
/nodes/api/icons/particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/api/icons/particle.png
--------------------------------------------------------------------------------
/nodes/api/icons/particle@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/api/icons/particle@2x.png
--------------------------------------------------------------------------------
/nodes/config/config.html:
--------------------------------------------------------------------------------
1 |
13 |
14 |
32 |
--------------------------------------------------------------------------------
/nodes/config/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = (RED) => {
4 | function ParticleConfigNode(n) {
5 | RED.nodes.createNode(this, n);
6 | this.clientId = n.clientId;
7 | this.clientSecret = n.clientSecret;
8 | }
9 |
10 | RED.nodes.registerType('particle-config', ParticleConfigNode);
11 |
12 | };
13 |
--------------------------------------------------------------------------------
/nodes/config/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (typeof window !== 'undefined' && !window.helpers) {
4 |
5 | const generateFieldScopeSelector = () => {
6 | return `
7 |
8 |
9 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | `;
24 | };
25 |
26 | const generateFieldSelect = (field) => {
27 | let html = `
28 |
29 |
30 |
31 |
44 |
45 | `;
46 |
47 | return html;
48 | };
49 |
50 | const generateField = (field) => {
51 | let html = '';
52 |
53 | if (field.type === 'scope-selector') {
54 | html = generateFieldScopeSelector(field);
55 | } else if (field.type === 'select') {
56 | html = generateFieldSelect(field);
57 | } else {
58 | html = `
59 |
60 |
61 |
62 |
63 | `;
64 | }
65 |
66 | return html;
67 | };
68 |
69 | const generateEditTemplate = ({ name, fields }) => {
70 | let html = '';
71 |
72 | fields.forEach(field => {
73 | html += generateField(field);
74 | });
75 |
76 | $(`script[data-template-name="${name}"]`).html(html);
77 | };
78 |
79 | const generateDocumentation = ({ name, title, inputs, outputs, details, resources }) => {
80 | let html = `
81 | ${title}
82 | `;
83 |
84 | if (inputs) {
85 | html += `
86 | Inputs
87 |
88 | `;
89 |
90 | inputs.forEach(input => {
91 | html += `
92 | - ${input.name} ${input.type}
93 | - ${input.description}
94 | `;
95 | });
96 | }
97 |
98 | html += `
99 |
100 | `;
101 |
102 | if (outputs) {
103 | html += `
104 | Outputs
105 |
106 | `;
107 |
108 | outputs.forEach(output => {
109 | html += `
110 | - ${output.name} ${output.type}
111 | - ${output.description}
112 | `;
113 | });
114 |
115 |
116 | html += `
117 |
118 | `;
119 | }
120 |
121 | html += `
122 |
123 | Details
124 | ${details}
125 | Resources
126 |
127 | `;
128 |
129 |
130 |
131 | resources.forEach(resource => {
132 | html += `
133 | -
134 | ${resource.name} - ${resource.description}
135 |
136 | `;
137 | });
138 |
139 | html += '
';
140 |
141 | $(`script[data-help-name="${name}"]`).html(html);
142 | };
143 |
144 | const generateClientConfig = (config) => {
145 | let nodeClient = Object.assign({}, config, {
146 | category: 'particle',
147 | color: '#C0DEED'
148 | });
149 |
150 | if (config._scopeSelector === true) {
151 | nodeClient.onScopeChange = function onscopechange() {
152 | let stream = $('input[name=node-input-scope-options]:checked').val() || 'user';
153 |
154 | if (stream === 'product') {
155 | $('#particle-scope-product').removeClass('hidden');
156 | } else {
157 | $('#particle-scope-product').addClass('hidden');
158 | $('#node-input-product').val('');
159 | }
160 |
161 | $('#node-input-stream').val(stream);
162 | };
163 |
164 | nodeClient.oneditprepare = function oneditprepare() {
165 | $('input[name=node-input-scope-options]')
166 | .prop('checked', null)
167 | .change(nodeClient.onScopeChange.bind(this));
168 |
169 | const isProduct = $('#node-input-product').val();
170 |
171 | if (isProduct) {
172 | $('#node-input-scope-product').prop('checked', true);
173 | } else {
174 | $('#node-input-scope-user').prop('checked', true);
175 | }
176 |
177 | nodeClient.onScopeChange();
178 | };
179 |
180 | nodeClient.oneditsave = function oneditsave() {
181 | nodeClient.onScopeChange();
182 | };
183 | }
184 |
185 | return nodeClient;
186 | };
187 |
188 | const addCSS = () => {
189 | $('body').append(`
190 |
191 |
204 | `);
205 | };
206 |
207 | const generateNodeFromDefinition = (name, definition) => {
208 | generateEditTemplate(definition.editTemplate);
209 | generateDocumentation(definition.documentation);
210 | addCSS();
211 |
212 | RED.nodes.registerType(name, generateClientConfig(definition.client));
213 | };
214 |
215 | /*global window*/
216 | window.helpers = {
217 | generateEditTemplate,
218 | generateDocumentation,
219 | generateClientConfig,
220 | generateNodeFromDefinition
221 | };
222 |
223 | }
--------------------------------------------------------------------------------
/nodes/function/function.html:
--------------------------------------------------------------------------------
1 |
2 |
308 |
309 |
310 |
--------------------------------------------------------------------------------
/nodes/function/function.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ParticleBaseNode = require('../../src/particle-base-node');
4 |
5 | module.exports = (RED) => {
6 | function ParticleFunctionNode(node) {
7 | RED.nodes.createNode(this, node);
8 |
9 | new ParticleBaseNode({
10 | self: this,
11 | node: node,
12 | RED: RED,
13 | properties: ['name', 'scope', 'device', 'product', 'function', 'argument'],
14 | inputProperties: ['device', 'function', 'argument'],
15 | functionName: 'callFunction',
16 | functionArguments: [
17 | { key: 'deviceId', value: 'device' },
18 | { key: 'name', value: 'function' },
19 | { key: 'argument', value: 'argument' },
20 | { key: 'product', value: 'product' }
21 | ],
22 | mandatoryArguments: ['deviceId', 'name'],
23 | success: {
24 | fields: [
25 | { key: 'payload' },
26 | { key: 'function', value: 'function' },
27 | { key: 'device', value: 'device' }
28 | ],
29 | status: 'called'
30 | },
31 | info: {
32 | status: 'calling'
33 | },
34 | error: {
35 | status: 'failed',
36 | onRuntime: 'Failed to call function'
37 | }
38 | });
39 | }
40 |
41 | RED.nodes.registerType('particle-function', ParticleFunctionNode);
42 |
43 | };
44 |
--------------------------------------------------------------------------------
/nodes/function/icons/particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/function/icons/particle.png
--------------------------------------------------------------------------------
/nodes/function/icons/particle@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/function/icons/particle@2x.png
--------------------------------------------------------------------------------
/nodes/publish/icons/particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/publish/icons/particle.png
--------------------------------------------------------------------------------
/nodes/publish/icons/particle@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/publish/icons/particle@2x.png
--------------------------------------------------------------------------------
/nodes/publish/publish.html:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
312 |
313 |
314 |
315 |
--------------------------------------------------------------------------------
/nodes/publish/publish.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ParticleBaseNode = require('../../src/particle-base-node');
4 |
5 | module.exports = (RED) => {
6 | function ParticlePublishNode(node) {
7 | RED.nodes.createNode(this, node);
8 |
9 | new ParticleBaseNode({
10 | self: this,
11 | node: node,
12 | RED: RED,
13 | properties: ['name', 'scope', 'product', 'event', 'payload'],
14 | inputProperties: ['event', 'payload'],
15 | functionName: 'publishEvent',
16 | functionArguments: [
17 | { key: 'name', value: 'event' },
18 | { key: 'product', value: 'product' },
19 | { key: 'data', value: 'payload' },
20 | { key: 'isPrivate', defaultValue: true }
21 | ],
22 | mandatoryArguments: ['name'],
23 | success: {
24 | status: 'published'
25 | },
26 | info: {
27 | status: 'publishing'
28 | },
29 | error: {
30 | status: 'failed',
31 | onRuntime: 'Failed to publish event'
32 | }
33 | });
34 | }
35 |
36 | RED.nodes.registerType('particle-publish', ParticlePublishNode);
37 |
38 | };
39 |
--------------------------------------------------------------------------------
/nodes/subscribe/icons/particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/subscribe/icons/particle.png
--------------------------------------------------------------------------------
/nodes/subscribe/icons/particle@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/subscribe/icons/particle@2x.png
--------------------------------------------------------------------------------
/nodes/subscribe/subscribe.html:
--------------------------------------------------------------------------------
1 |
2 |
311 |
312 |
313 |
314 |
--------------------------------------------------------------------------------
/nodes/subscribe/subscribe.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const helpers = require('../../src/helpers');
4 | const ParticleBaseNode = require('../../src/particle-base-node');
5 |
6 | module.exports = (RED) => {
7 | function ParticleSubscribeNode(node) {
8 | RED.nodes.createNode(this, node);
9 |
10 | new ParticleBaseNode({
11 | self: this,
12 | node: node,
13 | RED: RED,
14 | properties: ['name', 'scope', 'device', 'product', 'event'],
15 | functionName: 'listenToEventStream',
16 | functionArguments: [
17 | { key: 'deviceId', customValue: ({ device, product }) => {
18 | if (product) {
19 | return device === '' ? undefined : device;
20 | } else {
21 | return device === '' ? 'mine' : device;
22 | }
23 | } },
24 | { key: 'name', value: 'event' },
25 | { key: 'product', value: 'product' },
26 | { key: 'onEvent', defaultValue: (data) => {
27 | helpers.onSuccess({ status: this.status.bind(this), message: 'new event' });
28 |
29 | this.send({
30 | event: data.name,
31 | payload: data.data,
32 | published_at: data.published_at,
33 | device: data.coreid
34 | });
35 | } }
36 | ],
37 | mandatoryArguments: [],
38 | runOnLoad: true,
39 | info: {
40 | status: 'setting up stream'
41 | },
42 | error: {
43 | status: 'failed to set up stream',
44 | onRuntime: 'Failed to set up stream'
45 | }
46 | });
47 | }
48 |
49 | RED.nodes.registerType('particle-subscribe', ParticleSubscribeNode);
50 |
51 | };
52 |
--------------------------------------------------------------------------------
/nodes/variable/icons/particle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/variable/icons/particle.png
--------------------------------------------------------------------------------
/nodes/variable/icons/particle@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/particle-iot/node-red-contrib-particle-official/62bd7258e41946e979722db8907deb7ca148dd23/nodes/variable/icons/particle@2x.png
--------------------------------------------------------------------------------
/nodes/variable/variable.html:
--------------------------------------------------------------------------------
1 |
2 |
305 |
306 |
307 |
308 |
--------------------------------------------------------------------------------
/nodes/variable/variable.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ParticleBaseNode = require('../../src/particle-base-node');
4 |
5 | module.exports = (RED) => {
6 | function ParticleVariableNode(node) {
7 | RED.nodes.createNode(this, node);
8 |
9 | new ParticleBaseNode({
10 | self: this,
11 | node: node,
12 | RED: RED,
13 | properties: ['name', 'scope', 'device', 'product', 'variable'],
14 | inputProperties: ['device', 'variable'],
15 | functionName: 'getVariable',
16 | functionArguments: [
17 | { key: 'deviceId', value: 'device' },
18 | { key: 'name', value: 'variable' },
19 | { key: 'product', value: 'product' }
20 | ],
21 | mandatoryArguments: ['deviceId', 'name'],
22 | success: {
23 | fields: [
24 | { key: 'payload' },
25 | { key: 'variable', value: 'variable' },
26 | { key: 'device', value: 'device' }
27 | ],
28 | status: 'fetched'
29 | },
30 | info: {
31 | status: 'fetching'
32 | },
33 | error: {
34 | status: 'failed',
35 | onRuntime: 'Failed to fetch varaible'
36 | }
37 | });
38 | }
39 |
40 | RED.nodes.registerType('particle-variable', ParticleVariableNode);
41 |
42 |
43 | };
44 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@particle/node-red-contrib-particle-official",
3 | "version": "0.1.6",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "asynckit": {
8 | "version": "0.4.0",
9 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
10 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
11 | },
12 | "babel-runtime": {
13 | "version": "6.26.0",
14 | "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
15 | "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
16 | "requires": {
17 | "core-js": "^2.4.0",
18 | "regenerator-runtime": "^0.11.0"
19 | }
20 | },
21 | "builtin-status-codes": {
22 | "version": "2.0.0",
23 | "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-2.0.0.tgz",
24 | "integrity": "sha1-byIAO6rPADzNKHr+aHIVH93FhXk="
25 | },
26 | "combined-stream": {
27 | "version": "1.0.8",
28 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
29 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
30 | "requires": {
31 | "delayed-stream": "~1.0.0"
32 | }
33 | },
34 | "component-emitter": {
35 | "version": "1.3.0",
36 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
37 | "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
38 | },
39 | "cookiejar": {
40 | "version": "2.1.2",
41 | "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
42 | "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
43 | },
44 | "core-js": {
45 | "version": "2.6.11",
46 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
47 | "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
48 | },
49 | "debug": {
50 | "version": "4.1.1",
51 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
52 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
53 | "requires": {
54 | "ms": "^2.1.1"
55 | }
56 | },
57 | "delayed-stream": {
58 | "version": "1.0.0",
59 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
60 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
61 | },
62 | "fast-safe-stringify": {
63 | "version": "2.0.7",
64 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
65 | "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
66 | },
67 | "form-data": {
68 | "version": "3.0.0",
69 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
70 | "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
71 | "requires": {
72 | "asynckit": "^0.4.0",
73 | "combined-stream": "^1.0.8",
74 | "mime-types": "^2.1.12"
75 | }
76 | },
77 | "formidable": {
78 | "version": "1.2.2",
79 | "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz",
80 | "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q=="
81 | },
82 | "inherits": {
83 | "version": "2.0.4",
84 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
85 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
86 | },
87 | "lodash": {
88 | "version": "4.17.21",
89 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
90 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
91 | },
92 | "methods": {
93 | "version": "1.1.2",
94 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
95 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
96 | },
97 | "mime": {
98 | "version": "2.4.6",
99 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
100 | "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA=="
101 | },
102 | "mime-db": {
103 | "version": "1.44.0",
104 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
105 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
106 | },
107 | "mime-types": {
108 | "version": "2.1.27",
109 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
110 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
111 | "requires": {
112 | "mime-db": "1.44.0"
113 | }
114 | },
115 | "ms": {
116 | "version": "2.1.2",
117 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
118 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
119 | },
120 | "particle-api-js": {
121 | "version": "9.0.2",
122 | "resolved": "https://registry.npmjs.org/particle-api-js/-/particle-api-js-9.0.2.tgz",
123 | "integrity": "sha512-w+KG6ybdwFKJCiTZ6sQMAFX5FJS16cA5JyG4o8th80ULneTI9WYfj9Twa0WHdC598inF+rYJRouqHS47/VWqOA==",
124 | "requires": {
125 | "babel-runtime": "^6.9.2",
126 | "form-data": ">2.2.0",
127 | "stream-http": "https://github.com/particle-iot/stream-http/archive/v2.2.1.tar.gz",
128 | "superagent": "^5.1.2",
129 | "superagent-prefix": "0.0.2"
130 | }
131 | },
132 | "qs": {
133 | "version": "6.9.4",
134 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
135 | "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
136 | },
137 | "readable-stream": {
138 | "version": "3.6.0",
139 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
140 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
141 | "requires": {
142 | "inherits": "^2.0.3",
143 | "string_decoder": "^1.1.1",
144 | "util-deprecate": "^1.0.1"
145 | }
146 | },
147 | "regenerator-runtime": {
148 | "version": "0.11.1",
149 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
150 | "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
151 | },
152 | "safe-buffer": {
153 | "version": "5.2.1",
154 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
155 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
156 | },
157 | "semver": {
158 | "version": "7.3.2",
159 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
160 | "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
161 | },
162 | "stream-http": {
163 | "version": "https://github.com/particle-iot/stream-http/archive/v2.2.1.tar.gz",
164 | "integrity": "sha512-eiMmV7ssQY13N4JoAHkr9nWvq5TgbQp6VyeCA6/UGZjBSepFMhtfICRDNzkzPNY/NPJ/os0an1xExHISjfBkmg==",
165 | "requires": {
166 | "builtin-status-codes": "^2.0.0",
167 | "inherits": "^2.0.1",
168 | "to-arraybuffer": "^1.0.0",
169 | "xtend": "^4.0.0"
170 | }
171 | },
172 | "string_decoder": {
173 | "version": "1.3.0",
174 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
175 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
176 | "requires": {
177 | "safe-buffer": "~5.2.0"
178 | }
179 | },
180 | "superagent": {
181 | "version": "5.3.1",
182 | "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.3.1.tgz",
183 | "integrity": "sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==",
184 | "requires": {
185 | "component-emitter": "^1.3.0",
186 | "cookiejar": "^2.1.2",
187 | "debug": "^4.1.1",
188 | "fast-safe-stringify": "^2.0.7",
189 | "form-data": "^3.0.0",
190 | "formidable": "^1.2.2",
191 | "methods": "^1.1.2",
192 | "mime": "^2.4.6",
193 | "qs": "^6.9.4",
194 | "readable-stream": "^3.6.0",
195 | "semver": "^7.3.2"
196 | }
197 | },
198 | "superagent-prefix": {
199 | "version": "0.0.2",
200 | "resolved": "https://registry.npmjs.org/superagent-prefix/-/superagent-prefix-0.0.2.tgz",
201 | "integrity": "sha1-sVu7E1P4ibANJa8QtPEbNQ0gOwY="
202 | },
203 | "to-arraybuffer": {
204 | "version": "1.0.1",
205 | "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
206 | "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M="
207 | },
208 | "util-deprecate": {
209 | "version": "1.0.2",
210 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
211 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
212 | },
213 | "xtend": {
214 | "version": "4.0.2",
215 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
216 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
217 | }
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@particle/node-red-contrib-particle-official",
3 | "version": "0.1.7",
4 | "description": "Official Node-RED Particle nodes",
5 | "scripts": {},
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/particle-iot/node-red-contrib-particle-official.git"
9 | },
10 | "keywords": [
11 | "node-red"
12 | ],
13 | "node-red": {
14 | "nodes": {
15 | "config": "nodes/config/config.js",
16 | "publish": "nodes/publish/publish.js",
17 | "subscribe": "nodes/subscribe/subscribe.js",
18 | "variable": "nodes/variable/variable.js",
19 | "function": "nodes/function/function.js",
20 | "api": "nodes/api/api.js"
21 | }
22 | },
23 | "author": "Wojtek 'suda' Siudzinski",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/particle-iot/node-red-contrib-particle-official/issues"
27 | },
28 | "homepage": "https://github.com/particle-iot/node-red-contrib-particle-official#readme",
29 | "dependencies": {
30 | "lodash": "^4.17.21",
31 | "particle-api-js": "^9.0.2"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/api.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Particle = require('particle-api-js');
4 |
5 | module.exports = class Api {
6 | /**
7 | * API wrapper constructor
8 | *
9 | * @param {ParticleConfigNode} auth
10 | * @param {Console} logger
11 | */
12 | constructor(auth, logger=console) {
13 | this._auth = auth;
14 | this._logger = logger;
15 | this._defaultExpiresIn = 2147483647;
16 | this._init();
17 | }
18 |
19 | /**
20 | * Login using the client credentials, store the access token
21 | * and set up token refresh before it expires
22 | *
23 | * @returns {Promise} Login promise
24 | */
25 | login() {
26 | return this._particle.loginAsClientOwner({}).then((res) => {
27 | this._logger.log('Authenticated with Particle');
28 | this._accessToken = res.body.access_token;
29 | // Reauthenticate before the token expires
30 | // There is res.body.expires_in property but
31 | // as setTimeout is using 32 bit int the maximum
32 | // we can set is 2147483647 (~24 days)
33 | const expiresIn = Math.min(this._defaultExpiresIn, res.body.expires_in);
34 | this._expirationTimer = setTimeout(
35 | this._reauthenticate.bind(this),
36 | expiresIn
37 | );
38 |
39 | return this._accessToken;
40 | });
41 | }
42 |
43 | /**
44 | * Get the EventStream and subscribe to the 'event' event
45 | * with onEvent callback
46 | *
47 | * @param {Function} onEvent
48 | * @param {String} options.deviceId
49 | * @param {String} options.name
50 | * @param {String} options.product
51 | * @returns {Promise} Resolves when adding listener succeeded
52 | */
53 | listenToEventStream({ onEvent, deviceId, name, product }) {
54 | this._lastListenArguments = arguments;
55 | return this._particle.getEventStream({
56 | deviceId, name, product, auth: this._accessToken
57 | }).then((stream) => {
58 | this._stream = stream;
59 | this._stream.on('event', onEvent);
60 | this._stream.on('error', this._reauthenticate.bind(this));
61 | this._stream.on('end', this._reauthenticate.bind(this));
62 | });
63 | }
64 |
65 | /**
66 | * Publish a Particle event
67 | *
68 | * @param {Object} params Event params
69 | * @param {String} params.name Event name
70 | * @param {String} params.data Event data
71 | * @param {String} params.product Event for this product ID or slug
72 | * @param {Boolean} params.isPrivate Should the event be publicly available?
73 | * @returns {Promise} Resolves when event has been published
74 | */
75 | publishEvent(params) {
76 | return this._particle.publishEvent(Object.assign({
77 | auth: this._accessToken
78 | }, params));
79 | }
80 |
81 | /**
82 | * Get the value of a device variable
83 | * @param {Object} params Options for this API call
84 | * @param {String} params.deviceId Device ID or Name
85 | * @param {String} params.name Variable name
86 | * @param {String} [params.product] Device in this product ID or slug
87 | * @param {String} params.auth Access Token
88 | * @return {Promise} Resolves when the variable was fetched
89 | */
90 | getVariable(params) {
91 | return this._particle.getVariable(Object.assign({
92 | auth: this._accessToken
93 | }, params))
94 | .then(response => {
95 | return response.body.result;
96 | }, (error) => {
97 | throw error;
98 | });
99 | }
100 |
101 | /**
102 | * Call a device function
103 | * @param {Object} options Options for this API call
104 | * @param {String} options.deviceId Device ID or Name
105 | * @param {String} options.name Function name
106 | * @param {String} options.argument Function argument
107 | * @param {String} [options.product] Device in this product ID or slug
108 | * @param {String} options.auth Access Token
109 | * @return {Promise} Resolves when the function was called
110 | */
111 | callFunction(options) {
112 | return this._particle.callFunction(Object.assign({
113 | auth: this._accessToken
114 | }, options))
115 | .then(response => {
116 | return response.body.return_value;
117 | }, (error) => {
118 | throw error;
119 | });
120 | }
121 |
122 | request(params) {
123 | return this._particle.request(Object.assign({
124 | auth: this._accessToken
125 | }, params));
126 | }
127 |
128 | /**
129 | * Close the stream, remove all timers, listeners etc.
130 | */
131 | cleanup() {
132 | clearTimeout(this._expirationTimer);
133 |
134 | if (this._stream) {
135 | this._stream.abort();
136 | }
137 |
138 | this._particle.deleteCurrentAccessToken({
139 | auth: this._accessToken
140 | });
141 | }
142 |
143 | /**
144 | * Instantiate Particle and set the config
145 | *
146 | * @private
147 | */
148 | _init() {
149 | this._particle = new Particle({
150 | clientId: this._auth.clientId,
151 | clientSecret: this._auth.clientSecret
152 | });
153 | this._particle.setContext('tool', {
154 | name: 'node-red-contrib-particle'
155 | });
156 | }
157 |
158 | /**
159 | * Callback that fetches a new token and recreates all
160 | * event callbacks to prevent data loss.
161 | *
162 | * @private
163 | */
164 | _reauthenticate() {
165 | this._logger.log('Reauthenticating...');
166 | this.cleanup();
167 |
168 | this.login().then(() => {
169 | this.listenToEventStream.apply(this, this._lastListenArguments);
170 | }, () => {
171 | const retryIn = 5;
172 | this._logger.error(`Failed to reauthenticate. Trying again in ${retryIn} seconds`);
173 | setTimeout(this._reauthenticate.bind(this), retryIn * 1000);
174 | });
175 | }
176 | };
177 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const resetStatus = ({ status, timeout = 0 }) => {
4 | setTimeout(() => {
5 | status({});
6 | }, timeout);
7 | };
8 |
9 | const onSuccess = ({ status, message }) => {
10 | status({ fill: 'green', shape: 'dot', text: message });
11 |
12 | resetStatus({
13 | status,
14 | timeout: 1000
15 | });
16 | };
17 |
18 | const onInfo = ({ status, message }) => {
19 | status({ fill: 'blue', shape: 'dot', text: message });
20 | };
21 |
22 | const onError = ({ status, message }) => {
23 | status({ fill: 'red', shape: 'ring', text: message });
24 | };
25 |
26 | module.exports = {
27 | onSuccess,
28 | onInfo,
29 | onError,
30 | resetStatus
31 | };
32 |
--------------------------------------------------------------------------------
/src/particle-base-node.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Api = require('./api');
4 | const _ = require('lodash');
5 | const helpers = require('./helpers');
6 |
7 | class ParticleBaseNode {
8 | constructor({ self, node, RED, properties, functionName, functionArguments, mandatoryArguments, onAPIResponse, onAPIError, inputProperties, runOnLoad, success, error, info }) {
9 | this.self = self;
10 | this.node = node;
11 | this.RED = RED;
12 | this.message = null;
13 | this.variables = {};
14 | this.functionName = functionName;
15 | this.functionArguments = functionArguments;
16 | this.mandatoryArguments = mandatoryArguments;
17 | this.inputProperties = inputProperties;
18 | this.onAPIResponse = onAPIResponse;
19 | this.onAPIError = onAPIError;
20 | this.runOnLoad = runOnLoad || node.runOnLoad;
21 | this.success = success || node.success;
22 | this.error = error;
23 | this.info = info || {};
24 |
25 | properties.forEach(field => {
26 | this.variables[field] = node[field];
27 | });
28 |
29 | this.auth = this.RED.nodes.getNode(node.auth);
30 |
31 | if (this.auth) {
32 | this.api = new Api(this.auth);
33 | } else if (node.testAuth) {
34 | this.api = new Api(node.testAuth);
35 | }
36 |
37 | this.run();
38 | }
39 |
40 | hasMandatoryArguments(args = [], params = {}) {
41 | let ok = true;
42 |
43 | args.forEach(arg => {
44 | if (!params[arg]) {
45 | ok = false;
46 |
47 | this.self.error(`Missing the ${arg} parameter`);
48 | }
49 | });
50 |
51 | return ok;
52 | }
53 |
54 | parametersFromVariables(variables, functionArguments) {
55 | let params = {};
56 | let ok = true;
57 |
58 | functionArguments.forEach(argument => {
59 | let value = null;
60 |
61 | if (argument.defaultValue) {
62 | value = argument.defaultValue;
63 | }
64 |
65 | if (argument.value && variables[argument.value]) {
66 | value = variables[argument.value];
67 | }
68 |
69 | if (argument.customValue) {
70 | value = argument.customValue(variables);
71 | }
72 |
73 | if (argument.format === 'object' && typeof value !== 'object') {
74 | try {
75 | value = JSON.parse(value);
76 |
77 | if (typeof value !== 'object') {
78 | throw 'Argument not a valid JSON object';
79 | }
80 | } catch (e) {
81 | if (this.error.onConfig) {
82 | this.self.error(this.error.onConfig);
83 | }
84 |
85 | ok = false;
86 | }
87 | }
88 |
89 | if (argument.prefix && value) {
90 | value = `${argument.prefix}${value}`;
91 | }
92 |
93 | params[argument.key] = value;
94 | });
95 |
96 | return ok ? params : null;
97 | }
98 |
99 | successStatus(message = 'OK') {
100 | return helpers.onSuccess({ status: this.self.status.bind(this.self), message });
101 | }
102 |
103 | errorStatus(message = 'NOT OK') {
104 | return helpers.onError({ status: this.self.status.bind(this.self), message });
105 | }
106 |
107 | infoStatus(message = 'processing') {
108 | return helpers.onInfo({ status: this.self.status.bind(this.self), message });
109 | }
110 |
111 | hideStatus() {
112 | return helpers.resetStatus({ status: this.self.status.bind(this.self), timeout: 0 });
113 | }
114 |
115 | makeAPIRequest(variables) {
116 | const parameters = this.parametersFromVariables(variables, this.functionArguments);
117 |
118 | if (!parameters) {
119 | return null;
120 | }
121 |
122 | const hasMandatoryArguments = this.hasMandatoryArguments(this.mandatoryArguments, parameters);
123 |
124 | if (!hasMandatoryArguments) {
125 | return null;
126 | }
127 |
128 | this.infoStatus(this.info.status);
129 |
130 | this.api[this.functionName](parameters)
131 | .then(result => {
132 | this.hideStatus();
133 |
134 | if (this.success && this.success.status) {
135 | this.successStatus(this.success.status);
136 | }
137 |
138 | if (this.onAPIResponse) {
139 | return this.onAPIResponse(variables, this.message, result);
140 | } else if (this.success && this.success.fields) {
141 | const fields = this.success.fields;
142 | let message = this.message || {};
143 |
144 | fields.forEach(field => {
145 | if (field.path) {
146 | message[field.key] = _.get(result, field.path);
147 | } else if (field.value) {
148 | message[field.key] = variables[field.value];
149 | } else if (field.key === 'payload') {
150 | message.payload = result;
151 | }
152 | });
153 |
154 | return this.self.send(message);
155 | } else {
156 | return true;
157 | }
158 | }, (error) => {
159 | this.hideStatus();
160 |
161 | if (this.error && this.error.status) {
162 | this.errorStatus(this.error.status);
163 | }
164 |
165 | if (this.onAPIError) {
166 | return this.onAPIError(error);
167 | }
168 |
169 | if (this.error && this.error.onRuntime) {
170 | this.self.error(`${this.error.onRuntime} (${error})`, this.message || {});
171 | }
172 |
173 | return false;
174 | });
175 | }
176 |
177 | onClose() {
178 | this.api.cleanup();
179 | }
180 |
181 | mergePropertiesFromMsg(variables, inputProperties, msg) {
182 | let _var = Object.assign({}, variables);
183 | const data = msg;
184 |
185 | if (data) {
186 | inputProperties.forEach(field => {
187 | if (data[field] && !_var[field]) {
188 | _var[field] = data[field];
189 | }
190 | });
191 | }
192 |
193 | return _var;
194 | }
195 |
196 | onInput(msg) {
197 | if (msg) {
198 | this.message = msg;
199 | }
200 |
201 | let variables = this.mergePropertiesFromMsg(this.variables, this.inputProperties, msg);
202 |
203 | this.makeAPIRequest(variables);
204 | }
205 |
206 | run() {
207 | if (!this.api) {
208 | return null;
209 | }
210 |
211 | this.api.login()
212 | .then(() => {
213 | this.self.on('input', this.onInput.bind(this));
214 | this.self.on('close', this.onClose.bind(this));
215 |
216 | if (this.runOnLoad) {
217 | this.makeAPIRequest(this.variables);
218 | }
219 | });
220 | }
221 | }
222 |
223 | module.exports = ParticleBaseNode;
224 |
--------------------------------------------------------------------------------