]> git.basschouten.com Git - openhab-addons.git/blob
2d02cc7ceb1b1523248c8c7c467d1a8d560a7f41
[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.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.time.LocalDateTime;
19 import java.time.temporal.ChronoUnit;
20 import java.util.Arrays;
21 import java.util.List;
22 import java.util.Set;
23
24 import javax.validation.constraints.NotNull;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
28 import org.openhab.binding.vesync.internal.VeSyncConstants;
29 import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
30 import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestV1ManagedDeviceDetails;
31 import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassPurifierStatus;
32 import org.openhab.binding.vesync.internal.dto.responses.v1.VeSyncV1AirPurifierDeviceDetailsResponse;
33 import org.openhab.core.cache.ExpiringCache;
34 import org.openhab.core.library.items.DateTimeItem;
35 import org.openhab.core.library.types.DateTimeType;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.library.unit.Units;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingTypeUID;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 /**
51  * The {@link VeSyncDeviceAirPurifierHandler} is responsible for handling commands, which are
52  * sent to one of the channels.
53  *
54  * @author David Goodyear - Initial contribution
55  */
56 @NonNullByDefault
57 public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
58
59     public static final int DEFAULT_AIR_PURIFIER_POLL_RATE = 120;
60     // "Device Type" values
61     public static final String DEV_TYPE_CORE_600S = "LAP-C601S-WUS";
62     public static final String DEV_TYPE_CORE_400S = "Core400S";
63     public static final String DEV_TYPE_CORE_300S = "Core300S";
64     public static final String DEV_TYPE_CORE_201S = "LAP-C201S-AUSR";
65     public static final String DEV_TYPE_CORE_200S = "Core200S";
66     public static final String DEV_TYPE_LV_PUR131S = "LV-PUR131S";
67     public static final List<String> SUPPORTED_DEVICE_TYPES = Arrays.asList(DEV_TYPE_CORE_600S, DEV_TYPE_CORE_400S,
68             DEV_TYPE_CORE_300S, DEV_TYPE_CORE_201S, DEV_TYPE_CORE_200S, DEV_TYPE_LV_PUR131S);
69
70     private static final List<String> CORE_400S600S_FAN_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP);
71     private static final List<String> CORE_200S300S_FAN_MODES = Arrays.asList(MODE_MANUAL, MODE_SLEEP);
72     private static final List<String> CORE_200S300S_NIGHT_LIGHT_MODES = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF);
73
74     private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirPurifierHandler.class);
75
76     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_PURIFIER);
77
78     private final Object pollLock = new Object();
79
80     public VeSyncDeviceAirPurifierHandler(Thing thing) {
81         super(thing);
82     }
83
84     @Override
85     public void initialize() {
86         super.initialize();
87         customiseChannels();
88     }
89
90     @Override
91     protected @NotNull String[] getChannelsToRemove() {
92         String[] toRemove = new String[] {};
93         final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
94         if (deviceType != null) {
95             switch (deviceType) {
96                 case DEV_TYPE_CORE_600S:
97                 case DEV_TYPE_CORE_400S:
98                     toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT };
99                     break;
100                 case DEV_TYPE_LV_PUR131S:
101                     toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT, DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE,
102                             DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF, DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME,
103                             DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING, DEVICE_CHANNEL_AIRQUALITY_PM25,
104                             DEVICE_CHANNEL_AF_SCHEDULES_COUNT, DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER };
105                     break;
106                 default:
107                     toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT };
108             }
109         }
110         return toRemove;
111     }
112
113     @Override
114     public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) {
115         Integer pollRate = config.airPurifierPollInterval;
116         if (pollRate == null) {
117             pollRate = DEFAULT_AIR_PURIFIER_POLL_RATE;
118         }
119
120         if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
121             setBackgroundPollInterval(-1);
122         } else {
123             setBackgroundPollInterval(pollRate);
124         }
125     }
126
127     @Override
128     public void dispose() {
129         this.setBackgroundPollInterval(-1);
130     }
131
132     @Override
133     protected boolean isDeviceSupported() {
134         final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
135         if (deviceType == null) {
136             return false;
137         }
138         return SUPPORTED_DEVICE_TYPES.contains(deviceType);
139     }
140
141     @Override
142     public void handleCommand(final ChannelUID channelUID, final Command command) {
143         final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
144         if (deviceType == null) {
145             return;
146         }
147
148         scheduler.submit(() -> {
149
150             if (command instanceof OnOffType) {
151                 switch (channelUID.getId()) {
152                     case DEVICE_CHANNEL_ENABLED:
153                         sendV2BypassControlCommand(DEVICE_SET_SWITCH,
154                                 new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON),
155                                         0));
156                         break;
157                     case DEVICE_CHANNEL_DISPLAY_ENABLED:
158                         sendV2BypassControlCommand(DEVICE_SET_DISPLAY,
159                                 new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON)));
160                         break;
161                     case DEVICE_CHANNEL_CHILD_LOCK_ENABLED:
162                         sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK,
163                                 new VeSyncRequestManagedDeviceBypassV2.SetChildLock(command.equals(OnOffType.ON)));
164                         break;
165                 }
166             } else if (command instanceof StringType) {
167                 switch (channelUID.getId()) {
168                     case DEVICE_CHANNEL_FAN_MODE_ENABLED:
169                         final String targetFanMode = command.toString().toLowerCase();
170                         switch (deviceType) {
171                             case DEV_TYPE_CORE_600S:
172                             case DEV_TYPE_CORE_400S:
173                                 if (!CORE_400S600S_FAN_MODES.contains(targetFanMode)) {
174                                     logger.warn(
175                                             "Fan mode command for \"{}\" is not valid in the (Core400S) API possible options {}",
176                                             command, String.join(",", CORE_400S600S_FAN_MODES));
177                                     return;
178                                 }
179                                 break;
180                             case DEV_TYPE_CORE_200S:
181                             case DEV_TYPE_CORE_201S:
182                             case DEV_TYPE_CORE_300S:
183                                 if (!CORE_200S300S_FAN_MODES.contains(targetFanMode)) {
184                                     logger.warn(
185                                             "Fan mode command for \"{}\" is not valid in the (Core200S/Core300S) API possible options {}",
186                                             command, String.join(",", CORE_200S300S_FAN_MODES));
187                                     return;
188                                 }
189                                 break;
190                         }
191
192                         sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE,
193                                 new VeSyncRequestManagedDeviceBypassV2.SetMode(targetFanMode));
194                         break;
195                     case DEVICE_CHANNEL_AF_NIGHT_LIGHT:
196                         final String targetNightLightMode = command.toString().toLowerCase();
197                         switch (deviceType) {
198                             case DEV_TYPE_CORE_600S:
199                             case DEV_TYPE_CORE_400S:
200                                 logger.warn("Core400S API does not support night light");
201                                 return;
202                             case DEV_TYPE_CORE_200S:
203                             case DEV_TYPE_CORE_201S:
204                             case DEV_TYPE_CORE_300S:
205                                 if (!CORE_200S300S_NIGHT_LIGHT_MODES.contains(targetNightLightMode)) {
206                                     logger.warn(
207                                             "Night light mode command for \"{}\" is not valid in the (Core200S/Core300S) API possible options {}",
208                                             command, String.join(",", CORE_200S300S_NIGHT_LIGHT_MODES));
209                                     return;
210                                 }
211
212                                 sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT,
213                                         new VeSyncRequestManagedDeviceBypassV2.SetNightLight(targetNightLightMode));
214
215                                 break;
216                         }
217                         break;
218                 }
219             } else if (command instanceof QuantityType) {
220                 switch (channelUID.getId()) {
221                     case DEVICE_CHANNEL_FAN_SPEED_ENABLED:
222                         // If the fan speed is being set enforce manual mode
223                         sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE,
224                                 new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false);
225
226                         int requestedLevel = ((QuantityType<?>) command).intValue();
227                         if (requestedLevel < 1) {
228                             logger.warn("Fan speed command less than 1 - adjusting to 1 as the valid API value");
229                             requestedLevel = 1;
230                         }
231
232                         switch (deviceType) {
233                             case DEV_TYPE_CORE_600S:
234                             case DEV_TYPE_CORE_400S:
235                                 if (requestedLevel > 4) {
236                                     logger.warn(
237                                             "Fan speed command greater than 4 - adjusting to 4 as the valid (Core400S) API value");
238                                     requestedLevel = 4;
239                                 }
240                                 break;
241                             case DEV_TYPE_CORE_200S:
242                             case DEV_TYPE_CORE_201S:
243                             case DEV_TYPE_CORE_300S:
244                                 if (requestedLevel > 3) {
245                                     logger.warn(
246                                             "Fan speed command greater than 3 - adjusting to 3 as the valid (Core200S/Core300S) API value");
247                                     requestedLevel = 3;
248                                 }
249                                 break;
250                         }
251
252                         sendV2BypassControlCommand(DEVICE_SET_LEVEL,
253                                 new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_WIND,
254                                         requestedLevel));
255                         break;
256                 }
257             } else if (command instanceof RefreshType) {
258                 pollForUpdate();
259             } else {
260                 logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID);
261             }
262         });
263     }
264
265     @Override
266     protected void pollForDeviceData(final ExpiringCache<String> cachedResponse) {
267         final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
268         if (deviceType == null) {
269             return;
270         }
271
272         switch (deviceType) {
273             case DEV_TYPE_CORE_600S:
274             case DEV_TYPE_CORE_400S:
275             case DEV_TYPE_CORE_300S:
276             case DEV_TYPE_CORE_201S:
277             case DEV_TYPE_CORE_200S:
278                 processV2BypassPoll(cachedResponse);
279                 break;
280             case DEV_TYPE_LV_PUR131S:
281                 processV1AirPurifierPoll(cachedResponse);
282                 break;
283         }
284     }
285
286     private void processV1AirPurifierPoll(final ExpiringCache<String> cachedResponse) {
287         final String deviceUuid = getThing().getProperties().get(DEVICE_PROP_DEVICE_UUID);
288         if (deviceUuid == null) {
289             return;
290         }
291
292         String response;
293         VeSyncV1AirPurifierDeviceDetailsResponse purifierStatus;
294         synchronized (pollLock) {
295             response = cachedResponse.getValue();
296             boolean cachedDataUsed = response != null;
297             if (response == null) {
298                 logger.trace("Requesting fresh response");
299                 response = sendV1Command("POST", "https://smartapi.vesync.com/131airPurifier/v1/device/deviceDetail",
300                         new VeSyncRequestV1ManagedDeviceDetails(deviceUuid));
301             } else {
302                 logger.trace("Using cached response {}", response);
303             }
304
305             if (response.equals(EMPTY_STRING)) {
306                 return;
307             }
308
309             purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV1AirPurifierDeviceDetailsResponse.class);
310
311             if (purifierStatus == null) {
312                 return;
313             }
314
315             if (!cachedDataUsed) {
316                 cachedResponse.putValue(response);
317             }
318         }
319
320         // Bail and update the status of the thing - it will be updated to online by the next search
321         // that detects it is online.
322         if (purifierStatus.isDeviceOnline()) {
323             updateStatus(ThingStatus.ONLINE);
324         } else {
325             updateStatus(ThingStatus.OFFLINE);
326             return;
327         }
328
329         if (!"0".equals(purifierStatus.getCode())) {
330             logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier");
331             return;
332         }
333
334         updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getDeviceStatus())));
335         updateState(DEVICE_CHANNEL_CHILD_LOCK_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getChildLock())));
336         updateState(DEVICE_CHANNEL_FAN_MODE_ENABLED, new StringType(purifierStatus.getMode()));
337         updateState(DEVICE_CHANNEL_FAN_SPEED_ENABLED, new DecimalType(String.valueOf(purifierStatus.getLevel())));
338         updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(MODE_ON.equals(purifierStatus.getScreenStatus())));
339         updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.getAirQuality()));
340     }
341
342     private void processV2BypassPoll(final ExpiringCache<String> cachedResponse) {
343         String response;
344         VeSyncV2BypassPurifierStatus purifierStatus;
345         synchronized (pollLock) {
346             response = cachedResponse.getValue();
347             boolean cachedDataUsed = response != null;
348             if (response == null) {
349                 logger.trace("Requesting fresh response");
350                 response = sendV2BypassCommand(DEVICE_GET_PURIFIER_STATUS,
351                         new VeSyncRequestManagedDeviceBypassV2.EmptyPayload());
352             } else {
353                 logger.trace("Using cached response {}", response);
354             }
355
356             if (response.equals(EMPTY_STRING)) {
357                 return;
358             }
359
360             purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassPurifierStatus.class);
361
362             if (purifierStatus == null) {
363                 return;
364             }
365
366             if (!cachedDataUsed) {
367                 cachedResponse.putValue(response);
368             }
369         }
370
371         // Bail and update the status of the thing - it will be updated to online by the next search
372         // that detects it is online.
373         if (purifierStatus.isMsgDeviceOffline()) {
374             updateStatus(ThingStatus.OFFLINE);
375             return;
376         } else if (purifierStatus.isMsgSuccess()) {
377             updateStatus(ThingStatus.ONLINE);
378         }
379
380         if (!"0".equals(purifierStatus.result.getCode())) {
381             logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier");
382             return;
383         }
384
385         updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(purifierStatus.result.result.enabled));
386         updateState(DEVICE_CHANNEL_CHILD_LOCK_ENABLED, OnOffType.from(purifierStatus.result.result.childLock));
387         updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(purifierStatus.result.result.display));
388         updateState(DEVICE_CHANNEL_AIR_FILTER_LIFE_PERCENTAGE_REMAINING,
389                 new QuantityType<>(purifierStatus.result.result.filterLife, Units.PERCENT));
390         updateState(DEVICE_CHANNEL_FAN_MODE_ENABLED, new StringType(purifierStatus.result.result.mode));
391         updateState(DEVICE_CHANNEL_FAN_SPEED_ENABLED, new DecimalType(purifierStatus.result.result.level));
392         updateState(DEVICE_CHANNEL_ERROR_CODE, new DecimalType(purifierStatus.result.result.deviceErrorCode));
393         updateState(DEVICE_CHANNEL_AIRQUALITY_BASIC, new DecimalType(purifierStatus.result.result.airQuality));
394         updateState(DEVICE_CHANNEL_AIRQUALITY_PM25,
395                 new QuantityType<>(purifierStatus.result.result.airQualityValue, Units.MICROGRAM_PER_CUBICMETRE));
396
397         updateState(DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER,
398                 OnOffType.from(purifierStatus.result.result.configuration.displayForever));
399
400         updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF,
401                 new StringType(purifierStatus.result.result.configuration.autoPreference.autoType));
402
403         updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE,
404                 new DecimalType(purifierStatus.result.result.configuration.autoPreference.roomSize));
405
406         // Only 400S appears to have this JSON extension object
407         if (purifierStatus.result.result.extension != null) {
408             if (purifierStatus.result.result.extension.timerRemain > 0) {
409                 updateState(DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, new DateTimeType(LocalDateTime.now()
410                         .plus(purifierStatus.result.result.extension.timerRemain, ChronoUnit.SECONDS).toString()));
411             } else {
412                 updateState(DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, new DateTimeItem("nullEnforcements").getState());
413             }
414             updateState(DEVICE_CHANNEL_AF_SCHEDULES_COUNT,
415                     new DecimalType(purifierStatus.result.result.extension.scheduleCount));
416         }
417
418         // Not applicable to 400S payload's
419         if (purifierStatus.result.result.nightLight != null) {
420             updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new DecimalType(purifierStatus.result.result.nightLight));
421         }
422     }
423 }