[mqtt.homeassistant] Improve Cover support (#15875)
* [mqtt.homeassistant] improve Cover support * Add support for covers that report position * Handle when command and state values for OPEN/CLOSE/STOP differ (as they do by default) * Expose the full cover state, since it can have tell you if the cover is moving or not * Handle covers that have a position only, but not a state * add constants to clarify up/down values * Be sure to parse percents from strings in RollshutterValue --------- Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
parent
0aab1f5818
commit
73559be058
@ -36,9 +36,46 @@ import org.openhab.core.types.Command;
|
|||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class RollershutterValue extends Value {
|
public class RollershutterValue extends Value {
|
||||||
private final @Nullable String upString;
|
// openHAB interprets open rollershutters as 0, and closed as 100
|
||||||
private final @Nullable String downString;
|
private static final String UP_VALUE = "0";
|
||||||
private final String stopString;
|
private static final String DOWN_VALUE = "100";
|
||||||
|
// other devices may interpret it the opposite, so we need to be able
|
||||||
|
// to invert it
|
||||||
|
private static final String INVERTED_UP_VALUE = DOWN_VALUE;
|
||||||
|
private static final String INVERTED_DOWN_VALUE = UP_VALUE;
|
||||||
|
|
||||||
|
private final @Nullable String upCommandString;
|
||||||
|
private final @Nullable String downCommandString;
|
||||||
|
private final @Nullable String stopCommandString;
|
||||||
|
private final @Nullable String upStateString;
|
||||||
|
private final @Nullable String downStateString;
|
||||||
|
private final boolean inverted;
|
||||||
|
private final boolean transformExtentsToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new rollershutter value.
|
||||||
|
*
|
||||||
|
* @param upCommandString The UP command string.
|
||||||
|
* @param downCommandString The DOWN command string.
|
||||||
|
* @param stopCommandString The STOP command string.
|
||||||
|
* @param upStateString The UP value string. This will be compared to MQTT messages.
|
||||||
|
* @param downStateString The DOWN value string. This will be compared to MQTT messages.
|
||||||
|
* @param inverted Whether to invert 0-100/100-0
|
||||||
|
* @param transformExtentsToString Whether 0/100 will be sent as UP/DOWN
|
||||||
|
*/
|
||||||
|
public RollershutterValue(@Nullable String upCommandString, @Nullable String downCommandString,
|
||||||
|
@Nullable String stopCommandString, @Nullable String upStateString, @Nullable String downStateString,
|
||||||
|
boolean inverted, boolean transformExtentsToString) {
|
||||||
|
super(CoreItemFactory.ROLLERSHUTTER,
|
||||||
|
List.of(UpDownType.class, StopMoveType.class, PercentType.class, StringType.class));
|
||||||
|
this.upCommandString = upCommandString;
|
||||||
|
this.downCommandString = downCommandString;
|
||||||
|
this.stopCommandString = stopCommandString;
|
||||||
|
this.upStateString = upStateString;
|
||||||
|
this.downStateString = downStateString;
|
||||||
|
this.inverted = inverted;
|
||||||
|
this.transformExtentsToString = transformExtentsToString;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new rollershutter value.
|
* Creates a new rollershutter value.
|
||||||
@ -48,17 +85,13 @@ public class RollershutterValue extends Value {
|
|||||||
* @param stopString The STOP value string. This will be compared to MQTT messages.
|
* @param stopString The STOP value string. This will be compared to MQTT messages.
|
||||||
*/
|
*/
|
||||||
public RollershutterValue(@Nullable String upString, @Nullable String downString, @Nullable String stopString) {
|
public RollershutterValue(@Nullable String upString, @Nullable String downString, @Nullable String stopString) {
|
||||||
super(CoreItemFactory.ROLLERSHUTTER,
|
this(upString, downString, stopString, upString, downString, false, true);
|
||||||
List.of(UpDownType.class, StopMoveType.class, PercentType.class, StringType.class));
|
|
||||||
this.upString = upString;
|
|
||||||
this.downString = downString;
|
|
||||||
this.stopString = stopString == null ? StopMoveType.STOP.name() : stopString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private Command parseType(Command command, @Nullable String upString, @Nullable String downString)
|
||||||
public Command parseCommand(Command command) throws IllegalArgumentException {
|
throws IllegalArgumentException {
|
||||||
if (command instanceof StopMoveType) {
|
if (command instanceof StopMoveType) {
|
||||||
if (command == StopMoveType.STOP) {
|
if (command == StopMoveType.STOP && stopCommandString != null) {
|
||||||
return command;
|
return command;
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException(command.toString() + " is not a valid command for MQTT.");
|
throw new IllegalArgumentException(command.toString() + " is not a valid command for MQTT.");
|
||||||
@ -68,12 +101,14 @@ public class RollershutterValue extends Value {
|
|||||||
if (upString != null) {
|
if (upString != null) {
|
||||||
return command;
|
return command;
|
||||||
} else {
|
} else {
|
||||||
|
// Do not handle inversion here. See parseCommand below
|
||||||
return PercentType.ZERO;
|
return PercentType.ZERO;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (downString != null) {
|
if (downString != null) {
|
||||||
return command;
|
return command;
|
||||||
} else {
|
} else {
|
||||||
|
// Do not handle inversion here. See parseCommand below
|
||||||
return PercentType.HUNDRED;
|
return PercentType.HUNDRED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,43 +120,70 @@ public class RollershutterValue extends Value {
|
|||||||
return UpDownType.UP;
|
return UpDownType.UP;
|
||||||
} else if (updatedValue.equals(downString)) {
|
} else if (updatedValue.equals(downString)) {
|
||||||
return UpDownType.DOWN;
|
return UpDownType.DOWN;
|
||||||
} else if (updatedValue.equals(stopString)) {
|
} else if (updatedValue.equals(stopCommandString)) {
|
||||||
return StopMoveType.STOP;
|
return StopMoveType.STOP;
|
||||||
|
} else {
|
||||||
|
return PercentType.valueOf(updatedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("Cannot call parseCommand() with " + command.toString());
|
throw new IllegalStateException("Cannot call parseCommand() with " + command.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Command parseCommand(Command command) throws IllegalArgumentException {
|
||||||
|
// Do not handle inversion in this code path. parseCommand might be called
|
||||||
|
// multiple times when sending a command TO an MQTT topic. The inversion is
|
||||||
|
// handled _only_ in getMQTTpublishValue
|
||||||
|
return parseType(command, upCommandString, downCommandString);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Command parseMessage(Command command) throws IllegalArgumentException {
|
||||||
|
command = parseType(command, upStateString, downStateString);
|
||||||
|
if (inverted && command instanceof PercentType percentType) {
|
||||||
|
return new PercentType(100 - percentType.intValue());
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMQTTpublishValue(Command command, @Nullable String pattern) {
|
public String getMQTTpublishValue(Command command, @Nullable String pattern) {
|
||||||
final String upString = this.upString;
|
return getMQTTpublishValue(command, transformExtentsToString);
|
||||||
final String downString = this.downString;
|
}
|
||||||
final String stopString = this.stopString;
|
|
||||||
|
public String getMQTTpublishValue(Command command, boolean transformExtentsToString) {
|
||||||
|
final String upCommandString = this.upCommandString;
|
||||||
|
final String downCommandString = this.downCommandString;
|
||||||
|
final String stopCommandString = this.stopCommandString;
|
||||||
if (command == UpDownType.UP) {
|
if (command == UpDownType.UP) {
|
||||||
if (upString != null) {
|
if (upCommandString != null) {
|
||||||
return upString;
|
return upCommandString;
|
||||||
} else {
|
} else {
|
||||||
return ((UpDownType) command).name();
|
return (inverted ? INVERTED_UP_VALUE : UP_VALUE);
|
||||||
}
|
}
|
||||||
} else if (command == UpDownType.DOWN) {
|
} else if (command == UpDownType.DOWN) {
|
||||||
if (downString != null) {
|
if (downCommandString != null) {
|
||||||
return downString;
|
return downCommandString;
|
||||||
} else {
|
} else {
|
||||||
return ((UpDownType) command).name();
|
return (inverted ? INVERTED_DOWN_VALUE : DOWN_VALUE);
|
||||||
}
|
}
|
||||||
} else if (command == StopMoveType.STOP) {
|
} else if (command == StopMoveType.STOP) {
|
||||||
if (stopString != null) {
|
if (stopCommandString != null) {
|
||||||
return stopString;
|
return stopCommandString;
|
||||||
} else {
|
} else {
|
||||||
return ((StopMoveType) command).name();
|
return ((StopMoveType) command).name();
|
||||||
}
|
}
|
||||||
} else if (command instanceof PercentType percentage) {
|
} else if (command instanceof PercentType percentage) {
|
||||||
if (command.equals(PercentType.HUNDRED) && downString != null) {
|
if (transformExtentsToString && command.equals(PercentType.HUNDRED) && downCommandString != null) {
|
||||||
return downString;
|
return downCommandString;
|
||||||
} else if (command.equals(PercentType.ZERO) && upString != null) {
|
} else if (transformExtentsToString && command.equals(PercentType.ZERO) && upCommandString != null) {
|
||||||
return upString;
|
return upCommandString;
|
||||||
} else {
|
} else {
|
||||||
return String.valueOf(percentage.intValue());
|
int value = percentage.intValue();
|
||||||
|
if (inverted) {
|
||||||
|
value = 100 - value;
|
||||||
|
}
|
||||||
|
return String.valueOf(value);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Invalid command type for Rollershutter item");
|
throw new IllegalArgumentException("Invalid command type for Rollershutter item");
|
||||||
|
|||||||
@ -227,6 +227,39 @@ public class ValueTests {
|
|||||||
// Test formatting 0/100
|
// Test formatting 0/100
|
||||||
assertThat(v.getMQTTpublishValue(PercentType.ZERO, null), is("fancyON"));
|
assertThat(v.getMQTTpublishValue(PercentType.ZERO, null), is("fancyON"));
|
||||||
assertThat(v.getMQTTpublishValue(PercentType.HUNDRED, null), is("fancyOff"));
|
assertThat(v.getMQTTpublishValue(PercentType.HUNDRED, null), is("fancyOff"));
|
||||||
|
|
||||||
|
// Test parsing from MQTT
|
||||||
|
assertThat(v.parseMessage(new StringType("fancyON")), is(UpDownType.UP));
|
||||||
|
assertThat(v.parseMessage(new StringType("fancyOff")), is(UpDownType.DOWN));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rollershutterUpdateWithDiscreteCommandAndStateStrings() {
|
||||||
|
RollershutterValue v = new RollershutterValue("OPEN", "CLOSE", "STOP", "open", "closed", false, true);
|
||||||
|
// Test with UP/DOWN/STOP command
|
||||||
|
assertThat(v.parseCommand(UpDownType.UP), is(UpDownType.UP));
|
||||||
|
assertThat(v.getMQTTpublishValue(UpDownType.UP, null), is("OPEN"));
|
||||||
|
assertThat(v.parseCommand(UpDownType.DOWN), is(UpDownType.DOWN));
|
||||||
|
assertThat(v.getMQTTpublishValue(UpDownType.DOWN, null), is("CLOSE"));
|
||||||
|
assertThat(v.parseCommand(StopMoveType.STOP), is(StopMoveType.STOP));
|
||||||
|
assertThat(v.getMQTTpublishValue(StopMoveType.STOP, null), is("STOP"));
|
||||||
|
|
||||||
|
// Test with custom string
|
||||||
|
assertThat(v.parseCommand(new StringType("OPEN")), is(UpDownType.UP));
|
||||||
|
assertThat(v.parseCommand(new StringType("CLOSE")), is(UpDownType.DOWN));
|
||||||
|
|
||||||
|
// Test with exact percent
|
||||||
|
Command command = new PercentType(27);
|
||||||
|
assertThat(v.parseCommand((Command) command), is(command));
|
||||||
|
assertThat(v.getMQTTpublishValue(command, null), is("27"));
|
||||||
|
|
||||||
|
// Test formatting 0/100
|
||||||
|
assertThat(v.getMQTTpublishValue(PercentType.ZERO, null), is("OPEN"));
|
||||||
|
assertThat(v.getMQTTpublishValue(PercentType.HUNDRED, null), is("CLOSE"));
|
||||||
|
|
||||||
|
// Test parsing from MQTT
|
||||||
|
assertThat(v.parseMessage(new StringType("open")), is(UpDownType.UP));
|
||||||
|
assertThat(v.parseMessage(new StringType("closed")), is(UpDownType.DOWN));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -19,6 +20,7 @@ import java.util.TreeMap;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.stream.Collectors;
|
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;
|
||||||
@ -26,7 +28,6 @@ import org.openhab.binding.mqtt.generic.AvailabilityTracker;
|
|||||||
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
|
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
|
||||||
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
|
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
|
||||||
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
|
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
|
||||||
import org.openhab.binding.mqtt.generic.utils.FutureCollector;
|
|
||||||
import org.openhab.binding.mqtt.generic.values.Value;
|
import org.openhab.binding.mqtt.generic.values.Value;
|
||||||
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
|
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
||||||
@ -66,6 +67,8 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
|||||||
|
|
||||||
// Channels and configuration
|
// Channels and configuration
|
||||||
protected final Map<String, ComponentChannel> channels = new TreeMap<>();
|
protected final Map<String, ComponentChannel> channels = new TreeMap<>();
|
||||||
|
protected final List<ComponentChannel> hiddenChannels = new ArrayList<>();
|
||||||
|
|
||||||
// The hash code ({@link String#hashCode()}) of the configuration string
|
// The hash code ({@link String#hashCode()}) of the configuration string
|
||||||
// Used to determine if a component has changed.
|
// Used to determine if a component has changed.
|
||||||
protected final int configHash;
|
protected final int configHash;
|
||||||
@ -155,8 +158,9 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
|||||||
*/
|
*/
|
||||||
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
|
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
|
||||||
int timeout) {
|
int timeout) {
|
||||||
return channels.values().stream().map(cChannel -> cChannel.start(connection, scheduler, timeout))
|
return Stream.concat(channels.values().stream(), hiddenChannels.stream())
|
||||||
.collect(FutureCollector.allOf());
|
.map(v -> v.start(connection, scheduler, timeout)) //
|
||||||
|
.reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -166,7 +170,10 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
|
|||||||
* exceptionally on errors.
|
* exceptionally on errors.
|
||||||
*/
|
*/
|
||||||
public CompletableFuture<@Nullable Void> stop() {
|
public CompletableFuture<@Nullable Void> stop() {
|
||||||
return channels.values().stream().map(ComponentChannel::stop).collect(FutureCollector.allOf());
|
return Stream.concat(channels.values().stream(), hiddenChannels.stream()) //
|
||||||
|
.filter(Objects::nonNull) //
|
||||||
|
.map(ComponentChannel::stop) //
|
||||||
|
.reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -15,20 +15,29 @@ 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.values.RollershutterValue;
|
import org.openhab.binding.mqtt.generic.values.RollershutterValue;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
||||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||||
|
import org.openhab.core.library.types.StopMoveType;
|
||||||
|
import org.openhab.core.library.types.StringType;
|
||||||
|
import org.openhab.core.library.types.UpDownType;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A MQTT Cover component, following the https://www.home-assistant.io/components/cover.mqtt/ specification.
|
* A MQTT Cover component, following the https://www.home-assistant.io/integrations/cover.mqtt specification.
|
||||||
*
|
*
|
||||||
* Only Open/Close/Stop works so far.
|
* Supports reporting state and/or position, and commanding OPEN/CLOSE/STOP
|
||||||
|
*
|
||||||
|
* Does not yet support tilt or covers that don't go from 0-100.
|
||||||
*
|
*
|
||||||
* @author David Graeff - Initial contribution
|
* @author David Graeff - Initial contribution
|
||||||
|
* @author Cody Cutrer - Add support for position and discrete state strings
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
|
public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
|
||||||
public static final String SWITCH_CHANNEL_ID = "cover"; // Randomly chosen channel "ID"
|
public static final String COVER_CHANNEL_ID = "cover";
|
||||||
|
public static final String STATE_CHANNEL_ID = "state";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration class for MQTT component
|
* Configuration class for MQTT component
|
||||||
@ -48,18 +57,97 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
|
|||||||
protected String payloadClose = "CLOSE";
|
protected String payloadClose = "CLOSE";
|
||||||
@SerializedName("payload_stop")
|
@SerializedName("payload_stop")
|
||||||
protected String payloadStop = "STOP";
|
protected String payloadStop = "STOP";
|
||||||
|
@SerializedName("position_closed")
|
||||||
|
protected int positionClosed = 0;
|
||||||
|
@SerializedName("position_open")
|
||||||
|
protected int positionOpen = 100;
|
||||||
|
@SerializedName("position_template")
|
||||||
|
protected @Nullable String positionTemplate;
|
||||||
|
@SerializedName("position_topic")
|
||||||
|
protected @Nullable String positionTopic;
|
||||||
|
@SerializedName("set_position_template")
|
||||||
|
protected @Nullable String setPositionTemplate;
|
||||||
|
@SerializedName("set_position_topic")
|
||||||
|
protected @Nullable String setPositionTopic;
|
||||||
|
@SerializedName("state_closed")
|
||||||
|
protected String stateClosed = "closed";
|
||||||
|
@SerializedName("state_closing")
|
||||||
|
protected String stateClosing = "closing";
|
||||||
|
@SerializedName("state_open")
|
||||||
|
protected String stateOpen = "open";
|
||||||
|
@SerializedName("state_opening")
|
||||||
|
protected String stateOpening = "opening";
|
||||||
|
@SerializedName("state_stopped")
|
||||||
|
protected String stateStopped = "stopped";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
ComponentChannel stateChannel = null;
|
||||||
|
|
||||||
public Cover(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
public Cover(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
||||||
super(componentConfiguration, ChannelConfiguration.class);
|
super(componentConfiguration, ChannelConfiguration.class);
|
||||||
|
|
||||||
RollershutterValue value = new RollershutterValue(channelConfiguration.payloadOpen,
|
String stateTopic = channelConfiguration.stateTopic;
|
||||||
channelConfiguration.payloadClose, channelConfiguration.payloadStop);
|
|
||||||
|
|
||||||
buildChannel(SWITCH_CHANNEL_ID, value, getName(), componentConfiguration.getUpdateListener())
|
// State can indicate additional information than just
|
||||||
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
|
// the current position, so expose it as a separate channel
|
||||||
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
if (stateTopic != null) {
|
||||||
channelConfiguration.getQos())
|
TextValue value = new TextValue(new String[] { channelConfiguration.stateClosed,
|
||||||
.build();
|
channelConfiguration.stateClosing, channelConfiguration.stateOpen,
|
||||||
|
channelConfiguration.stateOpening, channelConfiguration.stateStopped });
|
||||||
|
buildChannel(STATE_CHANNEL_ID, value, "State", componentConfiguration.getUpdateListener())
|
||||||
|
.stateTopic(stateTopic).isAdvanced(true).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelConfiguration.commandTopic != null) {
|
||||||
|
hiddenChannels.add(stateChannel = buildChannel(STATE_CHANNEL_ID, new TextValue(), "State",
|
||||||
|
componentConfiguration.getUpdateListener())
|
||||||
|
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
||||||
|
channelConfiguration.getQos())
|
||||||
|
.build(false));
|
||||||
|
} else {
|
||||||
|
// no command topic. we need to make sure we send
|
||||||
|
// integers for open and close
|
||||||
|
channelConfiguration.payloadOpen = String.valueOf(channelConfiguration.positionOpen);
|
||||||
|
channelConfiguration.payloadClose = String.valueOf(channelConfiguration.positionClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will either have positionTopic or stateTopic.
|
||||||
|
// positionTopic is more useful, but if we only have stateTopic,
|
||||||
|
// still build a Rollershutter channel so that UP/DOWN/STOP
|
||||||
|
// commands can be sent
|
||||||
|
String rollershutterStateTopic = channelConfiguration.positionTopic;
|
||||||
|
String stateTemplate = channelConfiguration.positionTemplate;
|
||||||
|
if (rollershutterStateTopic == null) {
|
||||||
|
rollershutterStateTopic = stateTopic;
|
||||||
|
stateTemplate = channelConfiguration.getValueTemplate();
|
||||||
|
}
|
||||||
|
String rollershutterCommandTopic = channelConfiguration.setPositionTopic;
|
||||||
|
if (rollershutterCommandTopic == null) {
|
||||||
|
rollershutterCommandTopic = channelConfiguration.commandTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean inverted = channelConfiguration.positionOpen > channelConfiguration.positionClosed;
|
||||||
|
final RollershutterValue value = new RollershutterValue(channelConfiguration.payloadOpen,
|
||||||
|
channelConfiguration.payloadClose, channelConfiguration.payloadStop, channelConfiguration.stateOpen,
|
||||||
|
channelConfiguration.stateClosed, inverted, channelConfiguration.setPositionTopic == null);
|
||||||
|
|
||||||
|
buildChannel(COVER_CHANNEL_ID, value, "Cover", componentConfiguration.getUpdateListener())
|
||||||
|
.stateTopic(rollershutterStateTopic, stateTemplate)
|
||||||
|
.commandTopic(rollershutterCommandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos())
|
||||||
|
.commandFilter(command -> {
|
||||||
|
if (stateChannel == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If we have a state channel, and this is UP/DOWN/STOP, then
|
||||||
|
// we need to send the command to _that_ channel's topic, not
|
||||||
|
// the position topic.
|
||||||
|
if (command instanceof UpDownType || command instanceof StopMoveType) {
|
||||||
|
command = new StringType(value.getMQTTpublishValue(command, false));
|
||||||
|
stateChannel.getState().publishValue(command);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,12 +13,7 @@
|
|||||||
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
package org.openhab.binding.mqtt.homeassistant.internal.component;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
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;
|
||||||
@ -32,7 +27,6 @@ import org.openhab.binding.mqtt.generic.values.TextValue;
|
|||||||
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
|
||||||
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.UnsupportedComponentException;
|
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
|
||||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
|
||||||
import org.openhab.core.library.unit.Units;
|
import org.openhab.core.library.unit.Units;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
import org.openhab.core.thing.ChannelUID;
|
||||||
import org.openhab.core.types.Command;
|
import org.openhab.core.types.Command;
|
||||||
@ -249,7 +243,6 @@ public abstract class Light extends AbstractComponent<Light.ChannelConfiguration
|
|||||||
protected final @Nullable TextValue effectValue;
|
protected final @Nullable TextValue effectValue;
|
||||||
protected final ColorValue colorValue = new ColorValue(ColorMode.HSB, null, null, 100);
|
protected final ColorValue colorValue = new ColorValue(ColorMode.HSB, null, null, 100);
|
||||||
|
|
||||||
protected final List<ComponentChannel> hiddenChannels = new ArrayList<>();
|
|
||||||
protected final ChannelStateUpdateListener channelStateUpdateListener;
|
protected final ChannelStateUpdateListener channelStateUpdateListener;
|
||||||
|
|
||||||
public static Light create(ComponentFactory.ComponentConfiguration builder) throws UnsupportedComponentException {
|
public static Light create(ComponentFactory.ComponentConfiguration builder) throws UnsupportedComponentException {
|
||||||
@ -302,22 +295,6 @@ public abstract class Light extends AbstractComponent<Light.ChannelConfiguration
|
|||||||
|
|
||||||
protected abstract void buildChannels();
|
protected abstract void buildChannels();
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
|
|
||||||
int timeout) {
|
|
||||||
return Stream.concat(channels.values().stream(), hiddenChannels.stream()) //
|
|
||||||
.map(v -> v.start(connection, scheduler, timeout)) //
|
|
||||||
.reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletableFuture<@Nullable Void> stop() {
|
|
||||||
return Stream.concat(channels.values().stream(), hiddenChannels.stream()) //
|
|
||||||
.filter(Objects::nonNull) //
|
|
||||||
.map(ComponentChannel::stop) //
|
|
||||||
.reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postChannelCommand(ChannelUID channelUID, Command value) {
|
public void postChannelCommand(ChannelUID channelUID, Command value) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
|||||||
@ -20,8 +20,11 @@ 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.RollershutterValue;
|
import org.openhab.binding.mqtt.generic.values.RollershutterValue;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||||
import org.openhab.core.library.types.PercentType;
|
import org.openhab.core.library.types.PercentType;
|
||||||
import org.openhab.core.library.types.StopMoveType;
|
import org.openhab.core.library.types.StopMoveType;
|
||||||
|
import org.openhab.core.library.types.StringType;
|
||||||
|
import org.openhab.core.library.types.UpDownType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link Cover}
|
* Tests for {@link Cover}
|
||||||
@ -34,7 +37,7 @@ public class CoverTests extends AbstractComponentTests {
|
|||||||
|
|
||||||
@SuppressWarnings("null")
|
@SuppressWarnings("null")
|
||||||
@Test
|
@Test
|
||||||
public void test() throws InterruptedException {
|
public void testStateOnly() throws InterruptedException {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
|
||||||
"""
|
"""
|
||||||
@ -63,27 +66,135 @@ public class CoverTests extends AbstractComponentTests {
|
|||||||
""");
|
""");
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
assertThat(component.channels.size(), is(1));
|
assertThat(component.channels.size(), is(2));
|
||||||
assertThat(component.getName(), is("cover"));
|
assertThat(component.getName(), is("cover"));
|
||||||
|
|
||||||
assertChannel(component, Cover.SWITCH_CHANNEL_ID, "zigbee2mqtt/cover/state", "zigbee2mqtt/cover/set/state",
|
assertChannel(component, Cover.STATE_CHANNEL_ID, "zigbee2mqtt/cover/state", "", "State", TextValue.class);
|
||||||
"cover", RollershutterValue.class);
|
assertChannel(component, Cover.COVER_CHANNEL_ID, "zigbee2mqtt/cover/state", "zigbee2mqtt/cover/set/state",
|
||||||
|
"Cover", RollershutterValue.class);
|
||||||
|
|
||||||
publishMessage("zigbee2mqtt/cover/state", "100");
|
publishMessage("zigbee2mqtt/cover/state", "closed");
|
||||||
assertState(component, Cover.SWITCH_CHANNEL_ID, PercentType.HUNDRED);
|
assertState(component, Cover.COVER_CHANNEL_ID, UpDownType.DOWN);
|
||||||
publishMessage("zigbee2mqtt/cover/state", "0");
|
assertState(component, Cover.STATE_CHANNEL_ID, new StringType("closed"));
|
||||||
assertState(component, Cover.SWITCH_CHANNEL_ID, PercentType.ZERO);
|
publishMessage("zigbee2mqtt/cover/state", "open");
|
||||||
|
assertState(component, Cover.STATE_CHANNEL_ID, new StringType("open"));
|
||||||
|
assertState(component, Cover.COVER_CHANNEL_ID, UpDownType.UP);
|
||||||
|
|
||||||
component.getChannel(Cover.SWITCH_CHANNEL_ID).getState().publishValue(PercentType.ZERO);
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(UpDownType.UP);
|
||||||
assertPublished("zigbee2mqtt/cover/set/state", "OPEN_");
|
assertPublished("zigbee2mqtt/cover/set/state", "OPEN_");
|
||||||
component.getChannel(Cover.SWITCH_CHANNEL_ID).getState().publishValue(PercentType.HUNDRED);
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(UpDownType.DOWN);
|
||||||
assertPublished("zigbee2mqtt/cover/set/state", "CLOSE_");
|
assertPublished("zigbee2mqtt/cover/set/state", "CLOSE_");
|
||||||
component.getChannel(Cover.SWITCH_CHANNEL_ID).getState().publishValue(StopMoveType.STOP);
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(StopMoveType.STOP);
|
||||||
assertPublished("zigbee2mqtt/cover/set/state", "STOP_");
|
assertPublished("zigbee2mqtt/cover/set/state", "STOP_");
|
||||||
component.getChannel(Cover.SWITCH_CHANNEL_ID).getState().publishValue(PercentType.ZERO);
|
}
|
||||||
assertPublished("zigbee2mqtt/cover/set/state", "OPEN_", 2);
|
|
||||||
component.getChannel(Cover.SWITCH_CHANNEL_ID).getState().publishValue(StopMoveType.STOP);
|
@SuppressWarnings("null")
|
||||||
assertPublished("zigbee2mqtt/cover/set/state", "STOP_", 2);
|
@Test
|
||||||
|
public void testPositionAndState() throws InterruptedException {
|
||||||
|
// @formatter:off
|
||||||
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"dev_cla":"garage",
|
||||||
|
"pos_t":"esphome/single-car-gdo/cover/door/position/state",
|
||||||
|
"set_pos_t":"esphome/single-car-gdo/cover/door/position/command",
|
||||||
|
"name":"Door",
|
||||||
|
"stat_t":"esphome/single-car-gdo/cover/door/state",
|
||||||
|
"cmd_t":"esphome/single-car-gdo/cover/door/command",
|
||||||
|
"avty_t":"esphome/single-car-gdo/status",
|
||||||
|
"uniq_id":"78e36d645710-cover-d27845ad",
|
||||||
|
"dev":{
|
||||||
|
"ids":"78e36d645710",
|
||||||
|
"name":"Single Car Garage Door Opener",
|
||||||
|
"sw":"esphome v2023.10.4 Nov 7 2023, 16:19:39",
|
||||||
|
"mdl":"esp32dev",
|
||||||
|
"mf":"espressif"}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(2));
|
||||||
|
assertThat(component.getName(), is("Door"));
|
||||||
|
|
||||||
|
assertChannel(component, Cover.STATE_CHANNEL_ID, "esphome/single-car-gdo/cover/door/state", "", "State",
|
||||||
|
TextValue.class);
|
||||||
|
assertChannel(component, Cover.COVER_CHANNEL_ID, "esphome/single-car-gdo/cover/door/position/state",
|
||||||
|
"esphome/single-car-gdo/cover/door/position/command", "Cover", RollershutterValue.class);
|
||||||
|
|
||||||
|
publishMessage("esphome/single-car-gdo/cover/door/state", "closed");
|
||||||
|
assertState(component, Cover.STATE_CHANNEL_ID, new StringType("closed"));
|
||||||
|
publishMessage("esphome/single-car-gdo/cover/door/state", "open");
|
||||||
|
assertState(component, Cover.STATE_CHANNEL_ID, new StringType("open"));
|
||||||
|
publishMessage("esphome/single-car-gdo/cover/door/state", "opening");
|
||||||
|
assertState(component, Cover.STATE_CHANNEL_ID, new StringType("opening"));
|
||||||
|
|
||||||
|
publishMessage("esphome/single-car-gdo/cover/door/position/state", "100");
|
||||||
|
assertState(component, Cover.COVER_CHANNEL_ID, PercentType.ZERO);
|
||||||
|
publishMessage("esphome/single-car-gdo/cover/door/position/state", "40");
|
||||||
|
assertState(component, Cover.COVER_CHANNEL_ID, new PercentType(60));
|
||||||
|
publishMessage("esphome/single-car-gdo/cover/door/position/state", "0");
|
||||||
|
assertState(component, Cover.COVER_CHANNEL_ID, PercentType.HUNDRED);
|
||||||
|
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(PercentType.ZERO);
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/position/command", "100");
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(PercentType.HUNDRED);
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/position/command", "0");
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(StopMoveType.STOP);
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/command", "STOP");
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(UpDownType.UP);
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/command", "OPEN");
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(UpDownType.DOWN);
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/command", "CLOSE");
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(new PercentType(40));
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/position/command", "60");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
@Test
|
||||||
|
public void testPositionOnly() throws InterruptedException {
|
||||||
|
// @formatter:off
|
||||||
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"dev_cla":"garage",
|
||||||
|
"pos_t":"esphome/single-car-gdo/cover/door/position/state",
|
||||||
|
"set_pos_t":"esphome/single-car-gdo/cover/door/position/command",
|
||||||
|
"name":"Door",
|
||||||
|
"avty_t":"esphome/single-car-gdo/status",
|
||||||
|
"uniq_id":"78e36d645710-cover-d27845ad",
|
||||||
|
"dev":{
|
||||||
|
"ids":"78e36d645710",
|
||||||
|
"name":"Single Car Garage Door Opener",
|
||||||
|
"sw":"esphome v2023.10.4 Nov 7 2023, 16:19:39",
|
||||||
|
"mdl":"esp32dev",
|
||||||
|
"mf":"espressif"}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(1));
|
||||||
|
assertThat(component.getName(), is("Door"));
|
||||||
|
|
||||||
|
assertChannel(component, Cover.COVER_CHANNEL_ID, "esphome/single-car-gdo/cover/door/position/state",
|
||||||
|
"esphome/single-car-gdo/cover/door/position/command", "Cover", RollershutterValue.class);
|
||||||
|
|
||||||
|
publishMessage("esphome/single-car-gdo/cover/door/position/state", "100");
|
||||||
|
assertState(component, Cover.COVER_CHANNEL_ID, PercentType.ZERO);
|
||||||
|
publishMessage("esphome/single-car-gdo/cover/door/position/state", "40");
|
||||||
|
assertState(component, Cover.COVER_CHANNEL_ID, new PercentType(60));
|
||||||
|
publishMessage("esphome/single-car-gdo/cover/door/position/state", "0");
|
||||||
|
assertState(component, Cover.COVER_CHANNEL_ID, PercentType.HUNDRED);
|
||||||
|
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(PercentType.ZERO);
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/position/command", "100");
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(PercentType.HUNDRED);
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/position/command", "0");
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(UpDownType.UP);
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/position/command", "100", 2);
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(UpDownType.DOWN);
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/position/command", "0", 2);
|
||||||
|
component.getChannel(Cover.COVER_CHANNEL_ID).getState().publishValue(new PercentType(40));
|
||||||
|
assertPublished("esphome/single-car-gdo/cover/door/position/command", "60");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user