[matter] Matter.js 0.17 Bridge Fixes (#20913)

* [matter] Matter.js 0.17 Bridge Fixes
Updates our bridge to support 0.17 changes to the bridge.
Fixes #20910

Signed-off-by: Dan Cunningham <dan@digitaldan.com>

* PR feedback

Signed-off-by: Dan Cunningham <dan@digitaldan.com>

---------

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This commit is contained in:
Dan Cunningham
2026-06-09 18:39:57 +02:00
committed by GitHub
parent 0e29105b1d
commit 4d87663b30
6 changed files with 68 additions and 43 deletions
@@ -1,7 +1,10 @@
import { Logger } from "@matter/main";
import { DoorLockServer } from "@matter/main/behaviors";
import { DoorLock } from "@matter/main/clusters";
import { DeviceFunctions } from "../DeviceFunctions";
const logger = Logger.get("CustomDoorLockServer");
export class CustomDoorLockServer extends DoorLockServer {
static readonly DEFAULTS = {
lockState: 0,
@@ -26,15 +29,20 @@ export class CustomDoorLockServer extends DoorLockServer {
}
/**
* For Lock/Unlock, we need to wait for the state update to be sent to the bridge controller if the state is not already the desired state.
* Locks can often take a while to lock/unlock so we need to wait longer for the state update to be sent to the bridge controller.
* Waits for openHAB to confirm the new lockState (longer timeout since locks can be slow).
*/
private async sendLockState(lockState: DoorLock.LockState) {
this.env.get(DeviceFunctions).sendAttributeChangedEvent(this.endpoint.id, "doorLock", "lockState", lockState);
if (this.endpoint.stateOf(CustomDoorLockServer).lockState !== lockState) {
const result = await this.env
.get(DeviceFunctions)
.waitForStateUpdate(this.endpoint.id, "doorLock", "lockState", 30000);
if (this.state.lockState !== lockState) {
let result: DoorLock.LockState | undefined;
try {
result = await this.env
.get(DeviceFunctions)
.waitForStateUpdate(this.endpoint.id, "doorLock", "lockState", 30000);
} catch {
logger.debug(`No lockState confirmation from openHAB for ${this.endpoint.id}, proceeding`);
return;
}
if (result !== lockState) {
throw new Error("Lock state failed", { cause: result });
}
@@ -1,8 +1,10 @@
import { MaybePromise } from "@matter/main";
import { Logger, MaybePromise } from "@matter/main";
import { LevelControlServer } from "@matter/main/behaviors";
import { LevelControl } from "@matter/main/clusters";
import { DeviceFunctions } from "../DeviceFunctions";
const logger = Logger.get("CustomLevelControlServer");
export class CustomLevelControlServer extends LevelControlServer {
static readonly DEFAULTS = { currentLevel: 254 } as const;
@@ -28,10 +30,14 @@ export class CustomLevelControlServer extends LevelControlServer {
this.env
.get(DeviceFunctions)
.sendAttributeChangedEvent(this.endpoint.id, "levelControl", "currentLevel", level);
if (this.endpoint.stateOf(CustomLevelControlServer).currentLevel !== level) {
await this.env
.get(DeviceFunctions)
.waitForStateUpdate(this.endpoint.id, "levelControl", "currentLevel", 15000);
if (this.state.currentLevel !== level) {
try {
await this.env
.get(DeviceFunctions)
.waitForStateUpdate(this.endpoint.id, "levelControl", "currentLevel", 15000);
} catch {
logger.debug(`No currentLevel confirmation from openHAB for ${this.endpoint.id}, proceeding`);
}
}
return super.moveToLevelLogic(level, transitionTime, withOnOff, options);
}
@@ -1,7 +1,10 @@
import { Logger } from "@matter/main";
import { ModeSelect } from "@matter/main/clusters";
import { ModeSelectServer } from "@matter/node/behaviors";
import { DeviceFunctions } from "../DeviceFunctions";
const logger = Logger.get("CustomModeSelectServer");
export class CustomModeSelectServer extends ModeSelectServer {
static readonly DEFAULTS = {
modeSelect: {
@@ -13,10 +16,14 @@ export class CustomModeSelectServer extends ModeSelectServer {
this.env
.get(DeviceFunctions)
.sendAttributeChangedEvent(this.endpoint.id, "modeSelect", "currentMode", request.newMode);
if (this.endpoint.stateOf(CustomModeSelectServer).currentMode !== request.newMode) {
await this.env
.get(DeviceFunctions)
.waitForStateUpdate(this.endpoint.id, "modeSelect", "currentMode", 15000);
if (this.state.currentMode !== request.newMode) {
try {
await this.env
.get(DeviceFunctions)
.waitForStateUpdate(this.endpoint.id, "modeSelect", "currentMode", 15000);
} catch {
logger.debug(`No currentMode confirmation from openHAB for ${this.endpoint.id}, proceeding`);
}
}
}
}
@@ -1,6 +1,9 @@
import { Logger } from "@matter/main";
import { OnOffServer } from "@matter/main/behaviors";
import { DeviceFunctions } from "../DeviceFunctions";
const logger = Logger.get("CustomOnOffServer");
/**
* This class is used to send on/off events to the bridge.
* The OnOff state will not be set until openHAB sends a state change event.
@@ -10,7 +13,7 @@ export class CustomOnOffServer extends OnOffServer {
override async on() {
// we only wait for ON/OFF if waiting for a different state
if (this.endpoint.stateOf(CustomOnOffServer).onOff !== true) {
if (this.state.onOff !== true) {
await this.sendOnOffAndWait(true);
} else {
this.sendOnOff(true);
@@ -18,7 +21,7 @@ export class CustomOnOffServer extends OnOffServer {
return super.on();
}
override async off() {
if (this.endpoint.stateOf(CustomOnOffServer).onOff !== false) {
if (this.state.onOff !== false) {
await this.sendOnOffAndWait(false);
} else {
this.sendOnOff(false);
@@ -32,11 +35,15 @@ export class CustomOnOffServer extends OnOffServer {
protected async sendOnOffAndWait(onOff: boolean) {
this.sendOnOff(onOff);
const result = await this.env
.get(DeviceFunctions)
.waitForStateUpdate(this.endpoint.id, "onOff", "onOff", 15000);
if (result !== onOff) {
throw new Error("On/Off failed", { cause: result });
try {
const result = await this.env
.get(DeviceFunctions)
.waitForStateUpdate(this.endpoint.id, "onOff", "onOff", 5000);
if (result !== onOff) {
logger.debug(`onOff confirmation for ${this.endpoint.id} returned ${result}, expected ${onOff}`);
}
} catch {
logger.debug(`No onOff confirmation from openHAB for ${this.endpoint.id}, proceeding`);
}
}
}
@@ -94,32 +94,30 @@ public class DimmableLightDevice extends BaseDevice {
public void updateState(Item item, State state) {
List<AttributeState> states = new ArrayList<>();
if (state instanceof HSBType hsb) {
boolean on = hsb.getBrightness().intValue() > 0;
lastLevel = ValueUtils.percentToLevel(hsb.getBrightness());
lastOnOffState = lastLevel > 0 ? OnOffType.ON : OnOffType.OFF;
states.add(new AttributeState(LevelControlCluster.CLUSTER_PREFIX,
LevelControlCluster.ATTRIBUTE_CURRENT_LEVEL, lastLevel));
states.add(new AttributeState(OnOffCluster.CLUSTER_PREFIX, OnOffCluster.ATTRIBUTE_ON_OFF,
hsb.getBrightness().intValue() > 0));
} else if (state instanceof PercentType percentType) {
lastLevel = ValueUtils.percentToLevel(percentType);
lastOnOffState = lastLevel > 0 ? OnOffType.ON : OnOffType.OFF;
states.add(new AttributeState(OnOffCluster.CLUSTER_PREFIX, OnOffCluster.ATTRIBUTE_ON_OFF,
percentType.intValue() > 0));
if (percentType.intValue() > 0) {
lastOnOffState = on ? OnOffType.ON : OnOffType.OFF;
states.add(new AttributeState(OnOffCluster.CLUSTER_PREFIX, OnOffCluster.ATTRIBUTE_ON_OFF, on));
if (on) {
states.add(new AttributeState(LevelControlCluster.CLUSTER_PREFIX,
LevelControlCluster.ATTRIBUTE_CURRENT_LEVEL, lastLevel));
lastOnOffState = OnOffType.ON;
} else {
}
} else if (state instanceof PercentType percentType) {
boolean on = percentType.intValue() > 0;
lastLevel = ValueUtils.percentToLevel(percentType);
lastOnOffState = on ? OnOffType.ON : OnOffType.OFF;
states.add(new AttributeState(OnOffCluster.CLUSTER_PREFIX, OnOffCluster.ATTRIBUTE_ON_OFF, on));
if (on) {
states.add(new AttributeState(LevelControlCluster.CLUSTER_PREFIX,
LevelControlCluster.ATTRIBUTE_CURRENT_LEVEL, 0));
lastOnOffState = OnOffType.OFF;
LevelControlCluster.ATTRIBUTE_CURRENT_LEVEL, lastLevel));
}
} else if (state instanceof OnOffType onOffType) {
states.add(new AttributeState(OnOffCluster.CLUSTER_PREFIX, OnOffCluster.ATTRIBUTE_ON_OFF,
onOffType == OnOffType.ON));
int level = onOffType == OnOffType.ON ? (lastLevel == 0 ? 1 : lastLevel) : 0;
states.add(new AttributeState(LevelControlCluster.CLUSTER_PREFIX,
LevelControlCluster.ATTRIBUTE_CURRENT_LEVEL, level));
boolean on = onOffType == OnOffType.ON;
states.add(new AttributeState(OnOffCluster.CLUSTER_PREFIX, OnOffCluster.ATTRIBUTE_ON_OFF, on));
if (on) {
states.add(new AttributeState(LevelControlCluster.CLUSTER_PREFIX,
LevelControlCluster.ATTRIBUTE_CURRENT_LEVEL, lastLevel == 0 ? 1 : lastLevel));
}
lastOnOffState = onOffType;
}
if (!states.isEmpty()) {
@@ -154,7 +154,6 @@ class DimmableLightDeviceTest {
dimmerDevice.updateState(dimmerItem, PercentType.ZERO);
expectedStates.clear();
expectedStates.add(new AttributeState("onOff", "onOff", false));
expectedStates.add(new AttributeState("levelControl", "currentLevel", 0));
verify(client).setEndpointStates(any(), eq(expectedStates));
}