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