]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mqtt] Treat incoming empty string as NULL for most types (#16307)
authorCody Cutrer <cody@cutrer.us>
Mon, 29 Jan 2024 22:05:37 +0000 (15:05 -0700)
committerGitHub <noreply@github.com>
Mon, 29 Jan 2024 22:05:37 +0000 (23:05 +0100)
* [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>
bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/ChannelConfig.java
bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/NumberValue.java
bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/RollershutterValue.java
bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/TextValue.java
bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/Value.java
bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/ValueFactory.java
bundles/org.openhab.binding.mqtt.generic/src/main/resources/OH-INF/config/string-channel-config.xml
bundles/org.openhab.binding.mqtt.generic/src/main/resources/OH-INF/i18n/mqtt.properties
bundles/org.openhab.binding.mqtt.generic/src/test/java/org/openhab/binding/mqtt/generic/values/ValueTests.java

index 0b2cb3f548676be1b10e8ff1f75672c6dc229e1b..d3adf2677861af70fd9801f4d4e03996829e9bc4 100644 (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();
index 20b391a757c51f3954130dfd38110a6d4c0e917c..cfece9d67bf815a1e257be3d0ea312935113425d 100644 (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);
     }
index f1f34e82867c9deafe99879270242ee13805160d..b8c754387c558b45551aac090befe78460d6b1a7 100644 (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());
index 2ed6cc4ea7837d26ae48bae5cf470ce63446e975..fb15998c41f27f55d8e6be95c8703afb91f77a0b 100644 (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)) {
index ffbc42feec5dd75d0d650b5fc08d7649a25e5160..e416cf824ba2a4467781d3904972d98d783526af 100644 (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);
     }
 
index f4295b9862a1cf87c39697e86a11667362839a1f..2a361d84276b04ad3a43692fd1b9a87b6a0c5395 100644 (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();
index 238c15de671df22e8a1857d8cc665de39ba96cef..6131607abb09ed22403aec8c1630671543b936c8 100644 (file)
                        <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>
index 80fd9264a2336f31acdc1a899aa060a1e2f4104c..655291465fbbcfe4a9a536babe6f9e1875e9de4d 100644 (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
index 8bb5365abefcf32f16ecbefa8db41c55c075a055..f91c919f567865563ce101ae7c7782b6e08cb06c 100644 (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("")));
+    }
 }