]> git.basschouten.com Git - openhab-addons.git/blob
b6cef937f496a31c643cc895518cf5c78a5cf802
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.gree.internal.handler;
14
15 import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
16
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;
26
27 import javax.measure.Unit;
28
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;
53
54 /**
55  * The {@link GreeHandler} is responsible for handling commands, which are sent to one of the channels.
56  *
57  * @author John Cunha - Initial contribution
58  * @author Markus Michels - Refactoring, adapted to OH 2.5x
59  */
60 @NonNullByDefault
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;
70
71     private @Nullable ScheduledFuture<?> refreshTask;
72     private @Nullable Future<?> initializeFuture;
73     private long lastRefreshTime = 0;
74     private long apiRetries = 0;
75
76     public GreeHandler(Thing thing, GreeTranslationProvider messages, GreeDeviceFinder deviceFinder) {
77         super(thing);
78         this.messages = messages;
79         this.deviceFinder = deviceFinder;
80         this.thingId = getThing().getUID().getId();
81     }
82
83     @Override
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);
90             return;
91         }
92
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);
96
97         // Start the automatic refresh cycles
98         startAutomaticRefresh();
99         initializeFuture = scheduler.submit(this::initializeThing);
100     }
101
102     private void initializeThing() {
103         String message = "";
104         try {
105             if (!clientSocket.isPresent()) {
106                 clientSocket = Optional.of(new DatagramSocket());
107                 clientSocket.get().setSoTimeout(DATAGRAM_SOCKET_TIMEOUT);
108             }
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
114                 device = newDevice;
115                 device.bindWithDevice(clientSocket.get());
116                 if (device.getIsBound()) {
117                     updateStatus(ThingStatus.ONLINE);
118                     return;
119                 }
120             }
121
122             message = messages.get("thinginit.failed");
123             logger.info("{}: {}", thingId, message);
124         } catch (GreeException e) {
125             logger.info("{}: {}", thingId, messages.get("thinginit.exception", e.getMessage()));
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);
130         }
131
132         if (getThing().getStatus() != ThingStatus.OFFLINE) {
133             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
134         }
135     }
136
137     @Override
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.
141         } else {
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,
145                     command.getClass());
146
147             int retries = MAX_API_RETRIES;
148             do {
149                 try {
150                     sendRequest(channelId, command);
151                     // force refresh on next status refresh cycle
152                     forceRefresh = true;
153                     apiRetries = 0;
154                     return; // successful
155                 } catch (GreeException e) {
156                     retries--;
157                     if (retries > 0) {
158                         logger.debug("{}: Command {} failed for channel {}, retry", thingId, command, channelId);
159                     } else {
160                         String message = logInfo(
161                                 messages.get("command.exception", command, channelId) + ": " + e.getMessage());
162                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
163                     }
164                 } catch (IllegalArgumentException e) {
165                     logInfo("command.invarg", command, channelId);
166                     retries = 0;
167                 } catch (RuntimeException e) {
168                     logger.warn("{}: {}", thingId, messages.get("command.exception", command, channelId), e);
169                     retries = 0;
170                 }
171             } while (retries > 0);
172         }
173     }
174
175     private void sendRequest(String channelId, Command command) throws GreeException {
176         DatagramSocket socket = clientSocket.get();
177         switch (channelId) {
178             case MODE_CHANNEL:
179                 handleModeCommand(socket, command);
180                 break;
181             case POWER_CHANNEL:
182                 device.setDevicePower(socket, getOnOff(command));
183                 break;
184             case TURBO_CHANNEL:
185                 device.setDeviceTurbo(socket, getOnOff(command));
186                 break;
187             case LIGHT_CHANNEL:
188                 device.setDeviceLight(socket, getOnOff(command));
189                 break;
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));
194                 break;
195             case SWINGUD_CHANNEL:
196                 device.setDeviceSwingUpDown(socket, getNumber(command));
197                 break;
198             case SWINGLR_CHANNEL:
199                 device.setDeviceSwingLeftRight(socket, getNumber(command));
200                 break;
201             case WINDSPEED_CHANNEL:
202                 device.setDeviceWindspeed(socket, getNumber(command));
203                 break;
204             case QUIET_CHANNEL:
205                 handleQuietCommand(socket, command);
206                 break;
207             case AIR_CHANNEL:
208                 device.setDeviceAir(socket, getOnOff(command));
209                 break;
210             case DRY_CHANNEL:
211                 device.setDeviceDry(socket, getOnOff(command));
212                 break;
213             case HEALTH_CHANNEL:
214                 device.setDeviceHealth(socket, getOnOff(command));
215                 break;
216             case PWRSAV_CHANNEL:
217                 device.setDevicePwrSaving(socket, getOnOff(command));
218                 break;
219         }
220     }
221
222     private void handleModeCommand(DatagramSocket socket, Command command) throws GreeException {
223         int mode = -1;
224         String modeStr = "";
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) {
230             // Switch
231             logger.debug("{}: Send Power-{}", thingId, command);
232             device.setDevicePower(socket, getOnOff(command));
233         } else /* String */ {
234             modeStr = command.toString().toLowerCase();
235             switch (modeStr) {
236                 case MODE_AUTO:
237                     mode = GREE_MODE_AUTO;
238                     break;
239                 case MODE_COOL:
240                     mode = GREE_MODE_COOL;
241                     break;
242                 case MODE_HEAT:
243                     mode = GREE_MODE_HEAT;
244                     break;
245                 case MODE_DRY:
246                     mode = GREE_MODE_DRY;
247                     break;
248                 case MODE_FAN:
249                 case MODE_FAN2:
250                     mode = GREE_MODE_FAN;
251                     break;
252                 case MODE_ECO:
253                     // power saving will be set after the uinit was turned on
254                     mode = GREE_MODE_COOL;
255                     break;
256                 case MODE_ON:
257                 case MODE_OFF:
258                     logger.debug("{}: Turn unit {}", thingId, modeStr);
259                     device.setDevicePower(socket, modeStr.equals(MODE_ON) ? 1 : 0);
260                     return;
261                 default:
262                     // fallback: mode number, pass transparent
263                     // if string is not parsable parseInt() throws an exception
264                     mode = Integer.parseInt(modeStr);
265                     isNumber = true;
266                     break;
267             }
268             logger.debug("{}: Mode {} mapped to {}", thingId, modeStr, mode);
269         }
270
271         if (mode == -1) {
272             throw new IllegalArgumentException("Invalid Mode selection");
273         }
274
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);
279         }
280
281         // Select mode
282         logger.debug("{}: Select mode {}", thingId, mode);
283         device.setDeviceMode(socket, mode);
284
285         // Check for secondary action
286         switch (modeStr) {
287             case MODE_ECO:
288                 // Turn on power saving for eco mode
289                 logger.debug("{}: Turn on Power-Saving", thingId);
290                 device.setDevicePwrSaving(socket, 1);
291                 break;
292         }
293     }
294
295     private void handleQuietCommand(DatagramSocket socket, Command command) throws GreeException {
296         int mode = -1;
297         if (command instanceof DecimalType) {
298             mode = ((DecimalType) command).intValue();
299         } else if (command instanceof StringType) {
300             switch (command.toString().toLowerCase()) {
301                 case QUIET_OFF:
302                     mode = GREE_QUIET_OFF;
303                     break;
304                 case QUIET_AUTO:
305                     mode = GREE_QUIET_AUTO;
306                     break;
307                 case QUIET_QUIET:
308                     mode = GREE_QUIET_QUIET;
309                     break;
310             }
311         }
312         if (mode != -1) {
313             device.setQuietMode(socket, mode);
314         } else {
315             throw new IllegalArgumentException("Invalid QuietType");
316         }
317     }
318
319     private int getOnOff(Command command) {
320         if (command instanceof OnOffType) {
321             return command == OnOffType.ON ? 1 : 0;
322         }
323         if (command instanceof DecimalType) {
324             int value = ((DecimalType) command).intValue();
325             if ((value == 0) || (value == 1)) {
326                 return value;
327             }
328         }
329         throw new IllegalArgumentException("Invalid OnOffType");
330     }
331
332     private int getNumber(Command command) {
333         if (command instanceof DecimalType) {
334             return ((DecimalType) command).intValue();
335         }
336         throw new IllegalArgumentException("Invalid Number type");
337     }
338
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);
346         }
347         if (command instanceof QuantityType) {
348             return (QuantityType<?>) command;
349         }
350         throw new IllegalArgumentException("Invalud Temp type");
351     }
352
353     private void startAutomaticRefresh() {
354         Runnable refresher = () -> {
355             try {
356                 // safeguard for multiple REFRESH commands
357                 if (isMinimumRefreshTimeExceeded()) {
358                     // Get the current status from the Airconditioner
359
360                     if (getThing().getStatus() == ThingStatus.OFFLINE) {
361                         // try to re-initialize thing access
362                         logger.debug("{}: Re-initialize device", thingId);
363                         initializeThing();
364                         return;
365                     }
366
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());
374                         }
375                     }
376                 }
377             } catch (GreeException e) {
378                 String subcode = "";
379                 if (e.getCause() != null) {
380                     subcode = " (" + e.getCause().getMessage() + ")";
381                 }
382                 String message = messages.get("update.exception", e.getMessage() + subcode);
383                 if (getThing().getStatus() == ThingStatus.OFFLINE) {
384                     logger.debug("{}: Thing still OFFLINE ({})", thingId, message);
385                 } else {
386                     if (!e.isTimeout()) {
387                         logger.info("{}: {}", thingId, message);
388                     } else {
389                         logger.debug("{}: {}", thingId, message);
390                     }
391
392                     apiRetries++;
393                     if (apiRetries > MAX_API_RETRIES) {
394                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
395                         apiRetries = 0;
396                     }
397                 }
398             } catch (RuntimeException e) {
399                 String message = messages.get("update.exception", "RuntimeException");
400                 logger.warn("{}: {}", thingId, message, e);
401                 apiRetries++;
402             }
403         };
404
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);
408             forceRefresh = true;
409         }
410     }
411
412     private boolean isMinimumRefreshTimeExceeded() {
413         long currentTime = Instant.now().toEpochMilli();
414         long timeSinceLastRefresh = currentTime - lastRefreshTime;
415         if (!forceRefresh && (timeSinceLastRefresh < config.refresh * 1000)) {
416             return false;
417         }
418         lastRefreshTime = currentTime;
419         return true;
420     }
421
422     private void publishChannel(ChannelUID channelUID) {
423         String channelID = channelUID.getId();
424         try {
425             State state = null;
426             switch (channelUID.getIdWithoutGroup()) {
427                 case POWER_CHANNEL:
428                     state = updateOnOff(GREE_PROP_POWER);
429                     break;
430                 case MODE_CHANNEL:
431                     state = updateMode();
432                     break;
433                 case TURBO_CHANNEL:
434                     state = updateOnOff(GREE_PROP_TURBO);
435                     break;
436                 case LIGHT_CHANNEL:
437                     state = updateOnOff(GREE_PROP_LIGHT);
438                     break;
439                 case TARGET_TEMP_CHANNEL:
440                     state = updateTargetTemp();
441                     break;
442                 case CURRENT_TEMP_CHANNEL:
443                     state = updateCurrentTemp();
444                     break;
445                 case SWINGUD_CHANNEL:
446                     state = updateNumber(GREE_PROP_SWINGUPDOWN);
447                     break;
448                 case SWINGLR_CHANNEL:
449                     state = updateNumber(GREE_PROP_SWINGLEFTRIGHT);
450                     break;
451                 case WINDSPEED_CHANNEL:
452                     state = updateNumber(GREE_PROP_WINDSPEED);
453                     break;
454                 case QUIET_CHANNEL:
455                     state = updateQuiet();
456                     break;
457                 case AIR_CHANNEL:
458                     state = updateOnOff(GREE_PROP_AIR);
459                     break;
460                 case DRY_CHANNEL:
461                     state = updateOnOff(GREE_PROP_DRY);
462                     break;
463                 case HEALTH_CHANNEL:
464                     state = updateOnOff(GREE_PROP_HEALTH);
465                     break;
466                 case PWRSAV_CHANNEL:
467                     state = updateOnOff(GREE_PROP_PWR_SAVING);
468                     break;
469             }
470             if (state != null) {
471                 logger.debug("{}: Updating channel {} : {}", thingId, channelID, state);
472                 updateState(channelID, state);
473             }
474         } catch (GreeException e) {
475             logger.info("{}: {}", thingId, messages.get("channel.exception", channelID, e.getMessage()));
476         } catch (RuntimeException e) {
477             logger.warn("{}: {}", thingId, messages.get("channel.exception", "RuntimeException"), e);
478         }
479     }
480
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;
484         }
485         return null;
486     }
487
488     private @Nullable State updateNumber(final String valueName) throws GreeException {
489         if (device.hasStatusValChanged(valueName)) {
490             return new DecimalType(device.getIntStatusVal(valueName));
491         }
492         return null;
493     }
494
495     private @Nullable State updateMode() throws GreeException {
496         if (device.hasStatusValChanged(GREE_PROP_MODE)) {
497             int mode = device.getIntStatusVal(GREE_PROP_MODE);
498             String modeStr = "";
499             switch (mode) {
500                 case GREE_MODE_AUTO:
501                     modeStr = MODE_AUTO;
502                     break;
503                 case GREE_MODE_COOL:
504                     boolean powerSave = device.getIntStatusVal(GREE_PROP_PWR_SAVING) == 1;
505                     modeStr = !powerSave ? MODE_COOL : MODE_ECO;
506                     break;
507                 case GREE_MODE_DRY:
508                     modeStr = MODE_DRY;
509                     break;
510                 case GREE_MODE_FAN:
511                     modeStr = MODE_FAN;
512                     break;
513                 case GREE_MODE_HEAT:
514                     modeStr = MODE_HEAT;
515                     break;
516                 default:
517                     modeStr = String.valueOf(mode);
518
519             }
520             if (!modeStr.isEmpty()) {
521                 logger.debug("{}: Updading mode channel with {}/{}", thingId, mode, modeStr);
522                 return new StringType(modeStr);
523             }
524         }
525         return null;
526     }
527
528     private @Nullable State updateQuiet() throws GreeException {
529         if (device.hasStatusValChanged(GREE_PROP_QUIET)) {
530             switch (device.getIntStatusVal(GREE_PROP_QUIET)) {
531                 case GREE_QUIET_OFF:
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);
537             }
538         }
539         return null;
540     }
541
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);
547         }
548         return null;
549     }
550
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);
554             return temp != 0
555                     ? new DecimalType(
556                             temp + INTERNAL_TEMP_SENSOR_OFFSET + config.currentTemperatureOffset.doubleValue())
557                     : UnDefType.UNDEF;
558         }
559         return null;
560     }
561
562     private String logInfo(String msgKey, Object... arg) {
563         String message = messages.get(msgKey, arg);
564         logger.info("{}: {}", thingId, message);
565         return message;
566     }
567
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);
571     }
572
573     private void stopRefreshTask() {
574         forceRefresh = false;
575         if (refreshTask == null) {
576             return;
577         }
578         ScheduledFuture<?> task = refreshTask;
579         if (task != null) {
580             task.cancel(true);
581         }
582         refreshTask = null;
583     }
584
585     @Override
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();
591         }
592         stopRefreshTask();
593         if (initializeFuture != null) {
594             initializeFuture.cancel(true);
595         }
596     }
597 }