]> git.basschouten.com Git - openhab-addons.git/blob
f4b526da894ed0647b9e03aa7dda8462e86aacce
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.ArrayList;
17 import java.util.Arrays;
18 import java.util.Collections;
19 import java.util.List;
20 import java.util.stream.Collectors;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
25 import org.openhab.binding.mqtt.generic.values.OnOffValue;
26 import org.openhab.binding.mqtt.generic.values.PercentageValue;
27 import org.openhab.binding.mqtt.generic.values.TextValue;
28 import org.openhab.binding.mqtt.generic.values.Value;
29 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
30 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 import com.google.gson.annotations.SerializedName;
35
36 /**
37  * A MQTT vacuum, following the https://www.home-assistant.io/components/vacuum.mqtt/ specification.
38  *
39  * @author Stefan Triller - Initial contribution
40  * @author Anton Kharuzhyi - Make it compilant with the Specification
41  */
42 @NonNullByDefault
43 public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
44     public static final String SCHEMA_LEGACY = "legacy";
45     public static final String SCHEMA_STATE = "state";
46
47     public static final String TRUE = "true";
48     public static final String FALSE = "false";
49     public static final String OFF = "off";
50
51     public static final String FEATURE_TURN_ON = "turn_on"; // Begin cleaning
52     public static final String FEATURE_TURN_OFF = "turn_off"; // Turn the Vacuum off
53     public static final String FEATURE_RETURN_HOME = "return_home"; // Return to base/dock
54     public static final String FEATURE_START = "start";
55     public static final String FEATURE_STOP = "stop"; // Stop the Vacuum
56     public static final String FEATURE_CLEAN_SPOT = "clean_spot"; // Initialize a spot cleaning cycle
57     public static final String FEATURE_LOCATE = "locate"; // Locate the vacuum (typically by playing a song)
58     public static final String FEATURE_PAUSE = "pause"; // Pause the vacuum
59     public static final String FEATURE_BATTERY = "battery";
60     public static final String FEATURE_STATUS = "status";
61     public static final String FEATURE_FAN_SPEED = "fan_speed";
62     public static final String FEATURE_SEND_COMMAND = "send_command";
63
64     // State Schema only
65     public static final String STATE_CLEANING = "cleaning";
66     public static final String STATE_DOCKED = "docked";
67     public static final String STATE_PAUSED = "paused";
68     public static final String STATE_IDLE = "idle";
69     public static final String STATE_RETURNING = "returning";
70     public static final String STATE_ERROR = "error";
71
72     public static final String COMMAND_CH_ID = "command";
73     public static final String FAN_SPEED_CH_ID = "fanSpeed";
74     public static final String CUSTOM_COMMAND_CH_ID = "customCommand";
75     public static final String BATTERY_LEVEL_CH_ID = "batteryLevel";
76     public static final String CHARGING_CH_ID = "charging";
77     public static final String CLEANING_CH_ID = "cleaning";
78     public static final String DOCKED_CH_ID = "docked";
79     public static final String ERROR_CH_ID = "error";
80     public static final String JSON_ATTRIBUTES_CH_ID = "jsonAttributes";
81     public static final String STATE_CH_ID = "state";
82
83     public static final List<String> LEGACY_DEFAULT_FEATURES = List.of(FEATURE_TURN_ON, FEATURE_TURN_OFF, FEATURE_STOP,
84             FEATURE_RETURN_HOME, FEATURE_BATTERY, FEATURE_STATUS, FEATURE_CLEAN_SPOT);
85     public static final List<String> LEGACY_SUPPORTED_FEATURES = List.of(FEATURE_TURN_ON, FEATURE_TURN_OFF,
86             FEATURE_PAUSE, FEATURE_STOP, FEATURE_RETURN_HOME, FEATURE_BATTERY, FEATURE_STATUS, FEATURE_LOCATE,
87             FEATURE_CLEAN_SPOT, FEATURE_FAN_SPEED, FEATURE_SEND_COMMAND);
88
89     public static final List<String> STATE_DEFAULT_FEATURES = List.of(FEATURE_START, FEATURE_STOP, FEATURE_RETURN_HOME,
90             FEATURE_STATUS, FEATURE_BATTERY, FEATURE_CLEAN_SPOT);
91     public static final List<String> STATE_SUPPORTED_FEATURES = List.of(FEATURE_START, FEATURE_STOP, FEATURE_PAUSE,
92             FEATURE_RETURN_HOME, FEATURE_BATTERY, FEATURE_STATUS, FEATURE_LOCATE, FEATURE_CLEAN_SPOT, FEATURE_FAN_SPEED,
93             FEATURE_SEND_COMMAND);
94
95     private static final Logger LOGGER = LoggerFactory.getLogger(Vacuum.class);
96
97     /**
98      * Configuration class for MQTT component
99      */
100     static class ChannelConfiguration extends AbstractChannelConfiguration {
101         ChannelConfiguration() {
102             super("MQTT Vacuum");
103         }
104
105         // Legacy and Common MQTT vacuum configuration section.
106
107         @SerializedName("battery_level_template")
108         protected @Nullable String batteryLevelTemplate;
109         @SerializedName("battery_level_topic")
110         protected @Nullable String batteryLevelTopic;
111
112         @SerializedName("charging_template")
113         protected @Nullable String chargingTemplate;
114         @SerializedName("charging_topic")
115         protected @Nullable String chargingTopic;
116
117         @SerializedName("cleaning_template")
118         protected @Nullable String cleaningTemplate;
119         @SerializedName("cleaning_topic")
120         protected @Nullable String cleaningTopic;
121
122         @SerializedName("command_topic")
123         protected @Nullable String commandTopic;
124
125         @SerializedName("docked_template")
126         protected @Nullable String dockedTemplate;
127         @SerializedName("docked_topic")
128         protected @Nullable String dockedTopic;
129
130         @SerializedName("enabled_by_default")
131         protected @Nullable Boolean enabledByDefault = true;
132
133         @SerializedName("error_template")
134         protected @Nullable String errorTemplate;
135         @SerializedName("error_topic")
136         protected @Nullable String errorTopic;
137
138         @SerializedName("fan_speed_list")
139         protected @Nullable List<String> fanSpeedList;
140         @SerializedName("fan_speed_template")
141         protected @Nullable String fanSpeedTemplate;
142         @SerializedName("fan_speed_topic")
143         protected @Nullable String fanSpeedTopic;
144
145         @SerializedName("payload_clean_spot")
146         protected @Nullable String payloadCleanSpot = "clean_spot";
147         @SerializedName("payload_locate")
148         protected @Nullable String payloadLocate = "locate";
149         @SerializedName("payload_return_to_base")
150         protected @Nullable String payloadReturnToBase = "return_to_base";
151         @SerializedName("payload_start_pause")
152         protected @Nullable String payloadStartPause = "start_pause"; // Legacy only
153         @SerializedName("payload_stop")
154         protected @Nullable String payloadStop = "stop";
155         @SerializedName("payload_turn_off")
156         protected @Nullable String payloadTurnOff = "turn_off";
157         @SerializedName("payload_turn_on")
158         protected @Nullable String payloadTurnOn = "turn_on";
159
160         @SerializedName("schema")
161         protected Schema schema = Schema.LEGACY;
162
163         @SerializedName("send_command_topic")
164         protected @Nullable String sendCommandTopic;
165
166         @SerializedName("set_fan_speed_topic")
167         protected @Nullable String setFanSpeedTopic;
168
169         @SerializedName("supported_features")
170         protected @Nullable List<String> supportedFeatures;
171
172         // State MQTT vacuum configuration section.
173
174         // Start/Pause replaced by 2 payloads
175         @SerializedName("payload_pause")
176         protected @Nullable String payloadPause = "pause";
177         @SerializedName("payload_start")
178         protected @Nullable String payloadStart = "start";
179
180         @SerializedName("state_topic")
181         protected @Nullable String stateTopic;
182
183         @SerializedName("json_attributes_template")
184         protected @Nullable String jsonAttributesTemplate;
185         @SerializedName("json_attributes_topic")
186         protected @Nullable String jsonAttributesTopic;
187     }
188
189     /**
190      * Creates component based on generic configuration and component configuration type.
191      *
192      * @param componentConfiguration generic componentConfiguration with not parsed JSON config
193      */
194     public Vacuum(ComponentFactory.ComponentConfiguration componentConfiguration) {
195         super(componentConfiguration, ChannelConfiguration.class);
196         final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
197
198         final var allowedSupportedFeatures = channelConfiguration.schema == Schema.LEGACY ? LEGACY_SUPPORTED_FEATURES
199                 : STATE_SUPPORTED_FEATURES;
200         final var configSupportedFeatures = channelConfiguration.supportedFeatures == null
201                 ? channelConfiguration.schema == Schema.LEGACY ? LEGACY_DEFAULT_FEATURES : STATE_DEFAULT_FEATURES
202                 : channelConfiguration.supportedFeatures;
203         List<String> deviceSupportedFeatures = Collections.emptyList();
204
205         if (!configSupportedFeatures.isEmpty()) {
206             deviceSupportedFeatures = allowedSupportedFeatures.stream().filter(configSupportedFeatures::contains)
207                     .collect(Collectors.toList());
208         }
209         if (deviceSupportedFeatures.size() != configSupportedFeatures.size()) {
210             LOGGER.warn("Vacuum discovery config has unsupported or duplicated features. Supported: {}, provided: {}",
211                     Arrays.toString(allowedSupportedFeatures.toArray()),
212                     Arrays.toString(configSupportedFeatures.toArray()));
213         }
214
215         final List<String> commands = new ArrayList<>();
216         addPayloadToList(deviceSupportedFeatures, FEATURE_CLEAN_SPOT, channelConfiguration.payloadCleanSpot, commands);
217         addPayloadToList(deviceSupportedFeatures, FEATURE_LOCATE, channelConfiguration.payloadLocate, commands);
218         addPayloadToList(deviceSupportedFeatures, FEATURE_RETURN_HOME, channelConfiguration.payloadReturnToBase,
219                 commands);
220         addPayloadToList(deviceSupportedFeatures, FEATURE_STOP, channelConfiguration.payloadStop, commands);
221         addPayloadToList(deviceSupportedFeatures, FEATURE_TURN_OFF, channelConfiguration.payloadTurnOff, commands);
222         addPayloadToList(deviceSupportedFeatures, FEATURE_TURN_ON, channelConfiguration.payloadTurnOn, commands);
223
224         if (channelConfiguration.schema == Schema.LEGACY) {
225             addPayloadToList(deviceSupportedFeatures, FEATURE_PAUSE, channelConfiguration.payloadStartPause, commands);
226         } else {
227             addPayloadToList(deviceSupportedFeatures, FEATURE_PAUSE, channelConfiguration.payloadPause, commands);
228             addPayloadToList(deviceSupportedFeatures, FEATURE_START, channelConfiguration.payloadStart, commands);
229         }
230
231         buildOptionalChannel(COMMAND_CH_ID, new TextValue(commands.toArray(new String[0])), updateListener, null,
232                 channelConfiguration.commandTopic, null, null);
233
234         final var fanSpeedList = channelConfiguration.fanSpeedList;
235         if (deviceSupportedFeatures.contains(FEATURE_FAN_SPEED) && fanSpeedList != null && !fanSpeedList.isEmpty()) {
236             if (!fanSpeedList.contains(OFF)) {
237                 fanSpeedList.add(OFF); // Off value is used when cleaning if OFF
238             }
239             var fanSpeedValue = new TextValue(fanSpeedList.toArray(new String[0]));
240             if (channelConfiguration.schema == Schema.LEGACY) {
241                 buildOptionalChannel(FAN_SPEED_CH_ID, fanSpeedValue, updateListener, null,
242                         channelConfiguration.setFanSpeedTopic, channelConfiguration.fanSpeedTemplate,
243                         channelConfiguration.fanSpeedTopic);
244             } else if (deviceSupportedFeatures.contains(FEATURE_STATUS)) {
245                 buildOptionalChannel(FAN_SPEED_CH_ID, fanSpeedValue, updateListener, null,
246                         channelConfiguration.setFanSpeedTopic, "{{ value_json.fan_speed }}",
247                         channelConfiguration.stateTopic);
248             } else {
249                 LOGGER.info("Status feature is disabled, unable to get fan speed.");
250                 buildOptionalChannel(FAN_SPEED_CH_ID, fanSpeedValue, updateListener, null,
251                         channelConfiguration.setFanSpeedTopic, null, null);
252             }
253         }
254
255         if (deviceSupportedFeatures.contains(FEATURE_SEND_COMMAND)) {
256             buildOptionalChannel(CUSTOM_COMMAND_CH_ID, new TextValue(), updateListener, null,
257                     channelConfiguration.sendCommandTopic, null, null);
258         }
259
260         if (channelConfiguration.schema == Schema.LEGACY) {
261             // I assume, that if these topics defined in config, then we don't need to check features
262             buildOptionalChannel(BATTERY_LEVEL_CH_ID,
263                     new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.ONE, null, null),
264                     updateListener, null, null, channelConfiguration.batteryLevelTemplate,
265                     channelConfiguration.batteryLevelTopic);
266             buildOptionalChannel(CHARGING_CH_ID, new OnOffValue(TRUE, FALSE), updateListener, null, null,
267                     channelConfiguration.chargingTemplate, channelConfiguration.chargingTopic);
268             buildOptionalChannel(CLEANING_CH_ID, new OnOffValue(TRUE, FALSE), updateListener, null, null,
269                     channelConfiguration.cleaningTemplate, channelConfiguration.cleaningTopic);
270             buildOptionalChannel(DOCKED_CH_ID, new OnOffValue(TRUE, FALSE), updateListener, null, null,
271                     channelConfiguration.dockedTemplate, channelConfiguration.dockedTopic);
272             buildOptionalChannel(ERROR_CH_ID, new TextValue(), updateListener, null, null,
273                     channelConfiguration.errorTemplate, channelConfiguration.errorTopic);
274         } else {
275             if (deviceSupportedFeatures.contains(FEATURE_STATUS)) {
276                 // state key is mandatory
277                 buildOptionalChannel(STATE_CH_ID,
278                         new TextValue(new String[] { STATE_CLEANING, STATE_DOCKED, STATE_PAUSED, STATE_IDLE,
279                                 STATE_RETURNING, STATE_ERROR }),
280                         updateListener, null, null, "{{ value_json.state }}", channelConfiguration.stateTopic);
281                 if (deviceSupportedFeatures.contains(FEATURE_BATTERY)) {
282                     buildOptionalChannel(BATTERY_LEVEL_CH_ID,
283                             new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.ONE, null, null),
284                             updateListener, null, null, "{{ value_json.battery_level }}",
285                             channelConfiguration.stateTopic);
286                 }
287             }
288         }
289
290         buildOptionalChannel(JSON_ATTRIBUTES_CH_ID, new TextValue(), updateListener, null, null,
291                 channelConfiguration.jsonAttributesTemplate, channelConfiguration.jsonAttributesTopic);
292     }
293
294     @Nullable
295     private ComponentChannel buildOptionalChannel(String channelId, Value valueState,
296             ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
297             @Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic) {
298         if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
299             return buildChannel(channelId, valueState, channelConfiguration.getName(), channelStateUpdateListener)
300                     .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
301                     .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
302                             commandTemplate)
303                     .build();
304         }
305         return null;
306     }
307
308     private void addPayloadToList(List<String> supportedFeatures, String feature, @Nullable String payload,
309             List<String> list) {
310         if (supportedFeatures.contains(feature) && payload != null && !payload.isEmpty()) {
311             list.add(payload);
312         }
313     }
314
315     public enum Schema {
316         @SerializedName("legacy")
317         LEGACY,
318         @SerializedName("state")
319         STATE
320     }
321 }