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