Skip to content

Commit

Permalink
[mqtt.homeassistant] improve Cover support
Browse files Browse the repository at this point in the history
 * 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
  • Loading branch information
ccutrer committed Nov 9, 2023
1 parent fef43d3 commit 37a0122
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,38 @@
*/
@NonNullByDefault
public class RollershutterValue extends Value {
private final @Nullable String upString;
private final @Nullable String downString;
private final String stopString;
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.
Expand All @@ -48,17 +77,13 @@ public class RollershutterValue extends Value {
* @param stopString The STOP value string. This will be compared to MQTT messages.
*/
public RollershutterValue(@Nullable String upString, @Nullable String downString, @Nullable String stopString) {
super(CoreItemFactory.ROLLERSHUTTER,
List.of(UpDownType.class, StopMoveType.class, PercentType.class, StringType.class));
this.upString = upString;
this.downString = downString;
this.stopString = stopString == null ? StopMoveType.STOP.name() : stopString;
this(upString, downString, stopString, upString, downString, false, true);
}

@Override
public Command parseCommand(Command command) throws IllegalArgumentException {
private Command parseType(Command command, @Nullable String upString, @Nullable String downString)
throws IllegalArgumentException {
if (command instanceof StopMoveType) {
if (command == StopMoveType.STOP) {
if (command == StopMoveType.STOP && stopCommandString != null) {
return command;
} else {
throw new IllegalArgumentException(command.toString() + " is not a valid command for MQTT.");
Expand All @@ -85,43 +110,65 @@ public Command parseCommand(Command command) throws IllegalArgumentException {
return UpDownType.UP;
} else if (updatedValue.equals(downString)) {
return UpDownType.DOWN;
} else if (updatedValue.equals(stopString)) {
} else if (updatedValue.equals(stopCommandString)) {
return StopMoveType.STOP;
}
}
throw new IllegalStateException("Cannot call parseCommand() with " + command.toString());
}

@Override
public Command parseCommand(Command command) throws IllegalArgumentException {
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
public String getMQTTpublishValue(Command command, @Nullable String pattern) {
final String upString = this.upString;
final String downString = this.downString;
final String stopString = this.stopString;
return getMQTTpublishValue(command, transformExtentsToString);
}

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 (upString != null) {
return upString;
if (upCommandString != null) {
return upCommandString;
} else {
return ((UpDownType) command).name();
return (inverted ? "100" : "0");
}
} else if (command == UpDownType.DOWN) {
if (downString != null) {
return downString;
if (downCommandString != null) {
return downCommandString;
} else {
return ((UpDownType) command).name();
return (inverted ? "0" : "100");
}
} else if (command == StopMoveType.STOP) {
if (stopString != null) {
return stopString;
if (stopCommandString != null) {
return stopCommandString;
} else {
return ((StopMoveType) command).name();
}
} else if (command instanceof PercentType percentage) {
if (command.equals(PercentType.HUNDRED) && downString != null) {
return downString;
} else if (command.equals(PercentType.ZERO) && upString != null) {
return upString;
if (transformExtentsToString && command.equals(PercentType.HUNDRED) && downCommandString != null) {
return downCommandString;
} else if (transformExtentsToString && command.equals(PercentType.ZERO) && upCommandString != null) {
return upCommandString;
} else {
return String.valueOf(percentage.intValue());
int value = percentage.intValue();
if (inverted) {
value = 100 - value;
}
return String.valueOf(value);
}
} else {
throw new IllegalArgumentException("Invalid command type for Rollershutter item");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,19 @@ public class TextValue extends Value {
* @param states Allowed states. Empty states are filtered out. If the resulting set is empty, all string values
* will be allowed.
*/
public TextValue(String[] states) {
public TextValue(Set<String> states) {
super(CoreItemFactory.STRING, List.of(StringType.class));
Set<String> s = Stream.of(states).filter(not(String::isBlank)).collect(Collectors.toSet());
if (!s.isEmpty()) {
this.states = s;
if (!states.isEmpty()) {
this.states = states;
} else {
this.states = null;
}
}

public TextValue(String[] states) {
this(Stream.of(states).filter(not(String::isBlank)).collect(Collectors.toSet()));
}

public TextValue() {
super(CoreItemFactory.STRING, List.of(StringType.class));
this.states = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,39 @@ public void rollershutterUpdateWithStrings() {
// Test formatting 0/100
assertThat(v.getMQTTpublishValue(PercentType.ZERO, null), is("fancyON"));
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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.MqttChannelTypeProvider;
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.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
Expand Down Expand Up @@ -62,6 +63,8 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>

// Channels and configuration
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
// Used to determine if a component has changed.
protected final int configHash;
Expand Down Expand Up @@ -129,8 +132,9 @@ public void setConfigSeen() {
*/
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
int timeout) {
return channels.values().stream().map(cChannel -> cChannel.start(connection, scheduler, timeout))
.collect(FutureCollector.allOf());
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));
}

/**
Expand All @@ -140,7 +144,10 @@ public void setConfigSeen() {
* exceptionally on errors.
*/
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));
}

/**
Expand Down
Loading

0 comments on commit 37a0122

Please sign in to comment.