2 * Copyright (c) 2010-2023 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.config.dto.AbstractChannelConfiguration;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.library.unit.ImperialUnits;
34 import org.openhab.core.library.unit.SIUnits;
35 import org.openhab.core.types.Command;
36 import org.openhab.core.types.State;
38 import com.google.gson.annotations.SerializedName;
41 * A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
43 * @author David Graeff - Initial contribution
44 * @author Anton Kharuzhy - Implementation
47 public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
48 public static final String ACTION_CH_ID = "action";
49 public static final String AUX_CH_ID = "aux";
50 public static final String AWAY_MODE_CH_ID = "awayMode";
51 public static final String CURRENT_TEMPERATURE_CH_ID = "currentTemperature";
52 public static final String FAN_MODE_CH_ID = "fanMode";
53 public static final String HOLD_CH_ID = "hold";
54 public static final String MODE_CH_ID = "mode";
55 public static final String SWING_CH_ID = "swing";
56 public static final String TEMPERATURE_CH_ID = "temperature";
57 public static final String TEMPERATURE_HIGH_CH_ID = "temperatureHigh";
58 public static final String TEMPERATURE_LOW_CH_ID = "temperatureLow";
59 public static final String POWER_CH_ID = "power";
61 public enum TemperatureUnit {
63 CELSIUS(SIUnits.CELSIUS, new BigDecimal("0.1")),
65 FAHRENHEIT(ImperialUnits.FAHRENHEIT, BigDecimal.ONE);
67 private final Unit<Temperature> unit;
68 private final BigDecimal defaultPrecision;
70 TemperatureUnit(Unit<Temperature> unit, BigDecimal defaultPrecision) {
72 this.defaultPrecision = defaultPrecision;
75 public Unit<Temperature> getUnit() {
79 public BigDecimal getDefaultPrecision() {
80 return defaultPrecision;
84 private static final String ACTION_OFF = "off";
85 private static final State ACTION_OFF_STATE = new StringType(ACTION_OFF);
86 private static final List<String> ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan");
89 * Configuration class for MQTT component
91 static class ChannelConfiguration extends AbstractChannelConfiguration {
92 ChannelConfiguration() {
96 @SerializedName("action_template")
97 protected @Nullable String actionTemplate;
98 @SerializedName("action_topic")
99 protected @Nullable String actionTopic;
101 @SerializedName("aux_command_topic")
102 protected @Nullable String auxCommandTopic;
103 @SerializedName("aux_state_template")
104 protected @Nullable String auxStateTemplate;
105 @SerializedName("aux_state_topic")
106 protected @Nullable String auxStateTopic;
108 @SerializedName("away_mode_command_topic")
109 protected @Nullable String awayModeCommandTopic;
110 @SerializedName("away_mode_state_template")
111 protected @Nullable String awayModeStateTemplate;
112 @SerializedName("away_mode_state_topic")
113 protected @Nullable String awayModeStateTopic;
115 @SerializedName("current_temperature_template")
116 protected @Nullable String currentTemperatureTemplate;
117 @SerializedName("current_temperature_topic")
118 protected @Nullable String currentTemperatureTopic;
120 @SerializedName("fan_mode_command_template")
121 protected @Nullable String fanModeCommandTemplate;
122 @SerializedName("fan_mode_command_topic")
123 protected @Nullable String fanModeCommandTopic;
124 @SerializedName("fan_mode_state_template")
125 protected @Nullable String fanModeStateTemplate;
126 @SerializedName("fan_mode_state_topic")
127 protected @Nullable String fanModeStateTopic;
128 @SerializedName("fan_modes")
129 protected List<String> fanModes = Arrays.asList("auto", "low", "medium", "high");
131 @SerializedName("hold_command_template")
132 protected @Nullable String holdCommandTemplate;
133 @SerializedName("hold_command_topic")
134 protected @Nullable String holdCommandTopic;
135 @SerializedName("hold_state_template")
136 protected @Nullable String holdStateTemplate;
137 @SerializedName("hold_state_topic")
138 protected @Nullable String holdStateTopic;
139 @SerializedName("hold_modes")
140 protected @Nullable List<String> holdModes; // Are there default modes? Now the channel will be ignored without
143 @SerializedName("json_attributes_template")
144 protected @Nullable String jsonAttributesTemplate; // Attributes are not supported yet
145 @SerializedName("json_attributes_topic")
146 protected @Nullable String jsonAttributesTopic;
148 @SerializedName("mode_command_template")
149 protected @Nullable String modeCommandTemplate;
150 @SerializedName("mode_command_topic")
151 protected @Nullable String modeCommandTopic;
152 @SerializedName("mode_state_template")
153 protected @Nullable String modeStateTemplate;
154 @SerializedName("mode_state_topic")
155 protected @Nullable String modeStateTopic;
156 protected List<String> modes = Arrays.asList("auto", "off", "cool", "heat", "dry", "fan_only");
158 @SerializedName("swing_command_template")
159 protected @Nullable String swingCommandTemplate;
160 @SerializedName("swing_command_topic")
161 protected @Nullable String swingCommandTopic;
162 @SerializedName("swing_state_template")
163 protected @Nullable String swingStateTemplate;
164 @SerializedName("swing_state_topic")
165 protected @Nullable String swingStateTopic;
166 @SerializedName("swing_modes")
167 protected List<String> swingModes = Arrays.asList("on", "off");
169 @SerializedName("temperature_command_template")
170 protected @Nullable String temperatureCommandTemplate;
171 @SerializedName("temperature_command_topic")
172 protected @Nullable String temperatureCommandTopic;
173 @SerializedName("temperature_state_template")
174 protected @Nullable String temperatureStateTemplate;
175 @SerializedName("temperature_state_topic")
176 protected @Nullable String temperatureStateTopic;
178 @SerializedName("temperature_high_command_template")
179 protected @Nullable String temperatureHighCommandTemplate;
180 @SerializedName("temperature_high_command_topic")
181 protected @Nullable String temperatureHighCommandTopic;
182 @SerializedName("temperature_high_state_template")
183 protected @Nullable String temperatureHighStateTemplate;
184 @SerializedName("temperature_high_state_topic")
185 protected @Nullable String temperatureHighStateTopic;
187 @SerializedName("temperature_low_command_template")
188 protected @Nullable String temperatureLowCommandTemplate;
189 @SerializedName("temperature_low_command_topic")
190 protected @Nullable String temperatureLowCommandTopic;
191 @SerializedName("temperature_low_state_template")
192 protected @Nullable String temperatureLowStateTemplate;
193 @SerializedName("temperature_low_state_topic")
194 protected @Nullable String temperatureLowStateTopic;
196 @SerializedName("power_command_topic")
197 protected @Nullable String powerCommandTopic;
199 protected Integer initial = 21;
200 @SerializedName("max_temp")
201 protected @Nullable BigDecimal maxTemp;
202 @SerializedName("min_temp")
203 protected @Nullable BigDecimal minTemp;
204 @SerializedName("temperature_unit")
205 protected TemperatureUnit temperatureUnit = TemperatureUnit.CELSIUS; // System unit by default
206 @SerializedName("temp_step")
207 protected BigDecimal tempStep = BigDecimal.ONE;
208 protected @Nullable BigDecimal precision;
209 @SerializedName("send_if_off")
210 protected Boolean sendIfOff = true;
213 public Climate(ComponentFactory.ComponentConfiguration componentConfiguration) {
214 super(componentConfiguration, ChannelConfiguration.class);
216 BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
217 : channelConfiguration.temperatureUnit.getDefaultPrecision();
218 final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
220 ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID,
221 new TextValue(ACTION_MODES.toArray(new String[0])), updateListener, null, null,
222 channelConfiguration.actionTemplate, channelConfiguration.actionTopic, null);
224 final Predicate<Command> commandFilter = channelConfiguration.sendIfOff ? null
225 : getCommandFilter(actionChannel);
227 buildOptionalChannel(AUX_CH_ID, new OnOffValue(), updateListener, null, channelConfiguration.auxCommandTopic,
228 channelConfiguration.auxStateTemplate, channelConfiguration.auxStateTopic, commandFilter);
230 buildOptionalChannel(AWAY_MODE_CH_ID, new OnOffValue(), updateListener, null,
231 channelConfiguration.awayModeCommandTopic, channelConfiguration.awayModeStateTemplate,
232 channelConfiguration.awayModeStateTopic, commandFilter);
234 buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID,
235 new NumberValue(null, null, precision, channelConfiguration.temperatureUnit.getUnit()), updateListener,
236 null, null, channelConfiguration.currentTemperatureTemplate,
237 channelConfiguration.currentTemperatureTopic, commandFilter);
239 buildOptionalChannel(FAN_MODE_CH_ID, new TextValue(channelConfiguration.fanModes.toArray(new String[0])),
240 updateListener, channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic,
241 channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter);
243 List<String> holdModes = channelConfiguration.holdModes;
244 if (holdModes != null && !holdModes.isEmpty()) {
245 buildOptionalChannel(HOLD_CH_ID, new TextValue(holdModes.toArray(new String[0])), updateListener,
246 channelConfiguration.holdCommandTemplate, channelConfiguration.holdCommandTopic,
247 channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter);
250 buildOptionalChannel(MODE_CH_ID, new TextValue(channelConfiguration.modes.toArray(new String[0])),
251 updateListener, channelConfiguration.modeCommandTemplate, channelConfiguration.modeCommandTopic,
252 channelConfiguration.modeStateTemplate, channelConfiguration.modeStateTopic, commandFilter);
254 buildOptionalChannel(SWING_CH_ID, new TextValue(channelConfiguration.swingModes.toArray(new String[0])),
255 updateListener, channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic,
256 channelConfiguration.swingStateTemplate, channelConfiguration.swingStateTopic, commandFilter);
258 buildOptionalChannel(TEMPERATURE_CH_ID,
259 new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
260 channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
261 updateListener, channelConfiguration.temperatureCommandTemplate,
262 channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate,
263 channelConfiguration.temperatureStateTopic, commandFilter);
265 buildOptionalChannel(TEMPERATURE_HIGH_CH_ID,
266 new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
267 channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
268 updateListener, channelConfiguration.temperatureHighCommandTemplate,
269 channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate,
270 channelConfiguration.temperatureHighStateTopic, commandFilter);
272 buildOptionalChannel(TEMPERATURE_LOW_CH_ID,
273 new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
274 channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
275 updateListener, channelConfiguration.temperatureLowCommandTemplate,
276 channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate,
277 channelConfiguration.temperatureLowStateTopic, commandFilter);
279 buildOptionalChannel(POWER_CH_ID, new OnOffValue(), updateListener, null,
280 channelConfiguration.powerCommandTopic, null, null, null);
284 private ComponentChannel buildOptionalChannel(String channelId, Value valueState,
285 ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
286 @Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic,
287 @Nullable Predicate<Command> commandFilter) {
288 if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
289 return buildChannel(channelId, valueState, getName(), channelStateUpdateListener)
290 .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
291 .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
293 .commandFilter(commandFilter).build();
298 private @Nullable Predicate<Command> getCommandFilter(@Nullable ComponentChannel actionChannel) {
299 if (actionChannel == null) {
302 final var val = actionChannel.getState().getCache();
303 return command -> !ACTION_OFF_STATE.equals(val.getChannelState());