2 * Copyright (c) 2010-2023 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.util.Arrays;
19 import java.util.List;
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;
43 * The {@link VeSyncDeviceAirHumidifierHandler} is responsible for handling commands, which are
44 * sent to one of the channels.
46 * @author David Goodyear - Initial contribution
49 public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
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";
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);
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);
66 private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirHumidifierHandler.class);
68 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_HUMIDIFIER);
70 private final Object pollLock = new Object();
72 public VeSyncDeviceAirHumidifierHandler(Thing thing) {
77 protected String[] getChannelsToRemove() {
78 String[] toRemove = new String[] {};
79 final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
80 if (deviceType != null) {
82 case DEV_TYPE_CLASSIC_300S:
83 toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL };
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 };
92 case DEV_TYPE_600S_EU:
93 toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT };
101 public void initialize() {
107 public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) {
108 Integer pollRate = config.airPurifierPollInterval;
109 if (pollRate == null) {
110 pollRate = DEFAULT_AIR_PURIFIER_POLL_RATE;
112 if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
113 setBackgroundPollInterval(-1);
115 setBackgroundPollInterval(pollRate);
120 public void dispose() {
121 this.setBackgroundPollInterval(-1);
125 protected boolean isDeviceSupported() {
126 final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
127 if (deviceType == null) {
130 return SUPPORTED_DEVICE_TYPES.contains(deviceType);
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) {
140 scheduler.submit(() -> {
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),
149 case DEVICE_CHANNEL_DISPLAY_ENABLED:
150 sendV2BypassControlCommand(DEVICE_SET_DISPLAY,
151 new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON)));
153 case DEVICE_CHANNEL_STOP_AT_TARGET:
154 sendV2BypassControlCommand(DEVICE_SET_AUTOMATIC_STOP,
155 new VeSyncRequestManagedDeviceBypassV2.EnabledPayload(command.equals(OnOffType.ON)));
157 case DEVICE_CHANNEL_WARM_ENABLED:
158 logger.warn("Warm mode API is unknown in order to send the command");
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");
168 } else if (targetHumidity > 80) {
169 logger.warn("Target Humidity greater than 80 - adjusting to 80 as the valid API value");
173 sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
174 new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_AUTO), false);
176 sendV2BypassControlCommand(DEVICE_SET_TARGET_HUMIDITY_MODE,
177 new VeSyncRequestManagedDeviceBypassV2.SetTargetHumidity(targetHumidity));
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");
187 } else if (targetMistLevel > 2) {
188 logger.warn("Target Mist Level greater than 2 - adjusting to 2 as the valid API value");
192 if (targetMistLevel < 1) {
193 logger.warn("Target Mist Level less than 1 - adjusting to 1 as the valid API value");
195 } else if (targetMistLevel > 3) {
196 logger.warn("Target Mist Level greater than 3 - adjusting to 3 as the valid API value");
199 // Re-map to what appears to be bitwise encoding of the states
200 switch (targetMistLevel) {
213 sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
214 new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false);
216 sendV2BypassControlCommand(DEVICE_SET_VIRTUAL_LEVEL,
217 new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_MIST,
220 case DEVICE_CHANNEL_WARM_LEVEL:
221 logger.warn("Warm level API is unknown in order to send the command");
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)) {
230 "Humidifier mode command for \"{}\" is not valid in the (Classic300S/600S) API possible options {}",
231 command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES));
234 sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
235 new VeSyncRequestManagedDeviceBypassV2.SetMode(targetMode));
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);
242 if (!CLASSIC_300S_NIGHT_LIGHT_MODES.contains(targetMode)) {
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));
249 switch (targetMode) {
260 return; // should never hit
262 sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT_BRIGHTNESS,
263 new VeSyncRequestManagedDeviceBypassV2.SetNightLightBrightness(targetValue));
265 } else if (command instanceof RefreshType) {
268 logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID);
274 protected void pollForDeviceData(final ExpiringCache<String> cachedResponse) {
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());
285 logger.trace("Using cached response {}", response);
288 if (response.equals(EMPTY_STRING)) {
292 humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassHumidifierStatus.class);
294 if (humidifierStatus == null) {
298 if (!cachedDataUsed) {
299 cachedResponse.putValue(response);
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);
308 } else if (humidifierStatus.isMsgSuccess()) {
309 updateStatus(ThingStatus.ONLINE);
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");
317 final String deviceType = getThing().getProperties().get(DEVICE_PROP_DEVICE_TYPE);
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));
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));
339 updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_DIM));
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));
346 updateState(DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY,
347 new QuantityType<>(humidifierStatus.result.result.configuration.autoTargetHumidity, Units.PERCENT));