2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.vesync.internal.handlers;
15 import static org.openhab.binding.vesync.internal.VeSyncConstants.*;
16 import static org.openhab.binding.vesync.internal.dto.requests.VeSyncProtocolConstants.*;
18 import java.time.LocalDateTime;
19 import java.time.temporal.ChronoUnit;
20 import java.util.Arrays;
21 import java.util.List;
24 import javax.validation.constraints.NotNull;
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;
51 * The {@link VeSyncDeviceAirPurifierHandler} is responsible for handling commands, which are
52 * sent to one of the channels.
54 * @author David Goodyear - Initial contribution
57 public class VeSyncDeviceAirPurifierHandler extends VeSyncBaseDeviceHandler {
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);
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);
74 private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirPurifierHandler.class);
76 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_PURIFIER);
78 private final Object pollLock = new Object();
80 public VeSyncDeviceAirPurifierHandler(Thing thing) {
85 public void initialize() {
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) {
96 case DEV_TYPE_CORE_600S:
97 case DEV_TYPE_CORE_400S:
98 toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT };
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 };
107 toRemove = new String[] { DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, DEVICE_CHANNEL_AF_SCHEDULES_COUNT };
114 public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) {
115 Integer pollRate = config.airPurifierPollInterval;
116 if (pollRate == null) {
117 pollRate = DEFAULT_AIR_PURIFIER_POLL_RATE;
120 if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
121 setBackgroundPollInterval(-1);
123 setBackgroundPollInterval(pollRate);
128 public void dispose() {
129 this.setBackgroundPollInterval(-1);
133 protected boolean isDeviceSupported() {
134 final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
135 if (deviceType == null) {
138 return SUPPORTED_DEVICE_TYPES.contains(deviceType);
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) {
148 scheduler.submit(() -> {
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),
157 case DEVICE_CHANNEL_DISPLAY_ENABLED:
158 sendV2BypassControlCommand(DEVICE_SET_DISPLAY,
159 new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON)));
161 case DEVICE_CHANNEL_CHILD_LOCK_ENABLED:
162 sendV2BypassControlCommand(DEVICE_SET_CHILD_LOCK,
163 new VeSyncRequestManagedDeviceBypassV2.SetChildLock(command.equals(OnOffType.ON)));
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)) {
175 "Fan mode command for \"{}\" is not valid in the (Core400S) API possible options {}",
176 command, String.join(",", CORE_400S600S_FAN_MODES));
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)) {
185 "Fan mode command for \"{}\" is not valid in the (Core200S/Core300S) API possible options {}",
186 command, String.join(",", CORE_200S300S_FAN_MODES));
192 sendV2BypassControlCommand(DEVICE_SET_PURIFIER_MODE,
193 new VeSyncRequestManagedDeviceBypassV2.SetMode(targetFanMode));
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");
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)) {
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));
212 sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT,
213 new VeSyncRequestManagedDeviceBypassV2.SetNightLight(targetNightLightMode));
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);
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");
232 switch (deviceType) {
233 case DEV_TYPE_CORE_600S:
234 case DEV_TYPE_CORE_400S:
235 if (requestedLevel > 4) {
237 "Fan speed command greater than 4 - adjusting to 4 as the valid (Core400S) API value");
241 case DEV_TYPE_CORE_200S:
242 case DEV_TYPE_CORE_201S:
243 case DEV_TYPE_CORE_300S:
244 if (requestedLevel > 3) {
246 "Fan speed command greater than 3 - adjusting to 3 as the valid (Core200S/Core300S) API value");
252 sendV2BypassControlCommand(DEVICE_SET_LEVEL,
253 new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_WIND,
257 } else if (command instanceof RefreshType) {
260 logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID);
266 protected void pollForDeviceData(final ExpiringCache<String> cachedResponse) {
267 final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
268 if (deviceType == null) {
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);
280 case DEV_TYPE_LV_PUR131S:
281 processV1AirPurifierPoll(cachedResponse);
286 private void processV1AirPurifierPoll(final ExpiringCache<String> cachedResponse) {
287 final String deviceUuid = getThing().getProperties().get(DEVICE_PROP_DEVICE_UUID);
288 if (deviceUuid == null) {
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));
302 logger.trace("Using cached response {}", response);
305 if (response.equals(EMPTY_STRING)) {
309 purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV1AirPurifierDeviceDetailsResponse.class);
311 if (purifierStatus == null) {
315 if (!cachedDataUsed) {
316 cachedResponse.putValue(response);
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);
325 updateStatus(ThingStatus.OFFLINE);
329 if (!"0".equals(purifierStatus.getCode())) {
330 logger.warn("Check Thing type has been set - API gave a unexpected response for an Air Purifier");
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()));
342 private void processV2BypassPoll(final ExpiringCache<String> cachedResponse) {
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());
353 logger.trace("Using cached response {}", response);
356 if (response.equals(EMPTY_STRING)) {
360 purifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassPurifierStatus.class);
362 if (purifierStatus == null) {
366 if (!cachedDataUsed) {
367 cachedResponse.putValue(response);
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);
376 } else if (purifierStatus.isMsgSuccess()) {
377 updateStatus(ThingStatus.ONLINE);
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");
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));
397 updateState(DEVICE_CHANNEL_AF_CONFIG_DISPLAY_FOREVER,
398 OnOffType.from(purifierStatus.result.result.configuration.displayForever));
400 updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_MODE_PREF,
401 new StringType(purifierStatus.result.result.configuration.autoPreference.autoType));
403 updateState(DEVICE_CHANNEL_AF_CONFIG_AUTO_ROOM_SIZE,
404 new DecimalType(purifierStatus.result.result.configuration.autoPreference.roomSize));
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()));
412 updateState(DEVICE_CHANNEL_AF_AUTO_OFF_CALC_TIME, new DateTimeItem("nullEnforcements").getState());
414 updateState(DEVICE_CHANNEL_AF_SCHEDULES_COUNT,
415 new DecimalType(purifierStatus.result.result.extension.scheduleCount));
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));