frugal-iot-client 2.0.2 → 2.0.3
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/admin.js +65 -0
- package/cards.js +1 -0
- package/core.js +25 -7
- package/package.json +1 -1
- package/test/graph.test.js +21 -0
- package/test/page.test.js +3 -3
package/admin.js
CHANGED
|
@@ -365,6 +365,59 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
365
365
|
}),
|
|
366
366
|
]);
|
|
367
367
|
}
|
|
368
|
+
/*
|
|
369
|
+
* Add a bridge, or re-issue an existing one's credentials.
|
|
370
|
+
*
|
|
371
|
+
* The result is shown once and is not stored anywhere it can be read back: the password is
|
|
372
|
+
* derived from a server secret and this row's timestamp, so the only copy is the one you put into
|
|
373
|
+
* the Pi's configuration. Re-running for the same site mints new ones and revokes the old, which
|
|
374
|
+
* is what makes it the way to recover from having lost them.
|
|
375
|
+
*/
|
|
376
|
+
onBridgeAdd() {
|
|
377
|
+
const input = this.state.elements.bridge_site;
|
|
378
|
+
const site = (input && input.value || '').trim();
|
|
379
|
+
if (!site) { this.message(getString("Give the Pi a short name first")); return; }
|
|
380
|
+
if (this.state.bridges && this.state.bridges.some((b) => b.site === site)
|
|
381
|
+
&& !window.confirm(
|
|
382
|
+
`"${site}" already exists.\n\n` +
|
|
383
|
+
`Adding it again issues a new broker password and a new replica token, and the ones that ` +
|
|
384
|
+
`Pi is using now stop working. It will stay disconnected until you put the new ones into ` +
|
|
385
|
+
`its configuration and restart its broker.`)) return;
|
|
386
|
+
POST(`/bridge_add/${this.state.org}`, { site }, (err, json) => {
|
|
387
|
+
if (err) { this.message(err.message); return; }
|
|
388
|
+
this.state.new_bridge = json; // shown once, below
|
|
389
|
+
this.getBridges(); // refreshes the table and re-renders the card
|
|
390
|
+
this.replaceElement("bridge_new", this.newBridgeDetails());
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/*
|
|
394
|
+
* What the Pi needs, once. Deliberately not fetchable again: there is no route that reads a
|
|
395
|
+
* bridge's password back, so losing it means issuing a new one.
|
|
396
|
+
*/
|
|
397
|
+
newBridgeDetails() {
|
|
398
|
+
const b = this.state.new_bridge;
|
|
399
|
+
if (!b) return el('span', {});
|
|
400
|
+
return el('div', {class: 'bridge-new'}, [
|
|
401
|
+
el('h4', {textContent: "Copy these now - they are not shown again"}),
|
|
402
|
+
el('p', {textContent:
|
|
403
|
+
"Run this on the Pi, in the directory the server is installed in. Replace the placeholder " +
|
|
404
|
+
"with this server's fully-qualified name - the one on its certificate, which you can check " +
|
|
405
|
+
"by opening https:// that name in a browser. It will then ask for the broker password and " +
|
|
406
|
+
"for the replica token, in that order, so have both of the values below to hand."}),
|
|
407
|
+
el('pre', {i8n: false, textContent: b.command}),
|
|
408
|
+
el('table', {class: 'nodestates'}, [
|
|
409
|
+
el('tr', {}, [el('th', {textContent: "Broker account"}), el('td', {i8n: false, textContent: b.account})]),
|
|
410
|
+
el('tr', {}, [el('th', {textContent: "Broker password"}), el('td', {i8n: false, textContent: b.password})]),
|
|
411
|
+
el('tr', {}, [el('th', {textContent: "Replica token"}), el('td', {i8n: false, textContent: b.replica_token})]),
|
|
412
|
+
]),
|
|
413
|
+
el('p', {textContent:
|
|
414
|
+
"Two credentials for two different connections: the password is what the MQTT bridge logs " +
|
|
415
|
+
"in with on port 8883, and the token is what lets that Pi pull this organization's logins " +
|
|
416
|
+
"over HTTPS, so the same people can log in there too. The token can be skipped - press " +
|
|
417
|
+
"Enter at its prompt - and added later by doing this again. Neither is asked for on the " +
|
|
418
|
+
"command line, so that neither ends up in the Pi's shell history."}),
|
|
419
|
+
]);
|
|
420
|
+
}
|
|
368
421
|
bridgesRestContent() {
|
|
369
422
|
return el('div', {}, [
|
|
370
423
|
this.refreshableHeading("Bridged Pis", this.getBridges),
|
|
@@ -375,6 +428,18 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
375
428
|
"this organization's logins, which it does over HTTP - so a Pi can be checking in while " +
|
|
376
429
|
"its bridge is down, and the two together say which half is broken."}),
|
|
377
430
|
this.state.elements.bridges_list = this.bridgesList(),
|
|
431
|
+
el('section', {}, [
|
|
432
|
+
el('h3', {textContent: "Add a bridge"}),
|
|
433
|
+
el('p', {textContent:
|
|
434
|
+
"Give the Pi a short name - it becomes its broker account and cannot be changed later " +
|
|
435
|
+
"without reconfiguring that Pi. Lower-case letters, digits and hyphens."}),
|
|
436
|
+
el('div', {}, [
|
|
437
|
+
this.state.elements.bridge_site = el('input',
|
|
438
|
+
{type: 'text', name: 'site', placeholder: 'pi4', size: 16}),
|
|
439
|
+
el('button', {textContent: "Add", onclick: this.onBridgeAdd.bind(this)}),
|
|
440
|
+
]),
|
|
441
|
+
this.state.elements.bridge_new = this.newBridgeDetails(),
|
|
442
|
+
]),
|
|
378
443
|
]);
|
|
379
444
|
}
|
|
380
445
|
onNodeReset(project, nodeid) {
|
package/cards.js
CHANGED
|
@@ -733,6 +733,7 @@ const ADMIN_CARDS = [
|
|
|
733
733
|
{ section: 'message', title: 'Publish Message', capability: 'ADMIN' },
|
|
734
734
|
{ section: 'retained', title: 'Retained messages', capability: 'ADMIN' },
|
|
735
735
|
{ section: 'nodes', title: 'Nodes', capability: 'ADMIN' },
|
|
736
|
+
{ section: 'bridges', title: 'Bridges', capability: 'ADMIN' },
|
|
736
737
|
{ section: 'api', title: 'API', capability: 'ADMIN' },
|
|
737
738
|
];
|
|
738
739
|
|
package/core.js
CHANGED
|
@@ -118,6 +118,25 @@ function unshiftUnique(arr, v) {
|
|
|
118
118
|
}
|
|
119
119
|
*/
|
|
120
120
|
|
|
121
|
+
/*
|
|
122
|
+
* Say what the broker made of a subscription.
|
|
123
|
+
*
|
|
124
|
+
* A broker that REFUSES one does not report an error: MQTT 3.1.1 answers with SUBACK return code
|
|
125
|
+
* 0x80, which mqtt.js hands back as granted qos 128 in the second argument. Checking only `err`
|
|
126
|
+
* therefore made a refusal completely silent - "Subscribing to X" and then nothing, for ever,
|
|
127
|
+
* looking exactly like a topic nobody publishes to. That is the difference between "no permission"
|
|
128
|
+
* and "no data", and it is the first thing you want to know.
|
|
129
|
+
*/
|
|
130
|
+
function subscribeResult(topic, err, granted) {
|
|
131
|
+
if (err) { console.error("Subscribe failed", topic, err); return; }
|
|
132
|
+
const refused = (granted || []).filter((g) => g.qos === 128);
|
|
133
|
+
if (refused.length) {
|
|
134
|
+
console.error(`Subscribe REFUSED by the broker: ${topic} - this login's broker account has no `
|
|
135
|
+
+ `permission for it. Its rights are set when it logs in; a permission added since then `
|
|
136
|
+
+ `needs a fresh login, or "frugal-iot-rebuild-dynsec" on the server.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
121
140
|
/*
|
|
122
141
|
* Subscribe to a topic. cb(topic, message, retained) - see mqtt_deliver, which passes all three.
|
|
123
142
|
*
|
|
@@ -130,9 +149,7 @@ function mqtt_subscribe(topic, cb) { // cb(topic, message, retained)
|
|
|
130
149
|
mqtt_subscriptions.push({topic, cb});
|
|
131
150
|
// There may be no client yet - it only connects once it knows which organization's credentials to use
|
|
132
151
|
if (mqtt_client && mqtt_client.connected) {
|
|
133
|
-
mqtt_client.subscribe(topic, (err) =>
|
|
134
|
-
if (err) console.error(err);
|
|
135
|
-
})
|
|
152
|
+
mqtt_client.subscribe(topic, (err, granted) => subscribeResult(topic, err, granted))
|
|
136
153
|
} else {
|
|
137
154
|
console.log("Delaying till connected"); // It will resubscribe from "subscriptions"
|
|
138
155
|
}
|
|
@@ -2061,10 +2078,13 @@ class MqttTopic {
|
|
|
2061
2078
|
type: this.type === 'exponential' ? 'logarithmic' : 'linear',
|
|
2062
2079
|
display: this.type !== 'bool',
|
|
2063
2080
|
title: {
|
|
2081
|
+
display: true, // Chart.js hides a scale title unless asked - without this the axis has no label at all
|
|
2064
2082
|
// noinspection JSUnresolvedReference
|
|
2065
2083
|
color: this.color, // The family colour; the per-line variation happens in MqttGraph.addDataset
|
|
2084
|
+
font: { size: 20 },
|
|
2085
|
+
// The unit is what tells two axes apart when both read as a number - "Temperature (°C)"
|
|
2066
2086
|
// noinspection JSUnresolvedReference
|
|
2067
|
-
text: getString(this.name.replace(/[0-9]+$/,'')),
|
|
2087
|
+
text: getString(this.name.replace(/[0-9]+$/,'')) + (unitSymbol(this.units) ? ` (${unitSymbol(this.units)})` : ''),
|
|
2068
2088
|
},
|
|
2069
2089
|
// 0 is not valid on a logarithmic axis, so fall back to auto-scaling (undefined) rather than 0
|
|
2070
2090
|
// noinspection JSUnresolvedReference
|
|
@@ -2829,9 +2849,7 @@ class MqttClient extends HTMLElementExtended {
|
|
|
2829
2849
|
mqtt_subscriptions.forEach((s) => {
|
|
2830
2850
|
if (!mqtt_client._resubscribeTopics[s.topic]) { // Not really public interface but cleaner console as not needed
|
|
2831
2851
|
console.log("Now connected, subscribing to", s.topic);
|
|
2832
|
-
mqtt_client.subscribe(s.topic, (err) =>
|
|
2833
|
-
if (err) console.error(err);
|
|
2834
|
-
});
|
|
2852
|
+
mqtt_client.subscribe(s.topic, (err, granted) => subscribeResult(s.topic, err, granted));
|
|
2835
2853
|
}
|
|
2836
2854
|
})
|
|
2837
2855
|
} else {
|
package/package.json
CHANGED
package/test/graph.test.js
CHANGED
|
@@ -67,3 +67,24 @@ describe('lines sharing a schema colour are told apart', () => {
|
|
|
67
67
|
assert.equal(new Set(colors).size, 3);
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
describe('an axis says what it measures', () => {
|
|
72
|
+
// graphdataset.graph is the route to a graph that does not need a connected element or a canvas
|
|
73
|
+
const axisOf = (mt, g) => { mt.graphdataset = { graph: g }; const id = mt.yaxisid; return g.state.scales[id]; };
|
|
74
|
+
|
|
75
|
+
test('the title is drawn, and carries the unit symbol', () => {
|
|
76
|
+
const { projectMt } = mock.runScenario('one-device');
|
|
77
|
+
const g = new (customElements.get('mqtt-graph'))();
|
|
78
|
+
const axis = axisOf(projectMt.nodes['esp8266-fb94bb'].groups.sht.topics.temperature, g);
|
|
79
|
+
assert.equal(axis.title.display, true);
|
|
80
|
+
assert.equal(axis.title.text, 'Temperature (°C)');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('a reading with no unit in the schema gets a bare name', () => {
|
|
84
|
+
const { projectMt } = mock.runScenario('one-device');
|
|
85
|
+
const g = new (customElements.get('mqtt-graph'))();
|
|
86
|
+
const mt = projectMt.nodes['esp8266-fb94bb'].groups.sht.topics.temperature;
|
|
87
|
+
mt.units = undefined;
|
|
88
|
+
assert.equal(axisOf(mt, g).title.text, 'Temperature');
|
|
89
|
+
});
|
|
90
|
+
});
|
package/test/page.test.js
CHANGED
|
@@ -84,7 +84,7 @@ describe('which admin cards a user gets', () => {
|
|
|
84
84
|
assert.deepEqual(cards.adminCardsFor('dev').map((c) => c.section), ['info', 'flash']);
|
|
85
85
|
withCapabilities('READ', 'ADMIN');
|
|
86
86
|
assert.deepEqual(cards.adminCardsFor('dev').map((c) => c.section),
|
|
87
|
-
['info', 'admin', 'projects', 'message', 'retained', 'nodes', 'api']);
|
|
87
|
+
['info', 'admin', 'projects', 'message', 'retained', 'nodes', 'bridges', 'api']);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
test('flashing and pushing an OTA binary are separate capabilities', () => {
|
|
@@ -108,7 +108,7 @@ describe('the project back', () => {
|
|
|
108
108
|
const back = document.createElement('mqtt-projectback');
|
|
109
109
|
back.setAttribute('organization', 'dev');
|
|
110
110
|
document.body.append(back);
|
|
111
|
-
assert.equal(back.querySelectorAll('.fi-admincard').length,
|
|
111
|
+
assert.equal(back.querySelectorAll('.fi-admincard').length, 8);
|
|
112
112
|
back.remove();
|
|
113
113
|
});
|
|
114
114
|
|
|
@@ -228,7 +228,7 @@ describe('the project back', () => {
|
|
|
228
228
|
document.body.append(back);
|
|
229
229
|
back.querySelectorAll('.fi-admincard__head').forEach((h) => h.click());
|
|
230
230
|
const sections = [...back.querySelectorAll('mqtt-admin')].map((a) => a.getAttribute('section'));
|
|
231
|
-
assert.deepEqual(sections, ['admin', 'projects', 'message', 'retained', 'nodes', 'api']);
|
|
231
|
+
assert.deepEqual(sections, ['admin', 'projects', 'message', 'retained', 'nodes', 'bridges', 'api']);
|
|
232
232
|
back.remove();
|
|
233
233
|
});
|
|
234
234
|
});
|