Skip to content

Commit ab59bc8

Browse files
authored
[mqtt.homeassistant] Implement optimistic components with AutoUpdatePolicy.RECOMMEND (openhab#17520)
Signed-off-by: Cody Cutrer <cody@cutrer.us>
1 parent b36877e commit ab59bc8

File tree

13 files changed

+290
-53
lines changed

13 files changed

+290
-53
lines changed

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java

+12
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,18 @@ public Builder withFormat(String format) {
225225
return this;
226226
}
227227

228+
// If the component explicitly specifies optimistic, or it's missing a state topic
229+
// put it in optimistic mode (which, in openHAB parlance, means to auto-update the
230+
// item).
231+
public Builder inferOptimistic(@Nullable Boolean optimistic) {
232+
String localStateTopic = stateTopic;
233+
if (optimistic == null && (localStateTopic == null || localStateTopic.isBlank())
234+
|| optimistic != null && optimistic == true) {
235+
this.autoUpdatePolicy = AutoUpdatePolicy.RECOMMEND;
236+
}
237+
return this;
238+
}
239+
228240
public ComponentChannel build() {
229241
return build(true);
230242
}

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
9494
super("MQTT HVAC");
9595
}
9696

97+
protected @Nullable Boolean optimistic;
98+
9799
@SerializedName("action_template")
98100
protected @Nullable String actionTemplate;
99101
@SerializedName("action_topic")
@@ -297,7 +299,7 @@ private ComponentChannel buildOptionalChannel(String channelId, ComponentChannel
297299
.stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
298300
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
299301
commandTemplate)
300-
.commandFilter(commandFilter).build();
302+
.inferOptimistic(channelConfiguration.optimistic).commandFilter(commandFilter).build();
301303
}
302304
return null;
303305
}

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.openhab.core.library.types.StopMoveType;
2323
import org.openhab.core.library.types.StringType;
2424
import org.openhab.core.library.types.UpDownType;
25+
import org.openhab.core.thing.type.AutoUpdatePolicy;
2526

2627
import com.google.gson.annotations.SerializedName;
2728

@@ -48,6 +49,8 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
4849
super("MQTT Cover");
4950
}
5051

52+
protected @Nullable Boolean optimistic;
53+
5154
@SerializedName("state_topic")
5255
protected @Nullable String stateTopic;
5356
@SerializedName("command_topic")
@@ -88,6 +91,12 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
8891
public Cover(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
8992
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
9093

94+
boolean optimistic = false;
95+
Boolean localOptimistic = channelConfiguration.optimistic;
96+
if (localOptimistic != null && localOptimistic == true
97+
|| channelConfiguration.stateTopic == null && channelConfiguration.positionTopic == null) {
98+
optimistic = true;
99+
}
91100
String stateTopic = channelConfiguration.stateTopic;
92101

93102
// State can indicate additional information than just
@@ -149,7 +158,7 @@ public Cover(ComponentFactory.ComponentConfiguration componentConfiguration, boo
149158
return false;
150159
}
151160
return true;
152-
}).build();
161+
}).withAutoUpdatePolicy(optimistic ? AutoUpdatePolicy.RECOMMEND : null).build();
153162
finalizeChannels();
154163
}
155164
}

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLight.java

+10-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.openhab.core.library.types.PercentType;
3030
import org.openhab.core.library.types.StringType;
3131
import org.openhab.core.thing.ChannelUID;
32+
import org.openhab.core.thing.type.AutoUpdatePolicy;
3233
import org.openhab.core.types.Command;
3334
import org.openhab.core.types.State;
3435
import org.openhab.core.types.UnDefType;
@@ -60,13 +61,14 @@ public DefaultSchemaLight(ComponentFactory.ComponentConfiguration builder, boole
6061

6162
@Override
6263
protected void buildChannels() {
64+
AutoUpdatePolicy autoUpdatePolicy = optimistic ? AutoUpdatePolicy.RECOMMEND : null;
6365
ComponentChannel localOnOffChannel;
6466
localOnOffChannel = onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue,
6567
"On/Off State", this)
6668
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate)
6769
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
6870
channelConfiguration.getQos())
69-
.commandFilter(this::handleRawOnOffCommand).build(false);
71+
.withAutoUpdatePolicy(autoUpdatePolicy).commandFilter(this::handleRawOnOffCommand).build(false);
7072

7173
@Nullable
7274
ComponentChannel localBrightnessChannel = null;
@@ -76,30 +78,31 @@ protected void buildChannels() {
7678
.stateTopic(channelConfiguration.brightnessStateTopic, channelConfiguration.brightnessValueTemplate)
7779
.commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(),
7880
channelConfiguration.getQos())
79-
.withFormat("%.0f").commandFilter(this::handleBrightnessCommand).build(false);
81+
.withAutoUpdatePolicy(autoUpdatePolicy).withFormat("%.0f")
82+
.commandFilter(this::handleBrightnessCommand).build(false);
8083
}
8184

8285
if (channelConfiguration.whiteCommandTopic != null) {
8386
buildChannel(WHITE_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
8487
"Go directly to white of a specific brightness", this)
8588
.commandTopic(channelConfiguration.whiteCommandTopic, channelConfiguration.isRetain(),
8689
channelConfiguration.getQos())
87-
.isAdvanced(true).build();
90+
.withAutoUpdatePolicy(autoUpdatePolicy).isAdvanced(true).build();
8891
}
8992

9093
if (channelConfiguration.colorModeStateTopic != null) {
9194
buildChannel(COLOR_MODE_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(), "Current color mode",
9295
this)
9396
.stateTopic(channelConfiguration.colorModeStateTopic, channelConfiguration.colorModeValueTemplate)
94-
.build();
97+
.inferOptimistic(channelConfiguration.optimistic).build();
9598
}
9699

97100
if (channelConfiguration.colorTempStateTopic != null || channelConfiguration.colorTempCommandTopic != null) {
98101
buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", this)
99102
.stateTopic(channelConfiguration.colorTempStateTopic, channelConfiguration.colorTempValueTemplate)
100103
.commandTopic(channelConfiguration.colorTempCommandTopic, channelConfiguration.isRetain(),
101104
channelConfiguration.getQos())
102-
.build();
105+
.inferOptimistic(channelConfiguration.optimistic).build();
103106
}
104107

105108
if (effectValue != null
@@ -109,7 +112,7 @@ protected void buildChannels() {
109112
.stateTopic(channelConfiguration.effectStateTopic, channelConfiguration.effectValueTemplate)
110113
.commandTopic(channelConfiguration.effectCommandTopic, channelConfiguration.isRetain(),
111114
channelConfiguration.getQos())
112-
.build();
115+
.inferOptimistic(channelConfiguration.optimistic).build();
113116
}
114117

115118
boolean hasColorChannel = false;
@@ -170,7 +173,7 @@ protected void buildChannels() {
170173
}
171174
colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
172175
.commandTopic(DUMMY_TOPIC, channelConfiguration.isRetain(), channelConfiguration.getQos())
173-
.commandFilter(this::handleColorCommand).build();
176+
.commandFilter(this::handleColorCommand).withAutoUpdatePolicy(autoUpdatePolicy).build();
174177
} else if (localBrightnessChannel != null) {
175178
hiddenChannels.add(localOnOffChannel);
176179
channels.put(BRIGHTNESS_CHANNEL_ID, localBrightnessChannel);

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Fan.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
5757
super("MQTT Fan");
5858
}
5959

60+
protected @Nullable Boolean optimistic;
61+
6062
@SerializedName("state_topic")
6163
protected @Nullable String stateTopic;
6264
@SerializedName("command_template")
@@ -136,6 +138,7 @@ public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boole
136138
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
137139
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
138140
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
141+
.inferOptimistic(channelConfiguration.optimistic)
139142
.build(channelConfiguration.percentageCommandTopic == null);
140143

141144
rawSpeedState = UnDefType.NULL;
@@ -152,7 +155,8 @@ public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boole
152155
.stateTopic(channelConfiguration.percentageStateTopic, channelConfiguration.percentageValueTemplate)
153156
.commandTopic(channelConfiguration.percentageCommandTopic, channelConfiguration.isRetain(),
154157
channelConfiguration.getQos(), channelConfiguration.percentageCommandTemplate)
155-
.commandFilter(this::handlePercentageCommand).build();
158+
.inferOptimistic(channelConfiguration.optimistic).commandFilter(this::handlePercentageCommand)
159+
.build();
156160
} else {
157161
primaryChannel = onOffChannel;
158162
speedChannel = null;
@@ -167,7 +171,7 @@ public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boole
167171
.stateTopic(channelConfiguration.presetModeStateTopic, channelConfiguration.presetModeValueTemplate)
168172
.commandTopic(channelConfiguration.presetModeCommandTopic, channelConfiguration.isRetain(),
169173
channelConfiguration.getQos(), channelConfiguration.presetModeCommandTemplate)
170-
.build();
174+
.inferOptimistic(channelConfiguration.optimistic).build();
171175
}
172176

173177
if (channelConfiguration.oscillationCommandTopic != null) {
@@ -179,7 +183,7 @@ public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boole
179183
channelConfiguration.oscillationValueTemplate)
180184
.commandTopic(channelConfiguration.oscillationCommandTopic, channelConfiguration.isRetain(),
181185
channelConfiguration.getQos(), channelConfiguration.oscillationCommandTemplate)
182-
.build();
186+
.inferOptimistic(channelConfiguration.optimistic).build();
183187
}
184188

185189
if (channelConfiguration.directionCommandTopic != null) {
@@ -189,7 +193,7 @@ public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boole
189193
.stateTopic(channelConfiguration.directionStateTopic, channelConfiguration.directionValueTemplate)
190194
.commandTopic(channelConfiguration.directionCommandTopic, channelConfiguration.isRetain(),
191195
channelConfiguration.getQos(), channelConfiguration.directionCommandTemplate)
192-
.build();
196+
.inferOptimistic(channelConfiguration.optimistic).build();
193197
}
194198
finalizeChannels();
195199
}

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/JSONSchemaLight.java

+13-6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.openhab.core.library.types.StringType;
3232
import org.openhab.core.library.unit.Units;
3333
import org.openhab.core.thing.ChannelUID;
34+
import org.openhab.core.thing.type.AutoUpdatePolicy;
3435
import org.openhab.core.types.Command;
3536
import org.openhab.core.types.State;
3637
import org.openhab.core.types.UnDefType;
@@ -79,6 +80,7 @@ public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean
7980
@Override
8081
protected void buildChannels() {
8182
boolean hasColorChannel = false;
83+
AutoUpdatePolicy autoUpdatePolicy = optimistic ? AutoUpdatePolicy.RECOMMEND : null;
8284
List<LightColorMode> supportedColorModes = channelConfiguration.supportedColorModes;
8385
if (supportedColorModes != null) {
8486
if (LightColorMode.hasColorChannel(supportedColorModes)) {
@@ -88,33 +90,38 @@ protected void buildChannels() {
8890
if (supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
8991
buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature",
9092
this).commandTopic(DUMMY_TOPIC, true, 1)
91-
.commandFilter(command -> handleColorTempCommand(command)).build();
93+
.commandFilter(command -> handleColorTempCommand(command))
94+
.withAutoUpdatePolicy(autoUpdatePolicy).build();
9295

9396
if (hasColorChannel) {
9497
colorModeValue = new TextValue(
9598
supportedColorModes.stream().map(LightColorMode::serializedName).toArray(String[]::new));
9699
buildChannel(COLOR_MODE_CHANNEL_ID, ComponentChannelType.STRING, colorModeValue, "Color Mode", this)
97-
.isAdvanced(true).build();
100+
.withAutoUpdatePolicy(autoUpdatePolicy).isAdvanced(true).build();
98101

99102
}
100103
}
101104
}
102105

103106
if (hasColorChannel) {
104107
colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
105-
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
108+
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand)
109+
.withAutoUpdatePolicy(autoUpdatePolicy).build();
106110
} else if (channelConfiguration.brightness) {
107111
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
108-
"Brightness", this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
112+
"Brightness", this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand)
113+
.withAutoUpdatePolicy(autoUpdatePolicy).build();
109114
} else {
110115
onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
111-
this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
116+
this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand)
117+
.withAutoUpdatePolicy(autoUpdatePolicy).build();
112118
}
113119

114120
if (effectValue != null) {
115121
buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, Objects.requireNonNull(effectValue),
116122
"Lighting Effect", this).commandTopic(DUMMY_TOPIC, true, 1)
117-
.commandFilter(command -> handleEffectCommand(command)).build();
123+
.commandFilter(command -> handleEffectCommand(command)).withAutoUpdatePolicy(autoUpdatePolicy)
124+
.build();
118125

119126
}
120127
}

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Number.java

+1-9
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import org.openhab.binding.mqtt.generic.values.NumberValue;
2020
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
2121
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
22-
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
2322
import org.openhab.core.types.util.UnitUtils;
2423

2524
import com.google.gson.annotations.SerializedName;
@@ -73,13 +72,6 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
7372
public Number(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
7473
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
7574

76-
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
77-
: channelConfiguration.stateTopic.isBlank();
78-
79-
if (optimistic && !channelConfiguration.stateTopic.isBlank()) {
80-
throw new ConfigurationException("Component:Number does not support forced optimistic mode");
81-
}
82-
8375
NumberValue value = new NumberValue(channelConfiguration.min, channelConfiguration.max,
8476
channelConfiguration.step, UnitUtils.parseUnit(channelConfiguration.unitOfMeasurement));
8577

@@ -88,7 +80,7 @@ public Number(ComponentFactory.ComponentConfiguration componentConfiguration, bo
8880
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
8981
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
9082
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
91-
.build();
83+
.inferOptimistic(channelConfiguration.optimistic).build();
9284
finalizeChannels();
9385
}
9486
}

bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Select.java

+1-9
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import org.openhab.binding.mqtt.generic.values.TextValue;
1818
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
1919
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
20-
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
2120

2221
import com.google.gson.annotations.SerializedName;
2322

@@ -58,21 +57,14 @@ static class ChannelConfiguration extends AbstractChannelConfiguration {
5857
public Select(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
5958
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
6059

61-
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
62-
: channelConfiguration.stateTopic.isBlank();
63-
64-
if (optimistic && !channelConfiguration.stateTopic.isBlank()) {
65-
throw new ConfigurationException("Component:Select does not support forced optimistic mode");
66-
}
67-
6860
TextValue value = new TextValue(channelConfiguration.options);
6961

7062
buildChannel(SELECT_CHANNEL_ID, ComponentChannelType.STRING, value, getName(),
7163
componentConfiguration.getUpdateListener())
7264
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
7365
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
7466
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
75-
.build();
67+
.inferOptimistic(channelConfiguration.optimistic).build();
7668
finalizeChannels();
7769
}
7870
}

0 commit comments

Comments
 (0)