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