[mqtt.homeassistant] Implement optimistic components with AutoUpdatePolicy.RECOMMEND (#17520)

Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2024-10-08 00:45:11 -06:00 committed by GitHub
parent b36877ec6d
commit ab59bc871d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 290 additions and 53 deletions

View 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);
}

View 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;
}

View 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();
}
}

View 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);

View 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();
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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
*

View 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 {

View 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);