matterbridge 3.4.0-dev-20251120-5724e43 → 3.4.0-dev-20251123-62db0d7

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
@@ -46,12 +46,13 @@ Removed the following long deprecated elements:
46
46
  - [doorLock]: Added autoRelockTime attribute with default 0.
47
47
  - [DevContainer]: Added instructions for testing a plugin with a paired controller when using DevContainer.
48
48
  - [platform]: Made internal use methods and properties hard-private.
49
- - [platform]: Added size(), getDeviceByName(), getDeviceByUniqueId(), getDeviceBySerialNumber(), getDeviceById(), getDeviceByOriginalId(), getDeviceByNumber() and hasDeviceUniqueId().
50
- - [platform]: Added isReady, isLoaded, isStarted and isConfigured.
49
+ - [platform]: Added size(), getDeviceByName(), getDeviceByUniqueId(), getDeviceBySerialNumber(), getDeviceById(), getDeviceByOriginalId(), getDeviceByNumber() and hasDeviceUniqueId() methods.
50
+ - [platform]: Added isReady, isLoaded, isStarted and isConfigured properties.
51
51
 
52
52
  ### Changed
53
53
 
54
54
  - [package]: Updated dependencies.
55
+ - [platform]: Bumped MatterbridgePlatform v.1.4.0.
55
56
  - [deviceManager]: Bumped DeviceManager v.1.1.1.
56
57
  - [pluginManager]: Bumped PluginManager v.1.3.1.
57
58
  - [broadcastServer]: Bumped BroadcastServer v.1.0.3.
@@ -88,6 +88,10 @@ export class BroadcastServer extends EventEmitter {
88
88
  this.log.debug(`Fetch response: ${debugStringify(msg)}`);
89
89
  resolve(msg);
90
90
  }
91
+ else if (this.isWorkerResponse(msg, message.type) && msg.id !== message.id) {
92
+ if (this.verbose)
93
+ this.log.debug(`Fetch received unrelated response: ${debugStringify(msg)}`);
94
+ }
91
95
  };
92
96
  this.on('broadcast_message', responseHandler);
93
97
  this.request(message);
@@ -184,7 +184,7 @@ export async function createMatterbridgeEnvironment(name) {
184
184
  matterbridge = await Matterbridge.loadInstance(false);
185
185
  expect(matterbridge).toBeDefined();
186
186
  expect(matterbridge).toBeInstanceOf(Matterbridge);
187
- matterbridge.matterbridgeVersion = '3.3.0';
187
+ matterbridge.matterbridgeVersion = '3.4.0';
188
188
  matterbridge.bridgeMode = 'bridge';
189
189
  matterbridge.rootDirectory = path.join('jest', name);
190
190
  matterbridge.homeDirectory = path.join('jest', name);
@@ -310,6 +310,13 @@ export function createTestEnvironment(name) {
310
310
  new MdnsService(environment);
311
311
  return environment;
312
312
  }
313
+ export async function destroyTestEnvironment() {
314
+ const mdns = environment.get(MdnsService);
315
+ if (mdns && typeof mdns[Symbol.asyncDispose] === 'function')
316
+ await mdns[Symbol.asyncDispose]();
317
+ if (mdns && typeof mdns.close === 'function')
318
+ await mdns.close();
319
+ }
313
320
  export async function flushAsync(ticks = 3, microTurns = 10, pause = 250) {
314
321
  for (let i = 0; i < ticks; i++)
315
322
  await new Promise((resolve) => setImmediate(resolve));
@@ -18,7 +18,7 @@ export class MatterbridgePlatform {
18
18
  name = '';
19
19
  type = 'AnyPlatform';
20
20
  version = '1.0.0';
21
- storage;
21
+ #storage;
22
22
  context;
23
23
  isReady = false;
24
24
  isLoaded = false;
@@ -48,7 +48,7 @@ export class MatterbridgePlatform {
48
48
  throw new Error('Platform: the plugin name is missing or invalid.');
49
49
  }
50
50
  this.log.debug(`Creating storage for plugin ${this.config.name} in ${path.join(this.matterbridge.matterbridgeDirectory, this.config.name)}`);
51
- this.storage = new NodeStorageManager({
51
+ this.#storage = new NodeStorageManager({
52
52
  dir: path.join(this.matterbridge.matterbridgeDirectory, this.config.name),
53
53
  writeQueue: false,
54
54
  expiredInterval: undefined,
@@ -56,13 +56,13 @@ export class MatterbridgePlatform {
56
56
  forgiveParseErrors: true,
57
57
  });
58
58
  this.log.debug(`Creating context for plugin ${this.config.name}`);
59
- this.#contextReady = this.storage.createStorage('context').then((context) => {
59
+ this.#contextReady = this.#storage.createStorage('context').then((context) => {
60
60
  this.context = context;
61
61
  this.log.debug(`Created context for plugin ${this.config.name}`);
62
62
  return;
63
63
  });
64
64
  this.log.debug(`Loading selectDevice for plugin ${this.config.name}`);
65
- this.#selectDeviceContextReady = this.storage.createStorage('selectDevice').then(async (context) => {
65
+ this.#selectDeviceContextReady = this.#storage.createStorage('selectDevice').then(async (context) => {
66
66
  const selectDevice = await context.get('selectDevice', []);
67
67
  for (const device of selectDevice)
68
68
  this.#selectDevices.set(device.serial, device);
@@ -70,7 +70,7 @@ export class MatterbridgePlatform {
70
70
  return;
71
71
  });
72
72
  this.log.debug(`Loading selectEntity for plugin ${this.config.name}`);
73
- this.#selectEntityContextReady = this.storage.createStorage('selectEntity').then(async (context) => {
73
+ this.#selectEntityContextReady = this.#storage.createStorage('selectEntity').then(async (context) => {
74
74
  const selectEntity = await context.get('selectEntity', []);
75
75
  for (const entity of selectEntity)
76
76
  this.#selectEntities.set(entity.name, entity);
@@ -91,7 +91,7 @@ export class MatterbridgePlatform {
91
91
  this.#registeredEndpoints.clear();
92
92
  await this.context?.close();
93
93
  this.context = undefined;
94
- await this.storage?.close();
94
+ await this.#storage?.close();
95
95
  this.#server.close();
96
96
  if (this.#verbose)
97
97
  this.log.debug(`Destroyed MatterbridgePlatform for plugin ${this.config.name}`);
@@ -122,25 +122,13 @@ export class MatterbridgePlatform {
122
122
  this.log.debug(`The plugin ${CYAN}${config.name}${db} doesn't override onConfigChanged. Received new config.`);
123
123
  }
124
124
  saveConfig(config) {
125
- const plugin = this.matterbridge.plugins.get(this.name);
126
- if (!plugin) {
127
- throw new Error(`Plugin ${this.name} not found`);
128
- }
129
- this.matterbridge.plugins.saveConfigFromJson(plugin, config);
125
+ this.#server.request({ type: 'plugins_saveconfigfromjson', src: 'platform', dst: 'plugins', params: { name: this.name, config } });
130
126
  }
131
- getSchema() {
132
- const plugin = this.matterbridge.plugins.get(this.name);
133
- if (!plugin || !isValidObject(plugin.schemaJson)) {
134
- throw new Error(`Plugin ${this.name} not found`);
135
- }
136
- return plugin.schemaJson;
127
+ async getSchema() {
128
+ return (await this.#server.fetch({ type: 'plugins_getschema', src: 'platform', dst: 'plugins', params: { name: this.name } })).response.schema;
137
129
  }
138
130
  setSchema(schema) {
139
- const plugin = this.matterbridge.plugins.get(this.name);
140
- if (!plugin) {
141
- throw new Error(`Plugin ${this.name} not found`);
142
- }
143
- plugin.schemaJson = schema;
131
+ this.#server.request({ type: 'plugins_setschema', src: 'platform', dst: 'plugins', params: { name: this.name, schema } });
144
132
  }
145
133
  wssSendRestartRequired(snackbar = true, fixed = false) {
146
134
  this.#server.request({ type: 'frontend_restartrequired', src: 'platform', dst: 'frontend', params: { snackbar, fixed } });
@@ -183,7 +171,7 @@ export class MatterbridgePlatform {
183
171
  if (this.matterbridge.bridgeMode === 'bridge') {
184
172
  aggregator = this.matterbridge.aggregatorNode;
185
173
  }
186
- else if (this.matterbridge.bridgeMode === 'childbridge') {
174
+ else if (this.matterbridge.bridgeMode === 'childbridge' && this.type === 'DynamicPlatform') {
187
175
  aggregator = this.matterbridge.plugins.get(this.name)?.aggregatorNode;
188
176
  }
189
177
  if (aggregator) {
@@ -197,6 +185,7 @@ export class MatterbridgePlatform {
197
185
  return true;
198
186
  }
199
187
  }
188
+ this.log.warn(`Virtual device ${name} not created. Virtual devices are only supported in bridge mode and childbridge mode with a DynamicPlatform.`);
200
189
  return false;
201
190
  }
202
191
  async registerDevice(device) {
@@ -250,13 +239,13 @@ export class MatterbridgePlatform {
250
239
  this.#registeredEndpoints.clear();
251
240
  }
252
241
  async saveSelects() {
253
- if (this.storage) {
242
+ if (this.#storage) {
254
243
  this.log.debug(`Saving ${this.#selectDevices.size} selectDevice...`);
255
- const selectDevice = await this.storage.createStorage('selectDevice');
244
+ const selectDevice = await this.#storage.createStorage('selectDevice');
256
245
  await selectDevice.set('selectDevice', Array.from(this.#selectDevices.values()));
257
246
  await selectDevice.close();
258
247
  this.log.debug(`Saving ${this.#selectEntities.size} selectEntity...`);
259
- const selectEntity = await this.storage.createStorage('selectEntity');
248
+ const selectEntity = await this.#storage.createStorage('selectEntity');
260
249
  await selectEntity.set('selectEntity', Array.from(this.#selectEntities.values()));
261
250
  await selectEntity.close();
262
251
  }
@@ -393,11 +382,13 @@ export class MatterbridgePlatform {
393
382
  }
394
383
  return true;
395
384
  }
385
+ async clearEndpointNumbers() {
386
+ const context = await this.#storage.createStorage('endpointNumbers');
387
+ await context.set('endpointMap', []);
388
+ }
396
389
  async checkEndpointNumbers() {
397
- if (!this.storage)
398
- return -1;
399
390
  this.log.debug('Checking endpoint numbers...');
400
- const context = await this.storage.createStorage('endpointNumbers');
391
+ const context = await this.#storage.createStorage('endpointNumbers');
401
392
  const separator = '|.|';
402
393
  const endpointMap = new Map(await context.get('endpointMap', []));
403
394
  for (const device of this.getDevices()) {
@@ -51,6 +51,7 @@ export class PluginManager extends EventEmitter {
51
51
  this.server.respond({ ...msg, response: { plugin: this.toApiPlugin(plugin) } });
52
52
  }
53
53
  else {
54
+ this.log.debug(`***Plugin ${plg}${msg.params.name}${db} not found in plugins_get`);
54
55
  this.server.respond({ ...msg, response: { plugin: undefined } });
55
56
  }
56
57
  }
@@ -158,6 +159,41 @@ export class PluginManager extends EventEmitter {
158
159
  }
159
160
  }
160
161
  break;
162
+ case 'plugins_getschema':
163
+ {
164
+ const plugin = this.get(msg.params.name);
165
+ if (plugin) {
166
+ this.server.respond({ ...msg, response: { schema: plugin.schemaJson } });
167
+ }
168
+ else {
169
+ this.server.respond({ ...msg, response: { schema: undefined } });
170
+ }
171
+ }
172
+ break;
173
+ case 'plugins_setschema':
174
+ {
175
+ const plugin = this.get(msg.params.name);
176
+ if (plugin) {
177
+ plugin.schemaJson = msg.params.schema;
178
+ this.server.respond({ ...msg, response: { success: true } });
179
+ }
180
+ else {
181
+ this.server.respond({ ...msg, response: { success: false } });
182
+ }
183
+ }
184
+ break;
185
+ case 'plugins_saveconfigfromjson':
186
+ {
187
+ const plugin = this.get(msg.params.name);
188
+ if (plugin) {
189
+ this.saveConfigFromJson(plugin, msg.params.config, msg.params.restartRequired);
190
+ this.server.respond({ ...msg, response: { success: true } });
191
+ }
192
+ else {
193
+ this.server.respond({ ...msg, response: { success: false } });
194
+ }
195
+ }
196
+ break;
161
197
  default:
162
198
  if (this.verbose)
163
199
  this.log.debug(`Unknown broadcast message ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.4.0-dev-20251120-5724e43",
3
+ "version": "3.4.0-dev-20251123-62db0d7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge",
9
- "version": "3.4.0-dev-20251120-5724e43",
9
+ "version": "3.4.0-dev-20251123-62db0d7",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@matter/main": "0.15.6",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.4.0-dev-20251120-5724e43",
3
+ "version": "3.4.0-dev-20251123-62db0d7",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",
@@ -11,13 +11,14 @@ import https from 'node:https';
11
11
  * - ZCL_OUT: output path for zcl.json (default: chip/zcl.json)
12
12
  * - ZCL_BRANCH: connectedhomeip branch to fetch from (default: v1.4-branch)
13
13
  */
14
- const OUT_PATH = process.env.ZCL_OUT || 'chip/zcl.json';
15
14
  // Single branch strategy from online only
16
- const BRANCH = process.env.ZCL_BRANCH || 'v1.4-branch';
17
- const ZCL_BASE = `https://raw.githubusercontent.com/project-chip/connectedhomeip/${BRANCH}/src/app/zap-templates/zcl/`;
18
- const ZCL_JSON_URL = ZCL_BASE + 'zcl.json';
19
- const XML_BASE = ZCL_BASE + 'data-model/chip/';
20
- const MANUFACTURERS_URL = ZCL_BASE + 'data-model/manufacturers.xml';
15
+ const BRANCH = process.env.ZCL_BRANCH || 'v1.4.2-branch';
16
+ const OUT_ZCL_PATH = process.env.ZCL_OUT || `chip/${BRANCH}/zcl.json`;
17
+ const OUT_MANUFACTURERS_PATH = process.env.MANUFACTURERS_OUT || `chip/${BRANCH}/manufacturers.xml`;
18
+ const ZCL_BASE_URL = `https://raw.githubusercontent.com/project-chip/connectedhomeip/${BRANCH}/src/app/zap-templates/zcl/`;
19
+ const ZCL_JSON_URL = ZCL_BASE_URL + 'zcl.json';
20
+ const MANUFACTURERS_URL = ZCL_BASE_URL + 'data-model/manufacturers.xml';
21
+ const XML_BASE_URL = ZCL_BASE_URL + 'data-model/chip/';
21
22
 
22
23
  function fetchUrl(url) {
23
24
  return new Promise((resolve, reject) => {
@@ -37,7 +38,7 @@ function fetchUrl(url) {
37
38
  }
38
39
 
39
40
  async function main() {
40
- await mkdir(dirname(OUT_PATH), { recursive: true });
41
+ await mkdir(dirname(OUT_ZCL_PATH), { recursive: true });
41
42
  // Always fetch online from the specified branch
42
43
  const data = await fetchUrl(ZCL_JSON_URL);
43
44
 
@@ -52,15 +53,14 @@ async function main() {
52
53
  process.stderr.write('Warning: zcl.json does not contain expected ZAP ZCL properties (xmlFile). Saving anyway for manual inspection.\n');
53
54
  }
54
55
 
55
- await writeFile(OUT_PATH, JSON.stringify(parsed, null, 2));
56
- process.stderr.write(`Saved ${OUT_PATH}.\n`);
56
+ await writeFile(OUT_ZCL_PATH, JSON.stringify(parsed, null, 2));
57
+ process.stderr.write(`Saved ${OUT_ZCL_PATH}. Branch=${BRANCH}\n`);
57
58
 
58
59
  // Also fetch manufacturers.xml to chip/manufacturers.xml from the same branch
59
60
  try {
60
- const outManu = 'chip/manufacturers.xml';
61
61
  const manuContent = await fetchUrl(MANUFACTURERS_URL);
62
- await writeFile(outManu, manuContent);
63
- process.stderr.write('Saved chip/manufacturers.xml.\n');
62
+ await writeFile(OUT_MANUFACTURERS_PATH, manuContent);
63
+ process.stderr.write(`Saved ${OUT_MANUFACTURERS_PATH}. Branch=${BRANCH}\n`);
64
64
  } catch (e) {
65
65
  process.stderr.write(`Warning: failed to fetch manufacturers.xml (${e.message}).\n`);
66
66
  }
@@ -70,14 +70,14 @@ async function main() {
70
70
  if (xmlFiles.length === 0) {
71
71
  process.stderr.write('No xmlFile entries found; skipping XML fetch.\n');
72
72
  } else {
73
- const outXmlBase = 'chip/xml';
73
+ const outXmlBase = `chip/${BRANCH}/xml`;
74
74
  await mkdir(outXmlBase, { recursive: true });
75
75
  let ok = 0;
76
76
  let fail = 0;
77
77
  for (const fileName of xmlFiles) {
78
78
  const relative = fileName.trim();
79
79
  try {
80
- const url = XML_BASE + relative;
80
+ const url = XML_BASE_URL + relative;
81
81
  const content = await fetchUrl(url);
82
82
  const outPath = pathJoin(outXmlBase, relative.replaceAll('/', pathSep));
83
83
  await mkdir(dirname(outPath), { recursive: true });