Skip to content

Commit

Permalink
[mqtt] Treat incoming empty string as NULL for most types (openhab#16307
Browse files Browse the repository at this point in the history
)

* [mqtt] Treat incoming empty string as NULL for most types

Empty strings are often received when deleting retained topics when a device
goes offline, or as the result of a transformation that is missing
a value (such as a "scene" event from zwave-js-ui, which sends JSON with
a timestamp and the scene value, then immediately sends a value to the topic
with only a timestamp).

For string channels, add a configuration value to allow setting a specific
string for treating as NULL, since empty string can make sense for that
type.

Signed-off-by: Cody Cutrer <[email protected]>
  • Loading branch information
ccutrer authored Jan 29, 2024
1 parent d5fc695 commit 5da9dda
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class ChannelConfig {
public @Nullable String stop;
public @Nullable String onState;
public @Nullable String offState;
public @Nullable String nullValue;

public int onBrightness = 10;
public String colorMode = ColorMode.HSB.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,12 @@ public Command parseCommand(Command command) throws IllegalArgumentException {

@Override
public Type parseMessage(Command command) throws IllegalArgumentException {
if (command instanceof StringType
&& (command.toString().equalsIgnoreCase(NAN) || command.toString().equalsIgnoreCase(NEGATIVE_NAN))) {
return UnDefType.UNDEF;
if (command instanceof StringType) {
if (command.toString().equalsIgnoreCase(NAN) || command.toString().equalsIgnoreCase(NEGATIVE_NAN)) {
return UnDefType.UNDEF;
} else if (command.toString().isEmpty()) {
return UnDefType.NULL;
}
}
return parseCommand(command);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;

/**
* Implements a rollershutter value.
Expand Down Expand Up @@ -146,7 +148,10 @@ public Command parseCommand(Command command) throws IllegalArgumentException {
}

@Override
public Command parseMessage(Command command) throws IllegalArgumentException {
public Type parseMessage(Command command) throws IllegalArgumentException {
if (command instanceof StringType string && string.toString().isEmpty()) {
return UnDefType.NULL;
}
command = parseType(command, upStateString, downStateString);
if (inverted && command instanceof PercentType percentType) {
return new PercentType(100 - percentType.intValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.openhab.core.types.State;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;

/**
* Implements a text/string value. Allows to restrict the incoming value to a set of states.
Expand All @@ -40,6 +41,8 @@ public class TextValue extends Value {
private final @Nullable Set<String> states;
private final @Nullable Set<String> commands;

protected @Nullable String nullValue = null;

/**
* Create a string value with a limited number of allowed states and commands.
*
Expand Down Expand Up @@ -80,6 +83,10 @@ public TextValue() {
this.commands = null;
}

public void setNullValue(@Nullable String nullValue) {
this.nullValue = nullValue;
}

@Override
public StringType parseCommand(Command command) throws IllegalArgumentException {
final Set<String> commands = this.commands;
Expand All @@ -92,6 +99,10 @@ public StringType parseCommand(Command command) throws IllegalArgumentException

@Override
public State parseMessage(Command command) throws IllegalArgumentException {
if (command instanceof StringType string && string.toString().equals(nullValue)) {
return UnDefType.NULL;
}

final Set<String> states = this.states;
String valueStr = command.toString();
if (states != null && !states.contains(valueStr)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.CommandDescriptionBuilder;
import org.openhab.core.types.State;
Expand Down Expand Up @@ -147,6 +148,9 @@ public void update(State newState) throws IllegalArgumentException {
* @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
*/
public Type parseMessage(Command command) throws IllegalArgumentException {
if (command instanceof StringType string && string.toString().isEmpty()) {
return UnDefType.NULL;
}
return parseCommand(command);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ public static Value createValueState(ChannelConfig config, String channelTypeID)
Value value;
switch (channelTypeID) {
case MqttBindingConstants.STRING:
value = config.allowedStates.isBlank() ? new TextValue()
TextValue textValue = config.allowedStates.isBlank() ? new TextValue()
: new TextValue(config.allowedStates.split(","));
textValue.setNullValue(config.nullValue);
value = textValue;
break;
case MqttBindingConstants.DATETIME:
value = new DateTimeValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="nullValue" type="text">
<label>NULL Value</label>
<description>If the received MQTT value matches this, treat it as NULL.</description>
<advanced>true</advanced>
</parameter>

<parameter name="allowedStates" type="text">
<label>Allowed States</label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ thing-type.config.mqtt.string_channel.transformationPattern.label = Incoming Val
thing-type.config.mqtt.string_channel.transformationPattern.description = Applies transformations to an incoming MQTT topic value. A transformation example for a received JSON would be "JSONPATH:$.device.status.temperature" for a json {device: {status: { temperature: 23.2 }}}. You can chain transformations by separating them with the intersection character ∩.
thing-type.config.mqtt.string_channel.transformationPatternOut.label = Outgoing Value Transformation
thing-type.config.mqtt.string_channel.transformationPatternOut.description = Applies a transformation before publishing a MQTT topic value. Transformations are specialised in extracting a value, but some transformations like the MAP one could be useful.
thing-type.config.mqtt.string_channel.nullValue.label = NULL Value
thing-type.config.mqtt.string_channel.nullValue.description = If the received MQTT value matches this, treat it as NULL.
thing-type.config.mqtt.switch_channel.commandTopic.label = MQTT Command Topic
thing-type.config.mqtt.switch_channel.commandTopic.description = An MQTT topic that this thing will send a command to. If not set, this will be a read-only switch.
thing-type.config.mqtt.switch_channel.formatBeforePublish.label = Outgoing Value Format
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public void colorUpdate() {
assertThat(hsb.getBrightness().intValue(), is(0));
hsb = (HSBType) v.parseCommand(p(v, "1"));
assertThat(hsb.getBrightness().intValue(), is(1));

assertThat(v.parseMessage(new StringType("")), is(UnDefType.NULL));
}

@Test
Expand Down Expand Up @@ -137,6 +139,8 @@ public void onoffUpdate() {
// Test custom formatting
assertThat(v.getMQTTpublishValue(OnOffType.OFF, "=%s"), is("=fancyOff"));
assertThat(v.getMQTTpublishValue(OnOffType.ON, "=%s"), is("=fancyON"));

assertThat(v.parseMessage(new StringType("")), is(UnDefType.NULL));
}

@Test
Expand Down Expand Up @@ -168,6 +172,8 @@ public void openCloseUpdate() {
// Test basic formatting
assertThat(v.getMQTTpublishValue(OpenClosedType.CLOSED, null), is("fancyOff"));
assertThat(v.getMQTTpublishValue(OpenClosedType.OPEN, null), is("fancyON"));

assertThat(v.parseMessage(new StringType("")), is(UnDefType.NULL));
}

@Test
Expand All @@ -191,6 +197,8 @@ public void numberUpdate() {
assertThat(v.parseMessage(new StringType("nan")), is(UnDefType.UNDEF));
assertThat(v.parseMessage(new StringType("-NaN")), is(UnDefType.UNDEF));
assertThat(v.parseMessage(new StringType("-nan")), is(UnDefType.UNDEF));

assertThat(v.parseMessage(new StringType("")), is(UnDefType.NULL));
}

@Test
Expand Down Expand Up @@ -243,6 +251,8 @@ public void rollershutterUpdateWithStrings() {
// Test parsing from MQTT
assertThat(v.parseMessage(new StringType("fancyON")), is(UpDownType.UP));
assertThat(v.parseMessage(new StringType("fancyOff")), is(UpDownType.DOWN));

assertThat(v.parseMessage(new StringType("")), is(UnDefType.NULL));
}

@Test
Expand Down Expand Up @@ -310,6 +320,8 @@ public void percentMQTTValue() {
command = v.parseCommand(new DecimalType(i));
assertThat(v.getMQTTpublishValue(command, null), is("" + i));
}

assertThat(v.parseMessage(new StringType("")), is(UnDefType.NULL));
}

@Test
Expand Down Expand Up @@ -394,4 +406,20 @@ public void percentCalcInvalid() {
null);
assertThrows(IllegalArgumentException.class, () -> v.parseCommand(new DecimalType(9.0)));
}

@Test
public void textUpdate() {
TextValue v = new TextValue();

assertThat(v.parseMessage(new StringType("")), is(new StringType("")));
assertThat(v.parseMessage(new StringType("NULL")), is(new StringType("NULL")));

v.setNullValue("");
assertThat(v.parseMessage(new StringType("")), is(UnDefType.NULL));
assertThat(v.parseMessage(new StringType("NULL")), is(new StringType("NULL")));

v.setNullValue("NULL");
assertThat(v.parseMessage(new StringType("NULL")), is(UnDefType.NULL));
assertThat(v.parseMessage(new StringType("")), is(new StringType("")));
}
}

0 comments on commit 5da9dda

Please sign in to comment.