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