[matter] Map contact sensor channels correctly (#20788)

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2026-05-31 14:05:47 +02:00
committed by GitHub
parent 8ec3fe4a76
commit 93c3afc34d
6 changed files with 246 additions and 0 deletions
@@ -235,6 +235,9 @@ public class MatterBindingConstants {
public static final String CHANNEL_ID_BOOLEANSTATE_STATEVALUE = "booleanstate-statevalue";
public static final ChannelTypeUID CHANNEL_BOOLEANSTATE_STATEVALUE = new ChannelTypeUID(BINDING_ID,
CHANNEL_ID_BOOLEANSTATE_STATEVALUE);
public static final String CHANNEL_ID_CONTACT_STATEVALUE = "contact-statevalue";
public static final ChannelTypeUID CHANNEL_CONTACT_STATEVALUE = new ChannelTypeUID(BINDING_ID,
CHANNEL_ID_CONTACT_STATEVALUE);
public static final String CHANNEL_ID_OTASOFTWAREUPDATEREQUESTOR_UPDATEAVAILABLE = "otasoftwareupdaterequestor-updateavailable";
public static final ChannelTypeUID CHANNEL_OTASOFTWAREUPDATEREQUESTOR_UPDATEAVAILABLE = new ChannelTypeUID(
BINDING_ID, CHANNEL_ID_OTASOFTWAREUPDATEREQUESTOR_UPDATEAVAILABLE);
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.matter.internal.controller.devices.converter;
import static org.openhab.binding.matter.internal.MatterBindingConstants.CHANNEL_CONTACT_STATEVALUE;
import static org.openhab.binding.matter.internal.MatterBindingConstants.CHANNEL_ID_CONTACT_STATEVALUE;
import java.util.Collections;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.matter.internal.client.dto.cluster.gen.BooleanStateCluster;
import org.openhab.binding.matter.internal.client.dto.ws.AttributeChangedMessage;
import org.openhab.binding.matter.internal.handler.MatterBaseThingHandler;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.UnDefType;
/**
* A converter for mapping {@link BooleanStateCluster} to Contact semantics.
*
* Matter contact sensors define TRUE as closed/contact and FALSE as open/no contact.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public class ContactStateConverter extends GenericConverter<BooleanStateCluster> {
public ContactStateConverter(BooleanStateCluster cluster, MatterBaseThingHandler handler, int endpointNumber,
String labelPrefix) {
super(cluster, handler, endpointNumber, labelPrefix);
}
@Override
public Map<Channel, @Nullable StateDescription> createChannels(ChannelGroupUID channelGroupUID) {
Channel contactStateChannel = ChannelBuilder
.create(new ChannelUID(channelGroupUID, CHANNEL_ID_CONTACT_STATEVALUE), CoreItemFactory.CONTACT)
.withType(CHANNEL_CONTACT_STATEVALUE).build();
return Collections.singletonMap(contactStateChannel, null);
}
@Override
public void onEvent(AttributeChangedMessage message) {
switch (message.path.attributeName) {
case BooleanStateCluster.ATTRIBUTE_STATE_VALUE -> {
if (message.value instanceof Boolean booleanValue) {
updateState(CHANNEL_ID_CONTACT_STATEVALUE,
booleanValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
}
}
}
super.onEvent(message);
}
@Override
public void initState() {
Boolean stateValue = initializingCluster.stateValue;
if (stateValue == null) {
updateState(CHANNEL_ID_CONTACT_STATEVALUE, UnDefType.NULL);
return;
}
updateState(CHANNEL_ID_CONTACT_STATEVALUE, stateValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
}
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.matter.internal.controller.devices.types;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.matter.internal.client.dto.cluster.gen.BaseCluster;
import org.openhab.binding.matter.internal.client.dto.cluster.gen.BooleanStateCluster;
import org.openhab.binding.matter.internal.controller.devices.converter.ContactStateConverter;
import org.openhab.binding.matter.internal.controller.devices.converter.GenericConverter;
import org.openhab.binding.matter.internal.handler.MatterBaseThingHandler;
/**
* A DeviceType for contact sensors.
*
* Contact sensors use BooleanState where TRUE means CLOSED and FALSE means OPEN.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
public class ContactSensorType extends DeviceType {
public ContactSensorType(Integer deviceType, MatterBaseThingHandler handler, Integer endpointNumber) {
super(deviceType, handler, endpointNumber);
}
@Override
protected @Nullable GenericConverter<? extends BaseCluster> createConverter(BaseCluster cluster,
Map<String, BaseCluster> allClusters, String labelPrefix) {
if (cluster instanceof BooleanStateCluster booleanStateCluster) {
return new ContactStateConverter(booleanStateCluster, handler, endpointNumber, labelPrefix);
}
return super.createConverter(cluster, allClusters, labelPrefix);
}
}
@@ -36,6 +36,7 @@ public class DeviceTypeRegistry {
DeviceTypes.DIMMABLE_LIGHT, DeviceTypes.DIMMABLE_PLUG_IN_UNIT, DeviceTypes.DIMMER_SWITCH,
DeviceTypes.COLOR_DIMMER_SWITCH, DeviceTypes.EXTENDED_COLOR_LIGHT, DeviceTypes.COLOR_TEMPERATURE_LIGHT)
.forEach(type -> DeviceTypeRegistry.registerDeviceType(type, LightingType.class));
DeviceTypeRegistry.registerDeviceType(DeviceTypes.CONTACT_SENSOR, ContactSensorType.class);
}
/**
@@ -378,6 +378,17 @@
<state readOnly="true"></state>
</channel-type>
<channel-type id="contact-statevalue">
<item-type>Contact</item-type>
<label>Contact State</label>
<description>Indicates a contact state, open or closed.</description>
<tags>
<tag>Status</tag>
<tag>OpenState</tag>
</tags>
<state readOnly="true"></state>
</channel-type>
<channel-type id="wifinetworkdiagnostics-rssi">
<item-type unitHint="dBm">Number:Power</item-type>
<label>Signal</label>
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.matter.internal.controller.devices.converter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.openhab.binding.matter.internal.client.dto.cluster.gen.BooleanStateCluster;
import org.openhab.binding.matter.internal.client.dto.ws.AttributeChangedMessage;
import org.openhab.binding.matter.internal.client.dto.ws.Path;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.UnDefType;
/**
* Test class for ContactStateConverter.
*
* @author Kai Kreuzer - Initial contribution
*/
@NonNullByDefault
class ContactStateConverterTest extends BaseMatterConverterTest {
@Mock
@NonNullByDefault({})
private BooleanStateCluster mockCluster;
@NonNullByDefault({})
private ContactStateConverter contactConverter;
@Override
@BeforeEach
void setUp() {
super.setUp();
contactConverter = new ContactStateConverter(mockCluster, mockHandler, 1, "TestLabel");
}
@Test
void testCreateChannels() {
ChannelGroupUID thingUID = new ChannelGroupUID("matter:node:test:12345:1");
Map<Channel, @Nullable StateDescription> channels = contactConverter.createChannels(thingUID);
assertEquals(1, channels.size());
Channel contactChannel = channels.keySet().stream()
.filter(channel -> channel.getUID().toString().endsWith("#contact-statevalue")).findFirst()
.orElseThrow();
assertEquals("Contact", contactChannel.getAcceptedItemType());
}
@Test
void testOnEventWithBooleanValueTrue() throws Exception {
AttributeChangedMessage message = new AttributeChangedMessage();
message.path = new Path();
message.path.attributeName = "stateValue";
message.value = true;
contactConverter.onEvent(message);
verify(mockHandler, times(1)).updateState(eq(1), eq("contact-statevalue"), eq(OpenClosedType.CLOSED));
}
@Test
void testOnEventWithBooleanValueFalse() throws Exception {
AttributeChangedMessage message = new AttributeChangedMessage();
message.path = new Path();
message.path.attributeName = "stateValue";
message.value = false;
contactConverter.onEvent(message);
verify(mockHandler, times(1)).updateState(eq(1), eq("contact-statevalue"), eq(OpenClosedType.OPEN));
}
@Test
void testInitState() throws Exception {
mockCluster.stateValue = true;
contactConverter.initState();
verify(mockHandler, times(1)).updateState(eq(1), eq("contact-statevalue"), eq(OpenClosedType.CLOSED));
}
@Test
void testInitStateNull() throws Exception {
mockCluster.stateValue = null;
contactConverter.initState();
verify(mockHandler, times(1)).updateState(eq(1), eq("contact-statevalue"), eq(UnDefType.NULL));
}
}