]> git.basschouten.com Git - openhab-addons.git/blob
ccb57f94615201a0a894e8a5a3a9cb21c888264a
[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.vesync.internal.handlers;
14
15 import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
16 import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*;
17
18 import java.util.Arrays;
19 import java.util.List;
20 import java.util.Set;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
24 import org.openhab.binding.vesync.internal.VeSyncConstants;
25 import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
26 import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassHumidifierStatus;
27 import org.openhab.core.cache.ExpiringCache;
28 import org.openhab.core.library.types.DecimalType;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.QuantityType;
31 import org.openhab.core.library.types.StringType;
32 import org.openhab.core.library.unit.Units;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingTypeUID;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.types.RefreshType;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * The {@link VeSyncDeviceAirHumidifierHandler} is responsible for handling commands, which are
44  * sent to one of the channels.
45  *
46  * @author David Goodyear - Initial contribution
47  */
48 @NonNullByDefault
49 public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
50
51     public static final int DEFAULT_AIR_PURIFIER_POLL_RATE = 120;
52     // "Device Type" values
53     public static final String DEV_TYPE_DUAL_200S = "Dual200S";
54     public static final String DEV_TYPE_CLASSIC_200S = "Classic200S";
55     public static final String DEV_TYPE_CORE_301S = "LUH-D301S-WEU";
56     public static final String DEV_TYPE_CLASSIC_300S = "Classic300S";
57     public static final String DEV_TYPE_600S = "LUH-A602S-WUS";
58     public static final String DEV_TYPE_600S_EU = "LUH-A602S-WEU";
59
60     private static final List<String> CLASSIC_300S_600S_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP);
61     private static final List<String> CLASSIC_300S_NIGHT_LIGHT_MODES = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF);
62
63     public static final List<String> SUPPORTED_DEVICE_TYPES = List.of(DEV_TYPE_DUAL_200S, DEV_TYPE_CLASSIC_200S,
64             DEV_TYPE_CLASSIC_300S, DEV_TYPE_CORE_301S, DEV_TYPE_600S, DEV_TYPE_600S_EU);
65
66     private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirHumidifierHandler.class);
67
68     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_HUMIDIFIER);
69
70     private final Object pollLock = new Object();
71
72     public VeSyncDeviceAirHumidifierHandler(Thing thing) {
73         super(thing);
74     }
75
76     @Override
77     protected String[] getChannelsToRemove() {
78         String[] toRemove = new String[] {};
79         final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
80         if (deviceType != null) {
81             switch (deviceType) {
82                 case DEV_TYPE_CLASSIC_300S:
83                     toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL };
84                     break;
85                 case DEV_TYPE_DUAL_200S:
86                 case DEV_TYPE_CLASSIC_200S:
87                 case DEV_TYPE_CORE_301S:
88                     toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL,
89                             DEVICE_CHANNEL_AF_NIGHT_LIGHT };
90                     break;
91                 case DEV_TYPE_600S:
92                 case DEV_TYPE_600S_EU:
93                     toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT };
94                     break;
95             }
96         }
97         return toRemove;
98     }
99
100     @Override
101     public void initialize() {
102         super.initialize();
103         customiseChannels();
104     }
105
106     @Override
107     public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) {
108         Integer pollRate = config.airPurifierPollInterval;
109         if (pollRate == null) {
110             pollRate = DEFAULT_AIR_PURIFIER_POLL_RATE;
111         }
112         if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
113             setBackgroundPollInterval(-1);
114         } else {
115             setBackgroundPollInterval(pollRate);
116         }
117     }
118
119     @Override
120     public void dispose() {
121         this.setBackgroundPollInterval(-1);
122     }
123
124     @Override
125     protected boolean isDeviceSupported() {
126         final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
127         if (deviceType == null) {
128             return false;
129         }
130         return SUPPORTED_DEVICE_TYPES.contains(deviceType);
131     }
132
133     @Override
134     public void handleCommand(final ChannelUID channelUID, final Command command) {
135         final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
136         if (deviceType == null) {
137             return;
138         }
139
140         scheduler.submit(() -> {
141
142             if (command instanceof OnOffType) {
143                 switch (channelUID.getId()) {
144                     case DEVICE_CHANNEL_ENABLED:
145                         sendV2BypassControlCommand(DEVICE_SET_SWITCH,
146                                 new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON),
147                                         0));
148                         break;
149                     case DEVICE_CHANNEL_DISPLAY_ENABLED:
150                         sendV2BypassControlCommand(DEVICE_SET_DISPLAY,
151                                 new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON)));
152                         break;
153                     case DEVICE_CHANNEL_STOP_AT_TARGET:
154                         sendV2BypassControlCommand(DEVICE_SET_AUTOMATIC_STOP,
155                                 new VeSyncRequestManagedDeviceBypassV2.EnabledPayload(command.equals(OnOffType.ON)));
156                         break;
157                     case DEVICE_CHANNEL_WARM_ENABLED:
158                         logger.warn("Warm mode API is unknown in order to send the command");
159                         break;
160                 }
161             } else if (command instanceof QuantityType) {
162                 switch (channelUID.getId()) {
163                     case DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY:
164                         int targetHumidity = ((QuantityType<?>) command).intValue();
165                         if (targetHumidity < 30) {
166                             logger.warn("Target Humidity less than 30 - adjusting to 30 as the valid API value");
167                             targetHumidity = 30;
168                         } else if (targetHumidity > 80) {
169                             logger.warn("Target Humidity greater than 80 - adjusting to 80 as the valid API value");
170                             targetHumidity = 80;
171                         }
172
173                         sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
174                                 new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_AUTO), false);
175
176                         sendV2BypassControlCommand(DEVICE_SET_TARGET_HUMIDITY_MODE,
177                                 new VeSyncRequestManagedDeviceBypassV2.SetTargetHumidity(targetHumidity));
178                         break;
179                     case DEVICE_CHANNEL_MIST_LEVEL:
180                         int targetMistLevel = ((QuantityType<?>) command).intValue();
181                         // If more devices have this the hope is it's those with the prefix LUH so the check can
182                         // be simplified, originally devices mapped 1/5/9 to 1/2/3.
183                         if (DEV_TYPE_CORE_301S.equals(deviceType)) {
184                             if (targetMistLevel < 1) {
185                                 logger.warn("Target Mist Level less than 1 - adjusting to 1 as the valid API value");
186                                 targetMistLevel = 1;
187                             } else if (targetMistLevel > 2) {
188                                 logger.warn("Target Mist Level greater than 2 - adjusting to 2 as the valid API value");
189                                 targetMistLevel = 2;
190                             }
191                         } else {
192                             if (targetMistLevel < 1) {
193                                 logger.warn("Target Mist Level less than 1 - adjusting to 1 as the valid API value");
194                                 targetMistLevel = 1;
195                             } else if (targetMistLevel > 3) {
196                                 logger.warn("Target Mist Level greater than 3 - adjusting to 3 as the valid API value");
197                                 targetMistLevel = 3;
198                             }
199                             // Re-map to what appears to be bitwise encoding of the states
200                             switch (targetMistLevel) {
201                                 case 1:
202                                     targetMistLevel = 1;
203                                     break;
204                                 case 2:
205                                     targetMistLevel = 5;
206                                     break;
207                                 case 3:
208                                     targetMistLevel = 9;
209                                     break;
210                             }
211                         }
212
213                         sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
214                                 new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false);
215
216                         sendV2BypassControlCommand(DEVICE_SET_VIRTUAL_LEVEL,
217                                 new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_MIST,
218                                         targetMistLevel));
219                         break;
220                     case DEVICE_CHANNEL_WARM_LEVEL:
221                         logger.warn("Warm level API is unknown in order to send the command");
222                         break;
223                 }
224             } else if (command instanceof StringType) {
225                 final String targetMode = command.toString().toLowerCase();
226                 switch (channelUID.getId()) {
227                     case DEVICE_CHANNEL_HUMIDIFIER_MODE:
228                         if (!CLASSIC_300S_600S_MODES.contains(targetMode)) {
229                             logger.warn(
230                                     "Humidifier mode command for \"{}\" is not valid in the (Classic300S/600S) API possible options {}",
231                                     command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES));
232                             return;
233                         }
234                         sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
235                                 new VeSyncRequestManagedDeviceBypassV2.SetMode(targetMode));
236                         break;
237                     case DEVICE_CHANNEL_AF_NIGHT_LIGHT:
238                         if (!DEV_TYPE_CLASSIC_300S.equals(deviceType) && !DEV_TYPE_CORE_301S.equals(deviceType)) {
239                             logger.warn("Humidifier night light is not valid for your device ({}})", deviceType);
240                             return;
241                         }
242                         if (!CLASSIC_300S_NIGHT_LIGHT_MODES.contains(targetMode)) {
243                             logger.warn(
244                                     "Humidifier night light mode command for \"{}\" is not valid in the (Classic300S) API possible options {}",
245                                     command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES));
246                             return;
247                         }
248                         int targetValue;
249                         switch (targetMode) {
250                             case MODE_OFF:
251                                 targetValue = 0;
252                                 break;
253                             case MODE_DIM:
254                                 targetValue = 50;
255                                 break;
256                             case MODE_ON:
257                                 targetValue = 100;
258                                 break;
259                             default:
260                                 return; // should never hit
261                         }
262                         sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT_BRIGHTNESS,
263                                 new VeSyncRequestManagedDeviceBypassV2.SetNightLightBrightness(targetValue));
264                 }
265             } else if (command instanceof RefreshType) {
266                 pollForUpdate();
267             } else {
268                 logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID);
269             }
270         });
271     }
272
273     @Override
274     protected void pollForDeviceData(final ExpiringCache<String> cachedResponse) {
275         String response;
276         VeSyncV2BypassHumidifierStatus humidifierStatus;
277         synchronized (pollLock) {
278             response = cachedResponse.getValue();
279             boolean cachedDataUsed = response != null;
280             if (response == null) {
281                 logger.trace("Requesting fresh response");
282                 response = sendV2BypassCommand(DEVICE_GET_HUMIDIFIER_STATUS,
283                         new VeSyncRequestManagedDeviceBypassV2.EmptyPayload());
284             } else {
285                 logger.trace("Using cached response {}", response);
286             }
287
288             if (response.equals(EMPTY_STRING)) {
289                 return;
290             }
291
292             humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassHumidifierStatus.class);
293
294             if (humidifierStatus == null) {
295                 return;
296             }
297
298             if (!cachedDataUsed) {
299                 cachedResponse.putValue(response);
300             }
301         }
302
303         // Bail and update the status of the thing - it will be updated to online by the next search
304         // that detects it is online.
305         if (humidifierStatus.isMsgDeviceOffline()) {
306             updateStatus(ThingStatus.OFFLINE);
307             return;
308         } else if (humidifierStatus.isMsgSuccess()) {
309             updateStatus(ThingStatus.ONLINE);
310         }
311
312         if (!"0".equals(humidifierStatus.result.getCode())) {
313             logger.warn("Check correct Thing type has been set - API gave a unexpected response for an Air Humidifier");
314             return;
315         }
316
317         final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
318
319         updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(humidifierStatus.result.result.enabled));
320         updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(humidifierStatus.result.result.display));
321         updateState(DEVICE_CHANNEL_WATER_LACKS, OnOffType.from(humidifierStatus.result.result.waterLacks));
322         updateState(DEVICE_CHANNEL_HUMIDITY_HIGH, OnOffType.from(humidifierStatus.result.result.humidityHigh));
323         updateState(DEVICE_CHANNEL_WATER_TANK_LIFTED, OnOffType.from(humidifierStatus.result.result.waterTankLifted));
324         updateState(DEVICE_CHANNEL_STOP_AT_TARGET,
325                 OnOffType.from(humidifierStatus.result.result.automaticStopReachTarget));
326         updateState(DEVICE_CHANNEL_HUMIDITY,
327                 new QuantityType<>(humidifierStatus.result.result.humidity, Units.PERCENT));
328         updateState(DEVICE_CHANNEL_MIST_LEVEL, new DecimalType(humidifierStatus.result.result.mistLevel));
329         updateState(DEVICE_CHANNEL_HUMIDIFIER_MODE, new StringType(humidifierStatus.result.result.mode));
330
331         // Only the 300S supports nightlight currently of tested devices.
332         if (DEV_TYPE_CLASSIC_300S.equals(deviceType) || DEV_TYPE_CORE_301S.equals(deviceType)) {
333             // Map the numeric that only applies to the same modes as the Air Filter 300S series.
334             if (humidifierStatus.result.result.nightLightBrightness == 0) {
335                 updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_OFF));
336             } else if (humidifierStatus.result.result.nightLightBrightness == 100) {
337                 updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_ON));
338             } else {
339                 updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_DIM));
340             }
341         } else if (DEV_TYPE_600S.equals(deviceType) || DEV_TYPE_600S_EU.equals(deviceType)) {
342             updateState(DEVICE_CHANNEL_WARM_ENABLED, OnOffType.from(humidifierStatus.result.result.warnEnabled));
343             updateState(DEVICE_CHANNEL_WARM_LEVEL, new DecimalType(humidifierStatus.result.result.warmLevel));
344         }
345
346         updateState(DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY,
347                 new QuantityType<>(humidifierStatus.result.result.configuration.autoTargetHumidity, Units.PERCENT));
348     }
349 }