2 * Copyright (c) 2010-2021 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.gree.internal.handler;
15 import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.net.DatagramSocket;
20 import java.time.Instant;
21 import java.util.List;
22 import java.util.Optional;
23 import java.util.concurrent.Future;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import javax.measure.Unit;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.gree.internal.GreeConfiguration;
32 import org.openhab.binding.gree.internal.GreeException;
33 import org.openhab.binding.gree.internal.GreeTranslationProvider;
34 import org.openhab.binding.gree.internal.discovery.GreeDeviceFinder;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.QuantityType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.library.unit.ImperialUnits;
40 import org.openhab.core.library.unit.SIUnits;
41 import org.openhab.core.thing.Channel;
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.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.State;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * The {@link GreeHandler} is responsible for handling commands, which are sent to one of the channels.
57 * @author John Cunha - Initial contribution
58 * @author Markus Michels - Refactoring, adapted to OH 2.5x
61 public class GreeHandler extends BaseThingHandler {
62 private final Logger logger = LoggerFactory.getLogger(GreeHandler.class);
63 private final GreeTranslationProvider messages;
64 private final GreeDeviceFinder deviceFinder;
65 private final String thingId;
66 private GreeConfiguration config = new GreeConfiguration();
67 private GreeAirDevice device = new GreeAirDevice();
68 private Optional<DatagramSocket> clientSocket = Optional.empty();
69 private boolean forceRefresh = false;
71 private @Nullable ScheduledFuture<?> refreshTask;
72 private @Nullable Future<?> initializeFuture;
73 private long lastRefreshTime = 0;
74 private long apiRetries = 0;
76 public GreeHandler(Thing thing, GreeTranslationProvider messages, GreeDeviceFinder deviceFinder) {
78 this.messages = messages;
79 this.deviceFinder = deviceFinder;
80 this.thingId = getThing().getUID().getId();
84 public void initialize() {
85 config = getConfigAs(GreeConfiguration.class);
86 if (config.ipAddress.isEmpty() || (config.refresh < 0)) {
87 String message = messages.get("thinginit.invconf");
88 logger.warn("{}: {}", thingId, message);
89 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message);
93 // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
94 // the framework is then able to reuse the resources from the thing handler initialization.
95 updateStatus(ThingStatus.UNKNOWN);
97 // Start the automatic refresh cycles
98 startAutomaticRefresh();
99 initializeFuture = scheduler.submit(this::initializeThing);
102 private void initializeThing() {
105 if (!clientSocket.isPresent()) {
106 clientSocket = Optional.of(new DatagramSocket());
107 clientSocket.get().setSoTimeout(DATAGRAM_SOCKET_TIMEOUT);
109 // Find the GREE device
110 deviceFinder.scan(clientSocket.get(), config.ipAddress, false);
111 GreeAirDevice newDevice = deviceFinder.getDeviceByIPAddress(config.ipAddress);
112 if (newDevice != null) {
113 // Ok, our device responded, now let's Bind with it
115 device.bindWithDevice(clientSocket.get());
116 if (device.getIsBound()) {
117 updateStatus(ThingStatus.ONLINE);
122 message = messages.get("thinginit.failed");
123 logger.info("{}: {}", thingId, message);
124 } catch (GreeException e) {
125 logger.info("{}: {}", thingId, messages.get("thinginit.exception", e.getMessageString()));
126 } catch (IOException e) {
127 logger.warn("{}: {}", thingId, messages.get("thinginit.exception", "I/O Error"), e);
128 } catch (RuntimeException e) {
129 logger.warn("{}: {}", thingId, messages.get("thinginit.exception", "RuntimeException"), e);
132 if (getThing().getStatus() != ThingStatus.OFFLINE) {
133 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
138 public void handleCommand(ChannelUID channelUID, Command command) {
139 if (command instanceof RefreshType) {
140 // The thing is updated by the scheduled automatic refresh so do nothing here.
142 logger.debug("{}: Issue command {} to channe {}", thingId, command, channelUID.getIdWithoutGroup());
143 String channelId = channelUID.getIdWithoutGroup();
144 logger.debug("{}: Handle command {} for channel {}, command class {}", thingId, command, channelId,
147 int retries = MAX_API_RETRIES;
150 sendRequest(channelId, command);
151 // force refresh on next status refresh cycle
154 return; // successful
155 } catch (GreeException e) {
158 logger.debug("{}: Command {} failed for channel {}, retry", thingId, command, channelId);
160 String message = logInfo(
161 messages.get("command.exception", command, channelId) + ": " + e.getMessageString());
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
164 } catch (IllegalArgumentException e) {
165 logInfo("command.invarg", command, channelId);
167 } catch (RuntimeException e) {
168 logger.warn("{}: {}", thingId, messages.get("command.exception", command, channelId), e);
171 } while (retries > 0);
175 private void sendRequest(String channelId, Command command) throws GreeException {
176 DatagramSocket socket = clientSocket.get();
179 handleModeCommand(socket, command);
182 device.setDevicePower(socket, getOnOff(command));
185 device.setDeviceTurbo(socket, getOnOff(command));
188 device.setDeviceLight(socket, getOnOff(command));
190 case TARGET_TEMP_CHANNEL:
191 // Set value, read back effective one and update channel
192 // e.g. 22.5C will result in 22.0, because the AC doesn't support half-steps for C
193 device.setDeviceTempSet(socket, convertTemp(command));
195 case SWINGUD_CHANNEL:
196 device.setDeviceSwingUpDown(socket, getNumber(command));
198 case SWINGLR_CHANNEL:
199 device.setDeviceSwingLeftRight(socket, getNumber(command));
201 case WINDSPEED_CHANNEL:
202 device.setDeviceWindspeed(socket, getNumber(command));
205 handleQuietCommand(socket, command);
208 device.setDeviceAir(socket, getOnOff(command));
211 device.setDeviceDry(socket, getOnOff(command));
214 device.setDeviceHealth(socket, getOnOff(command));
217 device.setDevicePwrSaving(socket, getOnOff(command));
222 private void handleModeCommand(DatagramSocket socket, Command command) throws GreeException {
225 boolean isNumber = false;
226 if (command instanceof DecimalType) {
227 // backward compatibility when channel was Number
228 mode = ((DecimalType) command).intValue();
229 } else if (command instanceof OnOffType) {
231 logger.debug("{}: Send Power-{}", thingId, command);
232 device.setDevicePower(socket, getOnOff(command));
233 } else /* String */ {
234 modeStr = command.toString().toLowerCase();
237 mode = GREE_MODE_AUTO;
240 mode = GREE_MODE_COOL;
243 mode = GREE_MODE_HEAT;
246 mode = GREE_MODE_DRY;
250 mode = GREE_MODE_FAN;
253 // power saving will be set after the uinit was turned on
254 mode = GREE_MODE_COOL;
258 logger.debug("{}: Turn unit {}", thingId, modeStr);
259 device.setDevicePower(socket, modeStr.equals(MODE_ON) ? 1 : 0);
262 // fallback: mode number, pass transparent
263 // if string is not parsable parseInt() throws an exception
264 mode = Integer.parseInt(modeStr);
268 logger.debug("{}: Mode {} mapped to {}", thingId, modeStr, mode);
272 throw new IllegalArgumentException("Invalid Mode selection");
275 // Turn on the unit if currently off
276 if (!isNumber && (device.getIntStatusVal(GREE_PROP_POWER) == 0)) {
277 logger.debug("{}: Send Auto-ON for mode {}", thingId, mode);
278 device.setDevicePower(socket, 1);
282 logger.debug("{}: Select mode {}", thingId, mode);
283 device.setDeviceMode(socket, mode);
285 // Check for secondary action
288 // Turn on power saving for eco mode
289 logger.debug("{}: Turn on Power-Saving", thingId);
290 device.setDevicePwrSaving(socket, 1);
295 private void handleQuietCommand(DatagramSocket socket, Command command) throws GreeException {
297 if (command instanceof DecimalType) {
298 mode = ((DecimalType) command).intValue();
299 } else if (command instanceof StringType) {
300 switch (command.toString().toLowerCase()) {
302 mode = GREE_QUIET_OFF;
305 mode = GREE_QUIET_AUTO;
308 mode = GREE_QUIET_QUIET;
313 device.setQuietMode(socket, mode);
315 throw new IllegalArgumentException("Invalid QuietType");
319 private int getOnOff(Command command) {
320 if (command instanceof OnOffType) {
321 return command == OnOffType.ON ? 1 : 0;
323 if (command instanceof DecimalType) {
324 int value = ((DecimalType) command).intValue();
325 if ((value == 0) || (value == 1)) {
329 throw new IllegalArgumentException("Invalid OnOffType");
332 private int getNumber(Command command) {
333 if (command instanceof DecimalType) {
334 return ((DecimalType) command).intValue();
336 throw new IllegalArgumentException("Invalid Number type");
339 private QuantityType<?> convertTemp(Command command) {
340 if (command instanceof DecimalType) {
341 // The Number alone doesn't specify the temp unit
342 // for this get current setting from the A/C unit
343 int unit = device.getIntStatusVal(GREE_PROP_TEMPUNIT);
344 return toQuantityType((DecimalType) command, DIGITS_TEMP,
345 unit == TEMP_UNIT_CELSIUS ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT);
347 if (command instanceof QuantityType) {
348 return (QuantityType<?>) command;
350 throw new IllegalArgumentException("Invalud Temp type");
353 private void startAutomaticRefresh() {
354 Runnable refresher = () -> {
356 // safeguard for multiple REFRESH commands
357 if (isMinimumRefreshTimeExceeded()) {
358 // Get the current status from the Airconditioner
360 if (getThing().getStatus() == ThingStatus.OFFLINE) {
361 // try to re-initialize thing access
362 logger.debug("{}: Re-initialize device", thingId);
367 if (clientSocket.isPresent()) {
368 device.getDeviceStatus(clientSocket.get());
369 apiRetries = 0; // the call was successful without an exception
370 logger.debug("{}: Executing automatic update of values", thingId);
371 List<Channel> channels = getThing().getChannels();
372 for (Channel channel : channels) {
373 publishChannel(channel.getUID());
377 } catch (GreeException e) {
379 if (e.getCause() != null) {
380 subcode = " (" + e.getCause().getMessage() + ")";
382 String message = messages.get("update.exception", e.getMessageString() + subcode);
383 if (getThing().getStatus() == ThingStatus.OFFLINE) {
384 logger.debug("{}: Thing still OFFLINE ({})", thingId, message);
386 if (!e.isTimeout()) {
387 logger.info("{}: {}", thingId, message);
389 logger.debug("{}: {}", thingId, message);
393 if (apiRetries > MAX_API_RETRIES) {
394 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
398 } catch (RuntimeException e) {
399 String message = messages.get("update.exception", "RuntimeException");
400 logger.warn("{}: {}", thingId, message, e);
405 if (refreshTask == null) {
406 refreshTask = scheduler.scheduleWithFixedDelay(refresher, 0, REFRESH_INTERVAL_SEC, TimeUnit.SECONDS);
407 logger.debug("{}: Automatic refresh started ({} second interval)", thingId, config.refresh);
412 private boolean isMinimumRefreshTimeExceeded() {
413 long currentTime = Instant.now().toEpochMilli();
414 long timeSinceLastRefresh = currentTime - lastRefreshTime;
415 if (!forceRefresh && (timeSinceLastRefresh < config.refresh * 1000)) {
418 lastRefreshTime = currentTime;
422 private void publishChannel(ChannelUID channelUID) {
423 String channelID = channelUID.getId();
426 switch (channelUID.getIdWithoutGroup()) {
428 state = updateOnOff(GREE_PROP_POWER);
431 state = updateMode();
434 state = updateOnOff(GREE_PROP_TURBO);
437 state = updateOnOff(GREE_PROP_LIGHT);
439 case TARGET_TEMP_CHANNEL:
440 state = updateTargetTemp();
442 case CURRENT_TEMP_CHANNEL:
443 state = updateCurrentTemp();
445 case SWINGUD_CHANNEL:
446 state = updateNumber(GREE_PROP_SWINGUPDOWN);
448 case SWINGLR_CHANNEL:
449 state = updateNumber(GREE_PROP_SWINGLEFTRIGHT);
451 case WINDSPEED_CHANNEL:
452 state = updateNumber(GREE_PROP_WINDSPEED);
455 state = updateQuiet();
458 state = updateOnOff(GREE_PROP_AIR);
461 state = updateOnOff(GREE_PROP_DRY);
464 state = updateOnOff(GREE_PROP_HEALTH);
467 state = updateOnOff(GREE_PROP_PWR_SAVING);
471 logger.debug("{}: Updating channel {} : {}", thingId, channelID, state);
472 updateState(channelID, state);
474 } catch (GreeException e) {
475 logger.info("{}: {}", thingId, messages.get("channel.exception", channelID, e.getMessageString()));
476 } catch (RuntimeException e) {
477 logger.warn("{}: {}", thingId, messages.get("channel.exception", "RuntimeException"), e);
481 private @Nullable State updateOnOff(final String valueName) throws GreeException {
482 if (device.hasStatusValChanged(valueName)) {
483 return device.getIntStatusVal(valueName) == 1 ? OnOffType.ON : OnOffType.OFF;
488 private @Nullable State updateNumber(final String valueName) throws GreeException {
489 if (device.hasStatusValChanged(valueName)) {
490 return new DecimalType(device.getIntStatusVal(valueName));
495 private @Nullable State updateMode() throws GreeException {
496 if (device.hasStatusValChanged(GREE_PROP_MODE)) {
497 int mode = device.getIntStatusVal(GREE_PROP_MODE);
504 boolean powerSave = device.getIntStatusVal(GREE_PROP_PWR_SAVING) == 1;
505 modeStr = !powerSave ? MODE_COOL : MODE_ECO;
517 modeStr = String.valueOf(mode);
520 if (!modeStr.isEmpty()) {
521 logger.debug("{}: Updading mode channel with {}/{}", thingId, mode, modeStr);
522 return new StringType(modeStr);
528 private @Nullable State updateQuiet() throws GreeException {
529 if (device.hasStatusValChanged(GREE_PROP_QUIET)) {
530 switch (device.getIntStatusVal(GREE_PROP_QUIET)) {
532 return new StringType(QUIET_OFF);
533 case GREE_QUIET_AUTO:
534 return new StringType(QUIET_AUTO);
535 case GREE_QUIET_QUIET:
536 return new StringType(QUIET_QUIET);
542 private @Nullable State updateTargetTemp() throws GreeException {
543 if (device.hasStatusValChanged(GREE_PROP_SETTEMP) || device.hasStatusValChanged(GREE_PROP_TEMPUNIT)) {
544 int unit = device.getIntStatusVal(GREE_PROP_TEMPUNIT);
545 return toQuantityType(device.getIntStatusVal(GREE_PROP_SETTEMP), DIGITS_TEMP,
546 unit == TEMP_UNIT_CELSIUS ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT);
551 private @Nullable State updateCurrentTemp() throws GreeException {
552 if (device.hasStatusValChanged(GREE_PROP_CURRENT_TEMP_SENSOR)) {
553 double temp = device.getIntStatusVal(GREE_PROP_CURRENT_TEMP_SENSOR);
556 temp + INTERNAL_TEMP_SENSOR_OFFSET + config.currentTemperatureOffset.doubleValue())
562 private String logInfo(String msgKey, Object... arg) {
563 String message = messages.get(msgKey, arg);
564 logger.info("{}: {}", thingId, message);
568 public static QuantityType<?> toQuantityType(Number value, int digits, Unit<?> unit) {
569 BigDecimal bd = new BigDecimal(value.doubleValue());
570 return new QuantityType<>(bd.setScale(digits, BigDecimal.ROUND_HALF_EVEN), unit);
573 private void stopRefreshTask() {
574 forceRefresh = false;
575 if (refreshTask == null) {
578 ScheduledFuture<?> task = refreshTask;
586 public void dispose() {
587 logger.debug("{}: Thing {} is disposing", thingId, thing.getUID());
588 if (clientSocket.isPresent()) {
589 clientSocket.get().close();
590 clientSocket = Optional.empty();
593 if (initializeFuture != null) {
594 initializeFuture.cancel(true);