frugal-iot-client 2.0.5 → 2.0.6

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 CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import {EL, GET, HTMLElementExtended} from '/node_modules/html-element-extended/htmlelementextended.js';
9
9
  import mqtt from '/node_modules/mqtt/dist/mqtt.esm.js'; // https://www.npmjs.com/package/mqtt
10
- import { CssUrl, DELETE, POST, XXX, brokerHost, configSet, el, getString, getStringWith, hasCapability, mqttTempConnect, mqtt_subscribe, retainedPattern, locationParameterChange, mqtt_client, preferedLanguageSet, preferedLanguages, redirectToLogin, server_config } from './core.js';
10
+ import { CssUrl, DELETE, POST, XXX, brokerHost, configSet, el, getString, getStringWith, hasCapability, mqttTempConnect, mqtt_subscribe, mqtt_unsubscribe, retainedPattern, locationParameterChange, mqtt_client, preferedLanguageSet, preferedLanguages, redirectToLogin, server_config } from './core.js';
11
11
 
12
12
 
13
13
  // The OTA upload is multipart, which POST() (JSON) cannot carry. Callback-style, like the rest.
@@ -173,8 +173,9 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
173
173
  ]);
174
174
  }
175
175
  // Fetch ota files and display
176
- getOrDeleteOtaFiles(url) {
176
+ getOrDeleteOtaFiles(url, cb = () => {}) {
177
177
  GET(url, {}, (err, json) => {
178
+ cb();
178
179
  if (err) {
179
180
  this.message(err);
180
181
  return;
@@ -184,11 +185,11 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
184
185
  }
185
186
  });
186
187
  }
187
- getOtaFiles() {
188
- if (this.state.org) {
189
- if (this.otaOrgs.map(o => o[0]).includes(this.state.org)) {
190
- this.getOrDeleteOtaFiles(`/ota_list/${this.state.org}`);
191
- }
188
+ getOtaFiles(cb = () => {}) {
189
+ if (this.state.org && this.otaOrgs.map(o => o[0]).includes(this.state.org)) {
190
+ this.getOrDeleteOtaFiles(`/ota_list/${this.state.org}`, cb);
191
+ } else {
192
+ cb();
192
193
  }
193
194
  }
194
195
  onOtaDelete(val, ev) {
@@ -241,8 +242,9 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
241
242
  );
242
243
  }
243
244
  // Fetch people and display
244
- getOrChangeAdminPeople(url) {
245
+ getOrChangeAdminPeople(url, cb = () => {}) {
245
246
  GET(url, {}, (err, json) => {
247
+ cb();
246
248
  if (err) {
247
249
  this.message(err.message);
248
250
  return;
@@ -328,9 +330,10 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
328
330
  onclick: this.onEnrolmentSecretAdd.bind(this)}),
329
331
  ]);
330
332
  }
331
- getEnrolledNodes() {
332
- if (!this.state.org) return;
333
+ getEnrolledNodes(cb = () => {}) {
334
+ if (!this.state.org) { cb(); return; }
333
335
  GET(`/nodes_list/${this.state.org}`, {}, (err, json) => {
336
+ cb();
334
337
  if (err) { this.message(err.message); return; }
335
338
  this.state.enrolled_nodes = json; // [{ project, nodeid, lora, enrolled_at }]
336
339
  this.replaceElement("enrolled_nodes", this.enrolledNodesList());
@@ -345,9 +348,10 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
345
348
  * itself is up. A Pi with a broken bridge still checks in, and that difference is exactly what
346
349
  * is hard to see from production any other way.
347
350
  */
348
- getBridges() {
349
- if (!this.state.org) return;
351
+ getBridges(cb = () => {}) {
352
+ if (!this.state.org) { cb(); return; }
350
353
  GET(`/bridges_list/${this.state.org}`, {}, (err, json) => {
354
+ cb();
351
355
  if (err) { this.message(err.message); return; }
352
356
  this.state.bridges = json; // [{ site, account, created_at, last_pull }]
353
357
  this.subscribeBridgeState();
@@ -554,9 +558,7 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
554
558
  const nodes = this.state.enrolled_nodes;
555
559
  if (!nodes || !nodes.length) {
556
560
  return el('p', {textContent:
557
- "No nodes have enrolled yet, and none is asking to. A node enrols itself the first time it " +
558
- "connects; one that asks and is refused is listed here too, so an empty list means nothing " +
559
- "is reaching this server at all - check that the node's org and enrol URL are this one."});
561
+ "No nodes have enrolled yet, and none have asked and been rejected."});
560
562
  }
561
563
  const states = this.nodeStates();
562
564
  return el('table', {class: 'nodestates'}, [
@@ -594,14 +596,17 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
594
596
  }),
595
597
  ]);
596
598
  }
597
- getPeopleList() {
599
+ getPeopleList(cb = () => {}) {
598
600
  if (this.state.org) {
599
- this.getOrChangeAdminPeople(`/people_list/${this.state.org}`);
601
+ this.getOrChangeAdminPeople(`/people_list/${this.state.org}`, cb);
602
+ } else {
603
+ cb();
600
604
  }
601
605
  }
602
606
  // Fetch projects and display
603
- getOrChangeAdminProjects(url) {
607
+ getOrChangeAdminProjects(url, cb = () => {}) {
604
608
  GET(url, {}, (err, json) => {
609
+ cb();
605
610
  if (err) {
606
611
  this.message(err.message);
607
612
  return;
@@ -611,9 +616,11 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
611
616
  }
612
617
  });
613
618
  }
614
- getProjectsList() {
619
+ getProjectsList(cb = () => {}) {
615
620
  if (this.state.org) {
616
- this.getOrChangeAdminProjects(`/projects_list/${this.state.org}`);
621
+ this.getOrChangeAdminProjects(`/projects_list/${this.state.org}`, cb);
622
+ } else {
623
+ cb();
617
624
  }
618
625
  }
619
626
  onPermissionsDelete(val, ev) {
@@ -717,9 +724,10 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
717
724
  }
718
725
  // Fetch all registered Farm-Platforms, and display. Not org-scoped - a platform is a many-to-many
719
726
  // relationship with orgs/projects, recorded per-mapping in api_farms (see getFarmsList).
720
- getPlatformsList() {
727
+ getPlatformsList(cb = () => {}) {
721
728
  if (this.state.org) {
722
729
  GET(`/api/platforms/list`, {}, (err, json) => {
730
+ cb();
723
731
  if (err) {
724
732
  this.message(err.message);
725
733
  return;
@@ -733,6 +741,8 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
733
741
  this.replaceElement("platforms_list_display", this.platformsListDisplay());
734
742
  this.replaceElement("add_farm", this.collapsibleArea('add_farm', "Add Farm", this.farmRegisterForm));
735
743
  });
744
+ } else {
745
+ cb();
736
746
  }
737
747
  }
738
748
  onPlatformSelect(ev) {
@@ -802,9 +812,10 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
802
812
  return el('div', {}, [el('h3', {textContent: "Register Platform"}), EL]);
803
813
  }
804
814
  // Fetch farm-to-project mappings registered for the selected organization, and display
805
- getFarmsList() {
815
+ getFarmsList(cb = () => {}) {
806
816
  if (this.state.org) {
807
817
  GET(`/api/farms/list?org=${encodeURIComponent(this.state.org)}`, {}, (err, json) => {
818
+ cb();
808
819
  if (err) {
809
820
  this.message(err.message);
810
821
  return;
@@ -819,6 +830,8 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
819
830
  this.replaceElement("farm_node_actions", this.farmNodeActions());
820
831
  this.getDeviceActionSchema();
821
832
  });
833
+ } else {
834
+ cb();
822
835
  }
823
836
  }
824
837
  onFarmSelect(ev) {
@@ -907,13 +920,13 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
907
920
  // server_config locally - this is the API tab, so it should exercise the same API a real
908
921
  // Farm-Platform would use. A farm can map to more than one project, so one request per project id,
909
922
  // joined into a single row set once all have replied.
910
- getFarmNodesList() {
923
+ getFarmNodesList(cb = () => {}) {
911
924
  const org = this.state.org;
912
925
  const projectIds = this.selectedFarmProjectIds();
913
926
  if (!org || projectIds.length === 0) {
914
927
  this.state.farm_nodes_list = [];
915
- this.state.enrolled_nodes = null;
916
928
  this.replaceElement("farm_nodes_table", this.farmNodesTable());
929
+ cb();
917
930
  return;
918
931
  }
919
932
  let remaining = projectIds.length;
@@ -936,6 +949,7 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
936
949
  });
937
950
  }
938
951
  if (--remaining === 0) {
952
+ cb();
939
953
  this.state.farm_nodes_list = allNodes;
940
954
  this.replaceElement("farm_nodes_table", this.farmNodesTable());
941
955
  }
@@ -999,15 +1013,17 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
999
1013
  }
1000
1014
  // Fetch the selected node's Device Schema so the Action section can list its actions - triggered
1001
1015
  // whenever the selected node changes.
1002
- getDeviceActionSchema() {
1016
+ getDeviceActionSchema(cb = () => {}) {
1003
1017
  const device = this.selectedFarmNodeDevice();
1004
1018
  this.state.device_schema = null;
1005
1019
  this.state.selected_action = null;
1006
1020
  if (!device) {
1007
1021
  this.replaceElement("action_section", this.actionSection());
1022
+ cb();
1008
1023
  return;
1009
1024
  }
1010
1025
  GET(`/api/devices/schema?device=${encodeURIComponent(device)}`, {}, (err, json) => {
1026
+ cb();
1011
1027
  if (err) {
1012
1028
  this.message(err.message);
1013
1029
  return;
@@ -1169,17 +1185,21 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
1169
1185
  }
1170
1186
  // Each of these lists is the result of one fetch made when the card opened - nothing pushes a
1171
1187
  // change, so the heading carries the way to ask again.
1188
+ // refreshFn is given a callback for when its reply has arrived, which is what stops the spin.
1172
1189
  refreshableHeading(title, refreshFn) {
1173
- return el('h3', {}, [
1174
- el('span', {textContent: title}),
1175
- el('span', {class: 'pseudolink refresh', i8n: false, title: getString("Refresh"),
1176
- textContent: ' \u27f3', onclick: refreshFn.bind(this)}),
1177
- ]);
1190
+ const iconEl = el('span', {class: 'pseudolink refresh', i8n: false, title: getString("Refresh"),
1191
+ textContent: '\u27f3', onclick: () => {
1192
+ if (iconEl.classList.contains('busy')) return;
1193
+ iconEl.classList.add('busy');
1194
+ refreshFn.call(this, () => iconEl.classList.remove('busy'));
1195
+ }});
1196
+ return el('h3', {}, [el('span', {textContent: title}), iconEl]);
1178
1197
  }
1179
1198
  // The organization's nodes are read out of server_config, so refreshing this table means fetching
1180
1199
  // /config.json again rather than re-rendering what is already here.
1181
- refreshNodesTable() {
1200
+ refreshNodesTable(cb = () => {}) {
1182
1201
  GET("/config.json", {}, (err, json) => {
1202
+ cb();
1183
1203
  if (err) { this.message(err.message); return; }
1184
1204
  configSet(json);
1185
1205
  this.replaceElement("nodes_table", this.nodesTable());
@@ -1204,9 +1224,7 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
1204
1224
  this.adminSections().forEach((section) => this.replaceElement(section.rest, this.gatedContent(section)));
1205
1225
  // Which tabs do their own network fetch, and so need one again for a new organization. Only
1206
1226
  // Dashboard does not (mqtt-wrapper handles it). "Nodes" is in the list because it now fetches
1207
- // the enrolled-node list as well as reading server_config - leaving it out was why that section
1208
- // always said "No nodes have enrolled yet": loadSectionIfNeeded returns early for a tab that is not
1209
- // in this set, so the fetch never happened.
1227
+ // the enrolled-node list as well as reading server_config:
1210
1228
  this.state.sectionsNeedingLoad = new Set(['OTA', 'Admin', 'Projects', 'Nodes', 'Bridges', 'API']);
1211
1229
  this.loadSectionIfNeeded(this.state.activeSectionTitle);
1212
1230
  // Only a card created without an organization has a dropdown of its own; replaceElement does
@@ -1393,32 +1411,55 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
1393
1411
  }
1394
1412
 
1395
1413
  onNodeProjectChange(nodeId, oldProjectId, selectElement, onChanged) {
1396
- // Handle project change for a node
1397
1414
  const newProjectId = selectElement.value;
1398
1415
  if (newProjectId === oldProjectId) {
1399
1416
  return; // No change
1400
1417
  }
1418
+ const org = this.state.org;
1419
+ const topic = `${org}/${oldProjectId}/${nodeId}/set/frugal_iot/project`;
1420
+ console.log(`Publishing ${topic} = ${newProjectId}`);
1421
+ // Not retained: a sleeping node still gets it, from its persistent session, when it wakes.
1422
+ mqtt_client.publish(topic, newProjectId, {retain: false, qos: 1});
1423
+ this.message(getStringWith("Asked %s to move", nodeId));
1424
+ this.watchNodeMove(org, nodeId, oldProjectId, newProjectId, onChanged);
1425
+ onChanged();
1426
+ }
1401
1427
 
1402
- // Send MQTT message to update project
1403
- const topic = `${this.state.org}/${oldProjectId}/${nodeId}/set/frugal_iot/project`;
1404
- const message = newProjectId;
1405
-
1406
- console.log(`Publishing ${topic} = ${message}`);
1407
- mqtt_client.publish(topic, message, {retain: false, qos: 1});
1408
- this.message(`${getString("Project changed to")} ${newProjectId} ${getString("for node")} ${nodeId}`);
1428
+ // The move is done when the node is heard at its new address - nothing else says so, because the
1429
+ // node restarts and enrols afresh there, and the server tidies the old address away only then.
1430
+ watchNodeMove(org, nodeId, fromProjectId, toProjectId, onChanged) {
1431
+ const moves = this.state.nodeMoves || (this.state.nodeMoves = {});
1432
+ if (moves[nodeId]) { mqtt_unsubscribe(moves[nodeId].watch); }
1433
+ const watch = `${org}/${toProjectId}/${nodeId}/#`;
1434
+ moves[nodeId] = {from: fromProjectId, to: toProjectId, watch};
1435
+ mqtt_subscribe(watch, (topic, message, retained) => {
1436
+ // A retained message is the broker's memory of an earlier stay in that project, not this node
1437
+ if (retained || moves[nodeId]?.watch !== watch) { return; }
1438
+ delete moves[nodeId];
1439
+ mqtt_unsubscribe(watch);
1440
+ this.moveNodeInConfig(org, nodeId, fromProjectId, toProjectId);
1441
+ onChanged();
1442
+ });
1443
+ }
1409
1444
 
1410
- // Refresh the table after a short delay
1411
- setTimeout(onChanged, 500);
1445
+ // server_config is only fetched at startup, so it is told here what the server now believes.
1446
+ moveNodeInConfig(org, nodeId, fromProjectId, toProjectId) {
1447
+ const projects = this.projectsOf(org);
1448
+ const from = projects[fromProjectId]?.nodes;
1449
+ const to = projects[toProjectId] && (projects[toProjectId].nodes || (projects[toProjectId].nodes = {}));
1450
+ if (!to) { return; }
1451
+ to[nodeId] = {...(from?.[nodeId] || {}), lastseen: new Date().toISOString()};
1452
+ if (from) { delete from[nodeId]; }
1412
1453
  }
1413
1454
 
1414
1455
  renderProjectIdCell(node, onChanged) {
1415
- // Create an editable cell for projectId that opens a dropdown on click
1456
+ const move = this.state.nodeMoves?.[node.nodeId];
1457
+ const pending = move && move.from === node.projectId;
1416
1458
  return el('td', {
1417
1459
  style: 'cursor: pointer; position: relative;',
1418
- title: 'Click to change project',
1460
+ title: pending ? getStringWith("Waiting for %s to report", node.nodeId) : getString("Click to change project"),
1419
1461
  onclick: (e) => {
1420
- // Replace with dropdown
1421
- const dropdown = this.projectsDropdownForNode(this.state.org, node.projectId);
1462
+ const dropdown = this.projectsDropdownForNode(this.state.org, pending ? move.to : node.projectId);
1422
1463
  dropdown.addEventListener('change', (changeEvent) => {
1423
1464
  this.onNodeProjectChange(node.nodeId, node.projectId, changeEvent.target, onChanged);
1424
1465
  });
@@ -1427,7 +1468,7 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
1427
1468
  // Close dropdown if focus lost
1428
1469
  dropdown.addEventListener('blur', onChanged);
1429
1470
  }
1430
- }, [node.projectId]);
1471
+ }, [pending ? `${node.projectId} → ${move.to} …` : node.projectId]);
1431
1472
  }
1432
1473
 
1433
1474
  // Content of the OTA tab below the organization dropdown - only rendered once an org is selected
@@ -1627,20 +1668,17 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
1627
1668
  el('section', {}, [
1628
1669
  this.refreshableHeading("Enrolled nodes", this.getEnrolledNodes),
1629
1670
  el('p', {textContent:
1630
- "Each node has its own broker credential, which it collects from this server the first " +
1631
- "time it connects. Forget one to have it issued a new credential - needed if its " +
1632
- "filesystem has been erased, because it can then no longer prove which node it is. A " +
1633
- "node that asked and was refused is listed here as well, with why: click Approve on it " +
1634
- "to accept its next request whatever secret it presents. It retries every half hour."}),
1671
+ "Nodes that have obtained broker credentials on first connection. " +
1672
+ "Forget reissues a new credential at next connection and is needed if memory has been erased. " +
1673
+ "Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes)."}),
1635
1674
  this.state.elements.enrolled_nodes = this.enrolledNodesList(),
1636
1675
  ]),
1637
1676
  el('section', {}, [
1638
1677
  el('h3', {textContent: "Enrolment secret"}),
1639
- el('p', {textContent:
1640
- "Compiled into a node's firmware, this is what lets it ask this server for its own " +
1641
- "broker credential. It grants nothing else - no reading and no publishing. Add a second " +
1642
- "one to change it: both are accepted, so nodes already flashed keep working until you " +
1643
- "withdraw the old one."}),
1678
+ el('p', {i8n: false, textContent: getString(
1679
+ "Compiled into firmware and used to request broker credential. " +
1680
+ "Add a second one to change it: both are accepted, so nodes already flashed keep working until you " +
1681
+ "withdraw the old one.")}),
1644
1682
  this.state.elements.enrolment_secrets = this.enrolmentSecretsList(),
1645
1683
  ]),
1646
1684
  ]);
package/base/boards.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": 7,
2
+ "version": 8,
3
3
  "coreVersion": "pioarduino framework-arduinoespressif32 3.3.9",
4
- "comment": "Base bundles for USB flashing - see FLASH_PLAN.md. Bootloaders MUST come from the same framework the firmware is built against: generated from pioarduino framework-arduinoespressif32-libs/<chip>/bin/bootloader_qio_80m.elf via 'esptool elf2image --flash-mode dio --flash-freq 80m --flash-size 4MB'. The C3 one is byte-identical to a real pioarduino build; S2/S3 use the same recipe but are unverified against a reference build. The Arduino IDE core's dio_80m ELF produces a DIFFERENT bootloader that did not work. Flash-size independent (esptool patches the header at write time). Partition tables are chip-independent. boot_app0.bin is universal.",
4
+ "comment": "Base bundles for USB flashing - see FLASH_PLAN.md. Bootloaders MUST come from the same framework the firmware is built against: generated from pioarduino framework-arduinoespressif32-libs/<chip>/bin/bootloader_qio_80m.elf via 'esptool elf2image --flash-mode dio --flash-freq 80m --flash-size 4MB'. The C3 one is byte-identical to a real pioarduino build; S2/S3 use the same recipe but are unverified against a reference build. The Arduino IDE core's dio_80m ELF produces a DIFFERENT bootloader that did not work. Flash-size independent (esptool patches the header at write time). Partition tables are chip-independent, and are the core's stock CSVs, so they match what a PlatformIO env using the same scheme writes. boot_app0.bin is universal.",
5
5
  "otadata": "boot_app0.bin",
6
6
  "partitionTableOffset": 32768,
7
7
  "bootloaderByChip": {
@@ -13,13 +13,13 @@
13
13
  "ESP32-C3-4MB": "min_spiffs",
14
14
  "ESP32-S2-4MB": "min_spiffs",
15
15
  "ESP32-S3-4MB": "min_spiffs",
16
- "ESP32-S3-8MB": "frugal_8mb",
17
- "ESP32-S3-16MB": "frugal_16mb"
16
+ "ESP32-S3-8MB": "default_8MB",
17
+ "ESP32-S3-16MB": "default_16MB"
18
18
  },
19
19
  "partitionsByScheme": {
20
20
  "min_spiffs": "partitions/min_spiffs.bin",
21
- "frugal_8mb": "partitions/frugal_8mb.bin",
22
- "frugal_16mb": "partitions/frugal_16mb.bin"
21
+ "default_8MB": "partitions/default_8MB.bin",
22
+ "default_16MB": "partitions/default_16MB.bin"
23
23
  },
24
24
  "wholeImageChips": [
25
25
  "ESP8266"
@@ -0,0 +1,7 @@
1
+ # Name, Type, SubType, Offset, Size, Flags
2
+ nvs, data, nvs, 0x9000, 0x5000,
3
+ otadata, data, ota, 0xe000, 0x2000,
4
+ app0, app, ota_0, 0x10000, 0x640000,
5
+ app1, app, ota_1, 0x650000,0x640000,
6
+ spiffs, data, spiffs, 0xc90000,0x360000,
7
+ coredump, data, coredump,0xFF0000,0x10000,
@@ -0,0 +1,7 @@
1
+ # Name, Type, SubType, Offset, Size, Flags
2
+ nvs, data, nvs, 0x9000, 0x5000,
3
+ otadata, data, ota, 0xe000, 0x2000,
4
+ app0, app, ota_0, 0x10000, 0x330000,
5
+ app1, app, ota_1, 0x340000,0x330000,
6
+ spiffs, data, spiffs, 0x670000,0x180000,
7
+ coredump, data, coredump,0x7F0000,0x10000,
package/cards.js CHANGED
@@ -384,11 +384,20 @@ class MqttDeviceCard extends HTMLElementExtendedMinimum {
384
384
  this.compact(hyst, {}),
385
385
  ]) : null,
386
386
  ].filter(Boolean)) : null;
387
+ // Any other control - a display's inputs, Blinken's timings - gets a plain row per topic, or its
388
+ // section came out empty and was dropped, leaving nothing to wire (control_lcd_ht did exactly that)
389
+ const laidOut = ['now', 'greater', 'limit', 'hysteresis', 'hysterisis', 'out', 'manual'];
390
+ const others = Object.entries(t).filter(([leaf]) => !laidOut.includes(leaf)).map(([, mt]) => {
391
+ const widget = this.widgetForTopic(mt, '', mt.wireable ? { wiring: 'open' } : {});
392
+ this.state.elements.widgets.push({ row: { mt }, widget });
393
+ return this.field(mt.name, widget);
394
+ });
387
395
  return this.section(groupMt.state.name || groupId, [
388
396
  t.now ? this.field(getString('Input'), this.widgetForTopic(t.now, '', { wiring: 'open' })) : null,
389
397
  when ? this.field(getString('When'), when) : null,
390
398
  t.out ? this.field(getString('Output'), this.widgetForTopic(t.out, '', { wiring: 'open' })) : null,
391
399
  t.manual ? this.field(getString('Manual'), this.widgetForTopic(t.manual, '')) : null,
400
+ ...others,
392
401
  ]);
393
402
  }
394
403
  // A bare input with no label of its own, for the inline "when" row
package/core.js CHANGED
@@ -208,6 +208,13 @@ function mqtt_deliver(topic, msg, retained) {
208
208
  }
209
209
  }
210
210
  }
211
+ // Drop one subscription, made with mqtt_subscribe, that was only wanted for a while.
212
+ function mqtt_unsubscribe(topic) {
213
+ mqtt_subscriptions = mqtt_subscriptions.filter((s) => s.topic !== topic);
214
+ if (mqtt_client && mqtt_client.connected) {
215
+ mqtt_client.unsubscribe(topic, (err) => { if (err) console.error(err); });
216
+ }
217
+ }
211
218
  // Drop every subscription belonging to an organization - all its topics start with the organization id.
212
219
  // Called when switching organization, both to tell the broker (if still connected) and so that the
213
220
  // connection made for the new organization does not resubscribe to the old organization's topics.
@@ -423,7 +430,7 @@ EN:
423
430
  close: close
424
431
  Collapse: Collapse
425
432
  Color: Color
426
- "Compiled into a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Compiled into a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one."
433
+ "Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one."
427
434
  Connect board: Connect board
428
435
  connected: connected
429
436
  connecting: connecting
@@ -446,7 +453,6 @@ EN:
446
453
  Done: Done
447
454
  ds18b20: ds18b20
448
455
  e.g. LiteFarm: e.g. LiteFarm
449
- Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.: Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.
450
456
  eCO2: eCO2
451
457
  Email: Email
452
458
  Enrol URL: Enrol URL
@@ -533,7 +539,7 @@ EN:
533
539
  No farms registered for this organization yet.: No farms registered for this organization yet.
534
540
  No nodes found for this farm's project(s).: No nodes found for this farm's project(s).
535
541
  No nodes found for this organization: No nodes found for this organization
536
- No nodes have enrolled yet. A node enrols itself the first time it connects.: No nodes have enrolled yet. A node enrols itself the first time it connects.
542
+ No nodes have enrolled yet, and none have asked and been rejected.: No nodes have enrolled yet, and none have asked and been rejected.
537
543
  No organization: No organization
538
544
  No organization selected: No organization selected
539
545
  No OTA files uploaded yet.: No OTA files uploaded yet.
@@ -550,6 +556,7 @@ EN:
550
556
  Nodes: Nodes
551
557
  Nodes in Farm: Nodes in Farm
552
558
  Nodes in Organization: Nodes in Organization
559
+ Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).: Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).
553
560
  Not in the schema: Not in the schema
554
561
  Not selected: Not selected
555
562
  Note this is your organization - not the organizations whose devices you want to access.: Note this is your organization - not the organizations whose devices you want to access.
@@ -581,6 +588,8 @@ EN:
581
588
  Pressure: Pressure
582
589
  Project: Project
583
590
  Project changed to: Project changed to
591
+ Asked %s to move: Asked %s to move
592
+ Waiting for %s to report: Waiting for %s to report
584
593
  Project ID: Project ID
585
594
  Project ID *: Project ID *
586
595
  Project Name: Project Name
@@ -733,7 +742,7 @@ FR:
733
742
  close: fermer
734
743
  Collapse: Réduire
735
744
  Color: Couleur
736
- "Compiled into a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Compilé dans le micrologiciel d'un nœud, c'est ce qui lui permet de demander à ce serveur son propre identifiant de broker. Il n'accorde rien d'autre - ni lecture ni publication. Ajoutez-en un second pour le changer : les deux sont acceptés, donc les nœuds déjà flashés continuent de fonctionner jusqu'à ce que vous retiriez l'ancien."
745
+ "Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Compilé dans le micrologiciel et utilisé pour demander l'identifiant de broker. Ajoutez-en un second pour le changer : les deux sont acceptés, donc les nœuds déjà flashés continuent de fonctionner jusqu'à ce que vous retiriez l'ancien."
737
746
  Connect board: Connecter la carte
738
747
  connected: connecté
739
748
  connecting: connexion
@@ -756,7 +765,6 @@ FR:
756
765
  Done: Fait
757
766
  ds18b20: ds18b20
758
767
  e.g. LiteFarm: par ex. LiteFarm
759
- Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.: Chaque nœud a son propre identifiant de broker, qu'il récupère sur ce serveur lors de sa première connexion. Oubliez-en un pour qu'un nouvel identifiant lui soit délivré - nécessaire si son système de fichiers a été effacé, car il ne peut alors plus prouver quel nœud il est.
760
768
  eCO2: eCO2
761
769
  Email: Email
762
770
  Enrol URL: URL d'enrôlement
@@ -843,7 +851,7 @@ FR:
843
851
  No farms registered for this organization yet.: Aucune ferme enregistrée pour cette organisation pour l'instant.
844
852
  No nodes found for this farm's project(s).: Aucun nœud trouvé pour le(s) projet(s) de cette ferme.
845
853
  No nodes found for this organization: Aucun nœud trouvé pour cette organisation
846
- No nodes have enrolled yet. A node enrols itself the first time it connects.: Aucun nœud ne s'est encore enrôlé. Un nœud s'enrôle lui-même lors de sa première connexion.
854
+ No nodes have enrolled yet, and none have asked and been rejected.: Aucun nœud ne s'est encore enrôlé, et aucun n'a été refusé.
847
855
  No organization: Aucune organisation
848
856
  No organization selected: Aucune organisation sélectionnée
849
857
  No OTA files uploaded yet.: Aucun fichier OTA téléversé pour l'instant.
@@ -860,6 +868,7 @@ FR:
860
868
  Nodes: Nœuds
861
869
  Nodes in Farm: Nœuds dans la ferme
862
870
  Nodes in Organization: Nœuds dans l'organisation
871
+ Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).: Nœuds ayant obtenu leur identifiant de broker à la première connexion. Oublier en délivre un nouveau à la prochaine connexion, nécessaire si la mémoire a été effacée. Les nœuds refusés sont listés - cliquez sur Approuver pour autoriser (à la prochaine tentative, généralement 30 minutes).
863
872
  Not in the schema: Absent du schéma
864
873
  Not selected: Non sélectionné
865
874
  Note this is your organization - not the organizations whose devices you want to access.: Ceci est votre organisation - pas les organisations dont vous voulez accéder aux appareils.
@@ -891,6 +900,8 @@ FR:
891
900
  Pressure: Pression
892
901
  Project: Projet
893
902
  Project changed to: Projet changé en
903
+ Asked %s to move: Déplacement demandé à %s
904
+ Waiting for %s to report: En attente du rapport de %s
894
905
  Project ID: ID du projet
895
906
  Project ID *: ID du projet *
896
907
  Project Name: Nom du projet
@@ -1043,7 +1054,7 @@ HI:
1043
1054
  close: बंद करें
1044
1055
  Collapse: छोटा करें
1045
1056
  Color: रंग
1046
- "Compiled into a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "नोड के फ़र्मवेयर में संकलित, यही उसे इस सर्वर से अपना ब्रोकर क्रेडेंशियल माँगने देता है। यह और कुछ नहीं देता - न पढ़ना, न प्रकाशित करना। इसे बदलने के लिए दूसरा जोड़ें: दोनों स्वीकार किए जाते हैं, इसलिए पहले फ़्लैश किए गए नोड तब तक काम करते रहते हैं जब तक आप पुराना वापस नहीं लेते।"
1057
+ "Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "फ़र्मवेयर में संकलित और ब्रोकर क्रेडेंशियल माँगने के लिए उपयोग किया जाता है। इसे बदलने के लिए दूसरा जोड़ें: दोनों स्वीकार किए जाते हैं, इसलिए पहले फ़्लैश किए गए नोड तब तक काम करते रहते हैं जब तक आप पुराना वापस नहीं लेते।"
1047
1058
  Connect board: बोर्ड कनेक्ट करें
1048
1059
  connected: जुड़े हुए
1049
1060
  connecting: कनेक्ट हो रहा है
@@ -1066,7 +1077,6 @@ HI:
1066
1077
  Done: पूर्ण
1067
1078
  ds18b20: ds18b20
1068
1079
  e.g. LiteFarm: उदाहरण के लिए LiteFarm
1069
- Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.: हर नोड का अपना ब्रोकर क्रेडेंशियल होता है, जो वह पहली बार जुड़ते समय इस सर्वर से लेता है। किसी को भुला दें ताकि उसे नया क्रेडेंशियल मिले - यह ज़रूरी है यदि उसका फ़ाइल-सिस्टम मिटा दिया गया हो, क्योंकि तब वह यह सिद्ध नहीं कर सकता कि वह कौन सा नोड है।
1070
1080
  eCO2: ईसीओ2
1071
1081
  Email: ईमेल
1072
1082
  Enrol URL: पंजीकरण URL
@@ -1153,7 +1163,7 @@ HI:
1153
1163
  No farms registered for this organization yet.: इस संगठन के लिए अभी तक कोई फार्म पंजीकृत नहीं है।
1154
1164
  No nodes found for this farm's project(s).: इस फार्म के प्रोजेक्ट (प्रोजेक्ट्स) के लिए कोई नोड नहीं मिला।
1155
1165
  No nodes found for this organization: इस संगठन के लिए कोई नोड नहीं मिला
1156
- No nodes have enrolled yet. A node enrols itself the first time it connects.: अभी तक कोई नोड पंजीकृत नहीं हुआ है। नोड पहली बार जुड़ते समय स्वयं पंजीकृत हो जाता है।
1166
+ No nodes have enrolled yet, and none have asked and been rejected.: अभी तक कोई नोड पंजीकृत नहीं हुआ है, और किसी को अस्वीकार नहीं किया गया है।
1157
1167
  No organization: कोई संगठन नहीं
1158
1168
  No organization selected: कोई संगठन चयनित नहीं
1159
1169
  No OTA files uploaded yet.: अभी तक कोई OTA फ़ाइल अपलोड नहीं की गई है।
@@ -1170,6 +1180,7 @@ HI:
1170
1180
  Nodes: नोड्स
1171
1181
  Nodes in Farm: फार्म में नोड्स
1172
1182
  Nodes in Organization: संगठन में नोड्स
1183
+ Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).: वे नोड जिन्होंने पहली बार जुड़ने पर ब्रोकर क्रेडेंशियल प्राप्त किए हैं। भुलाएँ अगली बार जुड़ने पर नया क्रेडेंशियल जारी करता है, और यह ज़रूरी है यदि मेमोरी मिटा दी गई हो। अस्वीकृत नोड भी सूचीबद्ध हैं - अनुमति देने के लिए स्वीकृत करें पर क्लिक करें (अगले प्रयास पर, आमतौर पर 30 मिनट)।
1173
1184
  Not in the schema: स्कीमा में नहीं
1174
1185
  Not selected: चयनित नहीं
1175
1186
  Note this is your organization - not the organizations whose devices you want to access.: ध्यान दें यह आपका संगठन है - वे संगठन नहीं जिनके उपकरणों तक आप पहुंचना चाहते हैं।
@@ -1201,6 +1212,8 @@ HI:
1201
1212
  Pressure: दाब
1202
1213
  Project: परियोजना
1203
1214
  Project changed to: प्रोजेक्ट बदलकर किया गया
1215
+ Asked %s to move: "%s को स्थानांतरित करने को कहा"
1216
+ Waiting for %s to report: "%s की रिपोर्ट की प्रतीक्षा"
1204
1217
  Project ID: प्रोजेक्ट आईडी
1205
1218
  Project ID *: प्रोजेक्ट आईडी *
1206
1219
  Project Name: प्रोजेक्ट का नाम
@@ -1353,7 +1366,7 @@ ID:
1353
1366
  close: tutup
1354
1367
  Collapse: Ciutkan
1355
1368
  Color: Warna
1356
- "Compiled into a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Dikompilasi ke dalam firmware sebuah node, inilah yang memungkinkannya meminta kredensial broker miliknya sendiri dari server ini. Ia tidak memberi apa pun selain itu - tidak membaca dan tidak menerbitkan. Tambahkan yang kedua untuk menggantinya: keduanya diterima, sehingga node yang sudah di-flash tetap bekerja sampai Anda mencabut yang lama."
1369
+ "Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Dikompilasi ke dalam firmware dan digunakan untuk meminta kredensial broker. Tambahkan yang kedua untuk menggantinya: keduanya diterima, sehingga node yang sudah di-flash tetap bekerja sampai Anda mencabut yang lama."
1357
1370
  Connect board: Hubungkan papan
1358
1371
  connected: terhubung
1359
1372
  connecting: menghubungkan
@@ -1376,7 +1389,6 @@ ID:
1376
1389
  Done: Selesai
1377
1390
  ds18b20: ds18b20
1378
1391
  e.g. LiteFarm: misalnya LiteFarm
1379
- Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.: Setiap node memiliki kredensial broker sendiri, yang diambilnya dari server ini saat pertama kali tersambung. Lupakan salah satu agar diberi kredensial baru - diperlukan jika sistem berkasnya telah dihapus, karena node itu tidak lagi dapat membuktikan node yang mana dirinya.
1380
1392
  eCO2: eCO2
1381
1393
  Email: Email
1382
1394
  Enrol URL: URL pendaftaran
@@ -1463,7 +1475,7 @@ ID:
1463
1475
  No farms registered for this organization yet.: Belum ada farm yang terdaftar untuk organisasi ini.
1464
1476
  No nodes found for this farm's project(s).: Tidak ada node ditemukan untuk proyek farm ini.
1465
1477
  No nodes found for this organization: Tidak ada node ditemukan untuk organisasi ini
1466
- No nodes have enrolled yet. A node enrols itself the first time it connects.: Belum ada node yang mendaftar. Sebuah node mendaftarkan dirinya sendiri saat pertama kali tersambung.
1478
+ No nodes have enrolled yet, and none have asked and been rejected.: Belum ada node yang terdaftar, dan tidak ada yang ditolak.
1467
1479
  No organization: Tidak ada organisasi
1468
1480
  No organization selected: Tidak ada organisasi yang dipilih
1469
1481
  No OTA files uploaded yet.: Belum ada berkas OTA yang diunggah.
@@ -1480,6 +1492,7 @@ ID:
1480
1492
  Nodes: Node
1481
1493
  Nodes in Farm: Node dalam Farm
1482
1494
  Nodes in Organization: Node dalam Organisasi
1495
+ Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).: Node yang telah memperoleh kredensial broker saat pertama kali tersambung. Lupakan menerbitkan kredensial baru pada sambungan berikutnya, dan diperlukan jika memori telah dihapus. Node yang ditolak juga dicantumkan - klik Setujui untuk mengizinkan (pada percobaan berikutnya, biasanya 30 menit).
1483
1496
  Not in the schema: Tidak ada di skema
1484
1497
  Not selected: Tidak dipilih
1485
1498
  Note this is your organization - not the organizations whose devices you want to access.: Ini adalah organisasi Anda - bukan organisasi yang perangkatnya ingin Anda akses.
@@ -1511,6 +1524,8 @@ ID:
1511
1524
  Pressure: Tekanan
1512
1525
  Project: Proyek
1513
1526
  Project changed to: Proyek diubah menjadi
1527
+ Asked %s to move: Meminta %s pindah
1528
+ Waiting for %s to report: Menunggu laporan %s
1514
1529
  Project ID: ID Proyek
1515
1530
  Project ID *: ID Proyek *
1516
1531
  Project Name: Nama Proyek
@@ -3332,6 +3347,7 @@ export {
3332
3347
  mqttTempConnect,
3333
3348
  mqtt_deliver,
3334
3349
  mqtt_subscribe,
3350
+ mqtt_unsubscribe,
3335
3351
  mqtt_unsubscribe_organization,
3336
3352
  nextUniqueId,
3337
3353
  nowMs,