]> git.basschouten.com Git - openhab-addons.git/blob
1f156d103c6a724a7c5447f417d8ee1f848375b2
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
14
15 import java.math.BigDecimal;
16 import java.util.Arrays;
17 import java.util.List;
18 import java.util.function.Predicate;
19
20 import javax.measure.Unit;
21 import javax.measure.quantity.Temperature;
22
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;
38
39 import com.google.gson.annotations.SerializedName;
40
41 /**
42  * A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
43  *
44  * @author David Graeff - Initial contribution
45  * @author Anton Kharuzhy - Implementation
46  */
47 @NonNullByDefault
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 = "away-mode";
52     public static final String AWAY_MODE_CH_ID_DEPRECATED = "awayMode";
53     public static final String CURRENT_TEMPERATURE_CH_ID = "current-temperature";
54     public static final String CURRENT_TEMPERATURE_CH_ID_DEPRECATED = "currentTemperature";
55     public static final String FAN_MODE_CH_ID = "fan-mode";
56     public static final String FAN_MODE_CH_ID_DEPRECATED = "fanMode";
57     public static final String HOLD_CH_ID = "hold";
58     public static final String MODE_CH_ID = "mode";
59     public static final String SWING_CH_ID = "swing";
60     public static final String TEMPERATURE_CH_ID = "temperature";
61     public static final String TEMPERATURE_HIGH_CH_ID = "temperature-high";
62     public static final String TEMPERATURE_HIGH_CH_ID_DEPRECATED = "temperatureHigh";
63     public static final String TEMPERATURE_LOW_CH_ID = "temperature-low";
64     public static final String TEMPERATURE_LOW_CH_ID_DEPRECATED = "temperatureLow";
65     public static final String POWER_CH_ID = "power";
66
67     public enum TemperatureUnit {
68         @SerializedName("C")
69         CELSIUS(SIUnits.CELSIUS, new BigDecimal("0.1")),
70         @SerializedName("F")
71         FAHRENHEIT(ImperialUnits.FAHRENHEIT, BigDecimal.ONE);
72
73         private final Unit<Temperature> unit;
74         private final BigDecimal defaultPrecision;
75
76         TemperatureUnit(Unit<Temperature> unit, BigDecimal defaultPrecision) {
77             this.unit = unit;
78             this.defaultPrecision = defaultPrecision;
79         }
80
81         public Unit<Temperature> getUnit() {
82             return unit;
83         }
84
85         public BigDecimal getDefaultPrecision() {
86             return defaultPrecision;
87         }
88     }
89
90     private static final String ACTION_OFF = "off";
91     private static final State ACTION_OFF_STATE = new StringType(ACTION_OFF);
92     private static final List<String> ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan");
93
94     /**
95      * Configuration class for MQTT component
96      */
97     static class ChannelConfiguration extends AbstractChannelConfiguration {
98         ChannelConfiguration() {
99             super("MQTT HVAC");
100         }
101
102         protected @Nullable Boolean optimistic;
103
104         @SerializedName("action_template")
105         protected @Nullable String actionTemplate;
106         @SerializedName("action_topic")
107         protected @Nullable String actionTopic;
108
109         @SerializedName("aux_command_topic")
110         protected @Nullable String auxCommandTopic;
111         @SerializedName("aux_state_template")
112         protected @Nullable String auxStateTemplate;
113         @SerializedName("aux_state_topic")
114         protected @Nullable String auxStateTopic;
115
116         @SerializedName("away_mode_command_topic")
117         protected @Nullable String awayModeCommandTopic;
118         @SerializedName("away_mode_state_template")
119         protected @Nullable String awayModeStateTemplate;
120         @SerializedName("away_mode_state_topic")
121         protected @Nullable String awayModeStateTopic;
122
123         @SerializedName("current_temperature_template")
124         protected @Nullable String currentTemperatureTemplate;
125         @SerializedName("current_temperature_topic")
126         protected @Nullable String currentTemperatureTopic;
127
128         @SerializedName("fan_mode_command_template")
129         protected @Nullable String fanModeCommandTemplate;
130         @SerializedName("fan_mode_command_topic")
131         protected @Nullable String fanModeCommandTopic;
132         @SerializedName("fan_mode_state_template")
133         protected @Nullable String fanModeStateTemplate;
134         @SerializedName("fan_mode_state_topic")
135         protected @Nullable String fanModeStateTopic;
136         @SerializedName("fan_modes")
137         protected List<String> fanModes = Arrays.asList("auto", "low", "medium", "high");
138
139         @SerializedName("hold_command_template")
140         protected @Nullable String holdCommandTemplate;
141         @SerializedName("hold_command_topic")
142         protected @Nullable String holdCommandTopic;
143         @SerializedName("hold_state_template")
144         protected @Nullable String holdStateTemplate;
145         @SerializedName("hold_state_topic")
146         protected @Nullable String holdStateTopic;
147         @SerializedName("hold_modes")
148         protected @Nullable List<String> holdModes; // Are there default modes? Now the channel will be ignored without
149                                                     // hold modes.
150
151         @SerializedName("json_attributes_template")
152         protected @Nullable String jsonAttributesTemplate; // Attributes are not supported yet
153         @SerializedName("json_attributes_topic")
154         protected @Nullable String jsonAttributesTopic;
155
156         @SerializedName("mode_command_template")
157         protected @Nullable String modeCommandTemplate;
158         @SerializedName("mode_command_topic")
159         protected @Nullable String modeCommandTopic;
160         @SerializedName("mode_state_template")
161         protected @Nullable String modeStateTemplate;
162         @SerializedName("mode_state_topic")
163         protected @Nullable String modeStateTopic;
164         protected List<String> modes = Arrays.asList("auto", "off", "cool", "heat", "dry", "fan_only");
165
166         @SerializedName("swing_command_template")
167         protected @Nullable String swingCommandTemplate;
168         @SerializedName("swing_command_topic")
169         protected @Nullable String swingCommandTopic;
170         @SerializedName("swing_state_template")
171         protected @Nullable String swingStateTemplate;
172         @SerializedName("swing_state_topic")
173         protected @Nullable String swingStateTopic;
174         @SerializedName("swing_modes")
175         protected List<String> swingModes = Arrays.asList("on", "off");
176
177         @SerializedName("temperature_command_template")
178         protected @Nullable String temperatureCommandTemplate;
179         @SerializedName("temperature_command_topic")
180         protected @Nullable String temperatureCommandTopic;
181         @SerializedName("temperature_state_template")
182         protected @Nullable String temperatureStateTemplate;
183         @SerializedName("temperature_state_topic")
184         protected @Nullable String temperatureStateTopic;
185
186         @SerializedName("temperature_high_command_template")
187         protected @Nullable String temperatureHighCommandTemplate;
188         @SerializedName("temperature_high_command_topic")
189         protected @Nullable String temperatureHighCommandTopic;
190         @SerializedName("temperature_high_state_template")
191         protected @Nullable String temperatureHighStateTemplate;
192         @SerializedName("temperature_high_state_topic")
193         protected @Nullable String temperatureHighStateTopic;
194
195         @SerializedName("temperature_low_command_template")
196         protected @Nullable String temperatureLowCommandTemplate;
197         @SerializedName("temperature_low_command_topic")
198         protected @Nullable String temperatureLowCommandTopic;
199         @SerializedName("temperature_low_state_template")
200         protected @Nullable String temperatureLowStateTemplate;
201         @SerializedName("temperature_low_state_topic")
202         protected @Nullable String temperatureLowStateTopic;
203
204         @SerializedName("power_command_topic")
205         protected @Nullable String powerCommandTopic;
206
207         protected Integer initial = 21;
208         @SerializedName("max_temp")
209         protected @Nullable BigDecimal maxTemp;
210         @SerializedName("min_temp")
211         protected @Nullable BigDecimal minTemp;
212         @SerializedName("temperature_unit")
213         protected TemperatureUnit temperatureUnit = TemperatureUnit.CELSIUS; // System unit by default
214         @SerializedName("temp_step")
215         protected BigDecimal tempStep = BigDecimal.ONE;
216         protected @Nullable BigDecimal precision;
217         @SerializedName("send_if_off")
218         protected Boolean sendIfOff = true;
219     }
220
221     public Climate(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
222         super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
223
224         BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
225                 : channelConfiguration.temperatureUnit.getDefaultPrecision();
226         final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
227
228         ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID, ComponentChannelType.STRING,
229                 new TextValue(ACTION_MODES.toArray(new String[0])), updateListener, null, null,
230                 channelConfiguration.actionTemplate, channelConfiguration.actionTopic, null);
231
232         final Predicate<Command> commandFilter = channelConfiguration.sendIfOff ? null
233                 : getCommandFilter(actionChannel);
234
235         buildOptionalChannel(AUX_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
236                 channelConfiguration.auxCommandTopic, channelConfiguration.auxStateTemplate,
237                 channelConfiguration.auxStateTopic, commandFilter);
238
239         buildOptionalChannel(newStyleChannels ? AWAY_MODE_CH_ID : AWAY_MODE_CH_ID_DEPRECATED,
240                 ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
241                 channelConfiguration.awayModeCommandTopic, channelConfiguration.awayModeStateTemplate,
242                 channelConfiguration.awayModeStateTopic, commandFilter);
243
244         buildOptionalChannel(newStyleChannels ? CURRENT_TEMPERATURE_CH_ID : CURRENT_TEMPERATURE_CH_ID_DEPRECATED,
245                 ComponentChannelType.NUMBER,
246                 new NumberValue(null, null, precision, channelConfiguration.temperatureUnit.getUnit()), updateListener,
247                 null, null, channelConfiguration.currentTemperatureTemplate,
248                 channelConfiguration.currentTemperatureTopic, commandFilter);
249
250         buildOptionalChannel(newStyleChannels ? FAN_MODE_CH_ID : FAN_MODE_CH_ID_DEPRECATED, ComponentChannelType.STRING,
251                 new TextValue(channelConfiguration.fanModes.toArray(new String[0])), updateListener,
252                 channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic,
253                 channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter);
254
255         List<String> holdModes = channelConfiguration.holdModes;
256         if (holdModes != null && !holdModes.isEmpty()) {
257             buildOptionalChannel(HOLD_CH_ID, ComponentChannelType.STRING,
258                     new TextValue(holdModes.toArray(new String[0])), updateListener,
259                     channelConfiguration.holdCommandTemplate, channelConfiguration.holdCommandTopic,
260                     channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter);
261         }
262
263         buildOptionalChannel(MODE_CH_ID, ComponentChannelType.STRING,
264                 new TextValue(channelConfiguration.modes.toArray(new String[0])), updateListener,
265                 channelConfiguration.modeCommandTemplate, channelConfiguration.modeCommandTopic,
266                 channelConfiguration.modeStateTemplate, channelConfiguration.modeStateTopic, commandFilter);
267
268         buildOptionalChannel(SWING_CH_ID, ComponentChannelType.STRING,
269                 new TextValue(channelConfiguration.swingModes.toArray(new String[0])), updateListener,
270                 channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic,
271                 channelConfiguration.swingStateTemplate, channelConfiguration.swingStateTopic, commandFilter);
272
273         buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.NUMBER,
274                 new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
275                         channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
276                 updateListener, channelConfiguration.temperatureCommandTemplate,
277                 channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate,
278                 channelConfiguration.temperatureStateTopic, commandFilter);
279
280         buildOptionalChannel(newStyleChannels ? TEMPERATURE_HIGH_CH_ID : TEMPERATURE_HIGH_CH_ID_DEPRECATED,
281                 ComponentChannelType.NUMBER,
282                 new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
283                         channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
284                 updateListener, channelConfiguration.temperatureHighCommandTemplate,
285                 channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate,
286                 channelConfiguration.temperatureHighStateTopic, commandFilter);
287
288         buildOptionalChannel(newStyleChannels ? TEMPERATURE_LOW_CH_ID : TEMPERATURE_LOW_CH_ID_DEPRECATED,
289                 ComponentChannelType.NUMBER,
290                 new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
291                         channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
292                 updateListener, channelConfiguration.temperatureLowCommandTemplate,
293                 channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate,
294                 channelConfiguration.temperatureLowStateTopic, commandFilter);
295
296         buildOptionalChannel(POWER_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
297                 channelConfiguration.powerCommandTopic, null, null, null);
298         finalizeChannels();
299     }
300
301     @Nullable
302     private ComponentChannel buildOptionalChannel(String channelId, ComponentChannelType channelType, Value valueState,
303             ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
304             @Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic,
305             @Nullable Predicate<Command> commandFilter) {
306         if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
307             return buildChannel(channelId, channelType, valueState, getName(), channelStateUpdateListener)
308                     .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
309                     .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
310                             commandTemplate)
311                     .inferOptimistic(channelConfiguration.optimistic).commandFilter(commandFilter).build();
312         }
313         return null;
314     }
315
316     private @Nullable Predicate<Command> getCommandFilter(@Nullable ComponentChannel actionChannel) {
317         if (actionChannel == null) {
318             return null;
319         }
320         final var val = actionChannel.getState().getCache();
321         return command -> !ACTION_OFF_STATE.equals(val.getChannelState());
322     }
323 }