]> git.basschouten.com Git - openhab-addons.git/blob
4c2bf94b760a415d77873ef322f64526acaa3781
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.bigassfan.internal.handler;
14
15 import static org.openhab.binding.bigassfan.internal.BigAssFanBindingConstants.*;
16
17 import java.io.DataOutputStream;
18 import java.io.IOException;
19 import java.io.UnsupportedEncodingException;
20 import java.net.InetAddress;
21 import java.net.InetSocketAddress;
22 import java.net.NetworkInterface;
23 import java.net.Socket;
24 import java.net.SocketException;
25 import java.net.UnknownHostException;
26 import java.nio.BufferOverflowException;
27 import java.time.Instant;
28 import java.time.ZoneId;
29 import java.time.ZonedDateTime;
30 import java.time.format.DateTimeParseException;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.Map;
35 import java.util.NoSuchElementException;
36 import java.util.Scanner;
37 import java.util.concurrent.ScheduledExecutorService;
38 import java.util.concurrent.ScheduledFuture;
39 import java.util.concurrent.TimeUnit;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42
43 import org.apache.commons.lang.StringUtils;
44 import org.openhab.binding.bigassfan.internal.BigAssFanConfig;
45 import org.openhab.binding.bigassfan.internal.utils.BigAssFanConverter;
46 import org.openhab.core.common.ThreadPoolManager;
47 import org.openhab.core.library.types.DateTimeType;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.library.types.PercentType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.State;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 /**
64  * The {@link BigAssFanHandler} is responsible for handling commands, which are
65  * sent to one of the channels.
66  *
67  * @author Mark Hilbush - Initial contribution
68  */
69 public class BigAssFanHandler extends BaseThingHandler {
70     private final Logger logger = LoggerFactory.getLogger(BigAssFanHandler.class);
71
72     private static final StringType LIGHT_COLOR = new StringType("COLOR");
73     private static final StringType LIGHT_PRESENT = new StringType("PRESENT");
74
75     private static final StringType OFF = new StringType("OFF");
76     private static final StringType COOLING = new StringType("COOLING");
77     private static final StringType HEATING = new StringType("HEATING");
78
79     private BigAssFanConfig config;
80     private String label = null;
81     private String ipAddress = null;
82     private String macAddress = null;
83
84     private FanListener fanListener;
85
86     protected Map<String, State> fanStateMap = Collections.synchronizedMap(new HashMap<>());
87
88     public BigAssFanHandler(Thing thing, String ipv4Address) {
89         super(thing);
90         this.thing = thing;
91
92         logger.debug("Creating FanListener object for {}", thing.getUID());
93         fanListener = new FanListener(ipv4Address);
94     }
95
96     @Override
97     public void initialize() {
98         logger.debug("BigAssFanHandler for {} is initializing", thing.getUID());
99
100         config = getConfig().as(BigAssFanConfig.class);
101         logger.debug("BigAssFanHandler config for {} is {}", thing.getUID(), config);
102
103         if (!config.isValid()) {
104             logger.debug("BigAssFanHandler config of {} is invalid. Check configuration", thing.getUID());
105             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
106                     "Invalid BigAssFan config. Check configuration.");
107             return;
108         }
109         label = config.getLabel();
110         ipAddress = config.getIpAddress();
111         macAddress = config.getMacAddress();
112
113         fanListener.startFanListener();
114     }
115
116     @Override
117     public void dispose() {
118         logger.debug("BigAssFanHandler for {} is disposing", thing.getUID());
119         fanListener.stopFanListener();
120     }
121
122     @Override
123     public void handleCommand(ChannelUID channelUID, Command command) {
124         if (command instanceof RefreshType) {
125             return;
126         }
127
128         logger.debug("Handle command for {} on channel {}: {}", thing.getUID(), channelUID, command);
129         if (channelUID.getId().equals(CHANNEL_FAN_POWER)) {
130             handleFanPower(command);
131         } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED)) {
132             handleFanSpeed(command);
133         } else if (channelUID.getId().equals(CHANNEL_FAN_AUTO)) {
134             handleFanAuto(command);
135         } else if (channelUID.getId().equals(CHANNEL_FAN_WHOOSH)) {
136             handleFanWhoosh(command);
137         } else if (channelUID.getId().equals(CHANNEL_FAN_SMARTMODE)) {
138             handleFanSmartmode(command);
139         } else if (channelUID.getId().equals(CHANNEL_FAN_LEARN_MINSPEED)) {
140             handleFanLearnSpeedMin(command);
141         } else if (channelUID.getId().equals(CHANNEL_FAN_LEARN_MAXSPEED)) {
142             handleFanLearnSpeedMax(command);
143         } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED_MIN)) {
144             handleFanSpeedMin(command);
145         } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED_MAX)) {
146             handleFanSpeedMax(command);
147         } else if (channelUID.getId().equals(CHANNEL_FAN_WINTERMODE)) {
148             handleFanWintermode(command);
149         } else if (channelUID.getId().equals(CHANNEL_LIGHT_POWER)) {
150             handleLightPower(command);
151         } else if (channelUID.getId().equals(CHANNEL_LIGHT_LEVEL)) {
152             handleLightLevel(command);
153         } else if (channelUID.getId().equals(CHANNEL_LIGHT_HUE)) {
154             handleLightHue(command);
155         } else if (channelUID.getId().equals(CHANNEL_LIGHT_AUTO)) {
156             handleLightAuto(command);
157         } else if (channelUID.getId().equals(CHANNEL_LIGHT_SMARTER)) {
158             handleLightSmarter(command);
159         } else if (channelUID.getId().equals(CHANNEL_LIGHT_LEVEL_MIN)) {
160             handleLightLevelMin(command);
161         } else if (channelUID.getId().equals(CHANNEL_LIGHT_LEVEL_MAX)) {
162             handleLightLevelMax(command);
163         } else if (channelUID.getId().equals(CHANNEL_FAN_SLEEP)) {
164             handleSleep(command);
165         } else {
166             logger.debug("Received command for {} on unknown channel {}", thing.getUID(), channelUID.getId());
167         }
168     }
169
170     private void handleFanPower(Command command) {
171         logger.debug("Handling fan power command for {}: {}", thing.getUID(), command);
172
173         // <mac;FAN;PWR;ON|OFF>
174         if (command instanceof OnOffType) {
175             if (command.equals(OnOffType.OFF)) {
176                 sendCommand(macAddress, ";FAN;PWR;OFF");
177             } else if (command.equals(OnOffType.ON)) {
178                 sendCommand(macAddress, ";FAN;PWR;ON");
179             }
180         }
181     }
182
183     private void handleFanSpeed(Command command) {
184         logger.debug("Handling fan speed command for {}: {}", thing.getUID(), command);
185
186         // <mac;FAN;SPD;SET;0..7>
187         if (command instanceof PercentType) {
188             sendCommand(macAddress, ";FAN;SPD;SET;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
189         }
190     }
191
192     private void handleFanAuto(Command command) {
193         logger.debug("Handling fan auto command {}", command);
194
195         // <mac;FAN;AUTO;ON|OFF>
196         if (command instanceof OnOffType) {
197             if (command.equals(OnOffType.OFF)) {
198                 sendCommand(macAddress, ";FAN;AUTO;OFF");
199             } else if (command.equals(OnOffType.ON)) {
200                 sendCommand(macAddress, ";FAN;AUTO;ON");
201             }
202         }
203     }
204
205     private void handleFanWhoosh(Command command) {
206         logger.debug("Handling fan whoosh command {}", command);
207
208         // <mac;FAN;WHOOSH;ON|OFF>
209         if (command instanceof OnOffType) {
210             if (command.equals(OnOffType.OFF)) {
211                 sendCommand(macAddress, ";FAN;WHOOSH;OFF");
212             } else if (command.equals(OnOffType.ON)) {
213                 sendCommand(macAddress, ";FAN;WHOOSH;ON");
214             }
215         }
216     }
217
218     private void handleFanSmartmode(Command command) {
219         logger.debug("Handling fan smartmode command {}", command);
220
221         // <mac;SMARTMODE;SET;OFF/COOLING/HEATING>
222         if (command instanceof StringType) {
223             if (command.equals(OFF)) {
224                 sendCommand(macAddress, ";SMARTMODE;STATE;SET;OFF");
225             } else if (command.equals(COOLING)) {
226                 sendCommand(macAddress, ";SMARTMODE;STATE;SET;COOLING");
227             } else if (command.equals(HEATING)) {
228                 sendCommand(macAddress, ";SMARTMODE;STATE;SET;HEATING");
229             } else {
230                 logger.debug("Unknown fan smartmode command: {}", command);
231             }
232         }
233     }
234
235     private void handleFanLearnSpeedMin(Command command) {
236         logger.debug("Handling fan learn speed minimum command {}", command);
237         // <mac;FAN;SPD;SET;MIN;0..7>
238         if (command instanceof PercentType) {
239             // Send min speed set command
240             sendCommand(macAddress,
241                     ";LEARN;MINSPEED;SET;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
242             fanStateMap.put(CHANNEL_FAN_LEARN_MINSPEED, (PercentType) command);
243             // Don't let max be less than min
244             adjustMaxSpeed((PercentType) command, CHANNEL_FAN_LEARN_MAXSPEED, ";LEARN;MAXSPEED;");
245         }
246     }
247
248     private void handleFanLearnSpeedMax(Command command) {
249         logger.debug("Handling fan learn speed maximum command {}", command);
250         // <mac;FAN;SPD;SET;MAX;0..7>
251         if (command instanceof PercentType) {
252             // Send max speed set command
253             sendCommand(macAddress,
254                     ";LEARN;MAXSPEED;SET;;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
255             fanStateMap.put(CHANNEL_FAN_LEARN_MAXSPEED, (PercentType) command);
256             // Don't let min be greater than max
257             adjustMinSpeed((PercentType) command, CHANNEL_FAN_LEARN_MINSPEED, ";LEARN;MINSPEED;");
258         }
259     }
260
261     private void handleFanSpeedMin(Command command) {
262         logger.debug("Handling fan speed minimum command {}", command);
263         // <mac;FAN;SPD;SET;MIN;0..7>
264         if (command instanceof PercentType) {
265             // Send min speed set command
266             sendCommand(macAddress,
267                     ";FAN;SPD;SET;MIN;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
268             fanStateMap.put(CHANNEL_FAN_SPEED_MIN, (PercentType) command);
269             // Don't let max be less than min
270             adjustMaxSpeed((PercentType) command, CHANNEL_FAN_SPEED_MAX, ";FAN;SPD;SET;MAX;");
271         }
272     }
273
274     private void handleFanSpeedMax(Command command) {
275         logger.debug("Handling fan speed maximum command {}", command);
276         // <mac;FAN;SPD;SET;MAX;0..7>
277         if (command instanceof PercentType) {
278             // Send max speed set command
279             sendCommand(macAddress,
280                     ";FAN;SPD;SET;MAX;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
281             fanStateMap.put(CHANNEL_FAN_SPEED_MAX, (PercentType) command);
282             // Don't let min be greater than max
283             adjustMinSpeed((PercentType) command, CHANNEL_FAN_SPEED_MIN, ";FAN;SPD;SET;MIN;");
284         }
285     }
286
287     private void handleFanWintermode(Command command) {
288         logger.debug("Handling fan wintermode command {}", command);
289
290         // <mac;FAN;WINTERMODE;ON|OFF>
291         if (command instanceof OnOffType) {
292             if (command.equals(OnOffType.OFF)) {
293                 sendCommand(macAddress, ";FAN;WINTERMODE;OFF");
294             } else if (command.equals(OnOffType.ON)) {
295                 sendCommand(macAddress, ";FAN;WINTERMODE;ON");
296             }
297         }
298     }
299
300     private void handleSleep(Command command) {
301         logger.debug("Handling fan sleep command {}", command);
302
303         // <mac;SLEEP;STATE;ON|OFF>
304         if (command instanceof OnOffType) {
305             if (command.equals(OnOffType.OFF)) {
306                 sendCommand(macAddress, ";SLEEP;STATE;OFF");
307             } else if (command.equals(OnOffType.ON)) {
308                 sendCommand(macAddress, ";SLEEP;STATE;ON");
309             }
310         }
311     }
312
313     private void adjustMaxSpeed(PercentType command, String channelId, String commandFragment) {
314         int newMin = command.intValue();
315         int currentMax = PercentType.ZERO.intValue();
316         if (fanStateMap.get(channelId) != null) {
317             currentMax = ((PercentType) fanStateMap.get(channelId)).intValue();
318         }
319         if (newMin > currentMax) {
320             updateState(CHANNEL_FAN_SPEED_MAX, command);
321             sendCommand(macAddress, commandFragment.concat(BigAssFanConverter.percentToSpeed(command)));
322         }
323     }
324
325     private void adjustMinSpeed(PercentType command, String channelId, String commandFragment) {
326         int newMax = command.intValue();
327         int currentMin = PercentType.HUNDRED.intValue();
328         if (fanStateMap.get(channelId) != null) {
329             currentMin = ((PercentType) fanStateMap.get(channelId)).intValue();
330         }
331         if (newMax < currentMin) {
332             updateState(channelId, command);
333             sendCommand(macAddress, commandFragment.concat(BigAssFanConverter.percentToSpeed(command)));
334         }
335     }
336
337     private void handleLightPower(Command command) {
338         if (!isLightPresent()) {
339             logger.debug("Fan does not have light installed for command {}", command);
340             return;
341         }
342
343         logger.debug("Handling light power command {}", command);
344         // <mac;LIGHT;PWR;ON|OFF>
345         if (command instanceof OnOffType) {
346             if (command.equals(OnOffType.OFF)) {
347                 sendCommand(macAddress, ";LIGHT;PWR;OFF");
348             } else if (command.equals(OnOffType.ON)) {
349                 sendCommand(macAddress, ";LIGHT;PWR;ON");
350             }
351         }
352     }
353
354     private void handleLightLevel(Command command) {
355         if (!isLightPresent()) {
356             logger.debug("Fan does not have light installed for command {}", command);
357             return;
358         }
359
360         logger.debug("Handling light level command {}", command);
361         // <mac;LIGHT;LEVEL;SET;0..16>
362         if (command instanceof PercentType) {
363             sendCommand(macAddress,
364                     ";LIGHT;LEVEL;SET;".concat(BigAssFanConverter.percentToLevel((PercentType) command)));
365         }
366     }
367
368     private void handleLightHue(Command command) {
369         if (!isLightPresent() || !isLightColor()) {
370             logger.debug("Fan does not have light installed or does not support hue for command {}", command);
371             return;
372         }
373
374         logger.debug("Handling light hue command {}", command);
375         // <mac;LIGHT;COLOR;TEMP;SET;2200..5000>
376         if (command instanceof PercentType) {
377             sendCommand(macAddress,
378                     ";LIGHT;COLOR;TEMP;VALUE;SET;".concat(BigAssFanConverter.percentToHue((PercentType) command)));
379         }
380     }
381
382     private void handleLightAuto(Command command) {
383         if (!isLightPresent()) {
384             logger.debug("Fan does not have light installed for command {}", command);
385             return;
386         }
387
388         logger.debug("Handling light auto command {}", command);
389         // <mac;LIGHT;AUTO;ON|OFF>
390         if (command instanceof OnOffType) {
391             if (command.equals(OnOffType.OFF)) {
392                 sendCommand(macAddress, ";LIGHT;AUTO;OFF");
393             } else if (command.equals(OnOffType.ON)) {
394                 sendCommand(macAddress, ";LIGHT;AUTO;ON");
395             }
396         }
397     }
398
399     private void handleLightSmarter(Command command) {
400         if (!isLightPresent()) {
401             logger.debug("Fan does not have light installed for command {}", command);
402             return;
403         }
404
405         logger.debug("Handling light smartmode command {}", command);
406         // <mac;LIGHT;SMART;ON/OFF>
407         if (command instanceof OnOffType) {
408             if (command.equals(OnOffType.OFF)) {
409                 sendCommand(macAddress, ";LIGHT;SMART;OFF");
410             } else if (command.equals(OnOffType.ON)) {
411                 sendCommand(macAddress, ";LIGHT;SMART;ON");
412             }
413         }
414     }
415
416     private void handleLightLevelMin(Command command) {
417         if (!isLightPresent()) {
418             logger.debug("Fan does not have light installed for command {}", command);
419             return;
420         }
421
422         logger.debug("Handling light level minimum command {}", command);
423         // <mac;LIGHT;LEVEL;MIN;0-16>
424         if (command instanceof PercentType) {
425             // Send min light level set command
426             sendCommand(macAddress,
427                     ";LIGHT;LEVEL;MIN;".concat(BigAssFanConverter.percentToLevel((PercentType) command)));
428             // Don't let max be less than min
429             adjustMaxLevel((PercentType) command);
430         }
431     }
432
433     private void handleLightLevelMax(Command command) {
434         if (!isLightPresent()) {
435             logger.debug("Fan does not have light installed for command {}", command);
436             return;
437         }
438
439         logger.debug("Handling light level maximum command {}", command);
440         // <mac;LIGHT;LEVEL;MAX;0-16>
441         if (command instanceof PercentType) {
442             // Send max light level set command
443             sendCommand(macAddress,
444                     ";LIGHT;LEVEL;MAX;".concat(BigAssFanConverter.percentToLevel((PercentType) command)));
445             // Don't let min be greater than max
446             adjustMinLevel((PercentType) command);
447         }
448     }
449
450     private void adjustMaxLevel(PercentType command) {
451         int newMin = command.intValue();
452         int currentMax = PercentType.ZERO.intValue();
453         if (fanStateMap.get(CHANNEL_LIGHT_LEVEL_MAX) != null) {
454             currentMax = ((PercentType) fanStateMap.get(CHANNEL_LIGHT_LEVEL_MAX)).intValue();
455         }
456         if (newMin > currentMax) {
457             updateState(CHANNEL_LIGHT_LEVEL_MAX, command);
458             sendCommand(macAddress, ";LIGHT;LEVEL;MAX;".concat(BigAssFanConverter.percentToLevel(command)));
459         }
460     }
461
462     private void adjustMinLevel(PercentType command) {
463         int newMax = command.intValue();
464         int currentMin = PercentType.HUNDRED.intValue();
465         if (fanStateMap.get(CHANNEL_LIGHT_LEVEL_MIN) != null) {
466             currentMin = ((PercentType) fanStateMap.get(CHANNEL_LIGHT_LEVEL_MIN)).intValue();
467         }
468         if (newMax < currentMin) {
469             updateState(CHANNEL_LIGHT_LEVEL_MIN, command);
470             sendCommand(macAddress, ";LIGHT;LEVEL;MIN;".concat(BigAssFanConverter.percentToLevel(command)));
471         }
472     }
473
474     private boolean isLightPresent() {
475         return fanStateMap.containsKey(CHANNEL_LIGHT_PRESENT)
476                 && LIGHT_PRESENT.equals(fanStateMap.get(CHANNEL_LIGHT_PRESENT));
477     }
478
479     private boolean isLightColor() {
480         return fanStateMap.containsKey(CHANNEL_LIGHT_COLOR) && LIGHT_COLOR.equals(fanStateMap.get(CHANNEL_LIGHT_COLOR));
481     }
482
483     /*
484      * Send a command to the fan
485      */
486     private void sendCommand(String mac, String commandFragment) {
487         if (fanListener == null) {
488             logger.error("Unable to send message to {} because fanListener object is null!", thing.getUID());
489             return;
490         }
491
492         StringBuilder sb = new StringBuilder();
493         sb.append("<").append(mac).append(commandFragment).append(">");
494         String message = sb.toString();
495         logger.trace("Sending message to {} at {}: {}", thing.getUID(), ipAddress, message);
496         fanListener.send(message);
497     }
498
499     private void updateChannel(String channelName, State state) {
500         Channel channel = thing.getChannel(channelName);
501         if (channel != null) {
502             updateState(channel.getUID(), state);
503         }
504     }
505
506     /*
507      * Manage the ONLINE/OFFLINE status of the thing
508      */
509     private void markOnline() {
510         if (!isOnline()) {
511             logger.debug("Changing status of {} from {}({}) to ONLINE", thing.getUID(), getStatus(), getDetail());
512             updateStatus(ThingStatus.ONLINE);
513         }
514     }
515
516     private void markOffline() {
517         if (isOnline()) {
518             logger.debug("Changing status of {} from {}({}) to OFFLINE", thing.getUID(), getStatus(), getDetail());
519             updateStatus(ThingStatus.OFFLINE);
520         }
521     }
522
523     private void markOfflineWithMessage(ThingStatusDetail statusDetail, String statusMessage) {
524         // If it's offline with no detail or if it's not offline, mark it offline with detailed status
525         if ((isOffline() && getDetail() == ThingStatusDetail.NONE) || !isOffline()) {
526             logger.debug("Changing status of {} from {}({}) to OFFLINE({})", thing.getUID(), getStatus(), getDetail(),
527                     statusDetail);
528             updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage);
529             return;
530         }
531     }
532
533     private boolean isOnline() {
534         return thing.getStatus().equals(ThingStatus.ONLINE);
535     }
536
537     private boolean isOffline() {
538         return thing.getStatus().equals(ThingStatus.OFFLINE);
539     }
540
541     private ThingStatus getStatus() {
542         return thing.getStatus();
543     }
544
545     private ThingStatusDetail getDetail() {
546         return thing.getStatusInfo().getStatusDetail();
547     }
548
549     /**
550      * The {@link FanListener} is responsible for sending and receiving messages to a fan.
551      *
552      * @author Mark Hilbush - Initial contribution
553      */
554     public class FanListener {
555         private final Logger logger = LoggerFactory.getLogger(FanListener.class);
556
557         // Our own thread pool for the long-running listener job
558         private ScheduledExecutorService scheduledExecutorService = ThreadPoolManager
559                 .getScheduledPool("bigassfanHandler" + "-" + thing.getUID());
560         private ScheduledFuture<?> listenerJob;
561
562         private final long FAN_LISTENER_DELAY = 2L;
563         private boolean terminate;
564
565         private final Pattern messagePattern = Pattern.compile("[(](.*)");
566
567         private ConnectionManager conn;
568
569         private Runnable fanListenerRunnable = () -> {
570             try {
571                 listener();
572             } catch (RuntimeException e) {
573                 logger.warn("FanListener for {} had unhandled exception: {}", thing.getUID(), e.getMessage(), e);
574             }
575         };
576
577         public FanListener(String ipv4Address) {
578             conn = new ConnectionManager(ipv4Address);
579         }
580
581         public void startFanListener() {
582             conn.connect();
583             conn.scheduleConnectionMonitorJob();
584
585             if (listenerJob == null) {
586                 terminate = false;
587                 logger.debug("Starting listener in {} sec for {} at {}", FAN_LISTENER_DELAY, thing.getUID(), ipAddress);
588                 listenerJob = scheduledExecutorService.schedule(fanListenerRunnable, FAN_LISTENER_DELAY,
589                         TimeUnit.SECONDS);
590             }
591         }
592
593         public void stopFanListener() {
594             if (listenerJob != null) {
595                 logger.debug("Stopping listener for {} at {}", thing.getUID(), ipAddress);
596                 terminate = true;
597                 listenerJob.cancel(true);
598                 listenerJob = null;
599             }
600             conn.cancelConnectionMonitorJob();
601             conn.disconnect();
602         }
603
604         public void send(String command) {
605             if (!conn.isConnected()) {
606                 logger.debug("Unable to send message; no connection to {}. Trying to reconnect: {}", thing.getUID(),
607                         command);
608                 conn.connect();
609                 if (!conn.isConnected()) {
610                     return;
611                 }
612             }
613
614             logger.debug("Sending message to {} at {}: {}", thing.getUID(), ipAddress, command);
615             byte[] buffer;
616             try {
617                 buffer = command.getBytes(CHARSET);
618             } catch (UnsupportedEncodingException e) {
619                 logger.warn("Unable to convert to string using {} charset: {}", CHARSET, e.getMessage(), e);
620                 return;
621             }
622             try {
623                 conn.write(buffer);
624             } catch (IOException e) {
625                 logger.warn("IO exception writing message to socket: {}", e.getMessage(), e);
626                 conn.disconnect();
627             }
628         }
629
630         private void listener() {
631             logger.debug("Fan listener thread is running for {} at {}", thing.getUID(), ipAddress);
632
633             while (!terminate) {
634                 try {
635                     // Wait for a message
636                     processMessage(waitForMessage());
637                 } catch (IOException ioe) {
638                     logger.warn("Listener for {} got IO exception waiting for message: {}", thing.getUID(),
639                             ioe.getMessage(), ioe);
640                     break;
641                 }
642             }
643             logger.debug("Fan listener thread is exiting for {} at {}", thing.getUID(), ipAddress);
644         }
645
646         private String waitForMessage() throws IOException {
647             if (!conn.isConnected()) {
648                 if (logger.isTraceEnabled()) {
649                     logger.trace("FanListener for {} can't receive message. No connection to fan", thing.getUID());
650                 }
651                 try {
652                     Thread.sleep(500);
653                 } catch (InterruptedException e) {
654                 }
655                 return null;
656             }
657             return readMessage();
658         }
659
660         private String readMessage() {
661             logger.trace("Waiting for message from {}  at {}", thing.getUID(), ipAddress);
662             String message = conn.read();
663             if (message != null) {
664                 logger.trace("FanListener for {} received message of length {}: {}", thing.getUID(), message.length(),
665                         message);
666             }
667             return message;
668         }
669
670         private void processMessage(String incomingMessage) {
671             if (StringUtils.isEmpty(incomingMessage)) {
672                 return;
673             }
674
675             // Match on (msg)
676             logger.debug("FanListener for {} received message from {}: {}", thing.getUID(), macAddress,
677                     incomingMessage);
678             Matcher matcher = messagePattern.matcher(incomingMessage);
679             if (!matcher.find()) {
680                 logger.debug("Unable to process message from {}, not in expected format: {}", thing.getUID(),
681                         incomingMessage);
682                 return;
683             }
684
685             String message = matcher.group(1);
686             String[] messageParts = message.split(";");
687
688             // Check to make sure it is my MAC address or my label
689             if (!isMe(messageParts[0])) {
690                 logger.trace("Message not for me ({}): {}", messageParts[0], macAddress);
691                 return;
692             }
693
694             logger.trace("Message is for me ({}): {}", messageParts[0], macAddress);
695             String messageUpperCase = message.toUpperCase();
696             if (messageUpperCase.contains(";FAN;PWR;")) {
697                 updateFanPower(messageParts);
698             } else if (messageUpperCase.contains(";FAN;SPD;ACTUAL;")) {
699                 updateFanSpeed(messageParts);
700             } else if (messageUpperCase.contains(";FAN;DIR;")) {
701                 updateFanDirection(messageParts);
702             } else if (messageUpperCase.contains(";FAN;AUTO;")) {
703                 updateFanAuto(messageParts);
704             } else if (messageUpperCase.contains(";FAN;WHOOSH;STATUS;")) {
705                 updateFanWhoosh(messageParts);
706             } else if (messageUpperCase.contains(";WINTERMODE;STATE;")) {
707                 updateFanWintermode(messageParts);
708             } else if (messageUpperCase.contains(";SMARTMODE;STATE;")) {
709                 updateFanSmartmode(messageParts);
710             } else if (messageUpperCase.contains(";FAN;SPD;MIN;")) {
711                 updateFanSpeedMin(messageParts);
712             } else if (messageUpperCase.contains(";FAN;SPD;MAX;")) {
713                 updateFanSpeedMax(messageParts);
714             } else if (messageUpperCase.contains(";SLEEP;STATE")) {
715                 updateFanSleepMode(messageParts);
716             } else if (messageUpperCase.contains(";LEARN;MINSPEED;")) {
717                 updateFanLearnMinSpeed(messageParts);
718             } else if (messageUpperCase.contains(";LEARN;MAXSPEED;")) {
719                 updateFanLearnMaxSpeed(messageParts);
720             } else if (messageUpperCase.contains(";LIGHT;PWR;")) {
721                 updateLightPower(messageParts);
722             } else if (messageUpperCase.contains(";LIGHT;LEVEL;ACTUAL;")) {
723                 updateLightLevel(messageParts);
724             } else if (messageUpperCase.contains(";LIGHT;COLOR;TEMP;VALUE;")) {
725                 updateLightHue(messageParts);
726             } else if (messageUpperCase.contains(";LIGHT;AUTO;")) {
727                 updateLightAuto(messageParts);
728             } else if (messageUpperCase.contains(";LIGHT;LEVEL;MIN;")) {
729                 updateLightLevelMin(messageParts);
730             } else if (messageUpperCase.contains(";LIGHT;LEVEL;MAX;")) {
731                 updateLightLevelMax(messageParts);
732             } else if (messageUpperCase.contains(";DEVICE;LIGHT;")) {
733                 updateLightPresent(messageParts);
734             } else if (messageUpperCase.contains(";SNSROCC;STATUS;")) {
735                 updateMotion(messageParts);
736             } else if (messageUpperCase.contains(";TIME;VALUE;")) {
737                 updateTime(messageParts);
738             } else {
739                 logger.trace("Received unsupported message from {}: {}", thing.getUID(), message);
740             }
741         }
742
743         private boolean isMe(String idFromDevice) {
744             // Check match on MAC address
745             if (StringUtils.equalsIgnoreCase(idFromDevice, macAddress)) {
746                 return true;
747             }
748             // Didn't match MAC address, check match for label
749             if (StringUtils.equalsIgnoreCase(idFromDevice, label)) {
750                 return true;
751             }
752             return false;
753         }
754
755         private void updateFanPower(String[] messageParts) {
756             if (messageParts.length != 4) {
757                 if (logger.isDebugEnabled()) {
758                     logger.debug("FAN;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
759                 }
760                 return;
761             }
762             logger.debug("Process fan power update for {}: {}", thing.getUID(), messageParts[3]);
763             OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
764             updateChannel(CHANNEL_FAN_POWER, state);
765             fanStateMap.put(CHANNEL_FAN_POWER, state);
766         }
767
768         private void updateFanSpeed(String[] messageParts) {
769             if (messageParts.length != 5) {
770                 logger.debug("FAN;SPD;ACTUAL has unexpected number of parameters: {}", Arrays.toString(messageParts));
771                 return;
772             }
773             logger.debug("Process fan speed update for {}: {}", thing.getUID(), messageParts[4]);
774             PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
775             updateChannel(CHANNEL_FAN_SPEED, state);
776             fanStateMap.put(CHANNEL_FAN_SPEED, state);
777         }
778
779         private void updateFanDirection(String[] messageParts) {
780             if (messageParts.length != 4) {
781                 logger.debug("FAN;DIR has unexpected number of parameters: {}", Arrays.toString(messageParts));
782                 return;
783             }
784             logger.debug("Process fan direction update for {}: {}", thing.getUID(), messageParts[3]);
785             StringType state = new StringType(messageParts[3]);
786             updateChannel(CHANNEL_FAN_DIRECTION, state);
787             fanStateMap.put(CHANNEL_FAN_DIRECTION, state);
788         }
789
790         private void updateFanAuto(String[] messageParts) {
791             if (messageParts.length != 4) {
792                 logger.debug("FAN;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
793                 return;
794             }
795             logger.debug("Process fan auto update for {}: {}", thing.getUID(), messageParts[3]);
796             OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
797             updateChannel(CHANNEL_FAN_AUTO, state);
798             fanStateMap.put(CHANNEL_FAN_AUTO, state);
799         }
800
801         private void updateFanWhoosh(String[] messageParts) {
802             if (messageParts.length != 5) {
803                 logger.debug("FAN;WHOOSH has unexpected number of parameters: {}", Arrays.toString(messageParts));
804                 return;
805             }
806             logger.debug("Process fan whoosh update for {}: {}", thing.getUID(), messageParts[4]);
807             OnOffType state = "ON".equalsIgnoreCase(messageParts[4]) ? OnOffType.ON : OnOffType.OFF;
808             updateChannel(CHANNEL_FAN_WHOOSH, state);
809             fanStateMap.put(CHANNEL_FAN_WHOOSH, state);
810         }
811
812         private void updateFanWintermode(String[] messageParts) {
813             if (messageParts.length != 4) {
814                 logger.debug("WINTERMODE;STATE has unexpected number of parameters: {}", Arrays.toString(messageParts));
815                 return;
816             }
817             logger.debug("Process fan wintermode update for {}: {}", thing.getUID(), messageParts[3]);
818             OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
819             updateChannel(CHANNEL_FAN_WINTERMODE, state);
820             fanStateMap.put(CHANNEL_FAN_WINTERMODE, state);
821         }
822
823         private void updateFanSmartmode(String[] messageParts) {
824             if (messageParts.length != 4) {
825                 logger.debug("Smartmode has unexpected number of parameters: {}", Arrays.toString(messageParts));
826                 return;
827             }
828             logger.debug("Process fan smartmode update for {}: {}", thing.getUID(), messageParts[3]);
829             StringType state = new StringType(messageParts[3]);
830             updateChannel(CHANNEL_FAN_SMARTMODE, state);
831             fanStateMap.put(CHANNEL_FAN_SMARTMODE, state);
832         }
833
834         private void updateFanSpeedMin(String[] messageParts) {
835             if (messageParts.length != 5) {
836                 logger.debug("FanSpeedMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
837                 return;
838             }
839             logger.debug("Process fan min speed update for {}: {}", thing.getUID(), messageParts[4]);
840             PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
841             updateChannel(CHANNEL_FAN_SPEED_MIN, state);
842             fanStateMap.put(CHANNEL_FAN_SPEED_MIN, state);
843         }
844
845         private void updateFanSpeedMax(String[] messageParts) {
846             if (messageParts.length != 5) {
847                 logger.debug("FanSpeedMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
848                 return;
849             }
850             logger.debug("Process fan speed max update for {}: {}", thing.getUID(), messageParts[4]);
851             PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
852             updateChannel(CHANNEL_FAN_SPEED_MAX, state);
853             fanStateMap.put(CHANNEL_FAN_SPEED_MAX, state);
854         }
855
856         private void updateFanSleepMode(String[] messageParts) {
857             if (messageParts.length != 4) {
858                 logger.debug("SLEEP;STATE; has unexpected number of parameters: {}", Arrays.toString(messageParts));
859                 return;
860             }
861             logger.debug("Process fan sleep mode for {}: {}", thing.getUID(), messageParts[3]);
862             OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
863             updateChannel(CHANNEL_FAN_SLEEP, state);
864             fanStateMap.put(CHANNEL_FAN_SLEEP, state);
865         }
866
867         private void updateFanLearnMinSpeed(String[] messageParts) {
868             if (messageParts.length != 4) {
869                 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
870                 return;
871             }
872             logger.debug("Process fan learn min speed update for {}: {}", thing.getUID(), messageParts[3]);
873             PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
874             updateChannel(CHANNEL_FAN_LEARN_MINSPEED, state);
875             fanStateMap.put(CHANNEL_FAN_LEARN_MINSPEED, state);
876         }
877
878         private void updateFanLearnMaxSpeed(String[] messageParts) {
879             if (messageParts.length != 4) {
880                 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
881                 return;
882             }
883             logger.debug("Process fan learn max speed update for {}: {}", thing.getUID(), messageParts[3]);
884             PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
885             updateChannel(CHANNEL_FAN_LEARN_MAXSPEED, state);
886             fanStateMap.put(CHANNEL_FAN_LEARN_MAXSPEED, state);
887         }
888
889         private void updateLightPower(String[] messageParts) {
890             if (messageParts.length != 4) {
891                 logger.debug("LIGHT;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
892                 return;
893             }
894             logger.debug("Process light power update for {}: {}", thing.getUID(), messageParts[3]);
895             OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
896             updateChannel(CHANNEL_LIGHT_POWER, state);
897             fanStateMap.put(CHANNEL_LIGHT_POWER, state);
898         }
899
900         private void updateLightLevel(String[] messageParts) {
901             if (messageParts.length != 5) {
902                 logger.debug("LIGHT;LEVEL has unexpected number of parameters: {}", Arrays.toString(messageParts));
903                 return;
904             }
905             logger.debug("Process light level update for {}: {}", thing.getUID(), messageParts[4]);
906             PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
907             updateChannel(CHANNEL_LIGHT_LEVEL, state);
908             fanStateMap.put(CHANNEL_LIGHT_LEVEL, state);
909         }
910
911         private void updateLightHue(String[] messageParts) {
912             if (messageParts.length != 6) {
913                 logger.debug("LIGHT;COLOR;TEMP;VALUE has unexpected number of parameters: {}",
914                         Arrays.toString(messageParts));
915                 return;
916             }
917             logger.debug("Process light hue update for {}: {}", thing.getUID(), messageParts[4]);
918             PercentType state = BigAssFanConverter.hueToPercent(messageParts[5]);
919             updateChannel(CHANNEL_LIGHT_HUE, state);
920             fanStateMap.put(CHANNEL_LIGHT_HUE, state);
921         }
922
923         private void updateLightAuto(String[] messageParts) {
924             if (messageParts.length != 4) {
925                 logger.debug("LIGHT;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
926                 return;
927             }
928             logger.debug("Process light auto update for {}: {}", thing.getUID(), messageParts[3]);
929             OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
930             updateChannel(CHANNEL_LIGHT_AUTO, state);
931             fanStateMap.put(CHANNEL_LIGHT_AUTO, state);
932         }
933
934         private void updateLightLevelMin(String[] messageParts) {
935             if (messageParts.length != 5) {
936                 logger.debug("LightLevelMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
937                 return;
938             }
939             logger.debug("Process light level min update for {}: {}", thing.getUID(), messageParts[4]);
940             PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
941             updateChannel(CHANNEL_LIGHT_LEVEL_MIN, state);
942             fanStateMap.put(CHANNEL_LIGHT_LEVEL_MIN, state);
943         }
944
945         private void updateLightLevelMax(String[] messageParts) {
946             if (messageParts.length != 5) {
947                 logger.debug("LightLevelMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
948                 return;
949             }
950             logger.debug("Process light level max update for {}: {}", thing.getUID(), messageParts[4]);
951             PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
952             updateChannel(CHANNEL_LIGHT_LEVEL_MAX, state);
953             fanStateMap.put(CHANNEL_LIGHT_LEVEL_MAX, state);
954         }
955
956         private void updateLightPresent(String[] messageParts) {
957             if (messageParts.length < 4) {
958                 logger.debug("LightPresent has unexpected number of parameters: {}", Arrays.toString(messageParts));
959                 return;
960             }
961             logger.debug("Process light present update for {}: {}", thing.getUID(), messageParts[3]);
962             StringType lightPresent = new StringType(messageParts[3]);
963             updateChannel(CHANNEL_LIGHT_PRESENT, lightPresent);
964             fanStateMap.put(CHANNEL_LIGHT_PRESENT, lightPresent);
965             if (messageParts.length == 5) {
966                 logger.debug("Light supports hue adjustment");
967                 StringType lightColor = new StringType(messageParts[4]);
968                 updateChannel(CHANNEL_LIGHT_COLOR, lightColor);
969                 fanStateMap.put(CHANNEL_LIGHT_COLOR, lightColor);
970             }
971         }
972
973         private void updateMotion(String[] messageParts) {
974             if (messageParts.length != 4) {
975                 logger.debug("SNSROCC has unexpected number of parameters: {}", Arrays.toString(messageParts));
976                 return;
977             }
978             logger.debug("Process motion sensor update for {}: {}", thing.getUID(), messageParts[3]);
979             OnOffType state = "OCCUPIED".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
980             updateChannel(CHANNEL_MOTION, state);
981             fanStateMap.put(CHANNEL_MOTION, state);
982         }
983
984         private void updateTime(String[] messageParts) {
985             if (messageParts.length != 4) {
986                 logger.debug("TIME has unexpected number of parameters: {}", Arrays.toString(messageParts));
987                 return;
988             }
989             logger.debug("Process time update for {}: {}", thing.getUID(), messageParts[3]);
990             // (mac|name;TIME;VALUE;2017-03-26T14:06:27Z)
991             try {
992                 Instant instant = Instant.parse(messageParts[3]);
993                 DateTimeType state = new DateTimeType(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()));
994                 updateChannel(CHANNEL_TIME, state);
995                 fanStateMap.put(CHANNEL_TIME, state);
996             } catch (DateTimeParseException e) {
997                 logger.info("Failed to parse date received from {}: {}", thing.getUID(), messageParts[3]);
998             }
999         }
1000     }
1001
1002     /*
1003      * The {@link ConnectionManager} class is responsible for managing the state of the TCP connection to the
1004      * fan.
1005      *
1006      * @author Mark Hilbush - Initial contribution
1007      */
1008     private class ConnectionManager {
1009         private Logger logger = LoggerFactory.getLogger(ConnectionManager.class);
1010
1011         private boolean deviceIsConnected;
1012
1013         private InetAddress ifAddress;
1014         private Socket fanSocket;
1015         private Scanner fanScanner;
1016         private DataOutputStream fanWriter;
1017         private final int SOCKET_CONNECT_TIMEOUT = 1500;
1018
1019         ScheduledFuture<?> connectionMonitorJob;
1020         private final long CONNECTION_MONITOR_FREQ = 120L;
1021         private final long CONNECTION_MONITOR_DELAY = 30L;
1022
1023         Runnable connectionMonitorRunnable = () -> {
1024             logger.trace("Performing connection check for {} at IP {}", thing.getUID(), ipAddress);
1025             checkConnection();
1026         };
1027
1028         public ConnectionManager(String ipv4Address) {
1029             deviceIsConnected = false;
1030             try {
1031                 ifAddress = InetAddress.getByName(ipv4Address);
1032                 logger.debug("Handler for {} using address {} on network interface {}", thing.getUID(),
1033                         ifAddress.getHostAddress(), NetworkInterface.getByInetAddress(ifAddress).getName());
1034             } catch (UnknownHostException e) {
1035                 logger.warn("Handler for {} got UnknownHostException getting local IPv4 net interface: {}",
1036                         thing.getUID(), e.getMessage(), e);
1037                 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1038             } catch (SocketException e) {
1039                 logger.warn("Handler for {} got SocketException getting local IPv4 network interface: {}",
1040                         thing.getUID(), e.getMessage(), e);
1041                 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1042             }
1043         }
1044
1045         /*
1046          * Connect to the command and serial port(s) on the device. The serial connections are established only for
1047          * devices that support serial.
1048          */
1049         protected synchronized void connect() {
1050             if (isConnected()) {
1051                 return;
1052             }
1053             logger.trace("Connecting to {} at {}", thing.getUID(), ipAddress);
1054
1055             // Open socket
1056             try {
1057                 fanSocket = new Socket();
1058                 fanSocket.bind(new InetSocketAddress(ifAddress, 0));
1059                 fanSocket.connect(new InetSocketAddress(ipAddress, BAF_PORT), SOCKET_CONNECT_TIMEOUT);
1060             } catch (IOException e) {
1061                 logger.debug("IOException connecting to  {} at {}: {}", thing.getUID(), ipAddress, e.getMessage());
1062                 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1063                 disconnect();
1064                 return;
1065             }
1066
1067             // Create streams
1068             try {
1069                 fanWriter = new DataOutputStream(fanSocket.getOutputStream());
1070                 fanScanner = new Scanner(fanSocket.getInputStream());
1071                 fanScanner.useDelimiter("[)]");
1072             } catch (IOException e) {
1073                 logger.warn("IOException getting streams for {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1074                         e);
1075                 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1076                 disconnect();
1077                 return;
1078             }
1079             logger.info("Connected to {} at {}", thing.getUID(), ipAddress);
1080             deviceIsConnected = true;
1081             markOnline();
1082         }
1083
1084         protected synchronized void disconnect() {
1085             if (!isConnected()) {
1086                 return;
1087             }
1088             logger.debug("Disconnecting from {} at {}", thing.getUID(), ipAddress);
1089
1090             try {
1091                 if (fanWriter != null) {
1092                     fanWriter.close();
1093                 }
1094                 if (fanScanner != null) {
1095                     fanScanner.close();
1096                 }
1097                 if (fanSocket != null) {
1098                     fanSocket.close();
1099                 }
1100             } catch (IOException e) {
1101                 logger.warn("IOException closing connection to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1102                         e);
1103             }
1104             deviceIsConnected = false;
1105             fanSocket = null;
1106             fanScanner = null;
1107             fanWriter = null;
1108             markOffline();
1109         }
1110
1111         public String read() {
1112             if (fanScanner == null) {
1113                 logger.warn("Scanner for {} is null when trying to scan from {}!", thing.getUID(), ipAddress);
1114                 return null;
1115             }
1116
1117             String nextToken;
1118             try {
1119                 nextToken = fanScanner.next();
1120             } catch (NoSuchElementException e) {
1121                 logger.debug("Scanner for {} threw NoSuchElementException; stream possibly closed", thing.getUID());
1122                 // Force a reconnect to the device
1123                 disconnect();
1124                 nextToken = null;
1125             } catch (IllegalStateException e) {
1126                 logger.debug("Scanner for {} threw IllegalStateException; scanner possibly closed", thing.getUID());
1127                 nextToken = null;
1128             } catch (BufferOverflowException e) {
1129                 logger.debug("Scanner for {} threw BufferOverflowException", thing.getUID());
1130                 nextToken = null;
1131             }
1132             return nextToken;
1133         }
1134
1135         public void write(byte[] buffer) throws IOException {
1136             if (fanWriter == null) {
1137                 logger.warn("fanWriter for {} is null when trying to write to {}!!!", thing.getUID(), ipAddress);
1138                 return;
1139             }
1140             fanWriter.write(buffer, 0, buffer.length);
1141         }
1142
1143         private boolean isConnected() {
1144             return deviceIsConnected;
1145         }
1146
1147         /*
1148          * Periodically validate the command connection to the device by executing a getversion command.
1149          */
1150         private void scheduleConnectionMonitorJob() {
1151             if (connectionMonitorJob == null) {
1152                 logger.debug("Starting connection monitor job in {} seconds for {} at {}", CONNECTION_MONITOR_DELAY,
1153                         thing.getUID(), ipAddress);
1154                 connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable,
1155                         CONNECTION_MONITOR_DELAY, CONNECTION_MONITOR_FREQ, TimeUnit.SECONDS);
1156             }
1157         }
1158
1159         private void cancelConnectionMonitorJob() {
1160             if (connectionMonitorJob != null) {
1161                 logger.debug("Canceling connection monitor job for {} at {}", thing.getUID(), ipAddress);
1162                 connectionMonitorJob.cancel(true);
1163                 connectionMonitorJob = null;
1164             }
1165         }
1166
1167         private void checkConnection() {
1168             logger.trace("Checking status of connection for {} at {}", thing.getUID(), ipAddress);
1169             if (!isConnected()) {
1170                 logger.debug("Connection check FAILED for {} at {}", thing.getUID(), ipAddress);
1171                 connect();
1172             } else {
1173                 logger.debug("Connection check OK for {} at {}", thing.getUID(), ipAddress);
1174                 logger.debug("Requesting status update from {} at {}", thing.getUID(), ipAddress);
1175                 sendCommand(macAddress, ";GETALL");
1176                 sendCommand(macAddress, ";SNSROCC;STATUS;GET");
1177             }
1178         }
1179     }
1180 }