*/
package org.openhab.binding.mqtt.generic.values;
+import static java.util.function.Predicate.not;
+
import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
*/
@NonNullByDefault
public class OnOffValue extends Value {
- private final String onState;
- private final String offState;
+ private final Set<String> onStates;
+ private final Set<String> offStates;
private final String onCommand;
private final String offCommand;
/**
- * Creates a switch On/Off type, that accepts "ON", "1" for on and "OFF","0" for off.
+ * Creates a switch On/Off type, that accepts "ON" for on and "OFF" for off.
*/
public OnOffValue() {
this(OnOffType.ON.name(), OnOffType.OFF.name());
/**
* Creates a new SWITCH On/Off value.
*
- * values send in messages will be the same as those expected in incomming messages
+ * values send in messages will be the same as those expected in incoming messages
*
- * @param onValue The ON value string. This will be compared to MQTT messages.
- * @param offValue The OFF value string. This will be compared to MQTT messages.
+ * @param onValue The ON value string. This will be compared to MQTT messages. Defaults to "ON".
+ * @param offValue The OFF value string. This will be compared to MQTT messages. Defaults to "OFF".
*/
public OnOffValue(@Nullable String onValue, @Nullable String offValue) {
this(onValue, offValue, onValue, offValue);
/**
* Creates a new SWITCH On/Off value.
*
- * @param onState The ON value string. This will be compared to MQTT messages.
- * @param offState The OFF value string. This will be compared to MQTT messages.
- * @param onCommand The ON value string. This will be send in MQTT messages.
- * @param offCommand The OFF value string. This will be send in MQTT messages.
+ * @param onState The ON value string. This will be compared to MQTT messages. Defaults to onCommand if null, or
+ * "ON" if both are null.
+ * @param offState The OFF value string. This will be compared to MQTT messages. Defaults to offComamand if null, or
+ * "OFF" if both are null.
+ * @param onCommand The ON value string. This will be send in MQTT messages. Defaults to onState if null, or "ON" if
+ * both are null.
+ * @param offCommand The OFF value string. This will be send in MQTT messages. Defaults to offCommand if null, or
+ * "OFF" if both are null.
*/
public OnOffValue(@Nullable String onState, @Nullable String offState, @Nullable String onCommand,
@Nullable String offCommand) {
+ this(new String[] { defaultArgument(onState, onCommand, OnOffType.ON.name()) },
+ new String[] { defaultArgument(offState, offCommand, OnOffType.OFF.name()) },
+ defaultArgument(onCommand, onState, OnOffType.ON.name()),
+ defaultArgument(offCommand, offState, OnOffType.OFF.name()));
+ }
+
+ /**
+ * Creates a new SWITCH On/Off value.
+ *
+ * @param onStates A list of valid ON value strings. This will be compared to MQTT messages.
+ * @param offStates A list of valid OFF value strings. This will be compared to MQTT messages.
+ * @param onCommand The ON value string. This will be send in MQTT messages.
+ * @param offCommand The OFF value string. This will be send in MQTT messages.
+ */
+ public OnOffValue(String[] onStates, String[] offStates, String onCommand, String offCommand) {
super(CoreItemFactory.SWITCH, List.of(OnOffType.class, StringType.class));
- this.onState = onState == null ? OnOffType.ON.name() : onState;
- this.offState = offState == null ? OnOffType.OFF.name() : offState;
- this.onCommand = onCommand == null ? OnOffType.ON.name() : onCommand;
- this.offCommand = offCommand == null ? OnOffType.OFF.name() : offCommand;
+ this.onStates = Stream.of(onStates).filter(not(String::isBlank)).collect(Collectors.toSet());
+ this.offStates = Stream.of(offStates).filter(not(String::isBlank)).collect(Collectors.toSet());
+ this.onCommand = onCommand;
+ this.offCommand = offCommand;
}
@Override
return onOffCommand;
} else {
final String updatedValue = command.toString();
- if (onState.equals(updatedValue)) {
+ if (onStates.contains(updatedValue)) {
return OnOffType.ON;
- } else if (offState.equals(updatedValue)) {
+ } else if (offStates.contains(updatedValue)) {
return OnOffType.OFF;
} else {
return OnOffType.valueOf(updatedValue);
builder = builder.withCommandOption(new CommandOption(offCommand, offCommand));
return builder;
}
+
+ private static String defaultArgument(@Nullable String arg1, @Nullable String arg2, String defaultValue) {
+ String result = arg1;
+ if (result == null) {
+ result = arg2;
+ }
+ if (result == null) {
+ result = defaultValue;
+ }
+ return result;
+ }
}
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
-import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.type.AutoUpdatePolicy;
import com.google.gson.annotations.SerializedName;
/**
- * A MQTT lock, following the https://www.home-assistant.io/components/lock.mqtt/ specification.
+ * A MQTT lock, following the https://www.home-assistant.io/integrations/lock.mqtt specification.
*
* @author David Graeff - Initial contribution
+ * @author Cody Cutrer - Support OPEN, full state, and optimistic mode.
*/
@NonNullByDefault
public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
- public static final String SWITCH_CHANNEL_ID = "lock"; // Randomly chosen channel "ID"
+ public static final String LOCK_CHANNEL_ID = "lock";
+ public static final String STATE_CHANNEL_ID = "state";
/**
* Configuration class for MQTT component
protected boolean optimistic = false;
+ @SerializedName("command_topic")
+ protected @Nullable String commandTopic;
@SerializedName("state_topic")
protected String stateTopic = "";
@SerializedName("payload_lock")
protected String payloadLock = "LOCK";
@SerializedName("payload_unlock")
protected String payloadUnlock = "UNLOCK";
- @SerializedName("command_topic")
- protected @Nullable String commandTopic;
+ @SerializedName("payload_open")
+ protected @Nullable String payloadOpen;
+ @SerializedName("state_jammed")
+ protected String stateJammed = "JAMMED";
+ @SerializedName("state_locked")
+ protected String stateLocked = "LOCKED";
+ @SerializedName("state_locking")
+ protected String stateLocking = "LOCKING";
+ @SerializedName("state_unlocked")
+ protected String stateUnlocked = "UNLOCKED";
+ @SerializedName("state_unlocking")
+ protected String stateUnlocking = "UNLOCKING";
}
+ private boolean optimistic = false;
+ private OnOffValue lockValue;
+ private TextValue stateValue;
+
public Lock(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
- // We do not support all HomeAssistant quirks
- if (channelConfiguration.optimistic && !channelConfiguration.stateTopic.isBlank()) {
- throw new ConfigurationException("Component:Lock does not support forced optimistic mode");
- }
+ this.optimistic = channelConfiguration.optimistic || channelConfiguration.stateTopic.isBlank();
+
+ lockValue = new OnOffValue(new String[] { channelConfiguration.stateLocked },
+ new String[] { channelConfiguration.stateUnlocked, channelConfiguration.stateLocking,
+ channelConfiguration.stateUnlocking, channelConfiguration.stateJammed },
+ channelConfiguration.payloadLock, channelConfiguration.payloadUnlock);
+
+ buildChannel(LOCK_CHANNEL_ID, lockValue, "Lock", componentConfiguration.getUpdateListener())
+ .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
+ .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
+ channelConfiguration.getQos())
+ .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).commandFilter(command -> {
+ if (command instanceof OnOffType) {
+ autoUpdate(command.equals(OnOffType.ON));
+ }
+ return true;
+ }).build();
- buildChannel(SWITCH_CHANNEL_ID,
- new OnOffValue(channelConfiguration.payloadLock, channelConfiguration.payloadUnlock), getName(),
- componentConfiguration.getUpdateListener())
+ String[] commands;
+ if (channelConfiguration.payloadOpen == null) {
+ commands = new String[] { channelConfiguration.payloadLock, channelConfiguration.payloadUnlock, };
+ } else {
+ commands = new String[] { channelConfiguration.payloadLock, channelConfiguration.payloadUnlock,
+ channelConfiguration.payloadOpen };
+ }
+ stateValue = new TextValue(new String[] { channelConfiguration.stateJammed, channelConfiguration.stateLocked,
+ channelConfiguration.stateLocking, channelConfiguration.stateUnlocked,
+ channelConfiguration.stateUnlocking }, commands);
+ buildChannel(STATE_CHANNEL_ID, stateValue, "State", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
- .build();
+ .isAdvanced(true).withAutoUpdatePolicy(AutoUpdatePolicy.VETO).commandFilter(command -> {
+ if (command instanceof StringType stringCommand) {
+ if (stringCommand.toString().equals(channelConfiguration.payloadLock)) {
+ autoUpdate(true);
+ } else if (stringCommand.toString().equals(channelConfiguration.payloadUnlock)
+ || stringCommand.toString().equals(channelConfiguration.payloadOpen)) {
+ autoUpdate(false);
+ }
+ }
+ return true;
+ }).build();
+ }
+
+ private void autoUpdate(boolean locking) {
+ if (!optimistic) {
+ return;
+ }
+
+ final ChannelUID lockChannelUID = buildChannelUID(LOCK_CHANNEL_ID);
+ final ChannelUID stateChannelUID = buildChannelUID(STATE_CHANNEL_ID);
+ final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
+
+ if (locking) {
+ stateValue.update(new StringType(channelConfiguration.stateLocked));
+ updateListener.updateChannelState(stateChannelUID, stateValue.getChannelState());
+ lockValue.update(OnOffType.ON);
+ updateListener.updateChannelState(lockChannelUID, OnOffType.ON);
+ } else {
+ stateValue.update(new StringType(channelConfiguration.stateUnlocked));
+ updateListener.updateChannelState(stateChannelUID, stateValue.getChannelState());
+ lockValue.update(OnOffType.OFF);
+ updateListener.updateChannelState(lockChannelUID, OnOffType.OFF);
+ }
}
}
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
+import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
/**
* Tests for {@link Lock}
@SuppressWarnings("null")
@Test
public void test() throws InterruptedException {
- // @formatter:off
- var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
- """
- { \
- "availability": [ \
- { \
- "topic": "zigbee2mqtt/bridge/state" \
- } \
- ], \
- "device": { \
- "identifiers": [ \
- "zigbee2mqtt_0x0000000000000000" \
- ], \
- "manufacturer": "Locks inc", \
- "model": "Lock", \
- "name": "LockBlower", \
- "sw_version": "Zigbee2MQTT 1.18.2" \
- }, \
- "name": "lock", \
- "payload_unlock": "UNLOCK_", \
- "payload_lock": "LOCK_", \
- "state_topic": "zigbee2mqtt/lock/state", \
- "command_topic": "zigbee2mqtt/lock/set/state" \
- }\
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "availability": [
+ {
+ "topic": "zigbee2mqtt/bridge/state"
+ }
+ ],
+ "device": {
+ "identifiers": [
+ "zigbee2mqtt_0x0000000000000000"
+ ],
+ "manufacturer": "Locks inc",
+ "model": "Lock",
+ "name": "LockBlower",
+ "sw_version": "Zigbee2MQTT 1.18.2"
+ },
+ "name": "lock",
+ "payload_unlock": "UNLOCK_",
+ "payload_lock": "LOCK_",
+ "state_unlocked": "UNLOCKED_",
+ "state_locked": "LOCKED_",
+ "state_topic": "zigbee2mqtt/lock/state",
+ "command_topic": "zigbee2mqtt/lock/set/state",
+ "optimistic": true
+ }
""");
- // @formatter:on
- assertThat(component.channels.size(), is(1));
+ assertThat(component.channels.size(), is(2));
assertThat(component.getName(), is("lock"));
- assertChannel(component, Lock.SWITCH_CHANNEL_ID, "zigbee2mqtt/lock/state", "zigbee2mqtt/lock/set/state", "lock",
+ assertChannel(component, Lock.LOCK_CHANNEL_ID, "zigbee2mqtt/lock/state", "zigbee2mqtt/lock/set/state", "Lock",
OnOffValue.class);
+ assertChannel(component, Lock.STATE_CHANNEL_ID, "zigbee2mqtt/lock/state", "zigbee2mqtt/lock/set/state", "State",
+ TextValue.class);
- publishMessage("zigbee2mqtt/lock/state", "LOCK_");
- assertState(component, Lock.SWITCH_CHANNEL_ID, OnOffType.ON);
- publishMessage("zigbee2mqtt/lock/state", "LOCK_");
- assertState(component, Lock.SWITCH_CHANNEL_ID, OnOffType.ON);
- publishMessage("zigbee2mqtt/lock/state", "UNLOCK_");
- assertState(component, Lock.SWITCH_CHANNEL_ID, OnOffType.OFF);
- publishMessage("zigbee2mqtt/lock/state", "LOCK_");
- assertState(component, Lock.SWITCH_CHANNEL_ID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/lock/state", "LOCKED_");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED_"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/lock/state", "UNLOCKED_");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("UNLOCKED_"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ publishMessage("zigbee2mqtt/lock/state", "JAMMED");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("JAMMED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ publishMessage("zigbee2mqtt/lock/state", "GARBAGE");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("JAMMED"));
- component.getChannel(Lock.SWITCH_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
+ component.getChannel(Lock.LOCK_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
assertPublished("zigbee2mqtt/lock/set/state", "UNLOCK_");
- component.getChannel(Lock.SWITCH_CHANNEL_ID).getState().publishValue(OnOffType.ON);
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("UNLOCKED_"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ component.getChannel(Lock.LOCK_CHANNEL_ID).getState().publishValue(OnOffType.ON);
assertPublished("zigbee2mqtt/lock/set/state", "LOCK_");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED_"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
+ component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("UNLOCK_"));
+ assertPublished("zigbee2mqtt/lock/set/state", "UNLOCK_", 2);
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("UNLOCKED_"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("LOCK_"));
+ assertPublished("zigbee2mqtt/lock/set/state", "LOCK_", 2);
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED_"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("LOCK")));
+ assertThrows(IllegalArgumentException.class,
+ () -> component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("OPEN")));
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED_"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
}
+ @SuppressWarnings("null")
@Test
- public void forceOptimisticIsNotSupported() {
+ public void testNoStateTopicIsOptimistic() throws InterruptedException {
// @formatter:off
- publishMessage(configTopicToMqtt(CONFIG_TOPIC),
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
"""
- { \
- "availability": [ \
- { \
- "topic": "zigbee2mqtt/bridge/state" \
- } \
- ], \
- "device": { \
- "identifiers": [ \
- "zigbee2mqtt_0x0000000000000000" \
- ], \
- "manufacturer": "Locks inc", \
- "model": "Lock", \
- "name": "LockBlower", \
- "sw_version": "Zigbee2MQTT 1.18.2" \
- }, \
- "name": "lock", \
- "payload_unlock": "UNLOCK_", \
- "payload_lock": "LOCK_", \
- "optimistic": "true", \
- "state_topic": "zigbee2mqtt/lock/state", \
- "command_topic": "zigbee2mqtt/lock/set/state" \
- }\
+ {
+ "availability": [
+ {
+ "topic": "zigbee2mqtt/bridge/state"
+ }
+ ],
+ "device": {
+ "identifiers": [
+ "zigbee2mqtt_0x0000000000000000"
+ ],
+ "manufacturer": "Locks inc",
+ "model": "Lock",
+ "name": "LockBlower",
+ "sw_version": "Zigbee2MQTT 1.18.2"
+ },
+ "name": "lock",
+ "command_topic": "zigbee2mqtt/lock/set/state"
+ }
""");
// @formatter:on
+
+ component.getChannel(Lock.LOCK_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/lock/set/state", "UNLOCK");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("UNLOCKED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ component.getChannel(Lock.LOCK_CHANNEL_ID).getState().publishValue(OnOffType.ON);
+ assertPublished("zigbee2mqtt/lock/set/state", "LOCK");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
+ component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("UNLOCK"));
+ assertPublished("zigbee2mqtt/lock/set/state", "UNLOCK", 2);
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("UNLOCKED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("LOCK"));
+ assertPublished("zigbee2mqtt/lock/set/state", "LOCK", 2);
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("OPEN")));
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testOpennable() throws InterruptedException {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "availability": [
+ {
+ "topic": "zigbee2mqtt/bridge/state"
+ }
+ ],
+ "device": {
+ "identifiers": [
+ "zigbee2mqtt_0x0000000000000000"
+ ],
+ "manufacturer": "Locks inc",
+ "model": "Lock",
+ "name": "LockBlower",
+ "sw_version": "Zigbee2MQTT 1.18.2"
+ },
+ "name": "lock",
+ "payload_open": "OPEN",
+ "state_topic": "zigbee2mqtt/lock/state",
+ "command_topic": "zigbee2mqtt/lock/set/state",
+ "optimistic": true
+ }
+ """);
+
+ component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("OPEN"));
+ assertPublished("zigbee2mqtt/lock/set/state", "OPEN");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("UNLOCKED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testNonOptimistic() throws InterruptedException {
+ var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+ {
+ "availability": [
+ {
+ "topic": "zigbee2mqtt/bridge/state"
+ }
+ ],
+ "device": {
+ "identifiers": [
+ "zigbee2mqtt_0x0000000000000000"
+ ],
+ "manufacturer": "Locks inc",
+ "model": "Lock",
+ "name": "LockBlower",
+ "sw_version": "Zigbee2MQTT 1.18.2"
+ },
+ "name": "lock",
+ "payload_open": "OPEN",
+ "state_topic": "zigbee2mqtt/lock/state",
+ "command_topic": "zigbee2mqtt/lock/set/state"
+ }
+ """);
+
+ publishMessage("zigbee2mqtt/lock/state", "LOCKED");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/lock/state", "UNLOCKED");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("UNLOCKED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ publishMessage("zigbee2mqtt/lock/state", "LOCKED");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
+ publishMessage("zigbee2mqtt/lock/state", "JAMMED");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("JAMMED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ publishMessage("zigbee2mqtt/lock/state", "GARBAGE");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("JAMMED"));
+
+ component.getChannel(Lock.LOCK_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
+ assertPublished("zigbee2mqtt/lock/set/state", "UNLOCK");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("JAMMED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ component.getChannel(Lock.LOCK_CHANNEL_ID).getState().publishValue(OnOffType.ON);
+ assertPublished("zigbee2mqtt/lock/set/state", "LOCK");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("JAMMED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("UNLOCK"));
+ assertPublished("zigbee2mqtt/lock/set/state", "UNLOCK", 2);
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("JAMMED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+ component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("LOCK"));
+ assertPublished("zigbee2mqtt/lock/set/state", "LOCK", 2);
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("JAMMED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
+
+ component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("OPEN"));
+ assertPublished("zigbee2mqtt/lock/set/state", "OPEN");
+ assertState(component, Lock.STATE_CHANNEL_ID, new StringType("JAMMED"));
+ assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.OFF);
}
@Override