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.Collections;
20 import java.util.List;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.openhab.binding.vesync.internal.VeSyncBridgeConfiguration;
25 import org.openhab.binding.vesync.internal.VeSyncConstants;
26 import org.openhab.binding.vesync.internal.dto.requests.VeSyncRequestManagedDeviceBypassV2;
27 import org.openhab.binding.vesync.internal.dto.responses.VeSyncV2BypassHumidifierStatus;
28 import org.openhab.core.cache.ExpiringCache;
29 import org.openhab.core.library.types.DecimalType;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.QuantityType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.library.unit.Units;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * The {@link VeSyncDeviceAirHumidifierHandler} is responsible for handling commands, which are
45 * sent to one of the channels.
47 * @author David Goodyear - Initial contribution
50 public class VeSyncDeviceAirHumidifierHandler extends VeSyncBaseDeviceHandler {
52 public static final String DEV_TYPE_FAMILY_AIR_HUMIDIFIER = "LUH";
54 public static final int DEFAULT_AIR_PURIFIER_POLL_RATE = 120;
56 public static final String DEV_FAMILY_CLASSIC_200S = "Classic 200S";
57 public static final String DEV_FAMILY_CLASSIC_300S = "Classic 300S";
58 public static final String DEV_FAMILY_DUAL_200S = "Dual 200S";
59 public static final String DEV_FAMILY_600S = "600S";
60 public static final String DEV_FAMILY_OASIS_MIST = "Oasis Mist";
62 public static final VeSyncDeviceMetadata CLASSIC200S = new VeSyncDeviceMetadata(DEV_FAMILY_CLASSIC_200S,
63 Collections.emptyList(), List.of("Classic200S"));
65 public static final VeSyncDeviceMetadata CLASSIC300S = new VeSyncDeviceMetadata(DEV_FAMILY_CLASSIC_300S,
66 Arrays.asList("A601S"), List.of("Classic300S"));
68 public static final VeSyncDeviceMetadata DUAL200S = new VeSyncDeviceMetadata(DEV_FAMILY_DUAL_200S,
69 Arrays.asList("D301S"), List.of("Dual200S"));
71 public static final VeSyncDeviceMetadata LV600S = new VeSyncDeviceMetadata(DEV_FAMILY_600S, Arrays.asList("A602S"),
72 Collections.emptyList());
74 public static final VeSyncDeviceMetadata OASIS_MIST = new VeSyncDeviceMetadata(DEV_FAMILY_OASIS_MIST,
75 Arrays.asList("O451S"), Collections.emptyList());
77 public static final List<VeSyncDeviceMetadata> SUPPORTED_MODEL_FAMILIES = Arrays.asList(LV600S, CLASSIC300S,
78 CLASSIC200S, DUAL200S, OASIS_MIST);
80 private static final List<String> CLASSIC_300S_600S_MODES = Arrays.asList(MODE_AUTO, MODE_MANUAL, MODE_SLEEP);
81 private static final List<String> CLASSIC_300S_NIGHT_LIGHT_MODES = Arrays.asList(MODE_ON, MODE_DIM, MODE_OFF);
83 private final Logger logger = LoggerFactory.getLogger(VeSyncDeviceAirHumidifierHandler.class);
85 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIR_HUMIDIFIER);
87 private final Object pollLock = new Object();
89 public VeSyncDeviceAirHumidifierHandler(Thing thing) {
94 protected String[] getChannelsToRemove() {
95 String[] toRemove = new String[] {};
96 final String deviceFamily = getThing().getProperties().get(DEVICE_PROP_DEVICE_FAMILY);
97 if (deviceFamily != null) {
98 switch (deviceFamily) {
99 case DEV_FAMILY_CLASSIC_300S:
100 toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL };
102 case DEV_FAMILY_DUAL_200S:
103 case DEV_FAMILY_CLASSIC_200S:
104 toRemove = new String[] { DEVICE_CHANNEL_WARM_ENABLED, DEVICE_CHANNEL_WARM_LEVEL,
105 DEVICE_CHANNEL_AF_NIGHT_LIGHT };
107 case DEV_FAMILY_OASIS_MIST:
108 toRemove = new String[] { DEVICE_CHANNEL_AF_NIGHT_LIGHT };
116 public void initialize() {
122 public void updateBridgeBasedPolls(final VeSyncBridgeConfiguration config) {
123 Integer pollRate = config.airPurifierPollInterval;
124 if (pollRate == null) {
125 pollRate = DEFAULT_AIR_PURIFIER_POLL_RATE;
127 if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
128 setBackgroundPollInterval(-1);
130 setBackgroundPollInterval(pollRate);
135 public void dispose() {
136 this.setBackgroundPollInterval(-1);
140 public String getDeviceFamilyProtocolPrefix() {
141 return DEV_TYPE_FAMILY_AIR_HUMIDIFIER;
145 public List<VeSyncDeviceMetadata> getSupportedDeviceMetadata() {
146 return SUPPORTED_MODEL_FAMILIES;
150 public void handleCommand(final ChannelUID channelUID, final Command command) {
151 final String deviceFamily = getThing().getProperties().get(DEVICE_PROP_DEVICE_FAMILY);
152 if (deviceFamily == null) {
156 scheduler.submit(() -> {
158 if (command instanceof OnOffType) {
159 switch (channelUID.getId()) {
160 case DEVICE_CHANNEL_ENABLED:
161 sendV2BypassControlCommand(DEVICE_SET_SWITCH,
162 new VeSyncRequestManagedDeviceBypassV2.SetSwitchPayload(command.equals(OnOffType.ON),
165 case DEVICE_CHANNEL_DISPLAY_ENABLED:
166 sendV2BypassControlCommand(DEVICE_SET_DISPLAY,
167 new VeSyncRequestManagedDeviceBypassV2.SetState(command.equals(OnOffType.ON)));
169 case DEVICE_CHANNEL_STOP_AT_TARGET:
170 sendV2BypassControlCommand(DEVICE_SET_AUTOMATIC_STOP,
171 new VeSyncRequestManagedDeviceBypassV2.EnabledPayload(command.equals(OnOffType.ON)));
173 case DEVICE_CHANNEL_WARM_ENABLED:
174 logger.warn("Warm mode API is unknown in order to send the command");
177 } else if (command instanceof QuantityType quantityCommand) {
178 switch (channelUID.getId()) {
179 case DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY:
180 int targetHumidity = quantityCommand.intValue();
181 if (targetHumidity < 30) {
182 logger.warn("Target Humidity less than 30 - adjusting to 30 as the valid API value");
184 } else if (targetHumidity > 80) {
185 logger.warn("Target Humidity greater than 80 - adjusting to 80 as the valid API value");
189 sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
190 new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_AUTO), false);
192 sendV2BypassControlCommand(DEVICE_SET_TARGET_HUMIDITY_MODE,
193 new VeSyncRequestManagedDeviceBypassV2.SetTargetHumidity(targetHumidity));
195 case DEVICE_CHANNEL_MIST_LEVEL:
196 int targetMistLevel = quantityCommand.intValue();
197 // If more devices have this the hope is it's those with the prefix LUH so the check can
198 // be simplified, originally devices mapped 1/5/9 to 1/2/3.
199 if (DEV_FAMILY_DUAL_200S.equals(deviceFamily)) {
200 if (targetMistLevel < 1) {
201 logger.warn("Target Mist Level less than 1 - adjusting to 1 as the valid API value");
203 } else if (targetMistLevel > 2) {
204 logger.warn("Target Mist Level greater than 2 - adjusting to 2 as the valid API value");
208 if (targetMistLevel < 1) {
209 logger.warn("Target Mist Level less than 1 - adjusting to 1 as the valid API value");
211 } else if (targetMistLevel > 3) {
212 logger.warn("Target Mist Level greater than 3 - adjusting to 3 as the valid API value");
215 // Re-map to what appears to be bitwise encoding of the states
216 switch (targetMistLevel) {
229 sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
230 new VeSyncRequestManagedDeviceBypassV2.SetMode(MODE_MANUAL), false);
232 sendV2BypassControlCommand(DEVICE_SET_VIRTUAL_LEVEL,
233 new VeSyncRequestManagedDeviceBypassV2.SetLevelPayload(0, DEVICE_LEVEL_TYPE_MIST,
236 case DEVICE_CHANNEL_WARM_LEVEL:
237 logger.warn("Warm level API is unknown in order to send the command");
240 } else if (command instanceof StringType) {
241 final String targetMode = command.toString().toLowerCase();
242 switch (channelUID.getId()) {
243 case DEVICE_CHANNEL_HUMIDIFIER_MODE:
244 if (!CLASSIC_300S_600S_MODES.contains(targetMode)) {
246 "Humidifier mode command for \"{}\" is not valid in the (Classic300S/600S) API possible options {}",
247 command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES));
250 sendV2BypassControlCommand(DEVICE_SET_HUMIDITY_MODE,
251 new VeSyncRequestManagedDeviceBypassV2.SetMode(targetMode));
253 case DEVICE_CHANNEL_AF_NIGHT_LIGHT:
254 if (!DEV_FAMILY_CLASSIC_300S.equals(deviceFamily) && !DEV_FAMILY_600S.equals(deviceFamily)) {
255 logger.warn("Humidifier night light is not valid for your device ({}})", deviceFamily);
258 if (!CLASSIC_300S_NIGHT_LIGHT_MODES.contains(targetMode)) {
260 "Humidifier night light mode command for \"{}\" is not valid in the (Classic300S) API possible options {}",
261 command, String.join(",", CLASSIC_300S_NIGHT_LIGHT_MODES));
265 switch (targetMode) {
276 return; // should never hit
278 sendV2BypassControlCommand(DEVICE_SET_NIGHT_LIGHT_BRIGHTNESS,
279 new VeSyncRequestManagedDeviceBypassV2.SetNightLightBrightness(targetValue));
281 } else if (command instanceof RefreshType) {
284 logger.trace("UNKNOWN COMMAND: {} {}", command.getClass().toString(), channelUID);
290 protected void pollForDeviceData(final ExpiringCache<String> cachedResponse) {
292 VeSyncV2BypassHumidifierStatus humidifierStatus;
293 synchronized (pollLock) {
294 response = cachedResponse.getValue();
295 boolean cachedDataUsed = response != null;
296 if (response == null) {
297 logger.trace("Requesting fresh response");
298 response = sendV2BypassCommand(DEVICE_GET_HUMIDIFIER_STATUS,
299 new VeSyncRequestManagedDeviceBypassV2.EmptyPayload());
301 logger.trace("Using cached response {}", response);
304 if (response.equals(EMPTY_STRING)) {
308 humidifierStatus = VeSyncConstants.GSON.fromJson(response, VeSyncV2BypassHumidifierStatus.class);
310 if (humidifierStatus == null) {
314 if (!cachedDataUsed) {
315 cachedResponse.putValue(response);
319 // Bail and update the status of the thing - it will be updated to online by the next search
320 // that detects it is online.
321 if (humidifierStatus.isMsgDeviceOffline()) {
322 updateStatus(ThingStatus.OFFLINE);
324 } else if (humidifierStatus.isMsgSuccess()) {
325 updateStatus(ThingStatus.ONLINE);
328 if (!"0".equals(humidifierStatus.result.getCode())) {
329 logger.warn("Check correct Thing type has been set - API gave a unexpected response for an Air Humidifier");
333 final String deviceFamily = getThing().getProperties().get(DEVICE_PROP_DEVICE_FAMILY);
335 updateState(DEVICE_CHANNEL_ENABLED, OnOffType.from(humidifierStatus.result.result.enabled));
336 updateState(DEVICE_CHANNEL_DISPLAY_ENABLED, OnOffType.from(humidifierStatus.result.result.display));
337 updateState(DEVICE_CHANNEL_WATER_LACKS, OnOffType.from(humidifierStatus.result.result.waterLacks));
338 updateState(DEVICE_CHANNEL_HUMIDITY_HIGH, OnOffType.from(humidifierStatus.result.result.humidityHigh));
339 updateState(DEVICE_CHANNEL_WATER_TANK_LIFTED, OnOffType.from(humidifierStatus.result.result.waterTankLifted));
340 updateState(DEVICE_CHANNEL_STOP_AT_TARGET,
341 OnOffType.from(humidifierStatus.result.result.automaticStopReachTarget));
342 updateState(DEVICE_CHANNEL_HUMIDITY,
343 new QuantityType<>(humidifierStatus.result.result.humidity, Units.PERCENT));
344 updateState(DEVICE_CHANNEL_MIST_LEVEL, new DecimalType(humidifierStatus.result.result.mistLevel));
345 updateState(DEVICE_CHANNEL_HUMIDIFIER_MODE, new StringType(humidifierStatus.result.result.mode));
347 // Only the 300S supports nightlight currently of tested devices.
348 if (DEV_FAMILY_CLASSIC_300S.equals(deviceFamily) || DEV_FAMILY_600S.equals(deviceFamily)) {
349 // Map the numeric that only applies to the same modes as the Air Filter 300S series.
350 if (humidifierStatus.result.result.nightLightBrightness == 0) {
351 updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_OFF));
352 } else if (humidifierStatus.result.result.nightLightBrightness == 100) {
353 updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_ON));
355 updateState(DEVICE_CHANNEL_AF_NIGHT_LIGHT, new StringType(MODE_DIM));
357 } else if (DEV_FAMILY_600S.equals(deviceFamily) || DEV_FAMILY_OASIS_MIST.equals(deviceFamily)) {
358 updateState(DEVICE_CHANNEL_WARM_ENABLED, OnOffType.from(humidifierStatus.result.result.warnEnabled));
359 updateState(DEVICE_CHANNEL_WARM_LEVEL, new DecimalType(humidifierStatus.result.result.warmLevel));
362 updateState(DEVICE_CHANNEL_CONFIG_TARGET_HUMIDITY,
363 new QuantityType<>(humidifierStatus.result.result.configuration.autoTargetHumidity, Units.PERCENT));