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);
}
super("MQTT HVAC");
}
+ protected @Nullable Boolean optimistic;
+
@SerializedName("action_template")
protected @Nullable String actionTemplate;
@SerializedName("action_topic")
.stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
commandTemplate)
- .commandFilter(commandFilter).build();
+ .inferOptimistic(channelConfiguration.optimistic).commandFilter(commandFilter).build();
}
return null;
}
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;
super("MQTT Cover");
}
+ protected @Nullable Boolean optimistic;
+
@SerializedName("state_topic")
protected @Nullable String stateTopic;
@SerializedName("command_topic")
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
return false;
}
return true;
- }).build();
+ }).withAutoUpdatePolicy(optimistic ? AutoUpdatePolicy.RECOMMEND : null).build();
finalizeChannels();
}
}
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;
@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;
.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) {
"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) {
.stateTopic(channelConfiguration.colorTempStateTopic, channelConfiguration.colorTempValueTemplate)
.commandTopic(channelConfiguration.colorTempCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
- .build();
+ .inferOptimistic(channelConfiguration.optimistic).build();
}
if (effectValue != null
.stateTopic(channelConfiguration.effectStateTopic, channelConfiguration.effectValueTemplate)
.commandTopic(channelConfiguration.effectCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
- .build();
+ .inferOptimistic(channelConfiguration.optimistic).build();
}
boolean hasColorChannel = false;
}
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);
super("MQTT Fan");
}
+ protected @Nullable Boolean optimistic;
+
@SerializedName("state_topic")
protected @Nullable String stateTopic;
@SerializedName("command_template")
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
+ .inferOptimistic(channelConfiguration.optimistic)
.build(channelConfiguration.percentageCommandTopic == null);
rawSpeedState = UnDefType.NULL;
.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;
.stateTopic(channelConfiguration.presetModeStateTopic, channelConfiguration.presetModeValueTemplate)
.commandTopic(channelConfiguration.presetModeCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.presetModeCommandTemplate)
- .build();
+ .inferOptimistic(channelConfiguration.optimistic).build();
}
if (channelConfiguration.oscillationCommandTopic != null) {
channelConfiguration.oscillationValueTemplate)
.commandTopic(channelConfiguration.oscillationCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.oscillationCommandTemplate)
- .build();
+ .inferOptimistic(channelConfiguration.optimistic).build();
}
if (channelConfiguration.directionCommandTopic != null) {
.stateTopic(channelConfiguration.directionStateTopic, channelConfiguration.directionValueTemplate)
.commandTopic(channelConfiguration.directionCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate)
- .build();
+ .inferOptimistic(channelConfiguration.optimistic).build();
}
finalizeChannels();
}
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;
@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)) {
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();
}
}
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();
}
}
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;
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));
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
- .build();
+ .inferOptimistic(channelConfiguration.optimistic).build();
finalizeChannels();
}
}
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;
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(),
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
- .build();
+ .inferOptimistic(channelConfiguration.optimistic).build();
finalizeChannels();
}
}
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;
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);
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
- .build();
+ .inferOptimistic(channelConfiguration.optimistic).build();
finalizeChannels();
}
}
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;
@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!");
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();
}
}
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;
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
*
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;
/**
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);
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 {
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}
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));
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);