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