Skip to content

Commit 4ce987c

Browse files
committed
Map contact sensor channels correctly
Signed-off-by: Kai Kreuzer <kai@openhab.org>
1 parent 9c123ae commit 4ce987c

6 files changed

Lines changed: 230 additions & 0 deletions

File tree

bundles/org.openhab.binding.matter/src/main/java/org/openhab/binding/matter/internal/MatterBindingConstants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ public class MatterBindingConstants {
235235
public static final String CHANNEL_ID_BOOLEANSTATE_STATEVALUE = "booleanstate-statevalue";
236236
public static final ChannelTypeUID CHANNEL_BOOLEANSTATE_STATEVALUE = new ChannelTypeUID(BINDING_ID,
237237
CHANNEL_ID_BOOLEANSTATE_STATEVALUE);
238+
public static final String CHANNEL_ID_CONTACT_STATEVALUE = "contact-statevalue";
239+
public static final ChannelTypeUID CHANNEL_CONTACT_STATEVALUE = new ChannelTypeUID(BINDING_ID,
240+
CHANNEL_ID_CONTACT_STATEVALUE);
238241
public static final String CHANNEL_ID_OTASOFTWAREUPDATEREQUESTOR_UPDATEAVAILABLE = "otasoftwareupdaterequestor-updateavailable";
239242
public static final ChannelTypeUID CHANNEL_OTASOFTWAREUPDATEREQUESTOR_UPDATEAVAILABLE = new ChannelTypeUID(
240243
BINDING_ID, CHANNEL_ID_OTASOFTWAREUPDATEREQUESTOR_UPDATEAVAILABLE);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.matter.internal.controller.devices.converter;
14+
15+
import static org.openhab.binding.matter.internal.MatterBindingConstants.CHANNEL_CONTACT_STATEVALUE;
16+
import static org.openhab.binding.matter.internal.MatterBindingConstants.CHANNEL_ID_CONTACT_STATEVALUE;
17+
18+
import java.util.Collections;
19+
import java.util.Map;
20+
21+
import org.eclipse.jdt.annotation.NonNullByDefault;
22+
import org.eclipse.jdt.annotation.Nullable;
23+
import org.openhab.binding.matter.internal.client.dto.cluster.gen.BooleanStateCluster;
24+
import org.openhab.binding.matter.internal.client.dto.ws.AttributeChangedMessage;
25+
import org.openhab.binding.matter.internal.handler.MatterBaseThingHandler;
26+
import org.openhab.core.library.CoreItemFactory;
27+
import org.openhab.core.library.types.OpenClosedType;
28+
import org.openhab.core.thing.Channel;
29+
import org.openhab.core.thing.ChannelGroupUID;
30+
import org.openhab.core.thing.ChannelUID;
31+
import org.openhab.core.thing.binding.builder.ChannelBuilder;
32+
import org.openhab.core.types.StateDescription;
33+
34+
/**
35+
* A converter for mapping {@link BooleanStateCluster} to Contact semantics.
36+
*
37+
* Matter contact sensors define TRUE as closed/contact and FALSE as open/no contact.
38+
*
39+
* @author Kai Kreuzer - Initial contribution
40+
*/
41+
@NonNullByDefault
42+
public class ContactStateConverter extends GenericConverter<BooleanStateCluster> {
43+
44+
public ContactStateConverter(BooleanStateCluster cluster, MatterBaseThingHandler handler, int endpointNumber,
45+
String labelPrefix) {
46+
super(cluster, handler, endpointNumber, labelPrefix);
47+
}
48+
49+
@Override
50+
public Map<Channel, @Nullable StateDescription> createChannels(ChannelGroupUID channelGroupUID) {
51+
Channel channel = ChannelBuilder
52+
.create(new ChannelUID(channelGroupUID, CHANNEL_ID_CONTACT_STATEVALUE), CoreItemFactory.CONTACT)
53+
.withType(CHANNEL_CONTACT_STATEVALUE).build();
54+
return Collections.singletonMap(channel, null);
55+
}
56+
57+
@Override
58+
public void onEvent(AttributeChangedMessage message) {
59+
switch (message.path.attributeName) {
60+
case BooleanStateCluster.ATTRIBUTE_STATE_VALUE -> {
61+
if (message.value instanceof Boolean booleanValue) {
62+
updateState(CHANNEL_ID_CONTACT_STATEVALUE,
63+
booleanValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
64+
}
65+
}
66+
}
67+
super.onEvent(message);
68+
}
69+
70+
@Override
71+
public void initState() {
72+
updateState(CHANNEL_ID_CONTACT_STATEVALUE,
73+
initializingCluster.stateValue ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.matter.internal.controller.devices.types;
14+
15+
import java.util.Map;
16+
17+
import org.eclipse.jdt.annotation.NonNullByDefault;
18+
import org.eclipse.jdt.annotation.Nullable;
19+
import org.openhab.binding.matter.internal.client.dto.cluster.gen.BaseCluster;
20+
import org.openhab.binding.matter.internal.client.dto.cluster.gen.BooleanStateCluster;
21+
import org.openhab.binding.matter.internal.controller.devices.converter.ContactStateConverter;
22+
import org.openhab.binding.matter.internal.controller.devices.converter.GenericConverter;
23+
import org.openhab.binding.matter.internal.handler.MatterBaseThingHandler;
24+
25+
/**
26+
* A DeviceType for contact sensors.
27+
*
28+
* Contact sensors use BooleanState where TRUE means CLOSED and FALSE means OPEN.
29+
*
30+
* @author Kai Kreuzer - Initial contribution
31+
*/
32+
@NonNullByDefault
33+
public class ContactSensorType extends DeviceType {
34+
35+
public ContactSensorType(Integer deviceType, MatterBaseThingHandler handler, Integer endpointNumber) {
36+
super(deviceType, handler, endpointNumber);
37+
}
38+
39+
@Override
40+
protected @Nullable GenericConverter<? extends BaseCluster> createConverter(BaseCluster cluster,
41+
Map<String, BaseCluster> allClusters, String labelPrefix) {
42+
if (cluster instanceof BooleanStateCluster booleanStateCluster) {
43+
return new ContactStateConverter(booleanStateCluster, handler, endpointNumber, labelPrefix);
44+
}
45+
46+
return super.createConverter(cluster, allClusters, labelPrefix);
47+
}
48+
}

bundles/org.openhab.binding.matter/src/main/java/org/openhab/binding/matter/internal/controller/devices/types/DeviceTypeRegistry.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public class DeviceTypeRegistry {
3636
DeviceTypes.DIMMABLE_LIGHT, DeviceTypes.DIMMABLE_PLUG_IN_UNIT, DeviceTypes.DIMMER_SWITCH,
3737
DeviceTypes.COLOR_DIMMER_SWITCH, DeviceTypes.EXTENDED_COLOR_LIGHT, DeviceTypes.COLOR_TEMPERATURE_LIGHT)
3838
.forEach(type -> DeviceTypeRegistry.registerDeviceType(type, LightingType.class));
39+
DeviceTypeRegistry.registerDeviceType(DeviceTypes.CONTACT_SENSOR, ContactSensorType.class);
3940
}
4041

4142
/**

bundles/org.openhab.binding.matter/src/main/resources/OH-INF/thing/channels.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,17 @@
378378
<state readOnly="true"></state>
379379
</channel-type>
380380

381+
<channel-type id="contact-statevalue">
382+
<item-type>Contact</item-type>
383+
<label>Contact State</label>
384+
<description>Indicates a contact state, open or closed.</description>
385+
<tags>
386+
<tag>Status</tag>
387+
<tag>OpenState</tag>
388+
</tags>
389+
<state readOnly="true"></state>
390+
</channel-type>
391+
381392
<channel-type id="wifinetworkdiagnostics-rssi">
382393
<item-type unitHint="dBm">Number:Power</item-type>
383394
<label>Signal</label>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.matter.internal.controller.devices.converter;
14+
15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.mockito.ArgumentMatchers.eq;
17+
import static org.mockito.Mockito.times;
18+
import static org.mockito.Mockito.verify;
19+
20+
import java.util.Map;
21+
22+
import org.eclipse.jdt.annotation.NonNullByDefault;
23+
import org.eclipse.jdt.annotation.Nullable;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import org.mockito.Mock;
27+
import org.openhab.binding.matter.internal.client.dto.cluster.gen.BooleanStateCluster;
28+
import org.openhab.binding.matter.internal.client.dto.ws.AttributeChangedMessage;
29+
import org.openhab.binding.matter.internal.client.dto.ws.Path;
30+
import org.openhab.core.library.types.OpenClosedType;
31+
import org.openhab.core.thing.Channel;
32+
import org.openhab.core.thing.ChannelGroupUID;
33+
import org.openhab.core.types.StateDescription;
34+
35+
/**
36+
* Test class for ContactStateConverter.
37+
*
38+
* @author Kai Kreuzer - Initial contribution
39+
*/
40+
@NonNullByDefault
41+
class ContactStateConverterTest extends BaseMatterConverterTest {
42+
43+
@Mock
44+
@NonNullByDefault({})
45+
private BooleanStateCluster mockCluster;
46+
@NonNullByDefault({})
47+
private ContactStateConverter contactConverter;
48+
49+
@Override
50+
@BeforeEach
51+
void setUp() {
52+
super.setUp();
53+
contactConverter = new ContactStateConverter(mockCluster, mockHandler, 1, "TestLabel");
54+
}
55+
56+
@Test
57+
void testCreateChannels() {
58+
ChannelGroupUID thingUID = new ChannelGroupUID("matter:node:test:12345:1");
59+
Map<Channel, @Nullable StateDescription> channels = contactConverter.createChannels(thingUID);
60+
assertEquals(1, channels.size());
61+
Channel channel = channels.keySet().iterator().next();
62+
assertEquals("matter:node:test:12345:1#contact-statevalue", channel.getUID().toString());
63+
assertEquals("Contact", channel.getAcceptedItemType());
64+
}
65+
66+
@Test
67+
void testOnEventWithBooleanValueTrue() throws Exception {
68+
AttributeChangedMessage message = new AttributeChangedMessage();
69+
message.path = new Path();
70+
message.path.attributeName = "stateValue";
71+
message.value = true;
72+
contactConverter.onEvent(message);
73+
verify(mockHandler, times(1)).updateState(eq(1), eq("contact-statevalue"), eq(OpenClosedType.CLOSED));
74+
}
75+
76+
@Test
77+
void testOnEventWithBooleanValueFalse() throws Exception {
78+
AttributeChangedMessage message = new AttributeChangedMessage();
79+
message.path = new Path();
80+
message.path.attributeName = "stateValue";
81+
message.value = false;
82+
contactConverter.onEvent(message);
83+
verify(mockHandler, times(1)).updateState(eq(1), eq("contact-statevalue"), eq(OpenClosedType.OPEN));
84+
}
85+
86+
@Test
87+
void testInitState() throws Exception {
88+
mockCluster.stateValue = true;
89+
contactConverter.initState();
90+
verify(mockHandler, times(1)).updateState(eq(1), eq("contact-statevalue"), eq(OpenClosedType.CLOSED));
91+
}
92+
}

0 commit comments

Comments
 (0)