2 * Copyright (c) 2010-2022 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 org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
23 import org.openhab.binding.mqtt.generic.values.NumberValue;
24 import org.openhab.binding.mqtt.generic.values.OnOffValue;
25 import org.openhab.binding.mqtt.generic.values.TextValue;
26 import org.openhab.binding.mqtt.generic.values.Value;
27 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
28 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
29 import org.openhab.core.library.types.StringType;
30 import org.openhab.core.types.Command;
31 import org.openhab.core.types.State;
33 import com.google.gson.annotations.SerializedName;
36 * A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
38 * @author David Graeff - Initial contribution
39 * @author Anton Kharuzhy - Implementation
42 public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
43 public static final String ACTION_CH_ID = "action";
44 public static final String AUX_CH_ID = "aux";
45 public static final String AWAY_MODE_CH_ID = "awayMode";
46 public static final String CURRENT_TEMPERATURE_CH_ID = "currentTemperature";
47 public static final String FAN_MODE_CH_ID = "fanMode";
48 public static final String HOLD_CH_ID = "hold";
49 public static final String MODE_CH_ID = "mode";
50 public static final String SWING_CH_ID = "swing";
51 public static final String TEMPERATURE_CH_ID = "temperature";
52 public static final String TEMPERATURE_HIGH_CH_ID = "temperatureHigh";
53 public static final String TEMPERATURE_LOW_CH_ID = "temperatureLow";
54 public static final String POWER_CH_ID = "power";
56 private static final String CELSIUM = "C";
57 private static final String FAHRENHEIT = "F";
58 private static final float DEFAULT_CELSIUM_PRECISION = 0.1f;
59 private static final float DEFAULT_FAHRENHEIT_PRECISION = 1f;
61 private static final String ACTION_OFF = "off";
62 private static final State ACTION_OFF_STATE = new StringType(ACTION_OFF);
63 private static final List<String> ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan");
66 * Configuration class for MQTT component
68 static class ChannelConfiguration extends AbstractChannelConfiguration {
69 ChannelConfiguration() {
73 @SerializedName("action_template")
74 protected @Nullable String actionTemplate;
75 @SerializedName("action_topic")
76 protected @Nullable String actionTopic;
78 @SerializedName("aux_command_topic")
79 protected @Nullable String auxCommandTopic;
80 @SerializedName("aux_state_template")
81 protected @Nullable String auxStateTemplate;
82 @SerializedName("aux_state_topic")
83 protected @Nullable String auxStateTopic;
85 @SerializedName("away_mode_command_topic")
86 protected @Nullable String awayModeCommandTopic;
87 @SerializedName("away_mode_state_template")
88 protected @Nullable String awayModeStateTemplate;
89 @SerializedName("away_mode_state_topic")
90 protected @Nullable String awayModeStateTopic;
92 @SerializedName("current_temperature_template")
93 protected @Nullable String currentTemperatureTemplate;
94 @SerializedName("current_temperature_topic")
95 protected @Nullable String currentTemperatureTopic;
97 @SerializedName("fan_mode_command_template")
98 protected @Nullable String fanModeCommandTemplate;
99 @SerializedName("fan_mode_command_topic")
100 protected @Nullable String fanModeCommandTopic;
101 @SerializedName("fan_mode_state_template")
102 protected @Nullable String fanModeStateTemplate;
103 @SerializedName("fan_mode_state_topic")
104 protected @Nullable String fanModeStateTopic;
105 @SerializedName("fan_modes")
106 protected List<String> fanModes = Arrays.asList("auto", "low", "medium", "high");
108 @SerializedName("hold_command_template")
109 protected @Nullable String holdCommandTemplate;
110 @SerializedName("hold_command_topic")
111 protected @Nullable String holdCommandTopic;
112 @SerializedName("hold_state_template")
113 protected @Nullable String holdStateTemplate;
114 @SerializedName("hold_state_topic")
115 protected @Nullable String holdStateTopic;
116 @SerializedName("hold_modes")
117 protected @Nullable List<String> holdModes; // Are there default modes? Now the channel will be ignored without
120 @SerializedName("json_attributes_template")
121 protected @Nullable String jsonAttributesTemplate; // Attributes are not supported yet
122 @SerializedName("json_attributes_topic")
123 protected @Nullable String jsonAttributesTopic;
125 @SerializedName("mode_command_template")
126 protected @Nullable String modeCommandTemplate;
127 @SerializedName("mode_command_topic")
128 protected @Nullable String modeCommandTopic;
129 @SerializedName("mode_state_template")
130 protected @Nullable String modeStateTemplate;
131 @SerializedName("mode_state_topic")
132 protected @Nullable String modeStateTopic;
133 protected List<String> modes = Arrays.asList("auto", "off", "cool", "heat", "dry", "fan_only");
135 @SerializedName("swing_command_template")
136 protected @Nullable String swingCommandTemplate;
137 @SerializedName("swing_command_topic")
138 protected @Nullable String swingCommandTopic;
139 @SerializedName("swing_state_template")
140 protected @Nullable String swingStateTemplate;
141 @SerializedName("swing_state_topic")
142 protected @Nullable String swingStateTopic;
143 @SerializedName("swing_modes")
144 protected List<String> swingModes = Arrays.asList("on", "off");
146 @SerializedName("temperature_command_template")
147 protected @Nullable String temperatureCommandTemplate;
148 @SerializedName("temperature_command_topic")
149 protected @Nullable String temperatureCommandTopic;
150 @SerializedName("temperature_state_template")
151 protected @Nullable String temperatureStateTemplate;
152 @SerializedName("temperature_state_topic")
153 protected @Nullable String temperatureStateTopic;
155 @SerializedName("temperature_high_command_template")
156 protected @Nullable String temperatureHighCommandTemplate;
157 @SerializedName("temperature_high_command_topic")
158 protected @Nullable String temperatureHighCommandTopic;
159 @SerializedName("temperature_high_state_template")
160 protected @Nullable String temperatureHighStateTemplate;
161 @SerializedName("temperature_high_state_topic")
162 protected @Nullable String temperatureHighStateTopic;
164 @SerializedName("temperature_low_command_template")
165 protected @Nullable String temperatureLowCommandTemplate;
166 @SerializedName("temperature_low_command_topic")
167 protected @Nullable String temperatureLowCommandTopic;
168 @SerializedName("temperature_low_state_template")
169 protected @Nullable String temperatureLowStateTemplate;
170 @SerializedName("temperature_low_state_topic")
171 protected @Nullable String temperatureLowStateTopic;
173 @SerializedName("power_command_topic")
174 protected @Nullable String powerCommandTopic;
176 protected Integer initial = 21;
177 @SerializedName("max_temp")
178 protected @Nullable Float maxTemp;
179 @SerializedName("min_temp")
180 protected @Nullable Float minTemp;
181 @SerializedName("temperature_unit")
182 protected String temperatureUnit = CELSIUM; // System unit by default
183 @SerializedName("temp_step")
184 protected Float tempStep = 1f;
185 protected @Nullable Float precision;
186 @SerializedName("send_if_off")
187 protected Boolean sendIfOff = true;
190 public Climate(ComponentFactory.ComponentConfiguration componentConfiguration) {
191 super(componentConfiguration, ChannelConfiguration.class);
193 BigDecimal minTemp = channelConfiguration.minTemp != null ? BigDecimal.valueOf(channelConfiguration.minTemp)
195 BigDecimal maxTemp = channelConfiguration.maxTemp != null ? BigDecimal.valueOf(channelConfiguration.maxTemp)
197 float precision = channelConfiguration.precision != null ? channelConfiguration.precision
198 : (FAHRENHEIT.equals(channelConfiguration.temperatureUnit) ? DEFAULT_FAHRENHEIT_PRECISION
199 : DEFAULT_CELSIUM_PRECISION);
200 final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
202 ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID,
203 new TextValue(ACTION_MODES.toArray(new String[0])), updateListener, null, null,
204 channelConfiguration.actionTemplate, channelConfiguration.actionTopic, null);
206 final Predicate<Command> commandFilter = channelConfiguration.sendIfOff ? null
207 : getCommandFilter(actionChannel);
209 buildOptionalChannel(AUX_CH_ID, new OnOffValue(), updateListener, null, channelConfiguration.auxCommandTopic,
210 channelConfiguration.auxStateTemplate, channelConfiguration.auxStateTopic, commandFilter);
212 buildOptionalChannel(AWAY_MODE_CH_ID, new OnOffValue(), updateListener, null,
213 channelConfiguration.awayModeCommandTopic, channelConfiguration.awayModeStateTemplate,
214 channelConfiguration.awayModeStateTopic, commandFilter);
216 buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID,
217 new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(precision), channelConfiguration.temperatureUnit),
218 updateListener, null, null, channelConfiguration.currentTemperatureTemplate,
219 channelConfiguration.currentTemperatureTopic, commandFilter);
221 buildOptionalChannel(FAN_MODE_CH_ID, new TextValue(channelConfiguration.fanModes.toArray(new String[0])),
222 updateListener, channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic,
223 channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter);
225 if (channelConfiguration.holdModes != null && !channelConfiguration.holdModes.isEmpty()) {
226 buildOptionalChannel(HOLD_CH_ID, new TextValue(channelConfiguration.holdModes.toArray(new String[0])),
227 updateListener, channelConfiguration.holdCommandTemplate, channelConfiguration.holdCommandTopic,
228 channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter);
231 buildOptionalChannel(MODE_CH_ID, new TextValue(channelConfiguration.modes.toArray(new String[0])),
232 updateListener, channelConfiguration.modeCommandTemplate, channelConfiguration.modeCommandTopic,
233 channelConfiguration.modeStateTemplate, channelConfiguration.modeStateTopic, commandFilter);
235 buildOptionalChannel(SWING_CH_ID, new TextValue(channelConfiguration.swingModes.toArray(new String[0])),
236 updateListener, channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic,
237 channelConfiguration.swingStateTemplate, channelConfiguration.swingStateTopic, commandFilter);
239 buildOptionalChannel(TEMPERATURE_CH_ID,
240 new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.tempStep),
241 channelConfiguration.temperatureUnit),
242 updateListener, channelConfiguration.temperatureCommandTemplate,
243 channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate,
244 channelConfiguration.temperatureStateTopic, commandFilter);
246 buildOptionalChannel(TEMPERATURE_HIGH_CH_ID,
247 new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.tempStep),
248 channelConfiguration.temperatureUnit),
249 updateListener, channelConfiguration.temperatureHighCommandTemplate,
250 channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate,
251 channelConfiguration.temperatureHighStateTopic, commandFilter);
253 buildOptionalChannel(TEMPERATURE_LOW_CH_ID,
254 new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.tempStep),
255 channelConfiguration.temperatureUnit),
256 updateListener, channelConfiguration.temperatureLowCommandTemplate,
257 channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate,
258 channelConfiguration.temperatureLowStateTopic, commandFilter);
260 buildOptionalChannel(POWER_CH_ID, new OnOffValue(), updateListener, null,
261 channelConfiguration.powerCommandTopic, null, null, null);
265 private ComponentChannel buildOptionalChannel(String channelId, Value valueState,
266 ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
267 @Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic,
268 @Nullable Predicate<Command> commandFilter) {
269 if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
270 return buildChannel(channelId, valueState, channelConfiguration.getName(), channelStateUpdateListener)
271 .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
272 .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
274 .commandFilter(commandFilter).build();
279 private @Nullable Predicate<Command> getCommandFilter(@Nullable ComponentChannel actionChannel) {
280 if (actionChannel == null) {
283 final var val = actionChannel.getState().getCache();
284 return command -> !ACTION_OFF_STATE.equals(val.getChannelState());