2 * Copyright (c) 2010-2021 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;
34 * A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
36 * @author David Graeff - Initial contribution
37 * @author Anton Kharuzhy - Implementation
40 public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
41 public static final String ACTION_CH_ID = "action";
42 public static final String AUX_CH_ID = "aux";
43 public static final String AWAY_MODE_CH_ID = "awayMode";
44 public static final String CURRENT_TEMPERATURE_CH_ID = "currentTemperature";
45 public static final String FAN_MODE_CH_ID = "fanMode";
46 public static final String HOLD_CH_ID = "hold";
47 public static final String MODE_CH_ID = "mode";
48 public static final String SWING_CH_ID = "swing";
49 public static final String TEMPERATURE_CH_ID = "temperature";
50 public static final String TEMPERATURE_HIGH_CH_ID = "temperatureHigh";
51 public static final String TEMPERATURE_LOW_CH_ID = "temperatureLow";
52 public static final String POWER_CH_ID = "power";
54 private static final String CELSIUM = "C";
55 private static final String FAHRENHEIT = "F";
56 private static final float DEFAULT_CELSIUM_PRECISION = 0.1f;
57 private static final float DEFAULT_FAHRENHEIT_PRECISION = 1f;
59 private static final String ACTION_OFF = "off";
60 private static final State ACTION_OFF_STATE = new StringType(ACTION_OFF);
61 private static final List<String> ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan");
64 * Configuration class for MQTT component
66 static class ChannelConfiguration extends AbstractChannelConfiguration {
67 ChannelConfiguration() {
71 protected @Nullable String action_template;
72 protected @Nullable String action_topic;
74 protected @Nullable String aux_command_topic;
75 protected @Nullable String aux_state_template;
76 protected @Nullable String aux_state_topic;
78 protected @Nullable String away_mode_command_topic;
79 protected @Nullable String away_mode_state_template;
80 protected @Nullable String away_mode_state_topic;
82 protected @Nullable String current_temperature_template;
83 protected @Nullable String current_temperature_topic;
85 protected @Nullable String fan_mode_command_template;
86 protected @Nullable String fan_mode_command_topic;
87 protected @Nullable String fan_mode_state_template;
88 protected @Nullable String fan_mode_state_topic;
89 protected List<String> fan_modes = Arrays.asList("auto", "low", "medium", "high");
91 protected @Nullable String hold_command_template;
92 protected @Nullable String hold_command_topic;
93 protected @Nullable String hold_state_template;
94 protected @Nullable String hold_state_topic;
95 protected @Nullable List<String> hold_modes; // Are there default modes? Now the channel will be ignored without
98 protected @Nullable String json_attributes_template; // Attributes are not supported yet
99 protected @Nullable String json_attributes_topic;
101 protected @Nullable String mode_command_template;
102 protected @Nullable String mode_command_topic;
103 protected @Nullable String mode_state_template;
104 protected @Nullable String mode_state_topic;
105 protected List<String> modes = Arrays.asList("auto", "off", "cool", "heat", "dry", "fan_only");
107 protected @Nullable String swing_command_template;
108 protected @Nullable String swing_command_topic;
109 protected @Nullable String swing_state_template;
110 protected @Nullable String swing_state_topic;
111 protected List<String> swing_modes = Arrays.asList("on", "off");
113 protected @Nullable String temperature_command_template;
114 protected @Nullable String temperature_command_topic;
115 protected @Nullable String temperature_state_template;
116 protected @Nullable String temperature_state_topic;
118 protected @Nullable String temperature_high_command_template;
119 protected @Nullable String temperature_high_command_topic;
120 protected @Nullable String temperature_high_state_template;
121 protected @Nullable String temperature_high_state_topic;
123 protected @Nullable String temperature_low_command_template;
124 protected @Nullable String temperature_low_command_topic;
125 protected @Nullable String temperature_low_state_template;
126 protected @Nullable String temperature_low_state_topic;
128 protected @Nullable String power_command_topic;
130 protected Integer initial = 21;
131 protected @Nullable Float max_temp;
132 protected @Nullable Float min_temp;
133 protected String temperature_unit = CELSIUM; // System unit by default
134 protected Float temp_step = 1f;
135 protected @Nullable Float precision;
136 protected Boolean send_if_off = true;
139 public Climate(ComponentFactory.ComponentConfiguration componentConfiguration) {
140 super(componentConfiguration, ChannelConfiguration.class);
142 BigDecimal minTemp = channelConfiguration.min_temp != null ? BigDecimal.valueOf(channelConfiguration.min_temp)
144 BigDecimal maxTemp = channelConfiguration.max_temp != null ? BigDecimal.valueOf(channelConfiguration.max_temp)
146 float precision = channelConfiguration.precision != null ? channelConfiguration.precision
147 : (FAHRENHEIT.equals(channelConfiguration.temperature_unit) ? DEFAULT_FAHRENHEIT_PRECISION
148 : DEFAULT_CELSIUM_PRECISION);
149 final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
151 ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID,
152 new TextValue(ACTION_MODES.toArray(new String[0])), updateListener, null, null,
153 channelConfiguration.action_template, channelConfiguration.action_topic, null);
155 final Predicate<Command> commandFilter = channelConfiguration.send_if_off ? null
156 : getCommandFilter(actionChannel);
158 buildOptionalChannel(AUX_CH_ID, new OnOffValue(), updateListener, null, channelConfiguration.aux_command_topic,
159 channelConfiguration.aux_state_template, channelConfiguration.aux_state_topic, commandFilter);
161 buildOptionalChannel(AWAY_MODE_CH_ID, new OnOffValue(), updateListener, null,
162 channelConfiguration.away_mode_command_topic, channelConfiguration.away_mode_state_template,
163 channelConfiguration.away_mode_state_topic, commandFilter);
165 buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID,
166 new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(precision), channelConfiguration.temperature_unit),
167 updateListener, null, null, channelConfiguration.current_temperature_template,
168 channelConfiguration.current_temperature_topic, commandFilter);
170 buildOptionalChannel(FAN_MODE_CH_ID, new TextValue(channelConfiguration.fan_modes.toArray(new String[0])),
171 updateListener, channelConfiguration.fan_mode_command_template,
172 channelConfiguration.fan_mode_command_topic, channelConfiguration.fan_mode_state_template,
173 channelConfiguration.fan_mode_state_topic, commandFilter);
175 if (channelConfiguration.hold_modes != null && !channelConfiguration.hold_modes.isEmpty()) {
176 buildOptionalChannel(HOLD_CH_ID, new TextValue(channelConfiguration.hold_modes.toArray(new String[0])),
177 updateListener, channelConfiguration.hold_command_template, channelConfiguration.hold_command_topic,
178 channelConfiguration.hold_state_template, channelConfiguration.hold_state_topic, commandFilter);
181 buildOptionalChannel(MODE_CH_ID, new TextValue(channelConfiguration.modes.toArray(new String[0])),
182 updateListener, channelConfiguration.mode_command_template, channelConfiguration.mode_command_topic,
183 channelConfiguration.mode_state_template, channelConfiguration.mode_state_topic, commandFilter);
185 buildOptionalChannel(SWING_CH_ID, new TextValue(channelConfiguration.swing_modes.toArray(new String[0])),
186 updateListener, channelConfiguration.swing_command_template, channelConfiguration.swing_command_topic,
187 channelConfiguration.swing_state_template, channelConfiguration.swing_state_topic, commandFilter);
189 buildOptionalChannel(TEMPERATURE_CH_ID,
190 new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.temp_step),
191 channelConfiguration.temperature_unit),
192 updateListener, channelConfiguration.temperature_command_template,
193 channelConfiguration.temperature_command_topic, channelConfiguration.temperature_state_template,
194 channelConfiguration.temperature_state_topic, commandFilter);
196 buildOptionalChannel(TEMPERATURE_HIGH_CH_ID,
197 new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.temp_step),
198 channelConfiguration.temperature_unit),
199 updateListener, channelConfiguration.temperature_high_command_template,
200 channelConfiguration.temperature_high_command_topic,
201 channelConfiguration.temperature_high_state_template, channelConfiguration.temperature_high_state_topic,
204 buildOptionalChannel(TEMPERATURE_LOW_CH_ID,
205 new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.temp_step),
206 channelConfiguration.temperature_unit),
207 updateListener, channelConfiguration.temperature_low_command_template,
208 channelConfiguration.temperature_low_command_topic, channelConfiguration.temperature_low_state_template,
209 channelConfiguration.temperature_low_state_topic, commandFilter);
211 buildOptionalChannel(POWER_CH_ID, new OnOffValue(), updateListener, null,
212 channelConfiguration.power_command_topic, null, null, null);
216 private ComponentChannel buildOptionalChannel(String channelId, Value valueState,
217 ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
218 @Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic,
219 @Nullable Predicate<Command> commandFilter) {
220 if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
221 return buildChannel(channelId, valueState, channelConfiguration.getName(), channelStateUpdateListener)
222 .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
223 .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
225 .commandFilter(commandFilter).build();
230 private @Nullable Predicate<Command> getCommandFilter(@Nullable ComponentChannel actionChannel) {
231 if (actionChannel == null) {
234 final var val = actionChannel.getState().getCache();
235 return command -> !ACTION_OFF_STATE.equals(val.getChannelState());