mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[mqtt.homeassistant] Improve support for Lock component (#16052)
* [mqtt.homeassistant] Improve support for Lock component * handle state and command payloads differing (as they do by default) * expose full state possibilities and OPEN command by adding a TextValue channel * Recognize intermediate lock states as unlocked on the switch channel Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
parent
f58898cd1d
commit
a53b740c51
@ -12,7 +12,12 @@
|
||||
*/
|
||||
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;
|
||||
@ -30,13 +35,13 @@ import org.openhab.core.types.CommandOption;
|
||||
*/
|
||||
@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());
|
||||
@ -45,10 +50,10 @@ public class OnOffValue extends Value {
|
||||
/**
|
||||
* 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);
|
||||
@ -57,18 +62,37 @@ public class OnOffValue extends Value {
|
||||
/**
|
||||
* 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
|
||||
@ -77,9 +101,9 @@ public class OnOffValue extends Value {
|
||||
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);
|
||||
@ -104,4 +128,15 @@ public class OnOffValue extends Value {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.CommandDescriptionBuilder;
|
||||
import org.openhab.core.types.CommandOption;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.openhab.core.types.StateOption;
|
||||
|
||||
@ -37,14 +38,17 @@ import org.openhab.core.types.StateOption;
|
||||
@NonNullByDefault
|
||||
public class TextValue extends Value {
|
||||
private final @Nullable Set<String> states;
|
||||
private final @Nullable Set<String> commands;
|
||||
|
||||
/**
|
||||
* Create a string value with a limited number of allowed states.
|
||||
* Create a string value with a limited number of allowed states and commands.
|
||||
*
|
||||
* @param states Allowed states. Empty states are filtered out. If the resulting set is empty, all string values
|
||||
* will be allowed.
|
||||
* @param commands Allowed commands. Empty commands are filtered out. If the resulting set is empty, all string
|
||||
* values will be allowed.
|
||||
*/
|
||||
public TextValue(String[] states) {
|
||||
public TextValue(String[] states, String[] commands) {
|
||||
super(CoreItemFactory.STRING, List.of(StringType.class));
|
||||
Set<String> s = Stream.of(states).filter(not(String::isBlank)).collect(Collectors.toSet());
|
||||
if (!s.isEmpty()) {
|
||||
@ -52,15 +56,42 @@ public class TextValue extends Value {
|
||||
} else {
|
||||
this.states = null;
|
||||
}
|
||||
Set<String> c = Stream.of(commands).filter(not(String::isBlank)).collect(Collectors.toSet());
|
||||
if (!c.isEmpty()) {
|
||||
this.commands = c;
|
||||
} else {
|
||||
this.commands = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a string value with a limited number of allowed states.
|
||||
*
|
||||
* @param states Allowed states. Empty states are filtered out. If the resulting set is empty, all string values
|
||||
* will be allowed. This same array is also used for allowed commands.
|
||||
*/
|
||||
public TextValue(String[] states) {
|
||||
this(states, states);
|
||||
}
|
||||
|
||||
public TextValue() {
|
||||
super(CoreItemFactory.STRING, List.of(StringType.class));
|
||||
this.states = null;
|
||||
this.commands = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringType parseCommand(Command command) throws IllegalArgumentException {
|
||||
final Set<String> commands = this.commands;
|
||||
String valueStr = command.toString();
|
||||
if (commands != null && !commands.contains(valueStr)) {
|
||||
throw new IllegalArgumentException("Value " + valueStr + " not within range");
|
||||
}
|
||||
return new StringType(valueStr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State parseMessage(Command command) throws IllegalArgumentException {
|
||||
final Set<String> states = this.states;
|
||||
String valueStr = command.toString();
|
||||
if (states != null && !states.contains(valueStr)) {
|
||||
@ -91,8 +122,8 @@ public class TextValue extends Value {
|
||||
@Override
|
||||
public CommandDescriptionBuilder createCommandDescription() {
|
||||
CommandDescriptionBuilder builder = super.createCommandDescription();
|
||||
final Set<String> commands = this.states;
|
||||
if (states != null) {
|
||||
final Set<String> commands = this.commands;
|
||||
if (commands != null) {
|
||||
for (String command : commands) {
|
||||
builder = builder.withCommandOption(new CommandOption(command, command));
|
||||
}
|
||||
|
@ -139,6 +139,16 @@ public class ValueTests {
|
||||
assertThat(v.getMQTTpublishValue(OnOffType.ON, "=%s"), is("=fancyON"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onoffMultiStates() {
|
||||
OnOffValue v = new OnOffValue(new String[] { "LOCKED" }, new String[] { "UNLOCKED", "JAMMED" }, "LOCK",
|
||||
"UNLOCK");
|
||||
|
||||
assertThat(v.parseCommand(new StringType("LOCKED")), is(OnOffType.ON));
|
||||
assertThat(v.parseCommand(new StringType("UNLOCKED")), is(OnOffType.OFF));
|
||||
assertThat(v.parseCommand(new StringType("JAMMED")), is(OnOffType.OFF));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openCloseUpdate() {
|
||||
OpenCloseValue v = new OpenCloseValue("fancyON", "fancyOff");
|
||||
|
@ -60,7 +60,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
||||
private static final String JINJA_PREFIX = "JINJA:";
|
||||
|
||||
// Component location fields
|
||||
private final ComponentConfiguration componentConfiguration;
|
||||
protected final ComponentConfiguration componentConfiguration;
|
||||
protected final @Nullable ChannelGroupTypeUID channelGroupTypeUID;
|
||||
protected final @Nullable ChannelGroupUID channelGroupUID;
|
||||
protected final HaID haID;
|
||||
|
@ -14,20 +14,27 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||
|
||||
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
|
||||
@ -39,30 +46,99 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
|
||||
|
||||
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();
|
||||
|
||||
buildChannel(SWITCH_CHANNEL_ID,
|
||||
new OnOffValue(channelConfiguration.payloadLock, channelConfiguration.payloadUnlock), getName(),
|
||||
componentConfiguration.getUpdateListener())
|
||||
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())
|
||||
.build();
|
||||
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).commandFilter(command -> {
|
||||
if (command instanceof OnOffType) {
|
||||
autoUpdate(command.equals(OnOffType.ON));
|
||||
}
|
||||
return true;
|
||||
}).build();
|
||||
|
||||
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())
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,13 +69,8 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
|
||||
throw new ConfigurationException("Component:Switch does not support forced optimistic mode");
|
||||
}
|
||||
|
||||
String stateOn = channelConfiguration.stateOn != null ? channelConfiguration.stateOn
|
||||
: channelConfiguration.payloadOn;
|
||||
String stateOff = channelConfiguration.stateOff != null ? channelConfiguration.stateOff
|
||||
: channelConfiguration.payloadOff;
|
||||
|
||||
OnOffValue value = new OnOffValue(stateOn, stateOff, channelConfiguration.payloadOn,
|
||||
channelConfiguration.payloadOff);
|
||||
OnOffValue value = new OnOffValue(channelConfiguration.stateOn, channelConfiguration.stateOff,
|
||||
channelConfiguration.payloadOn, channelConfiguration.payloadOff);
|
||||
|
||||
buildChannel(SWITCH_CHANNEL_ID, value, "state", componentConfiguration.getUpdateListener())
|
||||
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
|
||||
|
@ -14,13 +14,16 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||
|
||||
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}
|
||||
@ -34,83 +37,223 @@ public class LockTests extends AbstractComponentTests {
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void test() 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_unlock": "UNLOCK_",
|
||||
"payload_lock": "LOCK_",
|
||||
"state_unlocked": "UNLOCKED_",
|
||||
"state_locked": "LOCKED_",
|
||||
"state_topic": "zigbee2mqtt/lock/state",
|
||||
"command_topic": "zigbee2mqtt/lock/set/state",
|
||||
"optimistic": true
|
||||
}
|
||||
""");
|
||||
|
||||
assertThat(component.channels.size(), is(2));
|
||||
assertThat(component.getName(), is("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", "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.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("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 testNoStateTopicIsOptimistic() 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" \
|
||||
}\
|
||||
{
|
||||
"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
|
||||
|
||||
assertThat(component.channels.size(), is(1));
|
||||
assertThat(component.getName(), is("lock"));
|
||||
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);
|
||||
|
||||
assertChannel(component, Lock.SWITCH_CHANNEL_ID, "zigbee2mqtt/lock/state", "zigbee2mqtt/lock/set/state", "lock",
|
||||
OnOffValue.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);
|
||||
|
||||
component.getChannel(Lock.SWITCH_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
|
||||
assertPublished("zigbee2mqtt/lock/set/state", "UNLOCK_");
|
||||
component.getChannel(Lock.SWITCH_CHANNEL_ID).getState().publishValue(OnOffType.ON);
|
||||
assertPublished("zigbee2mqtt/lock/set/state", "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() {
|
||||
// @formatter:off
|
||||
publishMessage(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" \
|
||||
}\
|
||||
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
|
||||
}
|
||||
""");
|
||||
// @formatter:on
|
||||
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user