]> git.basschouten.com Git - openhab-addons.git/blob
5e68b56689ade951c5e00195295bf72f61bde23d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.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;
37
38 import com.google.gson.annotations.SerializedName;
39
40 /**
41  * A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
42  *
43  * @author David Graeff - Initial contribution
44  * @author Anton Kharuzhy - Implementation
45  */
46 @NonNullByDefault
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";
60
61     public enum TemperatureUnit {
62         @SerializedName("C")
63         CELSIUS(SIUnits.CELSIUS, new BigDecimal("0.1")),
64         @SerializedName("F")
65         FAHRENHEIT(ImperialUnits.FAHRENHEIT, BigDecimal.ONE);
66
67         private final Unit<Temperature> unit;
68         private final BigDecimal defaultPrecision;
69
70         TemperatureUnit(Unit<Temperature> unit, BigDecimal defaultPrecision) {
71             this.unit = unit;
72             this.defaultPrecision = defaultPrecision;
73         }
74
75         public Unit<Temperature> getUnit() {
76             return unit;
77         }
78
79         public BigDecimal getDefaultPrecision() {
80             return defaultPrecision;
81         }
82     }
83
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");
87
88     /**
89      * Configuration class for MQTT component
90      */
91     static class ChannelConfiguration extends AbstractChannelConfiguration {
92         ChannelConfiguration() {
93             super("MQTT HVAC");
94         }
95
96         @SerializedName("action_template")
97         protected @Nullable String actionTemplate;
98         @SerializedName("action_topic")
99         protected @Nullable String actionTopic;
100
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;
107
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;
114
115         @SerializedName("current_temperature_template")
116         protected @Nullable String currentTemperatureTemplate;
117         @SerializedName("current_temperature_topic")
118         protected @Nullable String currentTemperatureTopic;
119
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");
130
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
141                                                     // hold modes.
142
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;
147
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");
157
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");
168
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;
177
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;
186
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;
195
196         @SerializedName("power_command_topic")
197         protected @Nullable String powerCommandTopic;
198
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;
211     }
212
213     public Climate(ComponentFactory.ComponentConfiguration componentConfiguration) {
214         super(componentConfiguration, ChannelConfiguration.class);
215
216         BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
217                 : channelConfiguration.temperatureUnit.getDefaultPrecision();
218         final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
219
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);
223
224         final Predicate<Command> commandFilter = channelConfiguration.sendIfOff ? null
225                 : getCommandFilter(actionChannel);
226
227         buildOptionalChannel(AUX_CH_ID, new OnOffValue(), updateListener, null, channelConfiguration.auxCommandTopic,
228                 channelConfiguration.auxStateTemplate, channelConfiguration.auxStateTopic, commandFilter);
229
230         buildOptionalChannel(AWAY_MODE_CH_ID, new OnOffValue(), updateListener, null,
231                 channelConfiguration.awayModeCommandTopic, channelConfiguration.awayModeStateTemplate,
232                 channelConfiguration.awayModeStateTopic, commandFilter);
233
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);
238
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);
242
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);
248         }
249
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);
253
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);
257
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);
264
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);
271
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);
278
279         buildOptionalChannel(POWER_CH_ID, new OnOffValue(), updateListener, null,
280                 channelConfiguration.powerCommandTopic, null, null, null);
281     }
282
283     @Nullable
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(),
292                             commandTemplate)
293                     .commandFilter(commandFilter).build();
294         }
295         return null;
296     }
297
298     private @Nullable Predicate<Command> getCommandFilter(@Nullable ComponentChannel actionChannel) {
299         if (actionChannel == null) {
300             return null;
301         }
302         final var val = actionChannel.getState().getCache();
303         return command -> !ACTION_OFF_STATE.equals(val.getChannelState());
304     }
305 }