]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mqtt.homeassistant] Implement optimistic components with AutoUpdatePolicy.RECOMMEND...
authorCody Cutrer <cody@cutrer.us>
Tue, 8 Oct 2024 06:45:11 +0000 (00:45 -0600)
committerGitHub <noreply@github.com>
Tue, 8 Oct 2024 06:45:11 +0000 (08:45 +0200)
Signed-off-by: Cody Cutrer <cody@cutrer.us>
13 files changed:
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLight.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/JSONSchemaLight.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Number.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Select.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Switch.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/TemplateSchemaLight.java
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/NumberTests.java

index ac27112bff1ba71e37a6904518734ccc8ad0fb5b..1be08c07bea8120a5c72213332eca3123fd1ace7 100644 (file)
@@ -225,6 +225,18 @@ public class ComponentChannel {
             return this;
         }
 
+        // If the component explicitly specifies optimistic, or it's missing a state topic
+        // put it in optimistic mode (which, in openHAB parlance, means to auto-update the
+        // item).
+        public Builder inferOptimistic(@Nullable Boolean optimistic) {
+            String localStateTopic = stateTopic;
+            if (optimistic == null && (localStateTopic == null || localStateTopic.isBlank())
+                    || optimistic != null && optimistic == true) {
+                this.autoUpdatePolicy = AutoUpdatePolicy.RECOMMEND;
+            }
+            return this;
+        }
+
         public ComponentChannel build() {
             return build(true);
         }
index 1a21945962c48ebfe02e540f1e3400d2d8f52db0..b8439724809253b89f96e3225bb91ad98c32cb17 100644 (file)
@@ -94,6 +94,8 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
             super("MQTT HVAC");
         }
 
+        protected @Nullable Boolean optimistic;
+
         @SerializedName("action_template")
         protected @Nullable String actionTemplate;
         @SerializedName("action_topic")
@@ -297,7 +299,7 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
                     .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
                     .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
                             commandTemplate)
-                    .commandFilter(commandFilter).build();
+                    .inferOptimistic(channelConfiguration.optimistic).commandFilter(commandFilter).build();
         }
         return null;
     }
index f365ec33ead360b7a8f0c830186f38dfad60f623..cc40947d383339adfe1340056fb9348410aed371 100644 (file)
@@ -22,6 +22,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChanne
 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.thing.type.AutoUpdatePolicy;
 
 import com.google.gson.annotations.SerializedName;
 
@@ -48,6 +49,8 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
             super("MQTT Cover");
         }
 
+        protected @Nullable Boolean optimistic;
+
         @SerializedName("state_topic")
         protected @Nullable String stateTopic;
         @SerializedName("command_topic")
@@ -88,6 +91,12 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
     public Cover(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
         super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
 
+        boolean optimistic = false;
+        Boolean localOptimistic = channelConfiguration.optimistic;
+        if (localOptimistic != null && localOptimistic == true
+                || channelConfiguration.stateTopic == null && channelConfiguration.positionTopic == null) {
+            optimistic = true;
+        }
         String stateTopic = channelConfiguration.stateTopic;
 
         // State can indicate additional information than just
@@ -149,7 +158,7 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
                         return false;
                     }
                     return true;
-                }).build();
+                }).withAutoUpdatePolicy(optimistic ? AutoUpdatePolicy.RECOMMEND : null).build();
         finalizeChannels();
     }
 }
index ef5e12786273ac5fc2ec132896759a42d1d0ae4d..de960385bb7dbd329447114f8d97a939d062c469 100644 (file)
@@ -29,6 +29,7 @@ import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.PercentType;
 import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.type.AutoUpdatePolicy;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.State;
 import org.openhab.core.types.UnDefType;
@@ -60,13 +61,14 @@ public class DefaultSchemaLight extends Light {
 
     @Override
     protected void buildChannels() {
+        AutoUpdatePolicy autoUpdatePolicy = optimistic ? AutoUpdatePolicy.RECOMMEND : null;
         ComponentChannel localOnOffChannel;
         localOnOffChannel = onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue,
                 "On/Off State", this)
                 .stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate)
                 .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
                         channelConfiguration.getQos())
-                .commandFilter(this::handleRawOnOffCommand).build(false);
+                .withAutoUpdatePolicy(autoUpdatePolicy).commandFilter(this::handleRawOnOffCommand).build(false);
 
         @Nullable
         ComponentChannel localBrightnessChannel = null;
@@ -76,7 +78,8 @@ public class DefaultSchemaLight extends Light {
                     .stateTopic(channelConfiguration.brightnessStateTopic, channelConfiguration.brightnessValueTemplate)
                     .commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(),
                             channelConfiguration.getQos())
-                    .withFormat("%.0f").commandFilter(this::handleBrightnessCommand).build(false);
+                    .withAutoUpdatePolicy(autoUpdatePolicy).withFormat("%.0f")
+                    .commandFilter(this::handleBrightnessCommand).build(false);
         }
 
         if (channelConfiguration.whiteCommandTopic != null) {
@@ -84,14 +87,14 @@ public class DefaultSchemaLight extends Light {
                     "Go directly to white of a specific brightness", this)
                     .commandTopic(channelConfiguration.whiteCommandTopic, channelConfiguration.isRetain(),
                             channelConfiguration.getQos())
-                    .isAdvanced(true).build();
+                    .withAutoUpdatePolicy(autoUpdatePolicy).isAdvanced(true).build();
         }
 
         if (channelConfiguration.colorModeStateTopic != null) {
             buildChannel(COLOR_MODE_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(), "Current color mode",
                     this)
                     .stateTopic(channelConfiguration.colorModeStateTopic, channelConfiguration.colorModeValueTemplate)
-                    .build();
+                    .inferOptimistic(channelConfiguration.optimistic).build();
         }
 
         if (channelConfiguration.colorTempStateTopic != null || channelConfiguration.colorTempCommandTopic != null) {
@@ -99,7 +102,7 @@ public class DefaultSchemaLight extends Light {
                     .stateTopic(channelConfiguration.colorTempStateTopic, channelConfiguration.colorTempValueTemplate)
                     .commandTopic(channelConfiguration.colorTempCommandTopic, channelConfiguration.isRetain(),
                             channelConfiguration.getQos())
-                    .build();
+                    .inferOptimistic(channelConfiguration.optimistic).build();
         }
 
         if (effectValue != null
@@ -109,7 +112,7 @@ public class DefaultSchemaLight extends Light {
                     .stateTopic(channelConfiguration.effectStateTopic, channelConfiguration.effectValueTemplate)
                     .commandTopic(channelConfiguration.effectCommandTopic, channelConfiguration.isRetain(),
                             channelConfiguration.getQos())
-                    .build();
+                    .inferOptimistic(channelConfiguration.optimistic).build();
         }
 
         boolean hasColorChannel = false;
@@ -170,7 +173,7 @@ public class DefaultSchemaLight extends Light {
             }
             colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
                     .commandTopic(DUMMY_TOPIC, channelConfiguration.isRetain(), channelConfiguration.getQos())
-                    .commandFilter(this::handleColorCommand).build();
+                    .commandFilter(this::handleColorCommand).withAutoUpdatePolicy(autoUpdatePolicy).build();
         } else if (localBrightnessChannel != null) {
             hiddenChannels.add(localOnOffChannel);
             channels.put(BRIGHTNESS_CHANNEL_ID, localBrightnessChannel);
index 3c8ed4139aba008a15e3a2d49b877bc3fc0fa688..6b44ba50a0e2268f9898b96f4a0eb59b9070d031 100644 (file)
@@ -57,6 +57,8 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
             super("MQTT Fan");
         }
 
+        protected @Nullable Boolean optimistic;
+
         @SerializedName("state_topic")
         protected @Nullable String stateTopic;
         @SerializedName("command_template")
@@ -136,6 +138,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
                 .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
                 .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
                         channelConfiguration.getQos(), channelConfiguration.commandTemplate)
+                .inferOptimistic(channelConfiguration.optimistic)
                 .build(channelConfiguration.percentageCommandTopic == null);
 
         rawSpeedState = UnDefType.NULL;
@@ -152,7 +155,8 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
                     .stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
                     .commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
                             channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
-                    .commandFilter(this::handlePercentageCommand).build();
+                    .inferOptimistic(channelConfiguration.optimistic).commandFilter(this::handlePercentageCommand)
+                    .build();
         } else {
             primaryChannel = onOffChannel;
             speedChannel = null;
@@ -167,7 +171,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
                     .stateTopic(channelConfiguration.presetModeStateTopic, channelConfiguration.presetModeValueTemplate)
                     .commandTopic(channelConfiguration.presetModeCommandTopic, channelConfiguration.isRetain(),
                             channelConfiguration.getQos(), channelConfiguration.presetModeCommandTemplate)
-                    .build();
+                    .inferOptimistic(channelConfiguration.optimistic).build();
         }
 
         if (channelConfiguration.oscillationCommandTopic != null) {
@@ -179,7 +183,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
                             channelConfiguration.oscillationValueTemplate)
                     .commandTopic(channelConfiguration.oscillationCommandTopic, channelConfiguration.isRetain(),
                             channelConfiguration.getQos(), channelConfiguration.oscillationCommandTemplate)
-                    .build();
+                    .inferOptimistic(channelConfiguration.optimistic).build();
         }
 
         if (channelConfiguration.directionCommandTopic != null) {
@@ -189,7 +193,7 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> implements
                     .stateTopic(channelConfiguration.directionStateTopic, channelConfiguration.directionValueTemplate)
                     .commandTopic(channelConfiguration.directionCommandTopic, channelConfiguration.isRetain(),
                             channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate)
-                    .build();
+                    .inferOptimistic(channelConfiguration.optimistic).build();
         }
         finalizeChannels();
     }
index 006031478c0071c2724a59ae1b2edde0b7fa11f3..7f5eae561a31113dce3800d6597d6a018b62d228 100644 (file)
@@ -31,6 +31,7 @@ import org.openhab.core.library.types.QuantityType;
 import org.openhab.core.library.types.StringType;
 import org.openhab.core.library.unit.Units;
 import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.type.AutoUpdatePolicy;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.State;
 import org.openhab.core.types.UnDefType;
@@ -79,6 +80,7 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
     @Override
     protected void buildChannels() {
         boolean hasColorChannel = false;
+        AutoUpdatePolicy autoUpdatePolicy = optimistic ? AutoUpdatePolicy.RECOMMEND : null;
         List<LightColorMode> supportedColorModes = channelConfiguration.supportedColorModes;
         if (supportedColorModes != null) {
             if (LightColorMode.hasColorChannel(supportedColorModes)) {
@@ -88,13 +90,14 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
             if (supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
                 buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature",
                         this).commandTopic(DUMMY_TOPIC, true, 1)
-                        .commandFilter(command -> handleColorTempCommand(command)).build();
+                        .commandFilter(command -> handleColorTempCommand(command))
+                        .withAutoUpdatePolicy(autoUpdatePolicy).build();
 
                 if (hasColorChannel) {
                     colorModeValue = new TextValue(
                             supportedColorModes.stream().map(LightColorMode::serializedName).toArray(String[]::new));
                     buildChannel(COLOR_MODE_CHANNEL_ID, ComponentChannelType.STRING, colorModeValue, "Color Mode", this)
-                            .isAdvanced(true).build();
+                            .withAutoUpdatePolicy(autoUpdatePolicy).isAdvanced(true).build();
 
                 }
             }
@@ -102,19 +105,23 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
 
         if (hasColorChannel) {
             colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
-                    .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
+                    .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand)
+                    .withAutoUpdatePolicy(autoUpdatePolicy).build();
         } else if (channelConfiguration.brightness) {
             brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
-                    "Brightness", this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
+                    "Brightness", this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand)
+                    .withAutoUpdatePolicy(autoUpdatePolicy).build();
         } else {
             onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
-                    this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
+                    this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand)
+                    .withAutoUpdatePolicy(autoUpdatePolicy).build();
         }
 
         if (effectValue != null) {
             buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, Objects.requireNonNull(effectValue),
                     "Lighting Effect", this).commandTopic(DUMMY_TOPIC, true, 1)
-                    .commandFilter(command -> handleEffectCommand(command)).build();
+                    .commandFilter(command -> handleEffectCommand(command)).withAutoUpdatePolicy(autoUpdatePolicy)
+                    .build();
 
         }
     }
index 2912ac9b7cc37ce7409f1f0ffe37bde11d722d2d..8de66574f32e987d2127ab64acc3f28047f245f4 100644 (file)
@@ -19,7 +19,6 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.mqtt.generic.values.NumberValue;
 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
-import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
 import org.openhab.core.types.util.UnitUtils;
 
 import com.google.gson.annotations.SerializedName;
@@ -73,13 +72,6 @@ public class Number extends AbstractComponent<Number.ChannelConfiguration> {
     public Number(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
         super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
 
-        boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
-                : channelConfiguration.stateTopic.isBlank();
-
-        if (optimistic && !channelConfiguration.stateTopic.isBlank()) {
-            throw new ConfigurationException("Component:Number does not support forced optimistic mode");
-        }
-
         NumberValue value = new NumberValue(channelConfiguration.min, channelConfiguration.max,
                 channelConfiguration.step, UnitUtils.parseUnit(channelConfiguration.unitOfMeasurement));
 
@@ -88,7 +80,7 @@ public class Number extends AbstractComponent<Number.ChannelConfiguration> {
                 .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
                 .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
                         channelConfiguration.getQos(), channelConfiguration.commandTemplate)
-                .build();
+                .inferOptimistic(channelConfiguration.optimistic).build();
         finalizeChannels();
     }
 }
index ad650c63117b02ee0dc388d69804cc3e3c43704d..90ad460d033c1557f1448b1444b342c6cb118832 100644 (file)
@@ -17,7 +17,6 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.mqtt.generic.values.TextValue;
 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
-import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
 
 import com.google.gson.annotations.SerializedName;
 
@@ -58,13 +57,6 @@ public class Select extends AbstractComponent<Select.ChannelConfiguration> {
     public Select(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
         super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
 
-        boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
-                : channelConfiguration.stateTopic.isBlank();
-
-        if (optimistic && !channelConfiguration.stateTopic.isBlank()) {
-            throw new ConfigurationException("Component:Select does not support forced optimistic mode");
-        }
-
         TextValue value = new TextValue(channelConfiguration.options);
 
         buildChannel(SELECT_CHANNEL_ID, ComponentChannelType.STRING, value, getName(),
@@ -72,7 +64,7 @@ public class Select extends AbstractComponent<Select.ChannelConfiguration> {
                 .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
                 .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
                         channelConfiguration.getQos(), channelConfiguration.commandTemplate)
-                .build();
+                .inferOptimistic(channelConfiguration.optimistic).build();
         finalizeChannels();
     }
 }
index 0027ffe1c77ef89cc01e3ca3589cde79ebda126a..949cf7a5665a5ad02fc73e41f6c470442032199c 100644 (file)
@@ -17,7 +17,6 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.mqtt.generic.values.OnOffValue;
 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
-import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
 
 import com.google.gson.annotations.SerializedName;
 
@@ -63,13 +62,6 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
     public Switch(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
         super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
 
-        boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
-                : channelConfiguration.stateTopic.isBlank();
-
-        if (optimistic && !channelConfiguration.stateTopic.isBlank()) {
-            throw new ConfigurationException("Component:Switch does not support forced optimistic mode");
-        }
-
         OnOffValue value = new OnOffValue(channelConfiguration.stateOn, channelConfiguration.stateOff,
                 channelConfiguration.payloadOn, channelConfiguration.payloadOff);
 
@@ -78,7 +70,7 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
                 .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
                 .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
                         channelConfiguration.getQos())
-                .build();
+                .inferOptimistic(channelConfiguration.optimistic).build();
         finalizeChannels();
     }
 }
index 3a056cb36f8a24c01ff15dc20cc001ea273d599b..fd4512ff12495877ffaced31b5da4649791018db 100644 (file)
@@ -35,6 +35,7 @@ import org.openhab.core.library.types.QuantityType;
 import org.openhab.core.library.types.StringType;
 import org.openhab.core.library.unit.Units;
 import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.type.AutoUpdatePolicy;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.State;
 import org.openhab.core.types.UnDefType;
@@ -76,6 +77,7 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
 
     @Override
     protected void buildChannels() {
+        AutoUpdatePolicy autoUpdatePolicy = optimistic ? AutoUpdatePolicy.RECOMMEND : null;
         if (channelConfiguration.commandOnTemplate == null || channelConfiguration.commandOffTemplate == null) {
             throw new UnsupportedComponentException("Template schema light component '" + getHaID()
                     + "' does not define command_on_template or command_off_template!");
@@ -87,25 +89,28 @@ public class TemplateSchemaLight extends AbstractRawSchemaLight {
         if (channelConfiguration.redTemplate != null && channelConfiguration.greenTemplate != null
                 && channelConfiguration.blueTemplate != null) {
             colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
-                    .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command)).build();
+                    .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command))
+                    .withAutoUpdatePolicy(autoUpdatePolicy).build();
         } else if (channelConfiguration.brightnessTemplate != null) {
             brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
                     "Brightness", this).commandTopic(DUMMY_TOPIC, true, 1)
-                    .commandFilter(command -> handleCommand(command)).build();
+                    .commandFilter(command -> handleCommand(command)).withAutoUpdatePolicy(autoUpdatePolicy).build();
         } else {
             onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
-                    this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command)).build();
+                    this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleCommand(command))
+                    .withAutoUpdatePolicy(autoUpdatePolicy).build();
         }
 
         if (channelConfiguration.colorTempTemplate != null) {
             buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", this)
                     .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command))
-                    .build();
+                    .withAutoUpdatePolicy(autoUpdatePolicy).build();
         }
         TextValue localEffectValue = effectValue;
         if (channelConfiguration.effectTemplate != null && localEffectValue != null) {
             buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, localEffectValue, "Effect", this)
-                    .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command)).build();
+                    .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command))
+                    .withAutoUpdatePolicy(autoUpdatePolicy).build();
         }
     }
 
index dc7d2b1ef1db9df8e479f58662365fd7cc59f215..bbb278d530fbfd3fcc0fda6358a3dd96913882bb 100644 (file)
@@ -45,6 +45,7 @@ import org.openhab.core.library.types.HSBType;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatusInfo;
 import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.thing.type.AutoUpdatePolicy;
 import org.openhab.core.thing.type.ChannelTypeRegistry;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.State;
@@ -167,6 +168,43 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
         assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
     }
 
+    /**
+     * Assert channel topics, label and value class
+     *
+     * @param component component
+     * @param channelId channel
+     * @param stateTopic state topic or empty string
+     * @param commandTopic command topic or empty string
+     * @param label label
+     * @param valueClass value class
+     * @param autoUpdatePolicy Auto Update Policy
+     */
+    protected static void assertChannel(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
+            String channelId, String stateTopic, String commandTopic, String label, Class<? extends Value> valueClass,
+            @Nullable AutoUpdatePolicy autoUpdatePolicy) {
+        var stateChannel = Objects.requireNonNull(component.getChannel(channelId));
+        assertChannel(stateChannel, stateTopic, commandTopic, label, valueClass);
+    }
+
+    /**
+     * Assert channel topics, label and value class
+     *
+     * @param stateChannel channel
+     * @param stateTopic state topic or empty string
+     * @param commandTopic command topic or empty string
+     * @param label label
+     * @param valueClass value class
+     * @param autoUpdatePolicy Auto Update Policy
+     */
+    protected static void assertChannel(ComponentChannel stateChannel, String stateTopic, String commandTopic,
+            String label, Class<? extends Value> valueClass, @Nullable AutoUpdatePolicy autoUpdatePolicy) {
+        assertThat(stateChannel.getChannel().getLabel(), is(label));
+        assertThat(stateChannel.getState().getStateTopic(), is(stateTopic));
+        assertThat(stateChannel.getState().getCommandTopic(), is(commandTopic));
+        assertThat(stateChannel.getState().getCache(), is(instanceOf(valueClass)));
+        assertThat(stateChannel.getChannel().getAutoUpdatePolicy(), is(autoUpdatePolicy));
+    }
+
     /**
      * Assert channel state
      *
index 5d9b0a2e9ced838a446889aaef424b369c1deb22..674d71e728c225f5d5e978ab1282210450befb15 100644 (file)
@@ -27,6 +27,7 @@ import org.openhab.binding.mqtt.generic.values.TextValue;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.PercentType;
 import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.type.AutoUpdatePolicy;
 import org.openhab.core.types.UnDefType;
 
 /**
@@ -72,7 +73,7 @@ public class FanTests extends AbstractComponentTests {
         assertThat(component.getName(), is("fan"));
 
         assertChannel(component, Fan.SWITCH_CHANNEL_ID, "zigbee2mqtt/fan/state", "zigbee2mqtt/fan/set/state",
-                "On/Off State", OnOffValue.class);
+                "On/Off State", OnOffValue.class, null);
 
         publishMessage("zigbee2mqtt/fan/state", "ON_");
         assertState(component, Fan.SWITCH_CHANNEL_ID, OnOffType.ON);
@@ -89,6 +90,117 @@ public class FanTests extends AbstractComponentTests {
         assertPublished("zigbee2mqtt/fan/set/state", "ON_");
     }
 
+    @SuppressWarnings("null")
+    @Test
+    public void testInferredOptimistic() throws InterruptedException {
+        // @formatter:off
+        var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+                """
+                { \
+                  "availability": [ \
+                    { \
+                      "topic": "zigbee2mqtt/bridge/state" \
+                    } \
+                  ], \
+                  "device": { \
+                    "identifiers": [ \
+                      "zigbee2mqtt_0x0000000000000000" \
+                    ], \
+                    "manufacturer": "Fans inc", \
+                    "model": "Fan", \
+                    "name": "FanBlower", \
+                    "sw_version": "Zigbee2MQTT 1.18.2" \
+                  }, \
+                  "name": "fan", \
+                  "payload_off": "OFF_", \
+                  "payload_on": "ON_", \
+                  "command_topic": "zigbee2mqtt/fan/set/state"
+                }\
+                """);
+        // @formatter:on
+
+        assertThat(component.channels.size(), is(1));
+        assertThat(component.getName(), is("fan"));
+
+        assertChannel(component, Fan.SWITCH_CHANNEL_ID, "", "zigbee2mqtt/fan/set/state", "On/Off State",
+                OnOffValue.class, AutoUpdatePolicy.RECOMMEND);
+    }
+
+    @SuppressWarnings("null")
+    @Test
+    public void testForcedOptimistic() throws InterruptedException {
+        // @formatter:off
+        var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+                """
+                { \
+                  "availability": [ \
+                    { \
+                      "topic": "zigbee2mqtt/bridge/state" \
+                    } \
+                  ], \
+                  "device": { \
+                    "identifiers": [ \
+                      "zigbee2mqtt_0x0000000000000000" \
+                    ], \
+                    "manufacturer": "Fans inc", \
+                    "model": "Fan", \
+                    "name": "FanBlower", \
+                    "sw_version": "Zigbee2MQTT 1.18.2" \
+                  }, \
+                  "name": "fan", \
+                  "payload_off": "OFF_", \
+                  "payload_on": "ON_", \
+                  "state_topic": "zigbee2mqtt/fan/state", \
+                  "command_topic": "zigbee2mqtt/fan/set/state", \
+                  "optimistic": true \
+                }\
+                """);
+        // @formatter:on
+
+        assertThat(component.channels.size(), is(1));
+        assertThat(component.getName(), is("fan"));
+
+        assertChannel(component, Fan.SWITCH_CHANNEL_ID, "zigbee2mqtt/fan/state", "zigbee2mqtt/fan/set/state",
+                "On/Off State", OnOffValue.class, AutoUpdatePolicy.RECOMMEND);
+    }
+
+    @SuppressWarnings("null")
+    @Test
+    public void testInferredOptimisticWithPosition() throws InterruptedException {
+        // @formatter:off
+        var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
+                """
+                { \
+                  "availability": [ \
+                    { \
+                      "topic": "zigbee2mqtt/bridge/state" \
+                    } \
+                  ], \
+                  "device": { \
+                    "identifiers": [ \
+                      "zigbee2mqtt_0x0000000000000000" \
+                    ], \
+                    "manufacturer": "Fans inc", \
+                    "model": "Fan", \
+                    "name": "FanBlower", \
+                    "sw_version": "Zigbee2MQTT 1.18.2" \
+                  }, \
+                  "name": "fan", \
+                  "payload_off": "OFF_", \
+                  "payload_on": "ON_", \
+                  "command_topic": "zigbee2mqtt/fan/set/state", \
+                  "percentage_command_topic": "bedroom_fan/speed/percentage" \
+                }\
+                """);
+        // @formatter:on
+
+        assertThat(component.channels.size(), is(1));
+        assertThat(component.getName(), is("fan"));
+
+        assertChannel(component, Fan.SPEED_CHANNEL_ID, "", "bedroom_fan/speed/percentage", "Speed",
+                PercentageValue.class, AutoUpdatePolicy.RECOMMEND);
+    }
+
     @SuppressWarnings("null")
     @Test
     public void testCommandTemplate() throws InterruptedException {
index 678040424f2891a560379daf0011aab3ef6d9576..e960d335f76c5322b246c9d011ae0019060f95a1 100644 (file)
@@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.junit.jupiter.api.Test;
 import org.openhab.binding.mqtt.generic.values.NumberValue;
 import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.type.AutoUpdatePolicy;
 
 /**
  * Tests for {@link Number}
@@ -62,7 +63,7 @@ public class NumberTests extends AbstractComponentTests {
         assertThat(component.getName(), is("BWA Link Hot Tub Pump 1"));
 
         assertChannel(component, Number.NUMBER_CHANNEL_ID, "homie/bwa/spa/pump1", "homie/bwa/spa/pump1/set",
-                "BWA Link Hot Tub Pump 1", NumberValue.class);
+                "BWA Link Hot Tub Pump 1", NumberValue.class, null);
 
         publishMessage("homie/bwa/spa/pump1", "1");
         assertState(component, Number.NUMBER_CHANNEL_ID, new DecimalType(1));
@@ -73,6 +74,74 @@ public class NumberTests extends AbstractComponentTests {
         assertPublished("homie/bwa/spa/pump1/set", "1");
     }
 
+    @SuppressWarnings("null")
+    @Test
+    public void testInferredOptimistic() throws InterruptedException {
+        var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+                    {
+                        "name": "BWA Link Hot Tub Pump 1",
+                        "availability_topic": "homie/bwa/$state",
+                        "payload_available": "ready",
+                        "payload_not_available": "lost",
+                        "qos": 1,
+                        "icon": "mdi:chart-bubble",
+                        "device": {
+                            "manufacturer": "Balboa Water Group",
+                            "sw_version": "2.1.3",
+                            "model": "BFBP20",
+                            "name": "BWA Link",
+                            "identifiers": "bwa"
+                        },
+                        "command_topic": "homie/bwa/spa/pump1/set",
+                        "command_template": "{{ value | round(0) }}",
+                        "min": 0,
+                        "max": 2,
+                        "unique_id": "bwa_spa_pump1"
+                    }
+                """);
+
+        assertThat(component.channels.size(), is(1));
+        assertThat(component.getName(), is("BWA Link Hot Tub Pump 1"));
+
+        assertChannel(component, Number.NUMBER_CHANNEL_ID, "", "homie/bwa/spa/pump1/set", "BWA Link Hot Tub Pump 1",
+                NumberValue.class, AutoUpdatePolicy.RECOMMEND);
+    }
+
+    @SuppressWarnings("null")
+    @Test
+    public void testForcedOptimistic() throws InterruptedException {
+        var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
+                    {
+                        "name": "BWA Link Hot Tub Pump 1",
+                        "availability_topic": "homie/bwa/$state",
+                        "payload_available": "ready",
+                        "payload_not_available": "lost",
+                        "qos": 1,
+                        "icon": "mdi:chart-bubble",
+                        "device": {
+                            "manufacturer": "Balboa Water Group",
+                            "sw_version": "2.1.3",
+                            "model": "BFBP20",
+                            "name": "BWA Link",
+                            "identifiers": "bwa"
+                        },
+                        "state_topic": "homie/bwa/spa/pump1",
+                        "command_topic": "homie/bwa/spa/pump1/set",
+                        "command_template": "{{ value | round(0) }}",
+                        "min": 0,
+                        "max": 2,
+                        "unique_id": "bwa_spa_pump1",
+                        "optimistic": true
+                    }
+                """);
+
+        assertThat(component.channels.size(), is(1));
+        assertThat(component.getName(), is("BWA Link Hot Tub Pump 1"));
+
+        assertChannel(component, Number.NUMBER_CHANNEL_ID, "homie/bwa/spa/pump1", "homie/bwa/spa/pump1/set",
+                "BWA Link Hot Tub Pump 1", NumberValue.class, AutoUpdatePolicy.RECOMMEND);
+    }
+
     @Override
     protected Set<String> getConfigTopics() {
         return Set.of(CONFIG_TOPIC);