matterbridge 3.1.8-dev-20250725-d6e57e3 → 3.1.8-dev-20250725-aa6ff9e

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/CHANGELOG.md CHANGED
@@ -12,13 +12,19 @@ If you like this project and find it useful, please consider giving it a star on
12
12
 
13
13
  ### Added
14
14
 
15
- - [certification]: Improved certification management in pairing.json.
16
- - [workflow]: Update permissions and change GitHub token for Docker build trigger
15
+ - [certification]: Improved certification management in pairing.json. Added pemToBuffer function for converting PEM strings to Uint8Array.
16
+ - [workflow]: Update permissions and change GitHub token for Docker build triggers.
17
+ - [frontend]: Added Changelog button when a new version is installed.
18
+ - [frontend]: Added restart plugin in childbridge mode.
17
19
 
18
20
  ### Changed
19
21
 
20
22
  - [package]: Updated dependencies.
21
23
 
24
+ ### Fixed
25
+
26
+ - [switch]: Added conditional handling for momentary switch events in MatterbridgeEndpoint for single press only switches.
27
+
22
28
  <a href="https://www.buymeacoffee.com/luligugithub">
23
29
  <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
24
30
  </a>
package/dist/frontend.js CHANGED
@@ -315,7 +315,7 @@ export class Frontend extends EventEmitter {
315
315
  });
316
316
  this.expressApp.get('/api/plugins', async (req, res) => {
317
317
  this.log.debug('The frontend sent /api/plugins');
318
- res.json(this.getBaseRegisteredPlugins());
318
+ res.json(this.getPlugins());
319
319
  });
320
320
  this.expressApp.get('/api/devices', async (req, res) => {
321
321
  this.log.debug('The frontend sent /api/devices');
@@ -783,7 +783,7 @@ export class Frontend extends EventEmitter {
783
783
  });
784
784
  return attributes.trimStart().trimEnd();
785
785
  }
786
- getBaseRegisteredPlugins() {
786
+ getPlugins() {
787
787
  if (this.matterbridge.hasCleanupStarted)
788
788
  return [];
789
789
  const baseRegisteredPlugins = [];
@@ -814,11 +814,11 @@ export class Frontend extends EventEmitter {
814
814
  schemaJson: plugin.schemaJson,
815
815
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
816
816
  hasBlackList: plugin.configJson?.blackList !== undefined,
817
- paired: plugin.serverNode?.state.commissioning.commissioned,
818
- qrPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode?.state.commissioning.pairingCodes.qrPairingCode,
819
- manualPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode?.state.commissioning.pairingCodes.manualPairingCode,
820
- fabricInformations: plugin.serverNode ? this.matterbridge.sanitizeFabricInformations(Object.values(plugin.serverNode?.state.commissioning.fabrics)) : undefined,
821
- sessionInformations: plugin.serverNode ? this.matterbridge.sanitizeSessionInformation(Object.values(plugin.serverNode?.state.sessions.sessions)) : undefined,
817
+ paired: plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.commissioned : undefined,
818
+ qrPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.pairingCodes.qrPairingCode : undefined,
819
+ manualPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.pairingCodes.manualPairingCode : undefined,
820
+ fabricInformations: plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? this.matterbridge.sanitizeFabricInformations(Object.values(plugin.serverNode.state.commissioning.fabrics)) : undefined,
821
+ sessionInformations: plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? this.matterbridge.sanitizeSessionInformation(Object.values(plugin.serverNode.state.sessions.sessions)) : undefined,
822
822
  });
823
823
  }
824
824
  return baseRegisteredPlugins;
@@ -1112,6 +1112,32 @@ export class Frontend extends EventEmitter {
1112
1112
  this.wssSendRefreshRequired('devices');
1113
1113
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
1114
1114
  }
1115
+ else if (data.method === '/api/restartplugin') {
1116
+ if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
1117
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/restartplugin' }));
1118
+ return;
1119
+ }
1120
+ const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1121
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1122
+ if (plugin.serverNode) {
1123
+ await this.matterbridge.stopServerNode(plugin.serverNode);
1124
+ plugin.serverNode = undefined;
1125
+ }
1126
+ for (const device of this.matterbridge.devices) {
1127
+ if (device.plugin === plugin.name) {
1128
+ this.log.debug(`Removing device ${device.deviceName} from plugin ${plugin.name}`);
1129
+ this.matterbridge.devices.remove(device);
1130
+ }
1131
+ }
1132
+ await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1133
+ if (plugin.serverNode) {
1134
+ await this.matterbridge.startServerNode(plugin.serverNode);
1135
+ }
1136
+ this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
1137
+ this.wssSendRefreshRequired('plugins');
1138
+ this.wssSendRefreshRequired('devices');
1139
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
1140
+ }
1115
1141
  else if (data.method === '/api/savepluginconfig') {
1116
1142
  if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
1117
1143
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/savepluginconfig' }));
@@ -1220,7 +1246,7 @@ export class Frontend extends EventEmitter {
1220
1246
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: await this.getApiSettings() }));
1221
1247
  }
1222
1248
  else if (data.method === '/api/plugins') {
1223
- const response = this.getBaseRegisteredPlugins();
1249
+ const response = this.getPlugins();
1224
1250
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
1225
1251
  }
1226
1252
  else if (data.method === '/api/devices') {
@@ -61,7 +61,7 @@ import { AnsiLogger, CYAN, YELLOW, db, debugStringify, hk, or, zb } from './logg
61
61
  import { bridgedNode } from './matterbridgeDeviceTypes.js';
62
62
  import { isValidNumber, isValidObject, isValidString } from './utils/export.js';
63
63
  import { MatterbridgeServer, MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeLiftWindowCoveringServer, MatterbridgeLiftTiltWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeSwitchServer, MatterbridgeOperationalStateServer, MatterbridgeDeviceEnergyManagementModeServer, MatterbridgeDeviceEnergyManagementServer, MatterbridgeActivatedCarbonFilterMonitoringServer, MatterbridgeHepaFilterMonitoringServer, } from './matterbridgeBehaviors.js';
64
- import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, createUniqueId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getDefaultOperationalStateClusterServer, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, getDefaultOccupancySensingClusterServer, lowercaseFirstLetter, updateAttribute, getClusterId, getAttributeId, setAttribute, getAttribute, checkNotLatinCharacters, generateUniqueId, subscribeAttribute, invokeBehaviorCommand, triggerEvent, } from './matterbridgeEndpointHelpers.js';
64
+ import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, createUniqueId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getDefaultOperationalStateClusterServer, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, getDefaultOccupancySensingClusterServer, lowercaseFirstLetter, updateAttribute, getClusterId, getAttributeId, setAttribute, getAttribute, checkNotLatinCharacters, generateUniqueId, subscribeAttribute, invokeBehaviorCommand, triggerEvent, featuresFor, } from './matterbridgeEndpointHelpers.js';
65
65
  export class MatterbridgeEndpoint extends Endpoint {
66
66
  static bridgeMode = '';
67
67
  static logLevel = "info";
@@ -1027,9 +1027,11 @@ export class MatterbridgeEndpoint extends Endpoint {
1027
1027
  await this.setAttribute(Switch.Cluster.id, 'currentPosition', 1, log);
1028
1028
  await this.triggerEvent(Switch.Cluster.id, 'initialPress', { newPosition: 1 }, log);
1029
1029
  await this.setAttribute(Switch.Cluster.id, 'currentPosition', 0, log);
1030
- await this.triggerEvent(Switch.Cluster.id, 'shortRelease', { previousPosition: 1 }, log);
1031
- await this.setAttribute(Switch.Cluster.id, 'currentPosition', 0, log);
1032
- await this.triggerEvent(Switch.Cluster.id, 'multiPressComplete', { previousPosition: 1, totalNumberOfPressesCounted: 1 }, log);
1030
+ if (featuresFor(this, 'Switch').momentarySwitchRelease) {
1031
+ await this.triggerEvent(Switch.Cluster.id, 'shortRelease', { previousPosition: 1 }, log);
1032
+ await this.setAttribute(Switch.Cluster.id, 'currentPosition', 0, log);
1033
+ await this.triggerEvent(Switch.Cluster.id, 'multiPressComplete', { previousPosition: 1, totalNumberOfPressesCounted: 1 }, log);
1034
+ }
1033
1035
  }
1034
1036
  if (event === 'Double') {
1035
1037
  log?.info(`${db}Trigger endpoint ${or}${this.id}:${this.number}${db} event ${hk}Switch.DoublePress${db}`);
package/dist/utils/hex.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createPublicKey, createPrivateKey, X509Certificate } from 'node:crypto';
1
2
  export function bufferToHex(buffer) {
2
3
  if (!(buffer instanceof ArrayBuffer || ArrayBuffer.isView(buffer))) {
3
4
  throw new TypeError('Expected input to be an ArrayBuffer or ArrayBufferView');
@@ -25,3 +26,93 @@ export function hexToBuffer(hex) {
25
26
  }
26
27
  return result;
27
28
  }
29
+ export function pemToBuffer(pem, validate = false) {
30
+ if (typeof pem !== 'string') {
31
+ throw new TypeError('Expected a string for PEM input');
32
+ }
33
+ const cleaned = pem.trim();
34
+ if (!cleaned.includes('-----BEGIN') || !cleaned.includes('-----END')) {
35
+ throw new Error('Invalid PEM format: missing BEGIN/END markers');
36
+ }
37
+ const lines = cleaned.split('\n');
38
+ const base64Lines = [];
39
+ let inContent = false;
40
+ for (const line of lines) {
41
+ const trimmedLine = line.trim();
42
+ if (trimmedLine.startsWith('-----BEGIN')) {
43
+ inContent = true;
44
+ continue;
45
+ }
46
+ if (trimmedLine.startsWith('-----END')) {
47
+ inContent = false;
48
+ break;
49
+ }
50
+ if (inContent && trimmedLine.length > 0) {
51
+ base64Lines.push(trimmedLine);
52
+ }
53
+ }
54
+ if (base64Lines.length === 0) {
55
+ throw new Error('Invalid PEM format: no content found between BEGIN/END markers');
56
+ }
57
+ const base64String = base64Lines.join('');
58
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64String)) {
59
+ throw new Error('Invalid PEM format: contains invalid base64 characters');
60
+ }
61
+ try {
62
+ const buffer = Buffer.from(base64String, 'base64');
63
+ const result = new Uint8Array(buffer);
64
+ if (validate) {
65
+ try {
66
+ const pemType = cleaned.match(/-----BEGIN\s+([^-]+)-----/)?.[1]?.trim();
67
+ if (pemType?.includes('CERTIFICATE')) {
68
+ const cert = new X509Certificate(pem);
69
+ if (cert.validFrom && cert.validTo) {
70
+ const now = Date.now();
71
+ const from = Date.parse(cert.validFrom);
72
+ const to = Date.parse(cert.validTo);
73
+ if (now < from || now > to) {
74
+ throw new Error('Certificate is not currently valid');
75
+ }
76
+ }
77
+ if (!cert.subject || !cert.issuer) {
78
+ throw new Error('Certificate missing subject or issuer');
79
+ }
80
+ }
81
+ else if (pemType?.includes('PRIVATE KEY')) {
82
+ createPrivateKey({ key: pem, format: 'pem' });
83
+ }
84
+ else if (pemType?.includes('PUBLIC KEY')) {
85
+ createPublicKey({ key: pem, format: 'pem' });
86
+ }
87
+ }
88
+ catch (validationError) {
89
+ throw new Error(`PEM validation failed: ${validationError instanceof Error ? validationError.message : String(validationError)}`);
90
+ }
91
+ }
92
+ return result;
93
+ }
94
+ catch (error) {
95
+ throw new Error(`Failed to decode base64 content: ${error instanceof Error ? error.message : String(error)}`);
96
+ }
97
+ }
98
+ export function extractPrivateKeyRaw(pemPrivateKey) {
99
+ if (typeof pemPrivateKey !== 'string') {
100
+ throw new TypeError('Expected a string for PEM private key input');
101
+ }
102
+ const keyBlock = /-----BEGIN (?:EC )?PRIVATE KEY-----[^-]+-----END (?:EC )?PRIVATE KEY-----/s.exec(pemPrivateKey);
103
+ if (!keyBlock) {
104
+ throw new Error('No EC PRIVATE KEY block found in the supplied PEM');
105
+ }
106
+ try {
107
+ const privateKey = createPrivateKey(keyBlock[0]);
108
+ const pkcs8Der = privateKey.export({ format: 'der', type: 'pkcs8' });
109
+ if (pkcs8Der.length < 32) {
110
+ throw new Error('Invalid private key: DER data too short');
111
+ }
112
+ const rawPrivateKey = pkcs8Der.subarray(pkcs8Der.length - 32);
113
+ return new Uint8Array(rawPrivateKey);
114
+ }
115
+ catch (error) {
116
+ throw new Error(`Failed to extract private key: ${error instanceof Error ? error.message : String(error)}`);
117
+ }
118
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "files": {
3
3
  "main.css": "./static/css/main.944b63c3.css",
4
- "main.js": "./static/js/main.1d25e0d8.js",
4
+ "main.js": "./static/js/main.bb47a7dc.js",
5
5
  "static/js/453.d855a71b.chunk.js": "./static/js/453.d855a71b.chunk.js",
6
6
  "static/media/roboto-latin-700-normal.woff2": "./static/media/roboto-latin-700-normal.c4d6cab43bec89049809.woff2",
7
7
  "static/media/roboto-latin-500-normal.woff2": "./static/media/roboto-latin-500-normal.599f66a60bdf974e578e.woff2",
@@ -77,11 +77,11 @@
77
77
  "static/media/roboto-greek-ext-300-normal.woff": "./static/media/roboto-greek-ext-300-normal.60729cafbded24073dfb.woff",
78
78
  "index.html": "./index.html",
79
79
  "main.944b63c3.css.map": "./static/css/main.944b63c3.css.map",
80
- "main.1d25e0d8.js.map": "./static/js/main.1d25e0d8.js.map",
80
+ "main.bb47a7dc.js.map": "./static/js/main.bb47a7dc.js.map",
81
81
  "453.d855a71b.chunk.js.map": "./static/js/453.d855a71b.chunk.js.map"
82
82
  },
83
83
  "entrypoints": [
84
84
  "static/css/main.944b63c3.css",
85
- "static/js/main.1d25e0d8.js"
85
+ "static/js/main.bb47a7dc.js"
86
86
  ]
87
87
  }
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="./"><link rel="icon" href="./matterbridge 32x32.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>Matterbridge</title><link rel="manifest" href="./manifest.json"/><script defer="defer" src="./static/js/main.1d25e0d8.js"></script><link href="./static/css/main.944b63c3.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="./"><link rel="icon" href="./matterbridge 32x32.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>Matterbridge</title><link rel="manifest" href="./manifest.json"/><script defer="defer" src="./static/js/main.bb47a7dc.js"></script><link href="./static/css/main.944b63c3.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>