2 * Copyright (c) 2010-2024 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.math.RoundingMode;
20 import java.net.DatagramSocket;
21 import java.time.Instant;
22 import java.util.List;
23 import java.util.Optional;
24 import java.util.concurrent.Future;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import javax.measure.Unit;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.gree.internal.GreeConfiguration;
33 import org.openhab.binding.gree.internal.GreeException;
34 import org.openhab.binding.gree.internal.GreeTranslationProvider;
35 import org.openhab.binding.gree.internal.discovery.GreeDeviceFinder;
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.ImperialUnits;
41 import org.openhab.core.library.unit.SIUnits;
42 import org.openhab.core.thing.Channel;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.openhab.core.types.State;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * The {@link GreeHandler} is responsible for handling commands, which are sent to one of the channels.
58 * @author John Cunha - Initial contribution
59 * @author Markus Michels - Refactoring, adapted to OH 2.5x
62 public class GreeHandler extends BaseThingHandler {
63 private final Logger logger = LoggerFactory.getLogger(GreeHandler.class);
64 private final GreeTranslationProvider messages;
65 private final GreeDeviceFinder deviceFinder;
66 private final String thingId;
67 private GreeConfiguration config = new GreeConfiguration();
68 private GreeAirDevice device = new GreeAirDevice();
69 private Optional<DatagramSocket> clientSocket = Optional.empty();
70 private boolean forceRefresh = false;
72 private @Nullable ScheduledFuture<?> refreshTask;
73 private @Nullable Future<?> initializeFuture;
74 private long lastRefreshTime = 0;
75 private long apiRetries = 0;
77 public GreeHandler(Thing thing, GreeTranslationProvider messages, GreeDeviceFinder deviceFinder) {
79 this.messages = messages;
80 this.deviceFinder = deviceFinder;
81 this.thingId = getThing().getUID().getId();
85 public void initialize() {
86 config = getConfigAs(GreeConfiguration.class);
87 if (config.ipAddress.isEmpty() || (config.refresh < 0)) {
88 String message = messages.get("thinginit.invconf");
89 logger.warn("{}: {}", thingId, message);
90 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message);
94 // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
95 // the framework is then able to reuse the resources from the thing handler initialization.
96 updateStatus(ThingStatus.UNKNOWN);
98 // Start the automatic refresh cycles
99 startAutomaticRefresh();
100 initializeFuture = scheduler.submit(this::initializeThing);
103 private void initializeThing() {
106 if (clientSocket.isEmpty()) {
107 clientSocket = Optional.of(new DatagramSocket());
108 clientSocket.get().setSoTimeout(DATAGRAM_SOCKET_TIMEOUT);
110 // Find the GREE device
111 deviceFinder.scan(clientSocket.get(), config.ipAddress, false);
112 GreeAirDevice newDevice = deviceFinder.getDeviceByIPAddress(config.ipAddress);
113 if (newDevice != null) {
114 // Ok, our device responded, now let's Bind with it
116 device.bindWithDevice(clientSocket.get());
117 if (device.getIsBound()) {
118 updateStatus(ThingStatus.ONLINE);
123 message = messages.get("thinginit.failed");
124 logger.info("{}: {}", thingId, message);
125 } catch (GreeException e) {
126 logger.info("{}: {}", thingId, messages.get("thinginit.exception", e.getMessageString()));
127 } catch (IOException e) {
128 logger.warn("{}: {}", thingId, messages.get("thinginit.exception", "I/O Error"), e);
129 } catch (RuntimeException e) {
130 logger.warn("{}: {}", thingId, messages.get("thinginit.exception", "RuntimeException"), e);
133 if (getThing().getStatus() != ThingStatus.OFFLINE) {
134 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
139 public void handleCommand(ChannelUID channelUID, Command command) {
140 if (command instanceof RefreshType) {
141 // The thing is updated by the scheduled automatic refresh so do nothing here.
143 logger.debug("{}: Issue command {} to channe {}", thingId, command, channelUID.getIdWithoutGroup());
144 String channelId = channelUID.getIdWithoutGroup();
145 logger.debug("{}: Handle command {} for channel {}, command class {}", thingId, command, channelId,
148 int retries = MAX_API_RETRIES;
151 sendRequest(channelId, command);
152 // force refresh on next status refresh cycle
155 return; // successful
156 } catch (GreeException e) {
159 logger.debug("{}: Command {} failed for channel {}, retry", thingId, command, channelId);
161 String message = logInfo(
162 messages.get("command.exception", command, channelId) + ": " + e.getMessageString());
163 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
165 } catch (IllegalArgumentException e) {
166 logInfo("command.invarg", command, channelId);
168 } catch (RuntimeException e) {
169 logger.warn("{}: {}", thingId, messages.get("command.exception", command, channelId), e);
172 } while (retries > 0);
176 private void sendRequest(String channelId, Command command) throws GreeException {
177 DatagramSocket socket = clientSocket.get();
180 handleModeCommand(socket, command);
183 device.setDevicePower(socket, getOnOff(command));
186 device.setDeviceTurbo(socket, getOnOff(command));
189 device.setDeviceLight(socket, getOnOff(command));
191 case TARGET_TEMP_CHANNEL:
192 // Set value, read back effective one and update channel
193 // e.g. 22.5C will result in 22.0, because the AC doesn't support half-steps for C
194 device.setDeviceTempSet(socket, convertTemp(command));
196 case SWINGUD_CHANNEL:
197 device.setDeviceSwingUpDown(socket, getNumber(command));
199 case SWINGLR_CHANNEL:
200 device.setDeviceSwingLeftRight(socket, getNumber(command));
202 case WINDSPEED_CHANNEL:
203 device.setDeviceWindspeed(socket, getNumber(command));
206 handleQuietCommand(socket, command);
209 device.setDeviceAir(socket, getOnOff(command));
212 device.setDeviceDry(socket, getOnOff(command));
215 device.setDeviceHealth(socket, getOnOff(command));
218 device.setDevicePwrSaving(socket, getOnOff(command));
223 private void handleModeCommand(DatagramSocket socket, Command command) throws GreeException {
226 boolean isNumber = false;
227 if (command instanceof DecimalType decimalCommand) {
228 // backward compatibility when channel was Number
229 mode = decimalCommand.intValue();
230 } else if (command instanceof OnOffType) {
232 logger.debug("{}: Send Power-{}", thingId, command);
233 device.setDevicePower(socket, getOnOff(command));
234 } else /* String */ {
235 modeStr = command.toString().toLowerCase();
238 mode = GREE_MODE_AUTO;
241 mode = GREE_MODE_COOL;
244 mode = GREE_MODE_HEAT;
247 mode = GREE_MODE_DRY;
251 mode = GREE_MODE_FAN;
254 // power saving will be set after the uinit was turned on
255 mode = GREE_MODE_COOL;
259 logger.debug("{}: Turn unit {}", thingId, modeStr);
260 device.setDevicePower(socket, modeStr.equals(MODE_ON) ? 1 : 0);
263 // fallback: mode number, pass transparent
264 // if string is not parsable parseInt() throws an exception
265 mode = Integer.parseInt(modeStr);
269 logger.debug("{}: Mode {} mapped to {}", thingId, modeStr, mode);
273 throw new IllegalArgumentException("Invalid Mode selection");
276 // Turn on the unit if currently off
277 if (!isNumber && (device.getIntStatusVal(GREE_PROP_POWER) == 0)) {
278 logger.debug("{}: Send Auto-ON for mode {}", thingId, mode);
279 device.setDevicePower(socket, 1);
283 logger.debug("{}: Select mode {}", thingId, mode);
284 device.setDeviceMode(socket, mode);
286 // Check for secondary action
289 // Turn on power saving for eco mode
290 logger.debug("{}: Turn on Power-Saving", thingId);
291 device.setDevicePwrSaving(socket, 1);
296 private void handleQuietCommand(DatagramSocket socket, Command command) throws GreeException {
298 if (command instanceof DecimalType decimalCommand) {
299 mode = decimalCommand.intValue();
300 } else if (command instanceof StringType) {
301 switch (command.toString().toLowerCase()) {
303 mode = GREE_QUIET_OFF;
306 mode = GREE_QUIET_AUTO;
309 mode = GREE_QUIET_QUIET;
314 device.setQuietMode(socket, mode);
316 throw new IllegalArgumentException("Invalid QuietType");
320 private int getOnOff(Command command) {
321 if (command instanceof OnOffType) {
322 return command == OnOffType.ON ? 1 : 0;
324 if (command instanceof DecimalType decimalCommand) {
325 int value = decimalCommand.intValue();
326 if ((value == 0) || (value == 1)) {
330 throw new IllegalArgumentException("Invalid OnOffType");
333 private int getNumber(Command command) {
334 if (command instanceof DecimalType decimalCommand) {
335 return decimalCommand.intValue();
337 throw new IllegalArgumentException("Invalid Number type");
340 private QuantityType<?> convertTemp(Command command) {
341 if (command instanceof DecimalType temperature) {
342 // The Number alone doesn't specify the temp unit
343 // for this get current setting from the A/C unit
344 int unit = device.getIntStatusVal(GREE_PROP_TEMPUNIT);
345 return toQuantityType(temperature, DIGITS_TEMP,
346 unit == TEMP_UNIT_CELSIUS ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT);
348 if (command instanceof QuantityType quantityCommand) {
349 return quantityCommand;
351 throw new IllegalArgumentException("Invalud Temp type");
354 private void startAutomaticRefresh() {
355 Runnable refresher = () -> {
357 // safeguard for multiple REFRESH commands
358 if (isMinimumRefreshTimeExceeded()) {
359 // Get the current status from the Airconditioner
361 if (getThing().getStatus() == ThingStatus.OFFLINE) {
362 // try to re-initialize thing access
363 logger.debug("{}: Re-initialize device", thingId);
368 if (clientSocket.isPresent()) {
369 device.getDeviceStatus(clientSocket.get());
370 apiRetries = 0; // the call was successful without an exception
371 logger.debug("{}: Executing automatic update of values", thingId);
372 List<Channel> channels = getThing().getChannels();
373 for (Channel channel : channels) {
374 publishChannel(channel.getUID());
378 } catch (GreeException e) {
380 if (e.getCause() != null) {
381 subcode = " (" + e.getCause().getMessage() + ")";
383 String message = messages.get("update.exception", e.getMessageString() + subcode);
384 if (getThing().getStatus() == ThingStatus.OFFLINE) {
385 logger.debug("{}: Thing still OFFLINE ({})", thingId, message);
387 if (!e.isTimeout()) {
388 logger.info("{}: {}", thingId, message);
390 logger.debug("{}: {}", thingId, message);
394 if (apiRetries > MAX_API_RETRIES) {
395 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
399 } catch (RuntimeException e) {
400 String message = messages.get("update.exception", "RuntimeException");
401 logger.warn("{}: {}", thingId, message, e);
406 if (refreshTask == null) {
407 refreshTask = scheduler.scheduleWithFixedDelay(refresher, 0, REFRESH_INTERVAL_SEC, TimeUnit.SECONDS);
408 logger.debug("{}: Automatic refresh started ({} second interval)", thingId, config.refresh);
413 private boolean isMinimumRefreshTimeExceeded() {
414 long currentTime = Instant.now().toEpochMilli();
415 long timeSinceLastRefresh = currentTime - lastRefreshTime;
416 if (!forceRefresh && (timeSinceLastRefresh < config.refresh * 1000)) {
419 lastRefreshTime = currentTime;
423 private void publishChannel(ChannelUID channelUID) {
424 String channelID = channelUID.getId();
427 switch (channelUID.getIdWithoutGroup()) {
429 state = updateOnOff(GREE_PROP_POWER);
432 state = updateMode();
435 state = updateOnOff(GREE_PROP_TURBO);
438 state = updateOnOff(GREE_PROP_LIGHT);
440 case TARGET_TEMP_CHANNEL:
441 state = updateTargetTemp();
443 case CURRENT_TEMP_CHANNEL:
444 state = updateCurrentTemp();
446 case SWINGUD_CHANNEL:
447 state = updateNumber(GREE_PROP_SWINGUPDOWN);
449 case SWINGLR_CHANNEL:
450 state = updateNumber(GREE_PROP_SWINGLEFTRIGHT);
452 case WINDSPEED_CHANNEL:
453 state = updateNumber(GREE_PROP_WINDSPEED);
456 state = updateQuiet();
459 state = updateOnOff(GREE_PROP_AIR);
462 state = updateOnOff(GREE_PROP_DRY);
465 state = updateOnOff(GREE_PROP_HEALTH);
468 state = updateOnOff(GREE_PROP_PWR_SAVING);
472 logger.debug("{}: Updating channel {} : {}", thingId, channelID, state);
473 updateState(channelID, state);
475 } catch (GreeException e) {
476 logger.info("{}: {}", thingId, messages.get("channel.exception", channelID, e.getMessageString()));
477 } catch (RuntimeException e) {
478 logger.warn("{}: {}", thingId, messages.get("channel.exception", "RuntimeException"), e);
482 private @Nullable State updateOnOff(final String valueName) throws GreeException {
483 if (device.hasStatusValChanged(valueName)) {
484 return OnOffType.from(device.getIntStatusVal(valueName) == 1);
489 private @Nullable State updateNumber(final String valueName) throws GreeException {
490 if (device.hasStatusValChanged(valueName)) {
491 return new DecimalType(device.getIntStatusVal(valueName));
496 private @Nullable State updateMode() throws GreeException {
497 if (device.hasStatusValChanged(GREE_PROP_MODE)) {
498 int mode = device.getIntStatusVal(GREE_PROP_MODE);
505 boolean powerSave = device.getIntStatusVal(GREE_PROP_PWR_SAVING) == 1;
506 modeStr = !powerSave ? MODE_COOL : MODE_ECO;
518 modeStr = String.valueOf(mode);
521 if (!modeStr.isEmpty()) {
522 logger.debug("{}: Updading mode channel with {}/{}", thingId, mode, modeStr);
523 return new StringType(modeStr);
529 private @Nullable State updateQuiet() throws GreeException {
530 if (device.hasStatusValChanged(GREE_PROP_QUIET)) {
531 switch (device.getIntStatusVal(GREE_PROP_QUIET)) {
533 return new StringType(QUIET_OFF);
534 case GREE_QUIET_AUTO:
535 return new StringType(QUIET_AUTO);
536 case GREE_QUIET_QUIET:
537 return new StringType(QUIET_QUIET);
543 private @Nullable State updateTargetTemp() throws GreeException {
544 if (device.hasStatusValChanged(GREE_PROP_SETTEMP) || device.hasStatusValChanged(GREE_PROP_TEMPUNIT)) {
545 int unit = device.getIntStatusVal(GREE_PROP_TEMPUNIT);
546 return toQuantityType(device.getIntStatusVal(GREE_PROP_SETTEMP), DIGITS_TEMP,
547 unit == TEMP_UNIT_CELSIUS ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT);
552 private @Nullable State updateCurrentTemp() throws GreeException {
553 if (device.hasStatusValChanged(GREE_PROP_CURRENT_TEMP_SENSOR)) {
554 double temp = device.getIntStatusVal(GREE_PROP_CURRENT_TEMP_SENSOR);
557 temp + INTERNAL_TEMP_SENSOR_OFFSET + config.currentTemperatureOffset.doubleValue())
563 private String logInfo(String msgKey, Object... arg) {
564 String message = messages.get(msgKey, arg);
565 logger.info("{}: {}", thingId, message);
569 public static QuantityType<?> toQuantityType(Number value, int digits, Unit<?> unit) {
570 BigDecimal bd = new BigDecimal(value.doubleValue());
571 return new QuantityType<>(bd.setScale(digits, RoundingMode.HALF_EVEN), unit);
574 private void stopRefreshTask() {
575 forceRefresh = false;
576 if (refreshTask == null) {
579 ScheduledFuture<?> task = refreshTask;
587 public void dispose() {
588 logger.debug("{}: Thing {} is disposing", thingId, thing.getUID());
589 if (clientSocket.isPresent()) {
590 clientSocket.get().close();
591 clientSocket = Optional.empty();
594 if (initializeFuture != null) {
595 initializeFuture.cancel(true);