2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
15 import java.math.BigDecimal;
16 import java.util.Arrays;
17 import java.util.List;
18 import java.util.function.Predicate;
20 import javax.measure.Unit;
21 import javax.measure.quantity.Temperature;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
26 import org.openhab.binding.mqtt.generic.values.NumberValue;
27 import org.openhab.binding.mqtt.generic.values.OnOffValue;
28 import org.openhab.binding.mqtt.generic.values.TextValue;
29 import org.openhab.binding.mqtt.generic.values.Value;
30 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
31 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
32 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
33 import org.openhab.core.library.types.StringType;
34 import org.openhab.core.library.unit.ImperialUnits;
35 import org.openhab.core.library.unit.SIUnits;
36 import org.openhab.core.types.Command;
37 import org.openhab.core.types.State;
39 import com.google.gson.annotations.SerializedName;
42 * A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
44 * @author David Graeff - Initial contribution
45 * @author Anton Kharuzhy - Implementation
48 public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
49 public static final String ACTION_CH_ID = "action";
50 public static final String AUX_CH_ID = "aux";
51 public static final String AWAY_MODE_CH_ID = "awayMode";
52 public static final String CURRENT_TEMPERATURE_CH_ID = "currentTemperature";
53 public static final String FAN_MODE_CH_ID = "fanMode";
54 public static final String HOLD_CH_ID = "hold";
55 public static final String MODE_CH_ID = "mode";
56 public static final String SWING_CH_ID = "swing";
57 public static final String TEMPERATURE_CH_ID = "temperature";
58 public static final String TEMPERATURE_HIGH_CH_ID = "temperatureHigh";
59 public static final String TEMPERATURE_LOW_CH_ID = "temperatureLow";
60 public static final String POWER_CH_ID = "power";
62 public enum TemperatureUnit {
64 CELSIUS(SIUnits.CELSIUS, new BigDecimal("0.1")),
66 FAHRENHEIT(ImperialUnits.FAHRENHEIT, BigDecimal.ONE);
68 private final Unit<Temperature> unit;
69 private final BigDecimal defaultPrecision;
71 TemperatureUnit(Unit<Temperature> unit, BigDecimal defaultPrecision) {
73 this.defaultPrecision = defaultPrecision;
76 public Unit<Temperature> getUnit() {
80 public BigDecimal getDefaultPrecision() {
81 return defaultPrecision;
85 private static final String ACTION_OFF = "off";
86 private static final State ACTION_OFF_STATE = new StringType(ACTION_OFF);
87 private static final List<String> ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan");
90 * Configuration class for MQTT component
92 static class ChannelConfiguration extends AbstractChannelConfiguration {
93 ChannelConfiguration() {
97 @SerializedName("action_template")
98 protected @Nullable String actionTemplate;
99 @SerializedName("action_topic")
100 protected @Nullable String actionTopic;
102 @SerializedName("aux_command_topic")
103 protected @Nullable String auxCommandTopic;
104 @SerializedName("aux_state_template")
105 protected @Nullable String auxStateTemplate;
106 @SerializedName("aux_state_topic")
107 protected @Nullable String auxStateTopic;
109 @SerializedName("away_mode_command_topic")
110 protected @Nullable String awayModeCommandTopic;
111 @SerializedName("away_mode_state_template")
112 protected @Nullable String awayModeStateTemplate;
113 @SerializedName("away_mode_state_topic")
114 protected @Nullable String awayModeStateTopic;
116 @SerializedName("current_temperature_template")
117 protected @Nullable String currentTemperatureTemplate;
118 @SerializedName("current_temperature_topic")
119 protected @Nullable String currentTemperatureTopic;
121 @SerializedName("fan_mode_command_template")
122 protected @Nullable String fanModeCommandTemplate;
123 @SerializedName("fan_mode_command_topic")
124 protected @Nullable String fanModeCommandTopic;
125 @SerializedName("fan_mode_state_template")
126 protected @Nullable String fanModeStateTemplate;
127 @SerializedName("fan_mode_state_topic")
128 protected @Nullable String fanModeStateTopic;
129 @SerializedName("fan_modes")
130 protected List<String> fanModes = Arrays.asList("auto", "low", "medium", "high");
132 @SerializedName("hold_command_template")
133 protected @Nullable String holdCommandTemplate;
134 @SerializedName("hold_command_topic")
135 protected @Nullable String holdCommandTopic;
136 @SerializedName("hold_state_template")
137 protected @Nullable String holdStateTemplate;
138 @SerializedName("hold_state_topic")
139 protected @Nullable String holdStateTopic;
140 @SerializedName("hold_modes")
141 protected @Nullable List<String> holdModes; // Are there default modes? Now the channel will be ignored without
144 @SerializedName("json_attributes_template")
145 protected @Nullable String jsonAttributesTemplate; // Attributes are not supported yet
146 @SerializedName("json_attributes_topic")
147 protected @Nullable String jsonAttributesTopic;
149 @SerializedName("mode_command_template")
150 protected @Nullable String modeCommandTemplate;
151 @SerializedName("mode_command_topic")
152 protected @Nullable String modeCommandTopic;
153 @SerializedName("mode_state_template")
154 protected @Nullable String modeStateTemplate;
155 @SerializedName("mode_state_topic")
156 protected @Nullable String modeStateTopic;
157 protected List<String> modes = Arrays.asList("auto", "off", "cool", "heat", "dry", "fan_only");
159 @SerializedName("swing_command_template")
160 protected @Nullable String swingCommandTemplate;
161 @SerializedName("swing_command_topic")
162 protected @Nullable String swingCommandTopic;
163 @SerializedName("swing_state_template")
164 protected @Nullable String swingStateTemplate;
165 @SerializedName("swing_state_topic")
166 protected @Nullable String swingStateTopic;
167 @SerializedName("swing_modes")
168 protected List<String> swingModes = Arrays.asList("on", "off");
170 @SerializedName("temperature_command_template")
171 protected @Nullable String temperatureCommandTemplate;
172 @SerializedName("temperature_command_topic")
173 protected @Nullable String temperatureCommandTopic;
174 @SerializedName("temperature_state_template")
175 protected @Nullable String temperatureStateTemplate;
176 @SerializedName("temperature_state_topic")
177 protected @Nullable String temperatureStateTopic;
179 @SerializedName("temperature_high_command_template")
180 protected @Nullable String temperatureHighCommandTemplate;
181 @SerializedName("temperature_high_command_topic")
182 protected @Nullable String temperatureHighCommandTopic;
183 @SerializedName("temperature_high_state_template")
184 protected @Nullable String temperatureHighStateTemplate;
185 @SerializedName("temperature_high_state_topic")
186 protected @Nullable String temperatureHighStateTopic;
188 @SerializedName("temperature_low_command_template")
189 protected @Nullable String temperatureLowCommandTemplate;
190 @SerializedName("temperature_low_command_topic")
191 protected @Nullable String temperatureLowCommandTopic;
192 @SerializedName("temperature_low_state_template")
193 protected @Nullable String temperatureLowStateTemplate;
194 @SerializedName("temperature_low_state_topic")
195 protected @Nullable String temperatureLowStateTopic;
197 @SerializedName("power_command_topic")
198 protected @Nullable String powerCommandTopic;
200 protected Integer initial = 21;
201 @SerializedName("max_temp")
202 protected @Nullable BigDecimal maxTemp;
203 @SerializedName("min_temp")
204 protected @Nullable BigDecimal minTemp;
205 @SerializedName("temperature_unit")
206 protected TemperatureUnit temperatureUnit = TemperatureUnit.CELSIUS; // System unit by default
207 @SerializedName("temp_step")
208 protected BigDecimal tempStep = BigDecimal.ONE;
209 protected @Nullable BigDecimal precision;
210 @SerializedName("send_if_off")
211 protected Boolean sendIfOff = true;
214 public Climate(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
215 super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
217 BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
218 : channelConfiguration.temperatureUnit.getDefaultPrecision();
219 final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
221 ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID, ComponentChannelType.STRING,
222 new TextValue(ACTION_MODES.toArray(new String[0])), updateListener, null, null,
223 channelConfiguration.actionTemplate, channelConfiguration.actionTopic, null);
225 final Predicate<Command> commandFilter = channelConfiguration.sendIfOff ? null
226 : getCommandFilter(actionChannel);
228 buildOptionalChannel(AUX_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
229 channelConfiguration.auxCommandTopic, channelConfiguration.auxStateTemplate,
230 channelConfiguration.auxStateTopic, commandFilter);
232 buildOptionalChannel(AWAY_MODE_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
233 channelConfiguration.awayModeCommandTopic, channelConfiguration.awayModeStateTemplate,
234 channelConfiguration.awayModeStateTopic, commandFilter);
236 buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID, ComponentChannelType.NUMBER,
237 new NumberValue(null, null, precision, channelConfiguration.temperatureUnit.getUnit()), updateListener,
238 null, null, channelConfiguration.currentTemperatureTemplate,
239 channelConfiguration.currentTemperatureTopic, commandFilter);
241 buildOptionalChannel(FAN_MODE_CH_ID, ComponentChannelType.STRING,
242 new TextValue(channelConfiguration.fanModes.toArray(new String[0])), updateListener,
243 channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic,
244 channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter);
246 List<String> holdModes = channelConfiguration.holdModes;
247 if (holdModes != null && !holdModes.isEmpty()) {
248 buildOptionalChannel(HOLD_CH_ID, ComponentChannelType.STRING,
249 new TextValue(holdModes.toArray(new String[0])), updateListener,
250 channelConfiguration.holdCommandTemplate, channelConfiguration.holdCommandTopic,
251 channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter);
254 buildOptionalChannel(MODE_CH_ID, ComponentChannelType.STRING,
255 new TextValue(channelConfiguration.modes.toArray(new String[0])), updateListener,
256 channelConfiguration.modeCommandTemplate, channelConfiguration.modeCommandTopic,
257 channelConfiguration.modeStateTemplate, channelConfiguration.modeStateTopic, commandFilter);
259 buildOptionalChannel(SWING_CH_ID, ComponentChannelType.STRING,
260 new TextValue(channelConfiguration.swingModes.toArray(new String[0])), updateListener,
261 channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic,
262 channelConfiguration.swingStateTemplate, channelConfiguration.swingStateTopic, commandFilter);
264 buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.NUMBER,
265 new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
266 channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
267 updateListener, channelConfiguration.temperatureCommandTemplate,
268 channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate,
269 channelConfiguration.temperatureStateTopic, commandFilter);
271 buildOptionalChannel(TEMPERATURE_HIGH_CH_ID, ComponentChannelType.NUMBER,
272 new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
273 channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
274 updateListener, channelConfiguration.temperatureHighCommandTemplate,
275 channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate,
276 channelConfiguration.temperatureHighStateTopic, commandFilter);
278 buildOptionalChannel(TEMPERATURE_LOW_CH_ID, ComponentChannelType.NUMBER,
279 new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
280 channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
281 updateListener, channelConfiguration.temperatureLowCommandTemplate,
282 channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate,
283 channelConfiguration.temperatureLowStateTopic, commandFilter);
285 buildOptionalChannel(POWER_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
286 channelConfiguration.powerCommandTopic, null, null, null);
291 private ComponentChannel buildOptionalChannel(String channelId, ComponentChannelType channelType, Value valueState,
292 ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
293 @Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic,
294 @Nullable Predicate<Command> commandFilter) {
295 if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
296 return buildChannel(channelId, channelType, valueState, getName(), channelStateUpdateListener)
297 .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
298 .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
300 .commandFilter(commandFilter).build();
305 private @Nullable Predicate<Command> getCommandFilter(@Nullable ComponentChannel actionChannel) {
306 if (actionChannel == null) {
309 final var val = actionChannel.getState().getCache();
310 return command -> !ACTION_OFF_STATE.equals(val.getChannelState());