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