[mqtt] Treat incoming empty string as NULL for most types (#16307)

* [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 <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2024-01-29 15:05:37 -07:00 committed by GitHub
parent d5fc6950dc
commit 5da9dda3e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 66 additions and 5 deletions

View File

@ -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();

View File

@ -121,9 +121,12 @@ public class NumberValue extends Value {
@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);
}

View File

@ -22,6 +22,8 @@ import org.openhab.core.library.types.StopMoveType;
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.
@ -146,7 +148,10 @@ public class RollershutterValue extends Value {
}
@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());

View File

@ -29,6 +29,7 @@ import org.openhab.core.types.CommandOption;
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.
@ -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.
*
@ -80,6 +83,10 @@ public class TextValue extends Value {
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;
@ -92,6 +99,10 @@ public class TextValue extends Value {
@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)) {

View File

@ -23,6 +23,7 @@ import org.openhab.core.library.CoreItemFactory;
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;
@ -147,6 +148,9 @@ public abstract class Value {
* @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);
}

View File

@ -36,8 +36,10 @@ public class ValueFactory {
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();

View File

@ -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>

View File

@ -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

View File

@ -82,6 +82,8 @@ public class ValueTests {
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
@ -137,6 +139,8 @@ public class ValueTests {
// 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
@ -168,6 +172,8 @@ public class ValueTests {
// 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
@ -191,6 +197,8 @@ public class ValueTests {
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
@ -243,6 +251,8 @@ public class ValueTests {
// 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
@ -310,6 +320,8 @@ public class ValueTests {
command = v.parseCommand(new DecimalType(i));
assertThat(v.getMQTTpublishValue(command, null), is("" + i));
}
assertThat(v.parseMessage(new StringType("")), is(UnDefType.NULL));
}
@Test
@ -394,4 +406,20 @@ public class ValueTests {
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("")));
}
}