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