mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +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;
|
package org.openhab.binding.mqtt.generic.values;
|
||||||
|
|
||||||
|
import static java.util.function.Predicate.not;
|
||||||
|
|
||||||
import java.util.List;
|
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.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
@ -30,13 +35,13 @@ import org.openhab.core.types.CommandOption;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class OnOffValue extends Value {
|
public class OnOffValue extends Value {
|
||||||
private final String onState;
|
private final Set<String> onStates;
|
||||||
private final String offState;
|
private final Set<String> offStates;
|
||||||
private final String onCommand;
|
private final String onCommand;
|
||||||
private final String offCommand;
|
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() {
|
public OnOffValue() {
|
||||||
this(OnOffType.ON.name(), OnOffType.OFF.name());
|
this(OnOffType.ON.name(), OnOffType.OFF.name());
|
||||||
@ -45,10 +50,10 @@ public class OnOffValue extends Value {
|
|||||||
/**
|
/**
|
||||||
* Creates a new SWITCH On/Off 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 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.
|
* @param offValue The OFF value string. This will be compared to MQTT messages. Defaults to "OFF".
|
||||||
*/
|
*/
|
||||||
public OnOffValue(@Nullable String onValue, @Nullable String offValue) {
|
public OnOffValue(@Nullable String onValue, @Nullable String offValue) {
|
||||||
this(onValue, offValue, onValue, offValue);
|
this(onValue, offValue, onValue, offValue);
|
||||||
@ -57,18 +62,37 @@ public class OnOffValue extends Value {
|
|||||||
/**
|
/**
|
||||||
* Creates a new SWITCH On/Off value.
|
* Creates a new SWITCH On/Off value.
|
||||||
*
|
*
|
||||||
* @param onState The ON value string. This will be compared to MQTT messages.
|
* @param onState The ON value string. This will be compared to MQTT messages. Defaults to onCommand if null, or
|
||||||
* @param offState The OFF value string. This will be compared to MQTT messages.
|
* "ON" if both are null.
|
||||||
* @param onCommand The ON value string. This will be send in MQTT messages.
|
* @param offState The OFF value string. This will be compared to MQTT messages. Defaults to offComamand if null, or
|
||||||
* @param offCommand The OFF value string. This will be send in MQTT messages.
|
* "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,
|
public OnOffValue(@Nullable String onState, @Nullable String offState, @Nullable String onCommand,
|
||||||
@Nullable String offCommand) {
|
@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));
|
super(CoreItemFactory.SWITCH, List.of(OnOffType.class, StringType.class));
|
||||||
this.onState = onState == null ? OnOffType.ON.name() : onState;
|
this.onStates = Stream.of(onStates).filter(not(String::isBlank)).collect(Collectors.toSet());
|
||||||
this.offState = offState == null ? OnOffType.OFF.name() : offState;
|
this.offStates = Stream.of(offStates).filter(not(String::isBlank)).collect(Collectors.toSet());
|
||||||
this.onCommand = onCommand == null ? OnOffType.ON.name() : onCommand;
|
this.onCommand = onCommand;
|
||||||
this.offCommand = offCommand == null ? OnOffType.OFF.name() : offCommand;
|
this.offCommand = offCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -77,9 +101,9 @@ public class OnOffValue extends Value {
|
|||||||
return onOffCommand;
|
return onOffCommand;
|
||||||
} else {
|
} else {
|
||||||
final String updatedValue = command.toString();
|
final String updatedValue = command.toString();
|
||||||
if (onState.equals(updatedValue)) {
|
if (onStates.contains(updatedValue)) {
|
||||||
return OnOffType.ON;
|
return OnOffType.ON;
|
||||||
} else if (offState.equals(updatedValue)) {
|
} else if (offStates.contains(updatedValue)) {
|
||||||
return OnOffType.OFF;
|
return OnOffType.OFF;
|
||||||
} else {
|
} else {
|
||||||
return OnOffType.valueOf(updatedValue);
|
return OnOffType.valueOf(updatedValue);
|
||||||
@ -104,4 +128,15 @@ public class OnOffValue extends Value {
|
|||||||
builder = builder.withCommandOption(new CommandOption(offCommand, offCommand));
|
builder = builder.withCommandOption(new CommandOption(offCommand, offCommand));
|
||||||
return builder;
|
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.Command;
|
||||||
import org.openhab.core.types.CommandDescriptionBuilder;
|
import org.openhab.core.types.CommandDescriptionBuilder;
|
||||||
import org.openhab.core.types.CommandOption;
|
import org.openhab.core.types.CommandOption;
|
||||||
|
import org.openhab.core.types.State;
|
||||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||||
import org.openhab.core.types.StateOption;
|
import org.openhab.core.types.StateOption;
|
||||||
|
|
||||||
@ -37,14 +38,17 @@ import org.openhab.core.types.StateOption;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class TextValue extends Value {
|
public class TextValue extends Value {
|
||||||
private final @Nullable Set<String> states;
|
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
|
* @param states Allowed states. Empty states are filtered out. If the resulting set is empty, all string values
|
||||||
* will be allowed.
|
* 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));
|
super(CoreItemFactory.STRING, List.of(StringType.class));
|
||||||
Set<String> s = Stream.of(states).filter(not(String::isBlank)).collect(Collectors.toSet());
|
Set<String> s = Stream.of(states).filter(not(String::isBlank)).collect(Collectors.toSet());
|
||||||
if (!s.isEmpty()) {
|
if (!s.isEmpty()) {
|
||||||
@ -52,15 +56,42 @@ public class TextValue extends Value {
|
|||||||
} else {
|
} else {
|
||||||
this.states = null;
|
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() {
|
public TextValue() {
|
||||||
super(CoreItemFactory.STRING, List.of(StringType.class));
|
super(CoreItemFactory.STRING, List.of(StringType.class));
|
||||||
this.states = null;
|
this.states = null;
|
||||||
|
this.commands = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StringType parseCommand(Command command) throws IllegalArgumentException {
|
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;
|
final Set<String> states = this.states;
|
||||||
String valueStr = command.toString();
|
String valueStr = command.toString();
|
||||||
if (states != null && !states.contains(valueStr)) {
|
if (states != null && !states.contains(valueStr)) {
|
||||||
@ -91,8 +122,8 @@ public class TextValue extends Value {
|
|||||||
@Override
|
@Override
|
||||||
public CommandDescriptionBuilder createCommandDescription() {
|
public CommandDescriptionBuilder createCommandDescription() {
|
||||||
CommandDescriptionBuilder builder = super.createCommandDescription();
|
CommandDescriptionBuilder builder = super.createCommandDescription();
|
||||||
final Set<String> commands = this.states;
|
final Set<String> commands = this.commands;
|
||||||
if (states != null) {
|
if (commands != null) {
|
||||||
for (String command : commands) {
|
for (String command : commands) {
|
||||||
builder = builder.withCommandOption(new CommandOption(command, command));
|
builder = builder.withCommandOption(new CommandOption(command, command));
|
||||||
}
|
}
|
||||||
|
@ -139,6 +139,16 @@ public class ValueTests {
|
|||||||
assertThat(v.getMQTTpublishValue(OnOffType.ON, "=%s"), is("=fancyON"));
|
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
|
@Test
|
||||||
public void openCloseUpdate() {
|
public void openCloseUpdate() {
|
||||||
OpenCloseValue v = new OpenCloseValue("fancyON", "fancyOff");
|
OpenCloseValue v = new OpenCloseValue("fancyON", "fancyOff");
|
||||||
|
@ -60,7 +60,7 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
|||||||
private static final String JINJA_PREFIX = "JINJA:";
|
private static final String JINJA_PREFIX = "JINJA:";
|
||||||
|
|
||||||
// Component location fields
|
// Component location fields
|
||||||
private final ComponentConfiguration componentConfiguration;
|
protected final ComponentConfiguration componentConfiguration;
|
||||||
protected final @Nullable ChannelGroupTypeUID channelGroupTypeUID;
|
protected final @Nullable ChannelGroupTypeUID channelGroupTypeUID;
|
||||||
protected final @Nullable ChannelGroupUID channelGroupUID;
|
protected final @Nullable ChannelGroupUID channelGroupUID;
|
||||||
protected final HaID haID;
|
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.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
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.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.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;
|
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 David Graeff - Initial contribution
|
||||||
|
* @author Cody Cutrer - Support OPEN, full state, and optimistic mode.
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
|
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
|
* Configuration class for MQTT component
|
||||||
@ -39,30 +46,99 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
|
|||||||
|
|
||||||
protected boolean optimistic = false;
|
protected boolean optimistic = false;
|
||||||
|
|
||||||
|
@SerializedName("command_topic")
|
||||||
|
protected @Nullable String commandTopic;
|
||||||
@SerializedName("state_topic")
|
@SerializedName("state_topic")
|
||||||
protected String stateTopic = "";
|
protected String stateTopic = "";
|
||||||
@SerializedName("payload_lock")
|
@SerializedName("payload_lock")
|
||||||
protected String payloadLock = "LOCK";
|
protected String payloadLock = "LOCK";
|
||||||
@SerializedName("payload_unlock")
|
@SerializedName("payload_unlock")
|
||||||
protected String payloadUnlock = "UNLOCK";
|
protected String payloadUnlock = "UNLOCK";
|
||||||
@SerializedName("command_topic")
|
@SerializedName("payload_open")
|
||||||
protected @Nullable String commandTopic;
|
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) {
|
public Lock(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
||||||
super(componentConfiguration, ChannelConfiguration.class);
|
super(componentConfiguration, ChannelConfiguration.class);
|
||||||
|
|
||||||
// We do not support all HomeAssistant quirks
|
this.optimistic = channelConfiguration.optimistic || channelConfiguration.stateTopic.isBlank();
|
||||||
if (channelConfiguration.optimistic && !channelConfiguration.stateTopic.isBlank()) {
|
|
||||||
throw new ConfigurationException("Component:Lock does not support forced optimistic mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
buildChannel(SWITCH_CHANNEL_ID,
|
lockValue = new OnOffValue(new String[] { channelConfiguration.stateLocked },
|
||||||
new OnOffValue(channelConfiguration.payloadLock, channelConfiguration.payloadUnlock), getName(),
|
new String[] { channelConfiguration.stateUnlocked, channelConfiguration.stateLocking,
|
||||||
componentConfiguration.getUpdateListener())
|
channelConfiguration.stateUnlocking, channelConfiguration.stateJammed },
|
||||||
|
channelConfiguration.payloadLock, channelConfiguration.payloadUnlock);
|
||||||
|
|
||||||
|
buildChannel(LOCK_CHANNEL_ID, lockValue, "Lock", componentConfiguration.getUpdateListener())
|
||||||
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
|
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
|
||||||
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
||||||
channelConfiguration.getQos())
|
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");
|
throw new ConfigurationException("Component:Switch does not support forced optimistic mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
String stateOn = channelConfiguration.stateOn != null ? channelConfiguration.stateOn
|
OnOffValue value = new OnOffValue(channelConfiguration.stateOn, channelConfiguration.stateOff,
|
||||||
: channelConfiguration.payloadOn;
|
channelConfiguration.payloadOn, channelConfiguration.payloadOff);
|
||||||
String stateOff = channelConfiguration.stateOff != null ? channelConfiguration.stateOff
|
|
||||||
: channelConfiguration.payloadOff;
|
|
||||||
|
|
||||||
OnOffValue value = new OnOffValue(stateOn, stateOff, channelConfiguration.payloadOn,
|
|
||||||
channelConfiguration.payloadOff);
|
|
||||||
|
|
||||||
buildChannel(SWITCH_CHANNEL_ID, value, "state", componentConfiguration.getUpdateListener())
|
buildChannel(SWITCH_CHANNEL_ID, value, "state", componentConfiguration.getUpdateListener())
|
||||||
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
|
.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.CoreMatchers.is;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
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.OnOffType;
|
||||||
|
import org.openhab.core.library.types.StringType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link Lock}
|
* Tests for {@link Lock}
|
||||||
@ -34,83 +37,223 @@ public class LockTests extends AbstractComponentTests {
|
|||||||
@SuppressWarnings("null")
|
@SuppressWarnings("null")
|
||||||
@Test
|
@Test
|
||||||
public void test() throws InterruptedException {
|
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
|
// @formatter:off
|
||||||
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
|
||||||
"""
|
"""
|
||||||
{ \
|
{
|
||||||
"availability": [ \
|
"availability": [
|
||||||
{ \
|
{
|
||||||
"topic": "zigbee2mqtt/bridge/state" \
|
"topic": "zigbee2mqtt/bridge/state"
|
||||||
} \
|
}
|
||||||
], \
|
],
|
||||||
"device": { \
|
"device": {
|
||||||
"identifiers": [ \
|
"identifiers": [
|
||||||
"zigbee2mqtt_0x0000000000000000" \
|
"zigbee2mqtt_0x0000000000000000"
|
||||||
], \
|
],
|
||||||
"manufacturer": "Locks inc", \
|
"manufacturer": "Locks inc",
|
||||||
"model": "Lock", \
|
"model": "Lock",
|
||||||
"name": "LockBlower", \
|
"name": "LockBlower",
|
||||||
"sw_version": "Zigbee2MQTT 1.18.2" \
|
"sw_version": "Zigbee2MQTT 1.18.2"
|
||||||
}, \
|
},
|
||||||
"name": "lock", \
|
"name": "lock",
|
||||||
"payload_unlock": "UNLOCK_", \
|
"command_topic": "zigbee2mqtt/lock/set/state"
|
||||||
"payload_lock": "LOCK_", \
|
}
|
||||||
"state_topic": "zigbee2mqtt/lock/state", \
|
|
||||||
"command_topic": "zigbee2mqtt/lock/set/state" \
|
|
||||||
}\
|
|
||||||
""");
|
""");
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
assertThat(component.channels.size(), is(1));
|
component.getChannel(Lock.LOCK_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
|
||||||
assertThat(component.getName(), is("lock"));
|
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",
|
assertThrows(IllegalArgumentException.class,
|
||||||
OnOffValue.class);
|
() -> component.getChannel(Lock.STATE_CHANNEL_ID).getState().publishValue(new StringType("OPEN")));
|
||||||
|
assertState(component, Lock.STATE_CHANNEL_ID, new StringType("LOCKED"));
|
||||||
publishMessage("zigbee2mqtt/lock/state", "LOCK_");
|
assertState(component, Lock.LOCK_CHANNEL_ID, OnOffType.ON);
|
||||||
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_");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
@Test
|
@Test
|
||||||
public void forceOptimisticIsNotSupported() {
|
public void testOpennable() throws InterruptedException {
|
||||||
// @formatter:off
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||||
publishMessage(configTopicToMqtt(CONFIG_TOPIC),
|
{
|
||||||
"""
|
"availability": [
|
||||||
{ \
|
{
|
||||||
"availability": [ \
|
"topic": "zigbee2mqtt/bridge/state"
|
||||||
{ \
|
}
|
||||||
"topic": "zigbee2mqtt/bridge/state" \
|
],
|
||||||
} \
|
"device": {
|
||||||
], \
|
"identifiers": [
|
||||||
"device": { \
|
"zigbee2mqtt_0x0000000000000000"
|
||||||
"identifiers": [ \
|
],
|
||||||
"zigbee2mqtt_0x0000000000000000" \
|
"manufacturer": "Locks inc",
|
||||||
], \
|
"model": "Lock",
|
||||||
"manufacturer": "Locks inc", \
|
"name": "LockBlower",
|
||||||
"model": "Lock", \
|
"sw_version": "Zigbee2MQTT 1.18.2"
|
||||||
"name": "LockBlower", \
|
},
|
||||||
"sw_version": "Zigbee2MQTT 1.18.2" \
|
"name": "lock",
|
||||||
}, \
|
"payload_open": "OPEN",
|
||||||
"name": "lock", \
|
"state_topic": "zigbee2mqtt/lock/state",
|
||||||
"payload_unlock": "UNLOCK_", \
|
"command_topic": "zigbee2mqtt/lock/set/state",
|
||||||
"payload_lock": "LOCK_", \
|
"optimistic": true
|
||||||
"optimistic": "true", \
|
}
|
||||||
"state_topic": "zigbee2mqtt/lock/state", \
|
|
||||||
"command_topic": "zigbee2mqtt/lock/set/state" \
|
|
||||||
}\
|
|
||||||
""");
|
""");
|
||||||
// @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
|
@Override
|
||||||
|
Loading…
Reference in New Issue
Block a user