matterbridge 3.0.5-dev-20250526-422f029 → 3.0.5-dev-20250528-9314890

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,6 +12,9 @@ If you like this project and find it useful, please consider giving it a star on
12
12
 
13
13
  ### Added
14
14
 
15
+ - [cli]: Added takeHeapSnapshot() and triggerGarbageCollection().
16
+ - [LaundryWasher]: Add LaundryWasher class and Jest test.
17
+
15
18
  ### Changed
16
19
 
17
20
  ### Fixed
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { inspect } from 'node:util';
7
7
  export const cliEmitter = new EventEmitter();
8
8
  export let instance;
9
9
  let session;
10
+ let snapshotInterval;
10
11
  let memoryCheckInterval;
11
12
  let prevCpus;
12
13
  export let lastCpuUsage = 0;
@@ -99,17 +100,26 @@ async function stopCpuMemoryCheck() {
99
100
  }
100
101
  async function startInspector() {
101
102
  const { Session } = await import('node:inspector');
102
- log.debug(`Starting heap sampling...`);
103
+ const { mkdirSync } = await import('node:fs');
104
+ log.debug(`***Starting heap sampling...`);
105
+ mkdirSync('heap_profile', { recursive: true });
103
106
  try {
104
107
  session = new Session();
105
108
  session.connect();
106
109
  await new Promise((resolve, reject) => {
107
110
  session?.post('HeapProfiler.startSampling', (err) => (err ? reject(err) : resolve()));
108
111
  });
109
- log.debug(`Started heap sampling`);
112
+ log.debug(`***Started heap sampling`);
113
+ const interval = getIntParameter('snapshotinterval');
114
+ if (interval && interval >= 30000) {
115
+ log.debug(`***Started heap snapshot interval of ${CYAN}${interval}${db} ms`);
116
+ snapshotInterval = setInterval(async () => {
117
+ await takeHeapSnapshot();
118
+ }, interval);
119
+ }
110
120
  }
111
121
  catch (err) {
112
- log.error(`Failed to start heap sampling: ${err instanceof Error ? err.message : err}`);
122
+ log.error(`***Failed to start heap sampling: ${err instanceof Error ? err.message : err}`);
113
123
  session?.disconnect();
114
124
  session = undefined;
115
125
  return;
@@ -117,9 +127,15 @@ async function startInspector() {
117
127
  }
118
128
  async function stopInspector() {
119
129
  const { writeFileSync } = await import('node:fs');
120
- log.debug(`Stopping heap sampling...`);
130
+ const path = await import('node:path');
131
+ log.debug(`***Stopping heap sampling...`);
132
+ if (snapshotInterval) {
133
+ log.debug(`***Clearing heap snapshot interval...`);
134
+ clearInterval(snapshotInterval);
135
+ await takeHeapSnapshot();
136
+ }
121
137
  if (!session) {
122
- log.error('No active inspector session.');
138
+ log.error('***No active inspector session.');
123
139
  return;
124
140
  }
125
141
  try {
@@ -127,16 +143,56 @@ async function stopInspector() {
127
143
  session?.post('HeapProfiler.stopSampling', (err, result) => (err ? reject(err) : resolve(result)));
128
144
  });
129
145
  const profile = JSON.stringify(result.profile);
130
- writeFileSync('Heap-sampling-profile.heapprofile', profile);
131
- log.debug('Heap sampling profile saved to Heap-sampling-profile.heapprofile');
146
+ const filename = path.join('heap_profile', `Heap-profile-${new Date().toISOString().replace(/[:]/g, '-')}.heapprofile`);
147
+ writeFileSync(filename, profile);
148
+ log.debug(`***Heap sampling profile saved to ${CYAN}${filename}${db}`);
132
149
  }
133
150
  catch (err) {
134
- log.error(`Failed to stop heap sampling: ${err instanceof Error ? err.message : err}`);
151
+ log.error(`***Failed to stop heap sampling: ${err instanceof Error ? err.message : err}`);
135
152
  }
136
153
  finally {
137
154
  session.disconnect();
138
155
  session = undefined;
139
- log.debug(`Stopped heap sampling`);
156
+ log.debug(`***Stopped heap sampling`);
157
+ }
158
+ }
159
+ async function takeHeapSnapshot() {
160
+ const { writeFileSync } = await import('node:fs');
161
+ const path = await import('node:path');
162
+ const filename = path.join('heap_profile', `Heap-snapshot-${new Date().toISOString().replace(/[:]/g, '-')}.heapsnapshot`);
163
+ if (!session) {
164
+ log.error('No active inspector session.');
165
+ return;
166
+ }
167
+ const chunks = [];
168
+ const chunksListener = (notification) => {
169
+ chunks.push(Buffer.from(notification.params.chunk));
170
+ };
171
+ session.on('HeapProfiler.addHeapSnapshotChunk', chunksListener);
172
+ await new Promise((resolve) => {
173
+ session?.post('HeapProfiler.takeHeapSnapshot', (err) => {
174
+ if (!err) {
175
+ session?.off('HeapProfiler.addHeapSnapshotChunk', chunksListener);
176
+ writeFileSync(filename, Buffer.concat(chunks));
177
+ log.debug(`***Heap sampling snapshot saved to ${CYAN}${filename}${db}`);
178
+ triggerGarbageCollection();
179
+ resolve();
180
+ }
181
+ else {
182
+ session?.off('HeapProfiler.addHeapSnapshotChunk', chunksListener);
183
+ log.error(`***Failed to take heap snapshot: ${err instanceof Error ? err.message : err}`);
184
+ resolve();
185
+ }
186
+ });
187
+ });
188
+ }
189
+ function triggerGarbageCollection() {
190
+ if (typeof global.gc === 'function') {
191
+ global.gc();
192
+ log.debug('***Manual garbage collection triggered via global.gc().');
193
+ }
194
+ else {
195
+ log.debug('***Garbage collection is not exposed. Start Node.js with --expose-gc to enable manual GC.');
140
196
  }
141
197
  }
142
198
  function registerHandlers() {
@@ -172,11 +228,11 @@ async function update() {
172
228
  }
173
229
  async function start() {
174
230
  log.debug('Received start memory check event');
175
- startCpuMemoryCheck();
231
+ await startCpuMemoryCheck();
176
232
  }
177
233
  async function stop() {
178
234
  log.debug('Received stop memory check event');
179
- stopCpuMemoryCheck();
235
+ await stopCpuMemoryCheck();
180
236
  }
181
237
  async function main() {
182
238
  log.debug(`Cli main() started`);
@@ -1,4 +1,3 @@
1
- import { OperationalState } from '@matter/main/clusters/operational-state';
2
1
  import { LaundryWasherControls } from '@matter/main/clusters/laundry-washer-controls';
3
2
  import { LaundryWasherMode } from '@matter/main/clusters/laundry-washer-mode';
4
3
  import { TemperatureControl } from '@matter/main/clusters/temperature-control';
@@ -10,25 +9,19 @@ import { laundryWasher } from './matterbridgeDeviceTypes.js';
10
9
  import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
11
10
  import { MatterbridgeOnOffServer, MatterbridgeServer } from './matterbridgeBehaviors.js';
12
11
  export class LaundryWasher extends MatterbridgeEndpoint {
13
- constructor(name, serial) {
12
+ constructor(name, serial, currentMode, supportedModes, spinSpeedCurrent, spinSpeeds, numberOfRinses, supportedRinses, selectedTemperatureLevel, supportedTemperatureLevels, temperatureSetpoint, minTemperature, maxTemperature, step, operationalState) {
14
13
  super(laundryWasher, { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
15
14
  this.createDefaultIdentifyClusterServer();
16
15
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Laundry Washer');
17
16
  this.createDefaultPowerSourceWiredClusterServer();
18
- this.createDeadFrontOnOffClusterServer();
19
- this.createLevelTemperatureControlClusterServer(3, ['Cold', '30°', '40°', '60°', '80°']);
20
- this.createDefaultLaundryWasherControlsClusterServer();
21
- this.createDefaultLaundryWasherModeClusterServer();
22
- this.createDefaultOperationalStateClusterServer(OperationalState.OperationalStateEnum.Stopped);
23
- }
24
- createDefaultLaundryWasherControlsClusterServer(spinSpeedCurrent = 3, spinSpeeds = ['400', '800', '1200', '1600'], numberOfRinses = LaundryWasherControls.NumberOfRinses.Normal, supportedRinses = [LaundryWasherControls.NumberOfRinses.None, LaundryWasherControls.NumberOfRinses.Normal, LaundryWasherControls.NumberOfRinses.Max, LaundryWasherControls.NumberOfRinses.Extra]) {
25
- this.behaviors.require(LaundryWasherControlsServer.with(LaundryWasherControls.Feature.Spin, LaundryWasherControls.Feature.Rinse), {
26
- spinSpeeds,
27
- spinSpeedCurrent,
28
- supportedRinses,
29
- numberOfRinses,
30
- });
31
- return this;
17
+ this.createDeadFrontOnOffClusterServer(true);
18
+ this.createDefaultLaundryWasherModeClusterServer(currentMode, supportedModes);
19
+ this.createDefaultLaundryWasherControlsClusterServer(spinSpeedCurrent, spinSpeeds, numberOfRinses, supportedRinses);
20
+ if (temperatureSetpoint)
21
+ this.createNumberTemperatureControlClusterServer(temperatureSetpoint, minTemperature, maxTemperature, step);
22
+ else
23
+ this.createLevelTemperatureControlClusterServer(selectedTemperatureLevel, supportedTemperatureLevels);
24
+ this.createDefaultOperationalStateClusterServer(operationalState);
32
25
  }
33
26
  createDefaultLaundryWasherModeClusterServer(currentMode = 2, supportedModes = [
34
27
  { label: 'Delicate', mode: 1, modeTags: [{ value: LaundryWasherMode.ModeTag.Delicate }] },
@@ -42,7 +35,16 @@ export class LaundryWasher extends MatterbridgeEndpoint {
42
35
  });
43
36
  return this;
44
37
  }
45
- createLevelTemperatureControlClusterServer(selectedTemperatureLevel = 1, supportedTemperatureLevels = ['Cold', 'Warm', 'Hot']) {
38
+ createDefaultLaundryWasherControlsClusterServer(spinSpeedCurrent = 2, spinSpeeds = ['400', '800', '1200', '1600'], numberOfRinses = LaundryWasherControls.NumberOfRinses.Normal, supportedRinses = [LaundryWasherControls.NumberOfRinses.None, LaundryWasherControls.NumberOfRinses.Normal, LaundryWasherControls.NumberOfRinses.Max, LaundryWasherControls.NumberOfRinses.Extra]) {
39
+ this.behaviors.require(LaundryWasherControlsServer.with(LaundryWasherControls.Feature.Spin, LaundryWasherControls.Feature.Rinse), {
40
+ spinSpeeds,
41
+ spinSpeedCurrent,
42
+ supportedRinses,
43
+ numberOfRinses,
44
+ });
45
+ return this;
46
+ }
47
+ createLevelTemperatureControlClusterServer(selectedTemperatureLevel = 1, supportedTemperatureLevels = ['Cold', 'Warm', 'Hot', '30°', '40°', '60°', '80°']) {
46
48
  this.behaviors.require(MatterbridgeLevelTemperatureControlServer.with(TemperatureControl.Feature.TemperatureLevel), {
47
49
  selectedTemperatureLevel,
48
50
  supportedTemperatureLevels,
@@ -59,11 +61,11 @@ export class LaundryWasher extends MatterbridgeEndpoint {
59
61
  return this;
60
62
  }
61
63
  }
62
- class MatterbridgeLevelTemperatureControlServer extends TemperatureControlServer.with(TemperatureControl.Feature.TemperatureLevel) {
64
+ export class MatterbridgeLevelTemperatureControlServer extends TemperatureControlServer.with(TemperatureControl.Feature.TemperatureLevel) {
63
65
  initialize() {
64
66
  if (this.state.supportedTemperatureLevels.length >= 2) {
65
67
  const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
66
- device.log.info('MatterbridgeLevelTemperatureControlServer initialized');
68
+ device.log.info(`MatterbridgeLevelTemperatureControlServer initialized with selectedTemperatureLevel ${this.state.selectedTemperatureLevel} and supportedTemperatureLevels: ${this.state.supportedTemperatureLevels.join(', ')}`);
67
69
  }
68
70
  }
69
71
  setTemperature(request) {
@@ -77,10 +79,10 @@ class MatterbridgeLevelTemperatureControlServer extends TemperatureControlServer
77
79
  }
78
80
  }
79
81
  }
80
- class MatterbridgeNumberTemperatureControlServer extends TemperatureControlServer.with(TemperatureControl.Feature.TemperatureNumber) {
82
+ export class MatterbridgeNumberTemperatureControlServer extends TemperatureControlServer.with(TemperatureControl.Feature.TemperatureNumber, TemperatureControl.Feature.TemperatureStep) {
81
83
  initialize() {
82
84
  const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
83
- device.log.info('MatterbridgeNumberTemperatureControlServer initialized');
85
+ device.log.info(`MatterbridgeNumberTemperatureControlServer initialized with temperatureSetpoint ${this.state.temperatureSetpoint} minTemperature ${this.state.minTemperature} maxTemperature ${this.state.maxTemperature} step ${this.state.step}`);
84
86
  }
85
87
  setTemperature(request) {
86
88
  const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
@@ -93,10 +95,10 @@ class MatterbridgeNumberTemperatureControlServer extends TemperatureControlServe
93
95
  }
94
96
  }
95
97
  }
96
- class MatterbridgeLaundryWasherModeServer extends LaundryWasherModeServer {
98
+ export class MatterbridgeLaundryWasherModeServer extends LaundryWasherModeServer {
97
99
  initialize() {
98
100
  const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
99
- device.log.info(`LaundryWasherModeServer initialized: currentMode is ${this.state.currentMode}`);
101
+ device.log.info(`MatterbridgeLaundryWasherModeServer initialized: currentMode is ${this.state.currentMode}`);
100
102
  this.reactTo(this.agent.get(MatterbridgeOnOffServer).events.onOff$Changed, this.handleOnOffChange);
101
103
  }
102
104
  handleOnOffChange(onOff) {
@@ -110,12 +112,13 @@ class MatterbridgeLaundryWasherModeServer extends LaundryWasherModeServer {
110
112
  const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
111
113
  const supportedMode = this.state.supportedModes.find((supportedMode) => supportedMode.mode === request.newMode);
112
114
  if (supportedMode) {
113
- device.log.info(`LaundryWasherModeServer: changeToMode called with mode ${supportedMode.mode} = ${supportedMode.label}`);
115
+ device.log.info(`MatterbridgeLaundryWasherModeServer: changeToMode called with mode ${supportedMode.mode} => ${supportedMode.label}`);
116
+ device.changeToMode({ newMode: request.newMode });
114
117
  this.state.currentMode = request.newMode;
115
118
  return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Success' };
116
119
  }
117
120
  else {
118
- device.log.error(`LaundryWasherModeServer: changeToMode called with invalid mode ${request.newMode}`);
121
+ device.log.error(`MatterbridgeLaundryWasherModeServer: changeToMode called with invalid mode ${request.newMode}`);
119
122
  return { status: ModeBase.ModeChangeStatus.InvalidInMode, statusText: 'Invalid mode' };
120
123
  }
121
124
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.0.5-dev-20250526-422f029",
3
+ "version": "3.0.5-dev-20250528-9314890",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge",
9
- "version": "3.0.5-dev-20250526-422f029",
9
+ "version": "3.0.5-dev-20250528-9314890",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@matter/main": "0.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.0.5-dev-20250526-422f029",
3
+ "version": "3.0.5-dev-20250528-9314890",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",