matterbridge-example-dynamic-platform 3.0.2-dev-20260802-681c9d9 → 3.0.2-dev-20260808-db970b3

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
@@ -35,15 +35,19 @@ If you like this project and find it useful, please consider giving it a star on
35
35
 
36
36
  ### Added
37
37
 
38
+ - [chip]: Add chip-test toolchain agents instruction and chip-test runner.
39
+ - [frontend]: Add plugin-frontend agents instructions.
38
40
  - [platform]: Add `closureGarageDoor` device.
41
+ - [platform]: Add `closureVenetianBlind` device with Lift and Tilt panels.
39
42
 
40
43
  ### Changed
41
44
 
42
45
  - [package]: Upgrade package.
43
46
  - [package]: Update dependencies.
44
- - [package]: Bump `@types/node` to v.26.1.2.
45
- - [package]: Bump `oxfmt` to v.0.61.0.
46
- - [package]: Bump `oxlint` to v.1.76.0.
47
+ - [package]: Bump `typescript` to v.7.0.2.
48
+ - [package]: Bump `@types/node` to v.26.2.0.
49
+ - [package]: Bump `oxfmt` to v.0.62.0.
50
+ - [package]: Bump `oxlint` to v.1.77.0.
47
51
  - [package]: Bump `oxlint-tsgolint` to v.7.0.2001.
48
52
  - [package]: Update agents configs.
49
53
 
package/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  Matterbridge dynamic platform example plugin is a template to develop your own plugin using the dynamic platform.
25
25
 
26
- It exposes 71 virtual devices:
26
+ It exposes 72 virtual devices:
27
27
 
28
28
  - a door contact sensor
29
29
  - a motion sensor
@@ -96,6 +96,7 @@ It exposes 71 virtual devices:
96
96
  - an irrigation system (Matter 1.5.0)
97
97
  - an irrigation system with four zones (Matter 1.5.0)
98
98
  - a closure device (Matter 1.5.0, supported by SmartThings)
99
+ - a closure device with Lift and Tilt panels (Matter 1.5.0, supported by SmartThings)
99
100
 
100
101
  All these devices continuously change their state and position. The plugin also shows how to use all the command handlers (so you can control all the devices), how to subscribe to attributes, and how to trigger events.
101
102
 
@@ -111,11 +112,11 @@ If you like this project and find it useful, please consider giving it a star on
111
112
 
112
113
  See the guidelines on [Matterbridge](https://github.com/Luligu/matterbridge/blob/main/README.md) for more information.
113
114
 
114
- ## Repository setup
115
+ ## Repository toolchain
115
116
 
116
- > **Note:** This repository uses a new toolchain. It replaces the traditional TypeScript / ESLint / Prettier / Jest stack with a faster, lighter setup.
117
+ > **Note:** This repository uses a new toolchain. It replaces the traditional TypeScript / ESLint / Prettier / Jest stack with a faster and lighter setup.
117
118
 
118
- - **No `typescript` package** — replaced by [TypeScript Native](https://github.com/microsoft/typescript-go). The `typescript` package is kept only as a publish-time dependency while tsgo is still in preview.
119
+ - **No `typescript 6.x` package** — replaced by [TypeScript Native 7.x](https://github.com/microsoft/typescript-go).
119
120
  - **No ESLint, no Prettier** — replaced by the [oxc](https://oxc.rs) stack: [oxlint](https://oxc.rs/docs/guide/usage/linter.html) for linting and [oxfmt](https://oxc.rs/docs/guide/usage/formatter.html) for formatting.
120
121
  - **No Jest** — replaced by [Vitest](https://vitest.dev), which is much faster and natively supports ESM without extra configuration.
121
122
  - **Far fewer development dependencies** — the number of installed packages drops from **~600** to **~75**. A clean install is much faster.
@@ -129,25 +130,33 @@ See also the [Style Guide](./STYLEGUIDE.md) for JSDoc, naming, and logging conve
129
130
 
130
131
  ## Copilot instructions
131
132
 
132
- | File | Notes |
133
- | ---------------------------------------------------------------- | ---------------------------------------------------------------- |
134
- | `.github/copilot-instructions.md` | Main project instructions — always loaded |
135
- | `.github/instructions/matterbridge/matterbridge.instructions.md` | Matterbridge endpoint guide — dedicated Copilot instruction file |
136
- | `.github/instructions/testing/unit-tests.instructions.md` | Testing standards — scoped to `**/*.test.ts` |
133
+ | File | Notes |
134
+ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
135
+ | `.github/copilot-instructions.md` | Main project instructions — always loaded |
136
+ | `.github/instructions/chip-tests/chip-tests.instructions.md` | CHIP conformance test harness — scoped to CHIP test files |
137
+ | `.github/instructions/matterbridge/matterbridge.instructions.md` | Matterbridge endpoint guide — dedicated Copilot instruction file |
138
+ | `.github/instructions/plugin-frontend/plugin-frontend.instructions.md` | Plugin frontend SPA and custom REST API guide — scoped to frontend and plugin code |
139
+ | `.github/instructions/testing/unit-tests.instructions.md` | Testing standards — scoped to `**/*.test.ts` |
137
140
 
138
141
  ## Claude instructions
139
142
 
140
- | File | Notes |
141
- | --------------------------------------------------------- | ----------------------------------------------------- |
142
- | `CLAUDE.md` | Main project instructions — always loaded |
143
- | `.claude/rules/matterbridge/matterbridge.instructions.md` | Matterbridge endpoint guide — loaded for all contexts |
144
- | `.claude/rules/testing/unit-tests.instructions.md` | Testing standards — scoped to `**/*.test.ts` |
143
+ | File | Notes |
144
+ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
145
+ | `CLAUDE.md` | Main project instructions — always loaded |
146
+ | `.claude/rules/chip-tests/chip-tests.instructions.md` | CHIP conformance test harness — scoped to CHIP test files |
147
+ | `.claude/rules/matterbridge/matterbridge.instructions.md` | Matterbridge endpoint guide — loaded for all contexts |
148
+ | `.claude/rules/plugin-frontend/plugin-frontend.instructions.md` | Plugin frontend SPA and custom REST API guide — scoped to frontend and plugin code |
149
+ | `.claude/rules/testing/unit-tests.instructions.md` | Testing standards — scoped to `**/*.test.ts` |
145
150
 
146
151
  ## Codex/Agents instructions
147
152
 
148
153
  | File | Notes |
149
154
  | ---------------------------- | ------------------------------------------------- |
150
155
  | `AGENTS.md` | Main project instructions |
156
+ | `.agents/chip-tests.md` | CHIP conformance test harness |
157
+ | `.agents/matterbridge.md` | Matterbridge endpoint guide |
158
+ | `.agents/plugin-frontend.md` | Plugin frontend SPA and custom REST API guide |
159
+ | `.agents/testing.md` | Testing and validation expectations |
151
160
  | `.codex/config.toml` | Codex project permissions, approvals, and profile |
152
161
  | `.codex/rules/default.rules` | Codex command allow, prompt, and deny rules |
153
162
 
package/dist/module.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { aggregator, airPurifier, airQualitySensor, bridgedNode, colorTemperatureLight, contactSensor, windowCovering, dimmableLight, mountedDimmableLoadControl, dimmablePlugInUnit, doorLock, electricalSensor, extendedColorLight, fan, flowSensor, genericSwitch, getSemtag, humiditySensor, lightSensor, MatterbridgeDynamicPlatform, MatterbridgeEndpoint, modeSelect, occupancySensor, onOffLight, mountedOnOffControl, onOffPlugInUnit, onOffLightSwitch, powerSource, pressureSensor, pump, rainSensor, smokeCoAlarm, temperatureSensor, thermostat, waterFreezeDetector, waterLeakDetector, waterValve, } from 'matterbridge';
2
2
  import { AirConditioner, BasicVideoPlayer, BatteryStorage, Closure, Cooktop, Dishwasher, Evse, ExtractorHood, HeatPump, IrrigationSystem, LaundryDryer, LaundryWasher, MicrowaveOven, Oven, Refrigerator, RoboticVacuumCleaner, SoilSensor, SolarPower, Speaker, WaterHeater, } from 'matterbridge/devices';
3
3
  import { debugStringify } from 'matterbridge/logger';
4
- import { ClosureTag, CommonAreaNamespaceTag, CommonLocationTag, CommonNumberTag, CommonPositionTag, RefrigeratorTag, SwitchesTag, UINT16_MAX, UINT32_MAX, } from 'matterbridge/matter';
5
- import { AirQuality, BooleanState, BridgedDeviceBasicInformation, CarbonDioxideConcentrationMeasurement, CarbonMonoxideConcentrationMeasurement, ClosureControl, ColorControl, Descriptor, DeviceEnergyManagement, DoorLock, ElectricalEnergyMeasurement, ElectricalPowerMeasurement, EnergyEvse, EnergyEvseMode, FanControl, FlowMeasurement, FormaldehydeConcentrationMeasurement, Identify, IlluminanceMeasurement, LevelControl, NitrogenDioxideConcentrationMeasurement, OccupancySensing, OnOff, OperationalState, OvenMode, OzoneConcentrationMeasurement, Pm1ConcentrationMeasurement, Pm10ConcentrationMeasurement, Pm25ConcentrationMeasurement, PowerSource, PressureMeasurement, RadonConcentrationMeasurement, RelativeHumidityMeasurement, RvcCleanMode, RvcOperationalState, RvcRunMode, SmokeCoAlarm, TemperatureMeasurement, Thermostat, TotalVolatileOrganicCompoundsConcentrationMeasurement, WindowCovering, } from 'matterbridge/matter/clusters';
4
+ import { ClosureCoveringTag, ClosurePanelTag, ClosureTag, CommonAreaNamespaceTag, CommonLocationTag, CommonNumberTag, CommonPositionTag, RefrigeratorTag, SwitchesTag, UINT16_MAX, UINT32_MAX, } from 'matterbridge/matter';
5
+ import { AirQuality, BooleanState, BridgedDeviceBasicInformation, CarbonDioxideConcentrationMeasurement, CarbonMonoxideConcentrationMeasurement, ClosureControl, ClosureDimension, ColorControl, Descriptor, DeviceEnergyManagement, DoorLock, ElectricalEnergyMeasurement, ElectricalPowerMeasurement, EnergyEvse, EnergyEvseMode, FanControl, FlowMeasurement, FormaldehydeConcentrationMeasurement, Identify, IlluminanceMeasurement, LevelControl, NitrogenDioxideConcentrationMeasurement, OccupancySensing, OnOff, OperationalState, OvenMode, OzoneConcentrationMeasurement, Pm1ConcentrationMeasurement, Pm10ConcentrationMeasurement, Pm25ConcentrationMeasurement, PowerSource, PressureMeasurement, RadonConcentrationMeasurement, RelativeHumidityMeasurement, RvcCleanMode, RvcOperationalState, RvcRunMode, SmokeCoAlarm, TemperatureMeasurement, Thermostat, TotalVolatileOrganicCompoundsConcentrationMeasurement, WindowCovering, } from 'matterbridge/matter/clusters';
6
6
  import { fireAndForget, getEnumDescription, isValidBoolean, isValidNumber, isValidObject, isValidString } from 'matterbridge/utils';
7
7
  function luxToMatter(lux) {
8
8
  if (!Number.isFinite(lux) || lux <= 0)
@@ -35,6 +35,7 @@ export class ExampleMatterbridgeDynamicPlatform extends MatterbridgeDynamicPlatf
35
35
  irrigation;
36
36
  irrigationSystem;
37
37
  closureGarageDoor;
38
+ closureVenetianBlind;
38
39
  select;
39
40
  climate;
40
41
  switch;
@@ -179,26 +180,127 @@ export class ExampleMatterbridgeDynamicPlatform extends MatterbridgeDynamicPlatf
179
180
  tagList: [getSemtag(ClosureTag.GarageDoor)],
180
181
  });
181
182
  this.closureGarageDoor = (await this.addDevice(this.closureGarageDoor));
182
- this.closureGarageDoor?.addCommandHandler('ClosureControl.moveTo', ({ request: { position, latch, speed } }) => {
183
+ let closureGarageDoorMoveTimeout;
184
+ function closureTargetToCurrentPosition(position) {
185
+ if (position === ClosureControl.TargetPosition.MoveToFullyClosed)
186
+ return ClosureControl.CurrentPosition.FullyClosed;
187
+ if (position === ClosureControl.TargetPosition.MoveToFullyOpen)
188
+ return ClosureControl.CurrentPosition.FullyOpened;
189
+ if (position === ClosureControl.TargetPosition.MoveToPedestrianPosition)
190
+ return ClosureControl.CurrentPosition.OpenedForPedestrian;
191
+ if (position === ClosureControl.TargetPosition.MoveToVentilationPosition)
192
+ return ClosureControl.CurrentPosition.OpenedForVentilation;
193
+ return ClosureControl.CurrentPosition.OpenedAtSignature;
194
+ }
195
+ this.closureGarageDoor?.addCommandHandler('ClosureControl.moveTo', ({ request: { position, latch, speed }, attributes }) => {
183
196
  this.closureGarageDoor?.log.info(`Command moveTo called position:${position} latch:${latch} speed:${speed}`);
184
- const currentStateTimeout = setTimeout(() => {
185
- const targetState = this.closureGarageDoor?.getAttribute(ClosureControl.id, 'overallTargetState');
186
- const currentState = this.closureGarageDoor?.getAttribute(ClosureControl.id, 'overallCurrentState');
187
- if (targetState === undefined || targetState === null)
197
+ attributes.countdownTime = 1;
198
+ closureGarageDoorMoveTimeout = setTimeout(() => {
199
+ const targetState = this.closureGarageDoor?.getAttribute(ClosureControl, 'overallTargetState');
200
+ if (targetState === undefined || targetState === null || targetState.position === undefined || targetState.position === null)
188
201
  return;
189
- void this.closureGarageDoor?.setAttribute(ClosureControl.id, 'mainState', ClosureControl.MainState.Stopped, this.closureGarageDoor.log);
190
- void this.closureGarageDoor?.setAttribute(ClosureControl.id, 'overallCurrentState', {
191
- position: targetState.position,
202
+ void this.closureGarageDoor?.setAttribute(ClosureControl, 'mainState', ClosureControl.MainState.Stopped, this.closureGarageDoor.log);
203
+ void this.closureGarageDoor?.setAttribute(ClosureControl, 'countdownTime', 0, this.closureGarageDoor.log);
204
+ void this.closureGarageDoor?.setAttribute(ClosureControl, 'overallCurrentState', {
205
+ position: closureTargetToCurrentPosition(targetState.position),
192
206
  latch: targetState.latch,
193
207
  speed: targetState.speed,
194
- secureState: targetState.position === ClosureControl.TargetPosition.MoveToFullyClosed ? true : (currentState?.secureState ?? false),
208
+ secureState: targetState.position === ClosureControl.TargetPosition.MoveToFullyClosed && targetState.latch === true,
195
209
  }, this.closureGarageDoor.log);
196
- }, 1000);
197
- currentStateTimeout.unref();
210
+ }, 1000).unref();
198
211
  });
199
212
  this.closureGarageDoor?.addCommandHandler('ClosureControl.stop', () => {
200
213
  this.closureGarageDoor?.log.info('Command stop called');
201
- });
214
+ clearTimeout(closureGarageDoorMoveTimeout);
215
+ closureGarageDoorMoveTimeout = undefined;
216
+ });
217
+ this.closureVenetianBlind = new Closure('Venetian Blind', 'VEN000072', {
218
+ tagList: [getSemtag(ClosureTag.Covering), getSemtag(ClosureCoveringTag.Venetian)],
219
+ });
220
+ const closureVenetianBlindLift = this.closureVenetianBlind.addPanel('Lift', [getSemtag(ClosurePanelTag.Lift)]);
221
+ const closureVenetianBlindTilt = this.closureVenetianBlind.addPanel('Tilt', [getSemtag(ClosurePanelTag.Tilt)]);
222
+ this.closureVenetianBlind = (await this.addDevice(this.closureVenetianBlind));
223
+ const closurePanelTargetPercent = (position) => {
224
+ if (position === ClosureControl.TargetPosition.MoveToFullyOpen)
225
+ return 0;
226
+ if (position === ClosureControl.TargetPosition.MoveToFullyClosed)
227
+ return 10000;
228
+ return 5000;
229
+ };
230
+ const closurePanelReachTimeouts = new Map();
231
+ let closureVenetianBlindMoveTimeout;
232
+ const clearClosureVenetianBlindTimeouts = () => {
233
+ clearTimeout(closureVenetianBlindMoveTimeout);
234
+ closureVenetianBlindMoveTimeout = undefined;
235
+ for (const pending of closurePanelReachTimeouts.values())
236
+ clearTimeout(pending);
237
+ closurePanelReachTimeouts.clear();
238
+ };
239
+ const syncClosureVenetianBlindFromPanels = () => {
240
+ const liftTarget = closureVenetianBlindLift.getAttribute(ClosureDimension, 'targetState');
241
+ const liftCurrent = closureVenetianBlindLift.getAttribute(ClosureDimension, 'currentState');
242
+ const tiltTarget = closureVenetianBlindTilt.getAttribute(ClosureDimension, 'targetState');
243
+ const tiltCurrent = closureVenetianBlindTilt.getAttribute(ClosureDimension, 'currentState');
244
+ if (!liftTarget || !liftCurrent || !tiltTarget || !tiltCurrent)
245
+ return;
246
+ if (liftCurrent.position !== liftTarget.position || tiltCurrent.position !== tiltTarget.position)
247
+ return;
248
+ const overallPosition = liftCurrent.position === 0
249
+ ? ClosureControl.CurrentPosition.FullyOpened
250
+ : liftCurrent.position === 10000
251
+ ? ClosureControl.CurrentPosition.FullyClosed
252
+ : ClosureControl.CurrentPosition.PartiallyOpened;
253
+ void this.closureVenetianBlind?.setAttribute(ClosureControl, 'mainState', ClosureControl.MainState.Stopped, this.closureVenetianBlind.log);
254
+ void this.closureVenetianBlind?.setAttribute(ClosureControl, 'overallCurrentState', { position: overallPosition, latch: liftCurrent.latch ?? true, speed: liftCurrent.speed, secureState: liftCurrent.position === 10000 && liftCurrent.latch === true }, this.closureVenetianBlind?.log);
255
+ };
256
+ const simulateClosurePanelReachingTarget = (panel) => {
257
+ const pending = closurePanelReachTimeouts.get(panel);
258
+ if (pending)
259
+ clearTimeout(pending);
260
+ const currentStateTimeout = setTimeout(() => {
261
+ void (async () => {
262
+ closurePanelReachTimeouts.delete(panel);
263
+ const targetState = panel.getAttribute(ClosureDimension, 'targetState');
264
+ if (targetState === undefined || targetState === null)
265
+ return;
266
+ await panel.setAttribute(ClosureDimension, 'currentState', targetState, panel.log);
267
+ syncClosureVenetianBlindFromPanels();
268
+ })();
269
+ }, 1000);
270
+ currentStateTimeout.unref();
271
+ closurePanelReachTimeouts.set(panel, currentStateTimeout);
272
+ };
273
+ const moveClosurePanelTo = (panel, position, latch, speed) => {
274
+ void panel.setAttribute(ClosureDimension, 'targetState', { position, latch: latch ?? true, speed }, panel.log);
275
+ simulateClosurePanelReachingTarget(panel);
276
+ };
277
+ this.closureVenetianBlind?.addCommandHandler('ClosureControl.moveTo', ({ request: { position, latch, speed }, attributes }) => {
278
+ this.closureVenetianBlind?.log.info(`Command moveTo called position:${position} latch:${latch} speed:${speed}`);
279
+ attributes.countdownTime = 1;
280
+ clearClosureVenetianBlindTimeouts();
281
+ closureVenetianBlindMoveTimeout = setTimeout(() => {
282
+ void this.closureVenetianBlind?.setAttribute(ClosureControl, 'countdownTime', 0, this.closureVenetianBlind.log);
283
+ closureVenetianBlindMoveTimeout = undefined;
284
+ const targetState = this.closureVenetianBlind?.getAttribute(ClosureControl, 'overallTargetState');
285
+ if (targetState === undefined || targetState === null || targetState.position === undefined || targetState.position === null)
286
+ return;
287
+ const panelPercent = closurePanelTargetPercent(targetState.position);
288
+ moveClosurePanelTo(closureVenetianBlindLift, panelPercent, targetState.latch, targetState.speed);
289
+ moveClosurePanelTo(closureVenetianBlindTilt, panelPercent, targetState.latch, targetState.speed);
290
+ }, 1000).unref();
291
+ });
292
+ this.closureVenetianBlind?.addCommandHandler('ClosureControl.stop', () => {
293
+ this.closureVenetianBlind?.log.info('Command stop called');
294
+ clearClosureVenetianBlindTimeouts();
295
+ });
296
+ const addClosurePanelSetTargetSimulation = (panel) => {
297
+ panel.addCommandHandler('ClosureDimension.setTarget', ({ request: { position, latch, speed } }) => {
298
+ panel.log.info(`Command setTarget called position:${position} latch:${latch} speed:${speed}`);
299
+ simulateClosurePanelReachingTarget(panel);
300
+ });
301
+ };
302
+ addClosurePanelSetTargetSimulation(closureVenetianBlindLift);
303
+ addClosurePanelSetTargetSimulation(closureVenetianBlindTilt);
202
304
  this.climate = new MatterbridgeEndpoint([bridgedNode, powerSource], { id: 'Climate' }, this.config.debug)
203
305
  .createDefaultBridgedDeviceBasicInformationClusterServer('Climate', 'CLI00008', 0xfff1, 'Matterbridge', 'Matterbridge Climate')
204
306
  .createDefaultPowerSourceReplaceableBatteryClusterServer(90, PowerSource.BatChargeLevel.Ok, 2990, '2 x AA', 2, PowerSource.BatReplaceability.UserReplaceable)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge-example-dynamic-platform",
3
- "version": "3.0.2-dev-20260802-681c9d9",
3
+ "version": "3.0.2-dev-20260808-db970b3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge-example-dynamic-platform",
9
- "version": "3.0.2-dev-20260802-681c9d9",
9
+ "version": "3.0.2-dev-20260808-db970b3",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "node-ansi-logger": "3.3.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-example-dynamic-platform",
3
- "version": "3.0.2-dev-20260802-681c9d9",
3
+ "version": "3.0.2-dev-20260808-db970b3",
4
4
  "description": "Matterbridge dynamic plugin",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",