matterbridge 1.2.11 → 1.2.12

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.
@@ -33,9 +33,7 @@ import os from 'os';
33
33
  import path from 'path';
34
34
  import WebSocket, { WebSocketServer } from 'ws';
35
35
  import { CommissioningController, CommissioningServer, MatterServer } from '@project-chip/matter-node.js';
36
- import { BasicInformationCluster, BooleanStateCluster,
37
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
38
- BridgedDeviceBasicInformation, BridgedDeviceBasicInformationCluster, ClusterServer, GeneralCommissioning, PowerSourceCluster, ThreadNetworkDiagnosticsCluster, getClusterNameById, } from '@project-chip/matter-node.js/cluster';
36
+ import { BasicInformationCluster, BooleanStateCluster, BridgedDeviceBasicInformation, BridgedDeviceBasicInformationCluster, ClusterServer, FixedLabelCluster, GeneralCommissioning, PowerSourceCluster, ThreadNetworkDiagnosticsCluster, getClusterNameById, } from '@project-chip/matter-node.js/cluster';
39
37
  import { DeviceTypeId, VendorId } from '@project-chip/matter-node.js/datatype';
40
38
  import { Aggregator, DeviceTypes, NodeStateInformation } from '@project-chip/matter-node.js/device';
41
39
  import { Format, Level, Logger } from '@project-chip/matter-node.js/log';
@@ -190,7 +188,7 @@ export class Matterbridge extends EventEmitter {
190
188
  await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
191
189
  // Set reachability to true and trigger event after 60 seconds
192
190
  setTimeout(() => {
193
- this.log.info(`*Setting reachability to true for ${plg}Matterbridge${db}`);
191
+ this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
194
192
  if (this.commissioningServer)
195
193
  this.setCommissioningServerReachability(this.commissioningServer, true);
196
194
  if (this.matterAggregator)
@@ -367,7 +365,7 @@ export class Matterbridge extends EventEmitter {
367
365
  // Delete matter storage file
368
366
  await fs.unlink(path.join(this.matterbridgeDirectory, 'matterbridge.json'));
369
367
  // Delete node storage directory with its subdirectories
370
- await fs.rmdir(path.join(this.matterbridgeDirectory, 'storage'), { recursive: true });
368
+ await fs.rm(path.join(this.matterbridgeDirectory, 'storage'), { recursive: true });
371
369
  this.log.info('Factory reset done! Remove all paired devices from the controllers.');
372
370
  this.emit('shutdown');
373
371
  process.exit(0);
@@ -611,6 +609,31 @@ export class Matterbridge extends EventEmitter {
611
609
  await this.cleanup('shutting down...', false);
612
610
  this.hasCleanupStarted = false;
613
611
  }
612
+ /**
613
+ * Shut down the process and reset.
614
+ */
615
+ async unregisterAndShutdownProcess() {
616
+ this.log.info('Unregistering all devices and shutting down...');
617
+ for (const plugin of this.registeredPlugins.filter((plugin) => plugin.enabled && !plugin.error)) {
618
+ await this.removeAllBridgedDevices(plugin.name);
619
+ }
620
+ await this.cleanup('unregistered all devices and shutting down...', false);
621
+ this.hasCleanupStarted = false;
622
+ }
623
+ /**
624
+ * Shut down the process and reset.
625
+ */
626
+ async shutdownProcessAndReset() {
627
+ await this.cleanup('shutting down with reset...', false);
628
+ this.hasCleanupStarted = false;
629
+ }
630
+ /**
631
+ * Shut down the process and factory reset.
632
+ */
633
+ async shutdownProcessAndFactoryReset() {
634
+ await this.cleanup('shutting down with factory reset...', false);
635
+ this.hasCleanupStarted = false;
636
+ }
614
637
  /**
615
638
  * Cleans up the Matterbridge instance.
616
639
  * @param message - The cleanup message.
@@ -628,7 +651,7 @@ export class Matterbridge extends EventEmitter {
628
651
  for (const plugin of this.registeredPlugins) {
629
652
  if (!plugin.enabled || plugin.error)
630
653
  continue;
631
- this.log.info(`*Shutting down plugin ${plg}${plugin.name}${nf}`);
654
+ this.log.info(`Shutting down plugin ${plg}${plugin.name}${nf}`);
632
655
  if (plugin.platform) {
633
656
  try {
634
657
  await plugin.platform.onShutdown('Matterbridge is closing: ' + message);
@@ -710,7 +733,7 @@ export class Matterbridge extends EventEmitter {
710
733
  });
711
734
  this.webSocketServer = undefined;
712
735
  }
713
- /*const cleanupTimeout1 =*/ setTimeout(async () => {
736
+ setTimeout(async () => {
714
737
  // Closing matter
715
738
  await this.stopMatter();
716
739
  // Closing storage
@@ -740,7 +763,7 @@ export class Matterbridge extends EventEmitter {
740
763
  this.registeredPlugins = [];
741
764
  this.registeredDevices = [];
742
765
  this.log.info('Waiting for matter to deliver last messages...');
743
- /*const cleanupTimeout2 =*/ setTimeout(async () => {
766
+ setTimeout(async () => {
744
767
  if (restart) {
745
768
  if (message === 'updating...') {
746
769
  this.log.info('Cleanup completed. Updating...');
@@ -754,14 +777,27 @@ export class Matterbridge extends EventEmitter {
754
777
  }
755
778
  }
756
779
  else {
780
+ if (message === 'shutting down with reset...') {
781
+ // Delete matter storage file
782
+ this.log.info('Resetting Matterbridge commissioning information...');
783
+ await fs.unlink(path.join(this.matterbridgeDirectory, 'matterbridge.json'));
784
+ this.log.info('Reset done! Remove all paired devices from the controllers.');
785
+ }
786
+ if (message === 'shutting down with factory reset...') {
787
+ // Delete matter storage file
788
+ this.log.info('Resetting Matterbridge commissioning information...');
789
+ await fs.unlink(path.join(this.matterbridgeDirectory, 'matterbridge.json'));
790
+ // Delete node storage directory with its subdirectories
791
+ this.log.info('Resetting Matterbridge storage...');
792
+ await fs.rm(path.join(this.matterbridgeDirectory, 'storage'), { recursive: true });
793
+ this.log.info('Factory reset done! Remove all paired devices from the controllers.');
794
+ }
757
795
  this.log.info('Cleanup completed. Shutting down...');
758
796
  Matterbridge.instance = undefined;
759
797
  this.emit('shutdown');
760
798
  }
761
799
  }, 2 * 1000);
762
- //cleanupTimeout2.unref();
763
800
  }, 3 * 1000);
764
- //cleanupTimeout1.unref();
765
801
  }
766
802
  }
767
803
  /**
@@ -911,7 +947,7 @@ export class Matterbridge extends EventEmitter {
911
947
  async removeAllBridgedDevices(pluginName) {
912
948
  const plugin = this.findPlugin(pluginName);
913
949
  if (this.bridgeMode === 'childbridge' && plugin?.type === 'AccessoryPlatform') {
914
- this.log.info(`Removing devices for plugin ${plg}${pluginName}${nf}: AccessoryPlatform not supported in childbridge mode`);
950
+ this.log.info(`Removing devices for plugin ${plg}${pluginName}${nf} type AccessoryPlatform is not supported in childbridge mode`);
915
951
  return;
916
952
  }
917
953
  const devicesToRemove = [];
@@ -1534,7 +1570,7 @@ export class Matterbridge extends EventEmitter {
1534
1570
  await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
1535
1571
  //if (hasParameter('advertise')) await this.commissioningServer.advertise();
1536
1572
  setTimeout(() => {
1537
- this.log.info(`*Setting reachability to true for ${plg}Matterbridge${db}`);
1573
+ this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
1538
1574
  if (this.commissioningServer)
1539
1575
  this.setCommissioningServerReachability(this.commissioningServer, true);
1540
1576
  if (this.matterAggregator)
@@ -1637,7 +1673,7 @@ export class Matterbridge extends EventEmitter {
1637
1673
  }
1638
1674
  await this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.nodeContext, plugin.name);
1639
1675
  // Setting reachability to true
1640
- this.log.info(`*Setting reachability to true for ${plg}${plugin.name}${db}`);
1676
+ this.log.info(`Setting reachability to true for ${plg}${plugin.name}${db}`);
1641
1677
  if (plugin.commissioningServer)
1642
1678
  this.setCommissioningServerReachability(plugin.commissioningServer, true);
1643
1679
  if (plugin.type === 'AccessoryPlatform' && plugin.device)
@@ -1756,7 +1792,7 @@ export class Matterbridge extends EventEmitter {
1756
1792
  await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
1757
1793
  }
1758
1794
  else {
1759
- this.log.info(`***The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is already commissioned . Waiting for controllers to connect ...`);
1795
+ this.log.info(`*The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is already commissioned . Waiting for controllers to connect ...`);
1760
1796
  if (pluginName !== 'Matterbridge') {
1761
1797
  const plugin = this.findPlugin(pluginName);
1762
1798
  if (plugin) {
@@ -1805,7 +1841,7 @@ export class Matterbridge extends EventEmitter {
1805
1841
  if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
1806
1842
  basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
1807
1843
  matterAggregator.getBridgedDevices().forEach((device) => {
1808
- this.log.debug(`*Setting reachability to true for bridged device: ${dev}${device.name}${nf}`);
1844
+ this.log.debug(`Setting reachability to true for bridged device: ${dev}${device.name}${nf}`);
1809
1845
  device.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(reachable);
1810
1846
  device.getClusterServer(BridgedDeviceBasicInformationCluster)?.triggerReachableChangedEvent({ reachableNewValue: reachable });
1811
1847
  });
@@ -1831,6 +1867,35 @@ export class Matterbridge extends EventEmitter {
1831
1867
  * @returns {CommissioningServer} The created commissioning server.
1832
1868
  */
1833
1869
  async createCommisioningServer(context, pluginName) {
1870
+ const getVendorIdName = (vendorId) => {
1871
+ if (!vendorId)
1872
+ return '';
1873
+ let vendorName = '';
1874
+ switch (vendorId) {
1875
+ case 4937:
1876
+ vendorName = '(AppleHome)';
1877
+ break;
1878
+ case 4362:
1879
+ vendorName = '(SmartThings)';
1880
+ break;
1881
+ case 4939:
1882
+ vendorName = '(HomeAssistant)';
1883
+ break;
1884
+ case 24582:
1885
+ vendorName = '(GoogleHome)';
1886
+ break;
1887
+ case 4701:
1888
+ vendorName = '(Tuya)';
1889
+ break;
1890
+ case 4742:
1891
+ vendorName = '(eWeLink)';
1892
+ break;
1893
+ default:
1894
+ vendorName = '(unknown)';
1895
+ break;
1896
+ }
1897
+ return vendorName;
1898
+ };
1834
1899
  this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db}`);
1835
1900
  const deviceName = await context.get('deviceName');
1836
1901
  const deviceType = await context.get('deviceType');
@@ -1873,16 +1938,9 @@ export class Matterbridge extends EventEmitter {
1873
1938
  const info = commissioningServer.getActiveSessionInformation(fabricIndex);
1874
1939
  let connected = false;
1875
1940
  info.forEach((session) => {
1876
- this.log.info(`***Active session changed on fabric ${fabricIndex} ${session.fabric?.rootVendorId}/${session.fabric?.label} for ${plg}${pluginName}${nf}`, debugStringify(session));
1941
+ this.log.info(`*Active session changed on fabric ${fabricIndex} ${session.fabric?.rootVendorId}${getVendorIdName(session.fabric?.rootVendorId)}/${session.fabric?.label} for ${plg}${pluginName}${nf}`, debugStringify(session));
1877
1942
  if (session.isPeerActive === true && session.secure === true && session.numberOfActiveSubscriptions >= 1) {
1878
- let controllerName = '';
1879
- if (session.fabric?.rootVendorId === 4937)
1880
- controllerName = 'AppleHome';
1881
- if (session.fabric?.rootVendorId === 4362)
1882
- controllerName = 'SmartThings';
1883
- if (session.fabric?.rootVendorId === 4939)
1884
- controllerName = 'HomeAssistant';
1885
- this.log.info(`***Controller ${session.fabric?.rootVendorId}${controllerName !== '' ? '(' + controllerName + ')' : ''}/${session.fabric?.label} connected to ${plg}${pluginName}${nf} on session ${session.name}`);
1943
+ this.log.info(`*Controller ${session.fabric?.rootVendorId}${getVendorIdName(session.fabric?.rootVendorId)}/${session.fabric?.label} connected to ${plg}${pluginName}${nf} on session ${session.name}`);
1886
1944
  connected = true;
1887
1945
  }
1888
1946
  });
@@ -1944,9 +2002,9 @@ export class Matterbridge extends EventEmitter {
1944
2002
  },
1945
2003
  commissioningChangedCallback: async (fabricIndex) => {
1946
2004
  const info = commissioningServer.getCommissionedFabricInformation(fabricIndex);
1947
- this.log.debug(`***Commissioning changed on fabric ${fabricIndex} for ${plg}${pluginName}${nf}`, debugStringify(info));
2005
+ this.log.debug(`*Commissioning changed on fabric ${fabricIndex} for ${plg}${pluginName}${nf}`, debugStringify(info));
1948
2006
  if (info.length === 0) {
1949
- this.log.warn(`***Commissioning removed from fabric ${fabricIndex} for ${plg}${pluginName}${wr}. Resetting the commissioning server ...`);
2007
+ this.log.warn(`*Commissioning removed from fabric ${fabricIndex} for ${plg}${pluginName}${wr}. Resetting the commissioning server ...`);
1950
2008
  await commissioningServer.factoryReset();
1951
2009
  if (pluginName === 'Matterbridge') {
1952
2010
  await this.matterbridgeContext?.clearAll();
@@ -1961,7 +2019,7 @@ export class Matterbridge extends EventEmitter {
1961
2019
  }
1962
2020
  }
1963
2021
  }
1964
- this.log.warn(`***Restart to activate the pairing for ${plg}${pluginName}${wr}`);
2022
+ this.log.warn(`*Restart to activate the pairing for ${plg}${pluginName}${wr}`);
1965
2023
  }
1966
2024
  },
1967
2025
  });
@@ -2522,6 +2580,7 @@ export class Matterbridge extends EventEmitter {
2522
2580
  //console.log(error);
2523
2581
  }
2524
2582
  data.push({
2583
+ endpoint: registeredDevice.device.number ? registeredDevice.device.number.toString() : '...',
2525
2584
  clusterName: clusterServer.name,
2526
2585
  clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
2527
2586
  attributeName: key,
@@ -2530,6 +2589,37 @@ export class Matterbridge extends EventEmitter {
2530
2589
  });
2531
2590
  });
2532
2591
  });
2592
+ registeredDevice.device.getChildEndpoints().forEach((childEndpoint) => {
2593
+ const name = registeredDevice.device.getChildEndpointName(childEndpoint);
2594
+ const clusterServers = childEndpoint.getAllClusterServers();
2595
+ clusterServers.forEach((clusterServer) => {
2596
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
2597
+ if (clusterServer.name === 'EveHistory')
2598
+ return;
2599
+ //this.log.debug(`***--clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute:${key}(${value.id}) ${value.isFixed} ${value.isWritable} ${value.isWritable}`);
2600
+ let attributeValue;
2601
+ try {
2602
+ if (typeof value.getLocal() === 'object')
2603
+ attributeValue = stringify(value.getLocal());
2604
+ else
2605
+ attributeValue = value.getLocal().toString();
2606
+ }
2607
+ catch (error) {
2608
+ attributeValue = 'Unavailable';
2609
+ this.log.debug(`GetLocal error ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
2610
+ //console.log(error);
2611
+ }
2612
+ data.push({
2613
+ endpoint: (childEndpoint.number ? childEndpoint.number.toString() : '...') + (name ? ' (' + name + ')' : ''),
2614
+ clusterName: clusterServer.name,
2615
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
2616
+ attributeName: key,
2617
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
2618
+ attributeValue,
2619
+ });
2620
+ });
2621
+ });
2622
+ });
2533
2623
  }
2534
2624
  });
2535
2625
  res.json(data);
@@ -2571,13 +2661,25 @@ export class Matterbridge extends EventEmitter {
2571
2661
  plugin.platform?.log.setLogDebug(this.debugEnabled);
2572
2662
  });
2573
2663
  }
2574
- // Handle the command shutdown from Header
2664
+ // Handle the command reset from Settings
2665
+ if (command === 'unregister') {
2666
+ await this.unregisterAndShutdownProcess();
2667
+ }
2668
+ // Handle the command reset from Settings
2669
+ if (command === 'reset') {
2670
+ this.shutdownProcessAndReset(); // No await do it asyncronously
2671
+ }
2672
+ // Handle the command factoryreset from Settings
2673
+ if (command === 'factoryreset') {
2674
+ this.shutdownProcessAndFactoryReset(); // No await do it asyncronously
2675
+ }
2676
+ // Handle the command restart from Header
2575
2677
  if (command === 'shutdown') {
2576
- this.shutdownProcess();
2678
+ this.shutdownProcess(); // No await do it asyncronously
2577
2679
  }
2578
2680
  // Handle the command restart from Header
2579
2681
  if (command === 'restart') {
2580
- this.restartProcess();
2682
+ this.restartProcess(); // No await do it asyncronously
2581
2683
  }
2582
2684
  // Handle the command update from Header
2583
2685
  if (command === 'update') {
@@ -2665,10 +2767,13 @@ export class Matterbridge extends EventEmitter {
2665
2767
  const plugin = plugins.find((plugin) => plugin.name === param);
2666
2768
  if (plugin) {
2667
2769
  plugin.enabled = true;
2770
+ plugin.error = undefined;
2668
2771
  plugin.loaded = undefined;
2669
2772
  plugin.started = undefined;
2670
2773
  plugin.configured = undefined;
2671
2774
  plugin.connected = undefined;
2775
+ plugin.platform = undefined;
2776
+ plugin.registeredDevices = undefined;
2672
2777
  await this.nodeContext?.set('plugins', plugins);
2673
2778
  this.log.info(`Enabled plugin ${plg}${param}${nf}`);
2674
2779
  }
@@ -2679,7 +2784,6 @@ export class Matterbridge extends EventEmitter {
2679
2784
  pluginToEnable.platform = await this.loadPlugin(pluginToEnable);
2680
2785
  if (pluginToEnable.platform) {
2681
2786
  await this.startPlugin(pluginToEnable, 'The plugin has been enabled', true);
2682
- pluginToEnable.enabled = false;
2683
2787
  }
2684
2788
  else {
2685
2789
  pluginToEnable.enabled = false;
@@ -2698,21 +2802,26 @@ export class Matterbridge extends EventEmitter {
2698
2802
  await this.savePluginConfig(pluginToDisable);
2699
2803
  }
2700
2804
  pluginToDisable.enabled = false;
2805
+ pluginToDisable.error = undefined;
2701
2806
  pluginToDisable.loaded = undefined;
2702
2807
  pluginToDisable.started = undefined;
2703
2808
  pluginToDisable.configured = undefined;
2704
2809
  pluginToDisable.connected = undefined;
2705
2810
  pluginToDisable.platform = undefined;
2811
+ pluginToDisable.registeredDevices = undefined;
2706
2812
  const plugins = await this.nodeContext?.get('plugins');
2707
2813
  if (!plugins)
2708
2814
  return;
2709
2815
  const plugin = plugins.find((plugin) => plugin.name === param);
2710
2816
  if (plugin) {
2711
2817
  plugin.enabled = false;
2818
+ plugin.error = undefined;
2712
2819
  plugin.loaded = undefined;
2713
2820
  plugin.started = undefined;
2714
2821
  plugin.configured = undefined;
2715
2822
  plugin.connected = undefined;
2823
+ plugin.platform = undefined;
2824
+ plugin.registeredDevices = undefined;
2716
2825
  await this.nodeContext?.set('plugins', plugins);
2717
2826
  this.log.info(`Disabled plugin ${plg}${param}${nf}`);
2718
2827
  }
@@ -2728,7 +2837,7 @@ export class Matterbridge extends EventEmitter {
2728
2837
  if (!useHttps) {
2729
2838
  // Listen on HTTP
2730
2839
  this.expressServer = this.expressApp.listen(port, () => {
2731
- this.log.info(`The frontend is running on ${UNDERLINE}http://localhost:${port}${UNDERLINEOFF}${rs}`);
2840
+ this.log.info(`The frontend is listening on ${UNDERLINE}http://${this.systemInformation.ipv4Address}:${port}${UNDERLINEOFF}${rs}`);
2732
2841
  });
2733
2842
  }
2734
2843
  else {
@@ -2742,7 +2851,7 @@ export class Matterbridge extends EventEmitter {
2742
2851
  // Specify the port to listen on, for example 443 for default HTTPS
2743
2852
  const PORT = 443;
2744
2853
  httpsServer.listen(PORT, () => {
2745
- this.log.info(`The frontend is running on ${UNDERLINE}https://localhost:${PORT}${UNDERLINEOFF}${rs}`);
2854
+ this.log.info(`The frontend is listening on ${UNDERLINE}https://${this.systemInformation.ipv4Address}:${PORT}${UNDERLINEOFF}${rs}`);
2746
2855
  });
2747
2856
  }
2748
2857
  this.log.debug(`Frontend initialized on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
@@ -2753,6 +2862,16 @@ export class Matterbridge extends EventEmitter {
2753
2862
  * @returns The attributes of the cluster servers in the device.
2754
2863
  */
2755
2864
  getClusterTextFromDevice(device) {
2865
+ const stringifyFixedLabel = (endpoint) => {
2866
+ const labelList = endpoint.getClusterServer(FixedLabelCluster)?.getLabelListAttribute();
2867
+ if (!labelList)
2868
+ return;
2869
+ const composed = labelList.find((entry) => entry.label === 'composed');
2870
+ if (composed)
2871
+ return 'Composed: ' + composed.value;
2872
+ else
2873
+ return ''; //'FixedLabel: ' + labelList.map((entry) => entry.label + ': ' + entry.value).join(' ');
2874
+ };
2756
2875
  let attributes = '';
2757
2876
  //this.log.debug(`getClusterTextFromDevice: ${device.name}`);
2758
2877
  const clusterServers = device.getAllClusterServers();
@@ -2789,7 +2908,9 @@ export class Matterbridge extends EventEmitter {
2789
2908
  if (clusterServer.name === 'PressureMeasurement')
2790
2909
  attributes += `Pressure: ${clusterServer.getMeasuredValueAttribute()} `;
2791
2910
  if (clusterServer.name === 'FlowMeasurement')
2792
- attributes += `Pressure: ${clusterServer.getMeasuredValueAttribute()} `;
2911
+ attributes += `Flow: ${clusterServer.getMeasuredValueAttribute()} `;
2912
+ if (clusterServer.name === 'FixedLabel')
2913
+ attributes += `${stringifyFixedLabel(device)} `;
2793
2914
  });
2794
2915
  return attributes;
2795
2916
  }
@@ -2819,6 +2940,31 @@ function restartProcess() {
2819
2940
  process.exit();
2820
2941
  }
2821
2942
 
2943
+ import React from 'react';
2944
+ import Form from "@rjsf/core";
2945
+
2946
+ const schema = {
2947
+ title: "Todo",
2948
+ type: "object",
2949
+ required: ["title"],
2950
+ properties: {
2951
+ title: {type: "string", title: "Title", default: "A new task"},
2952
+ done: {type: "boolean", title: "Done?", default: false}
2953
+ }
2954
+ };
2955
+
2956
+ const log = (type) => console.log.bind(console, type);
2957
+
2958
+ function Todo() {
2959
+ return (
2960
+ <Form schema={schema}
2961
+ onChange={log("changed")}
2962
+ onSubmit={log("submitted")}
2963
+ onError={log("errors")} />
2964
+ );
2965
+ }
2966
+
2967
+ export default Todo;
2822
2968
 
2823
2969
  /*
2824
2970
  How frontend was created