matterbridge-homey 0.3.0 → 0.4.0

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
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.4.0] - 2026-10-28
8
+
9
+ ### Changed
10
+
11
+ - [platform]: The `newDevices` setting replaces `exposeNewDevices` of 0.3.0: new Homey devices can be added to the white list or the black list automatically, also the devices added while Matterbridge was stopped. The default applies the lists as they are and exposes new devices right away.
12
+ - [build]: The plugin is published to npm by a GitHub workflow when a GitHub release is created.
13
+ - [test]: The tests use the test utilities of Matterbridge 3.11 (`matterbridge/test-utils`).
14
+
15
+ ### Fixed
16
+
17
+ - [devices]: Plugs have the Lighting feature of the OnOff cluster, and smoke and CO alarms without a battery have a wired power source, as Matter 1.4 requires. Matterbridge 3.11 warned about both.
18
+
7
19
  ## [0.3.0] - 2026-10-28
8
20
 
9
21
  ### Added
package/README.md CHANGED
@@ -25,20 +25,20 @@ The key is shown only once, so copy it right away.
25
25
 
26
26
  ## Configuration
27
27
 
28
- | Setting | Description |
29
- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
- | `address` | Local address of the Homey, e.g. `http://192.168.1.50`. `https://192-168-1-50.homey.homeylocal.com` also works, unless your router blocks DNS rebinding. |
31
- | `token` | The API key. |
32
- | `whiteList` | Only these devices are exposed (names or Homey device IDs). Empty exposes all supported devices. The frontend offers a device picker after the first start. |
33
- | `blackList` | These devices are never exposed. |
34
- | `zoneWhiteList` | Only devices in these Homey zones are exposed, including sub-zones. Empty exposes all zones. |
35
- | `lightList` | These devices are exposed as lights, whatever their Homey class. |
36
- | `outletList` | These devices are exposed as plugs, whatever their Homey class. |
37
- | `capabilityBlackList` | These Homey capabilities are never exposed, e.g. `meter_power`. |
38
- | `deviceCapabilityBlackList` | Capabilities not exposed for one device: the device name (or ID) and its list of capabilities, e.g. `{ "Multisensor": ["alarm_occupancy"] }`. |
39
- | `exposeNewDevices` | Expose devices added to Homey right away, without a restart (default on). The lists above still apply. When off, new devices are exposed after a restart. |
40
- | `debug` | Log the homey-api debug output. |
41
- | `unregisterOnShutdown` | Remove all devices from the controllers on shutdown. For development only. |
28
+ | Setting | Description |
29
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | `address` | Local address of the Homey, e.g. `http://192.168.1.50`. `https://192-168-1-50.homey.homeylocal.com` also works, unless your router blocks DNS rebinding. |
31
+ | `token` | The API key. |
32
+ | `whiteList` | Only these devices are exposed (names or Homey device IDs). Empty exposes all supported devices. The frontend offers a device picker after the first start. |
33
+ | `blackList` | These devices are never exposed. |
34
+ | `zoneWhiteList` | Only devices in these Homey zones are exposed, including sub-zones. Empty exposes all zones. |
35
+ | `lightList` | These devices are exposed as lights, whatever their Homey class. |
36
+ | `outletList` | These devices are exposed as plugs, whatever their Homey class. |
37
+ | `capabilityBlackList` | These Homey capabilities are never exposed, e.g. `meter_power`. |
38
+ | `deviceCapabilityBlackList` | Capabilities not exposed for one device: the device name (or ID) and its list of capabilities, e.g. `{ "Multisensor": ["alarm_occupancy"] }`. |
39
+ | `newDevices` | What happens to devices added to Homey, also while Matterbridge is stopped: `default` applies the lists as they are, `whiteList` adds them to the white list (when it isn't empty), `blackList` adds them to the black list. |
40
+ | `debug` | Log the homey-api debug output. |
41
+ | `unregisterOnShutdown` | Remove all devices from the controllers on shutdown. For development only. |
42
42
 
43
43
  ## Supported devices
44
44
 
@@ -114,7 +114,7 @@ The white and black lists accept Homey device IDs as well as names. Entries by n
114
114
 
115
115
  ### Limitations
116
116
 
117
- - New Homey devices are exposed right away and deleted devices are removed right away. With `exposeNewDevices` off, new devices need a Matterbridge restart; the frontend asks for it. To keep new devices out entirely, use the white list.
117
+ - New Homey devices are exposed right away, unless the lists or `newDevices` keep them out, and deleted devices are removed right away. `newDevices` adds a device to a list by its name: like the other entries by name, it stops matching when the device is renamed.
118
118
  - A new device is exposed with the capabilities Homey reports for it. Capabilities that its app adds later appear after a restart.
119
119
  - Devices with the same name get their zone appended, e.g. `Ceiling light Kitchen`.
120
120
  - Not supported yet: buttons and remotes, media players and TVs (only their `onoff`), window covering tilt, plant moisture, `level_aqi`.
@@ -139,7 +139,16 @@ Uploading a newer tarball replaces the installed version. Test builds are marked
139
139
 
140
140
  ## Development
141
141
 
142
- The plugin follows the [Matterbridge plugin template](https://github.com/Luligu/matterbridge-plugin-template). Matterbridge must never be a dependency of the plugin: link a global or locally built Matterbridge with `npm link matterbridge` (see the [development guide](https://matterbridge.io/README-DEV.html)).
142
+ The plugin follows the [Matterbridge plugin template](https://github.com/Luligu/matterbridge-plugin-template). Matterbridge must never be a dependency of the plugin: clone and build Matterbridge next to the plugin and link it, like the CI does (see the [development guide](https://matterbridge.io/README-DEV.html)):
143
+
144
+ ```bash
145
+ git clone --depth 1 --single-branch --no-tags https://github.com/Luligu/matterbridge.git ../matterbridge
146
+ (cd ../matterbridge && npm install && npm run build && npm link)
147
+ npm install
148
+ npm run link
149
+ ```
150
+
151
+ A Matterbridge installed with `npm install -g matterbridge` builds the plugin, but the tests and the typecheck fail: its test utilities need Vitest, which only a clone installs. Pull and rebuild the clone to update it. The tests need Matterbridge 3.11 or later.
143
152
 
144
153
  | Script | Purpose |
145
154
  | ----------------------- | ------------------------------- |
@@ -160,6 +169,16 @@ Source layout:
160
169
 
161
170
  The tests run the platform against a Matter server node with a fake Homey. `vitest/homey.real.test.ts` runs it against your own Homey, read-only (every write is refused): put `HOMEY_ADDRESS`, `HOMEY_TOKEN` and `HOMEY_REAL_TEST=1` in `.env` (ignored by git) and run `npm run test -- vitest/homey.real.test.ts`.
162
171
 
172
+ ### Publishing
173
+
174
+ The [Publish to npm](.github/workflows/publish.yml) workflow publishes the plugin when a GitHub release is created: it builds Matterbridge, runs the build, typecheck, lint and tests, and publishes with [npm trusted publishing](https://docs.npmjs.com/trusted-publishers), so neither an npm login nor a local Matterbridge is needed.
175
+
176
+ 1. `npm version minor --no-git-tag-version --ignore-scripts` (or `patch`), and move the `[Unreleased]` changes in `CHANGELOG.md` under the new version and date.
177
+ 2. Commit and push.
178
+ 3. `node scripts/create-release.mjs`: creates the GitHub release from the changelog (needs the [GitHub CLI](https://cli.github.com)), which starts the workflow.
179
+
180
+ Once, before the first release from the workflow: on npmjs.com, add the trusted publisher in the settings of the package (GitHub Actions, repository `stefan-schweiger/matterbridge-homey`, workflow `publish.yml`).
181
+
163
182
  ## License
164
183
 
165
184
  Apache-2.0
package/dist/features.js CHANGED
@@ -1,4 +1,4 @@
1
- import { airQualitySensor, colorTemperatureLight, contactSensor, dimmableLight, dimmablePlugInUnit, doorLock, electricalSensor, extendedColorLight, fan, humiditySensor, lightSensor, occupancySensor, onOffLight, onOffPlugInUnit, pressureSensor, rainSensor, roboticVacuumCleaner, smokeCoAlarm, temperatureSensor, thermostat, waterLeakDetector, windowCovering, } from 'matterbridge';
1
+ import { airQualitySensor, colorTemperatureLight, contactSensor, dimmableLight, dimmablePlugInUnit, doorLock, electricalSensor, extendedColorLight, fan, humiditySensor, lightSensor, occupancySensor, onOffLight, onOffPlugInUnit, powerSource, pressureSensor, rainSensor, roboticVacuumCleaner, smokeCoAlarm, temperatureSensor, thermostat, waterLeakDetector, windowCovering, } from 'matterbridge';
2
2
  import { MatterbridgeRvcCleanModeServer, MatterbridgeRvcOperationalStateServer, MatterbridgeRvcRunModeServer } from 'matterbridge/devices';
3
3
  import { AirQuality, BooleanState, CarbonDioxideConcentrationMeasurement, CarbonMonoxideConcentrationMeasurement, ColorControl, DoorLock, ElectricalEnergyMeasurement, ElectricalPowerMeasurement, FanControl, IlluminanceMeasurement, LevelControl, OccupancySensing, OnOff, Pm10ConcentrationMeasurement, Pm25ConcentrationMeasurement, PressureMeasurement, RelativeHumidityMeasurement, RvcCleanMode, RvcOperationalState, RvcRunMode, SmokeCoAlarm, TemperatureMeasurement, Thermostat, TotalVolatileOrganicCompoundsConcentrationMeasurement, WindowCovering, } from 'matterbridge/matter/clusters';
4
4
  import { kelvinToRGB, miredToKelvin, rgbColorToHslColor, xyToHsl } from 'matterbridge/utils';
@@ -119,7 +119,7 @@ function lightFeature(device) {
119
119
  deviceTypes: [deviceType],
120
120
  clusters,
121
121
  build: (context) => {
122
- buildOnOff(context, 'onoff', true);
122
+ buildOnOff(context, 'onoff');
123
123
  if (clusters.includes('LevelControl'))
124
124
  buildLevelControl(context);
125
125
  if (hasHueSaturation)
@@ -137,19 +137,16 @@ function plugFeature(device, capabilityId) {
137
137
  deviceTypes: [isDimmable ? dimmablePlugInUnit : onOffPlugInUnit],
138
138
  clusters: isDimmable ? ['OnOff', 'LevelControl'] : ['OnOff'],
139
139
  build: (context) => {
140
- buildOnOff(context, capabilityId, isDimmable);
140
+ buildOnOff(context, capabilityId);
141
141
  if (isDimmable)
142
142
  buildLevelControl(context);
143
143
  },
144
144
  };
145
145
  }
146
- function buildOnOff(context, capabilityId, isLighting) {
146
+ function buildOnOff(context, capabilityId) {
147
147
  const { device, endpoint } = context;
148
148
  const isOn = context.value(capabilityId) === true;
149
- if (isLighting)
150
- endpoint.createDefaultOnOffClusterServer(isOn);
151
- else
152
- endpoint.createOnOffClusterServer(isOn);
149
+ endpoint.createDefaultOnOffClusterServer(isOn);
153
150
  if (hasCapability(device, capabilityId)) {
154
151
  endpoint.addCommandHandler('on', () => context.set(capabilityId, true));
155
152
  endpoint.addCommandHandler('off', () => context.set(capabilityId, false));
@@ -578,18 +575,17 @@ function vacuumFeature(device) {
578
575
  else if (request.newMode === vacuumIdleMode)
579
576
  stop('stopped', true);
580
577
  });
578
+ const isInState = (state, ...states) => states.includes(state);
581
579
  endpoint.addCommandHandler('RvcOperationalState.pause', ({ attributes }) => {
582
- if (attributes.operationalState === OperationalState.Running || attributes.operationalState === OperationalState.SeekingCharger)
580
+ if (isInState(attributes.operationalState, OperationalState.Running, OperationalState.SeekingCharger))
583
581
  stop('stopped', false);
584
582
  });
585
583
  endpoint.addCommandHandler('RvcOperationalState.resume', ({ attributes }) => {
586
- if (attributes.operationalState === OperationalState.Paused)
584
+ if (isInState(attributes.operationalState, OperationalState.Paused))
587
585
  start();
588
586
  });
589
587
  endpoint.addCommandHandler('RvcOperationalState.goHome', ({ attributes }) => {
590
- if (attributes.operationalState === OperationalState.Stopped ||
591
- attributes.operationalState === OperationalState.Running ||
592
- attributes.operationalState === OperationalState.Paused)
588
+ if (isInState(attributes.operationalState, OperationalState.Stopped, OperationalState.Running, OperationalState.Paused))
593
589
  stop('docked', true);
594
590
  });
595
591
  if (cleanModeIds.length > 0) {
@@ -719,11 +715,12 @@ function booleanStateFeature(capabilityId, deviceType, toState) {
719
715
  function smokeCoAlarmFeature(device) {
720
716
  const hasSmoke = hasCapability(device, 'alarm_smoke');
721
717
  const hasCo = hasCapability(device, 'alarm_co');
718
+ const hasBattery = hasCapability(device, 'measure_battery') || hasCapability(device, 'alarm_battery');
722
719
  const alarmState = (value) => (value === true ? SmokeCoAlarm.AlarmState.Critical : SmokeCoAlarm.AlarmState.Normal);
723
720
  return {
724
721
  name: 'smokecoalarm',
725
722
  capabilities: ['alarm_smoke', 'alarm_co'],
726
- deviceTypes: [smokeCoAlarm],
723
+ deviceTypes: [smokeCoAlarm, powerSource],
727
724
  clusters: ['SmokeCoAlarm'],
728
725
  build: (context) => {
729
726
  const { endpoint } = context;
@@ -733,6 +730,8 @@ function smokeCoAlarmFeature(device) {
733
730
  endpoint.createSmokeOnlySmokeCOAlarmClusterServer(alarmState(context.value('alarm_smoke')));
734
731
  else
735
732
  endpoint.createCoOnlySmokeCOAlarmClusterServer(alarmState(context.value('alarm_co')));
733
+ if (!hasBattery)
734
+ endpoint.createDefaultPowerSourceWiredClusterServer();
736
735
  if (hasSmoke) {
737
736
  context.bind('alarm_smoke', async (value) => {
738
737
  await endpoint.updateAttribute(SmokeCoAlarm, 'smokeState', alarmState(value), endpoint.log);
package/dist/module.js CHANGED
@@ -55,6 +55,7 @@ export class HomeyPlatform extends MatterbridgeDynamicPlatform {
55
55
  bridgedDevices = new Map();
56
56
  zones = [];
57
57
  newDeviceIds = new Set();
58
+ knownDeviceIds = new Set();
58
59
  areDevicesConfigured = false;
59
60
  deviceChanges = Promise.resolve();
60
61
  constructor(matterbridge, log, config, homey) {
@@ -76,18 +77,12 @@ export class HomeyPlatform extends MatterbridgeDynamicPlatform {
76
77
  this.config.outletList = isValidArray(config.outletList) ? config.outletList : [];
77
78
  this.config.capabilityBlackList = isValidArray(config.capabilityBlackList) ? config.capabilityBlackList : [];
78
79
  this.config.deviceCapabilityBlackList = isValidObject(config.deviceCapabilityBlackList) ? config.deviceCapabilityBlackList : {};
79
- this.config.exposeNewDevices ??= true;
80
+ this.config.newDevices = config.newDevices === 'whiteList' || config.newDevices === 'blackList' ? config.newDevices : 'default';
80
81
  this.config.debug ??= false;
81
82
  this.config.unregisterOnShutdown ??= false;
82
83
  this.homey =
83
84
  homey ?? new HomeyApiConnection(config.address, config.token, 15000, config.debug ? (message) => this.log.debug(`[homey-api] ${message}`) : undefined);
84
85
  this.homey.on('device.create', (device) => {
85
- if (!this.config.exposeNewDevices) {
86
- this.log.notice(`New Homey device ${CYAN}${device.name}${nf}: restart Matterbridge to expose it`);
87
- if (this.isConfigured)
88
- this.wssSendRestartRequired();
89
- return;
90
- }
91
86
  this.log.notice(`New Homey device ${CYAN}${device.name}${nf}`);
92
87
  fireAndForget(this.serialize(async () => this.addNewDevice(device)), this.log, `addition of ${device.name}`);
93
88
  });
@@ -127,9 +122,12 @@ export class HomeyPlatform extends MatterbridgeDynamicPlatform {
127
122
  this.zones = zones;
128
123
  this.log.info(`Found ${CYAN}${devices.length}${nf} devices in ${CYAN}${zones.length}${nf} zones`);
129
124
  const names = getUniqueDeviceNames(devices, zones);
125
+ const knownDeviceIds = await this.context?.get('knownDeviceIds');
130
126
  for (const device of devices.toSorted((a, b) => a.name.localeCompare(b.name))) {
131
- await this.addDevice(device, names.get(device.id) ?? device.name, zones);
127
+ await this.addDevice(device, names.get(device.id) ?? device.name, zones, knownDeviceIds?.includes(device.id) === false);
128
+ this.knownDeviceIds.add(device.id);
132
129
  }
130
+ await this.saveKnownDevices();
133
131
  });
134
132
  this.log.info(`Started platform ${idn}${this.config.name}${rs}${nf} with ${CYAN}${this.bridgedDevices.size}${nf} devices`);
135
133
  }
@@ -170,7 +168,7 @@ export class HomeyPlatform extends MatterbridgeDynamicPlatform {
170
168
  this.bridgedDevices.clear();
171
169
  this.log.info(`Shut down platform ${idn}${this.config.name}${rs}${nf}`);
172
170
  }
173
- async addDevice(device, name, zones) {
171
+ async addDevice(device, name, zones, isNew = false) {
174
172
  const serial = getSerialNumber(device.id);
175
173
  const keys = [name, device.name, device.id, serial];
176
174
  const isListed = (list) => keys.some((key) => list.includes(key));
@@ -190,6 +188,8 @@ export class HomeyPlatform extends MatterbridgeDynamicPlatform {
190
188
  for (const entity of entities)
191
189
  this.setSelectEntity(entity.name, entity.description, 'component');
192
190
  this.setSelectDevice(serial, name, configUrl, 'hub', entities);
191
+ if (isNew)
192
+ this.listNewDevice(keys);
193
193
  if (!this.validateDevice(keys))
194
194
  return;
195
195
  const excludedCapabilities = new Set([...this.config.capabilityBlackList, ...keys.flatMap((key) => this.config.deviceCapabilityBlackList[key] ?? [])]);
@@ -220,12 +220,16 @@ export class HomeyPlatform extends MatterbridgeDynamicPlatform {
220
220
  async addNewDevice(device) {
221
221
  if (this.bridgedDevices.has(device.id))
222
222
  return;
223
+ if (!this.knownDeviceIds.has(device.id)) {
224
+ this.knownDeviceIds.add(device.id);
225
+ await this.saveKnownDevices();
226
+ }
223
227
  const [devices, zones] = await Promise.all([this.homey.getDevices(), this.homey.getZones()]);
224
228
  this.zones = zones;
225
229
  const names = getUniqueDeviceNames([...devices.filter((other) => other.id !== device.id), device], zones);
226
230
  for (const [deviceId, bridged] of this.bridgedDevices)
227
231
  await bridged.setName(names.get(deviceId) ?? bridged.device.name);
228
- await this.addDevice(device, names.get(device.id) ?? device.name, zones);
232
+ await this.addDevice(device, names.get(device.id) ?? device.name, zones, true);
229
233
  const bridgedDevice = this.bridgedDevices.get(device.id);
230
234
  if (!bridgedDevice) {
231
235
  this.newDeviceIds.add(device.id);
@@ -254,6 +258,8 @@ export class HomeyPlatform extends MatterbridgeDynamicPlatform {
254
258
  }
255
259
  async removeDevice(deviceId) {
256
260
  this.newDeviceIds.delete(deviceId);
261
+ if (this.knownDeviceIds.delete(deviceId))
262
+ await this.saveKnownDevices();
257
263
  const bridgedDevice = this.bridgedDevices.get(deviceId);
258
264
  if (!bridgedDevice)
259
265
  return;
@@ -262,6 +268,20 @@ export class HomeyPlatform extends MatterbridgeDynamicPlatform {
262
268
  this.bridgedDevices.delete(deviceId);
263
269
  await this.unregisterDevice(bridgedDevice.endpoint);
264
270
  }
271
+ listNewDevice(keys) {
272
+ const listName = this.config.newDevices;
273
+ if (listName === 'default' || (listName === 'whiteList' && this.config.whiteList.length === 0))
274
+ return;
275
+ const list = this.config[listName];
276
+ if (keys.some((key) => list.includes(key)))
277
+ return;
278
+ list.push(keys[0]);
279
+ this.log.notice(`Added the new device ${CYAN}${keys[0]}${nf} to the ${listName === 'whiteList' ? 'white' : 'black'} list`);
280
+ this.saveConfig(this.config);
281
+ }
282
+ async saveKnownDevices() {
283
+ await this.context?.set('knownDeviceIds', [...this.knownDeviceIds]);
284
+ }
265
285
  async serialize(change) {
266
286
  const result = this.deviceChanges.then(change);
267
287
  this.deviceChanges = result.catch(() => {
@@ -10,7 +10,7 @@
10
10
  "outletList": [],
11
11
  "capabilityBlackList": [],
12
12
  "deviceCapabilityBlackList": {},
13
- "exposeNewDevices": true,
13
+ "newDevices": "default",
14
14
  "debug": false,
15
15
  "unregisterOnShutdown": false
16
16
  }
@@ -104,11 +104,12 @@
104
104
  "selectDeviceEntityFrom": "name"
105
105
  }
106
106
  },
107
- "exposeNewDevices": {
108
- "title": "Expose New Devices",
109
- "description": "Expose the devices added to Homey right away, without restarting Matterbridge. The white list, black list and zone white list still apply. When off, new devices are exposed after the next restart.",
110
- "type": "boolean",
111
- "default": true
107
+ "newDevices": {
108
+ "title": "New Devices",
109
+ "description": "What happens to the devices added to Homey, also while Matterbridge is stopped. default: the lists apply as they are, so with an empty white list new devices are exposed right away. whiteList: new devices are added to the white list, so they are exposed (only when the white list isn't empty). blackList: new devices are added to the black list, so they aren't exposed until you remove them from it.",
110
+ "type": "string",
111
+ "enum": ["default", "whiteList", "blackList"],
112
+ "default": "default"
112
113
  },
113
114
  "debug": {
114
115
  "title": "Enable Debug",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge-homey",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge-homey",
9
- "version": "0.3.0",
9
+ "version": "0.4.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "homey-api": "3.20.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-homey",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Matterbridge plugin that exposes Athom Homey Pro devices to Matter",
5
5
  "author": "Stefan Schweiger",
6
6
  "license": "Apache-2.0",