2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bigassfan.internal.handler;
15 import static org.openhab.binding.bigassfan.internal.BigAssFanBindingConstants.*;
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;
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;
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;
66 * The {@link BigAssFanHandler} is responsible for handling commands, which are
67 * sent to one of the channels.
69 * @author Mark Hilbush - Initial contribution
72 public class BigAssFanHandler extends BaseThingHandler {
73 private final Logger logger = LoggerFactory.getLogger(BigAssFanHandler.class);
75 private static final StringType LIGHT_COLOR = new StringType("COLOR");
76 private static final StringType LIGHT_PRESENT = new StringType("PRESENT");
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");
82 private String label = "";
83 private String ipAddress = "";
84 private String macAddress = "";
86 private final FanListener fanListener;
88 protected final Map<String, State> fanStateMap = Collections.synchronizedMap(new HashMap<>());
90 public BigAssFanHandler(Thing thing, @Nullable String ipv4Address) {
94 logger.debug("Creating FanListener object for {}", thing.getUID());
95 fanListener = new FanListener(ipv4Address);
99 public void initialize() {
100 logger.debug("BigAssFanHandler for {} is initializing", thing.getUID());
102 BigAssFanConfig configuration = getConfig().as(BigAssFanConfig.class);
103 logger.debug("BigAssFanHandler config for {} is {}", thing.getUID(), configuration);
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.");
112 label = configuration.getLabel();
113 ipAddress = configuration.getIpAddress();
114 macAddress = configuration.getMacAddress();
116 fanListener.startFanListener();
120 public void dispose() {
121 logger.debug("BigAssFanHandler for {} is disposing", thing.getUID());
122 fanListener.stopFanListener();
126 public void handleCommand(ChannelUID channelUID, Command command) {
127 if (command instanceof RefreshType) {
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);
169 logger.debug("Received command for {} on unknown channel {}", thing.getUID(), channelUID.getId());
173 private void handleFanPower(Command command) {
174 logger.debug("Handling fan power command for {}: {}", thing.getUID(), command);
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");
186 private void handleFanSpeed(Command command) {
187 logger.debug("Handling fan speed command for {}: {}", thing.getUID(), command);
189 // <mac;FAN;SPD;SET;0..7>
190 if (command instanceof PercentType) {
191 sendCommand(macAddress, ";FAN;SPD;SET;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
195 private void handleFanAuto(Command command) {
196 logger.debug("Handling fan auto command {}", command);
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");
208 private void handleFanWhoosh(Command command) {
209 logger.debug("Handling fan whoosh command {}", command);
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");
221 private void handleFanSmartmode(Command command) {
222 logger.debug("Handling fan smartmode command {}", command);
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");
233 logger.debug("Unknown fan smartmode command: {}", command);
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;");
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;");
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;");
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;");
290 private void handleFanWintermode(Command command) {
291 logger.debug("Handling fan wintermode command {}", command);
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");
303 private void handleSleep(Command command) {
304 logger.debug("Handling fan sleep command {}", command);
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");
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();
323 if (newMin > currentMax) {
324 updateState(CHANNEL_FAN_SPEED_MAX, command);
325 sendCommand(macAddress, commandFragment.concat(BigAssFanConverter.percentToSpeed(command)));
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();
336 if (newMax < currentMin) {
337 updateState(channelId, command);
338 sendCommand(macAddress, commandFragment.concat(BigAssFanConverter.percentToSpeed(command)));
342 private void handleLightPower(Command command) {
343 if (!isLightPresent()) {
344 logger.debug("Fan does not have light installed for command {}", command);
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");
359 private void handleLightLevel(Command command) {
360 if (!isLightPresent()) {
361 logger.debug("Fan does not have light installed for command {}", command);
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)));
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);
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)));
387 private void handleLightAuto(Command command) {
388 if (!isLightPresent()) {
389 logger.debug("Fan does not have light installed for command {}", command);
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");
404 private void handleLightSmarter(Command command) {
405 if (!isLightPresent()) {
406 logger.debug("Fan does not have light installed for command {}", command);
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");
421 private void handleLightLevelMin(Command command) {
422 if (!isLightPresent()) {
423 logger.debug("Fan does not have light installed for command {}", command);
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);
438 private void handleLightLevelMax(Command command) {
439 if (!isLightPresent()) {
440 logger.debug("Fan does not have light installed for command {}", command);
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);
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();
462 if (newMin > currentMax) {
463 updateState(CHANNEL_LIGHT_LEVEL_MAX, command);
464 sendCommand(macAddress, ";LIGHT;LEVEL;MAX;".concat(BigAssFanConverter.percentToLevel(command)));
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();
475 if (newMax < currentMin) {
476 updateState(CHANNEL_LIGHT_LEVEL_MIN, command);
477 sendCommand(macAddress, ";LIGHT;LEVEL;MIN;".concat(BigAssFanConverter.percentToLevel(command)));
481 private boolean isLightPresent() {
482 return fanStateMap.containsKey(CHANNEL_LIGHT_PRESENT)
483 && LIGHT_PRESENT.equals(fanStateMap.get(CHANNEL_LIGHT_PRESENT));
486 private boolean isLightColor() {
487 return fanStateMap.containsKey(CHANNEL_LIGHT_COLOR) && LIGHT_COLOR.equals(fanStateMap.get(CHANNEL_LIGHT_COLOR));
491 * Send a command to the fan
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);
501 private void updateChannel(String channelName, State state) {
502 Channel channel = thing.getChannel(channelName);
503 if (channel != null) {
504 updateState(channel.getUID(), state);
509 * Manage the ONLINE/OFFLINE status of the thing
511 private void markOnline() {
513 logger.debug("Changing status of {} from {}({}) to ONLINE", thing.getUID(), getStatus(), getDetail());
514 updateStatus(ThingStatus.ONLINE);
518 private void markOffline() {
520 logger.debug("Changing status of {} from {}({}) to OFFLINE", thing.getUID(), getStatus(), getDetail());
521 updateStatus(ThingStatus.OFFLINE);
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(),
530 updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage);
535 private boolean isOnline() {
536 return thing.getStatus().equals(ThingStatus.ONLINE);
539 private boolean isOffline() {
540 return thing.getStatus().equals(ThingStatus.OFFLINE);
543 private ThingStatus getStatus() {
544 return thing.getStatus();
547 private ThingStatusDetail getDetail() {
548 return thing.getStatusInfo().getStatusDetail();
552 * The {@link FanListener} is responsible for sending and receiving messages to a fan.
554 * @author Mark Hilbush - Initial contribution
556 public class FanListener {
557 private final Logger logger = LoggerFactory.getLogger(FanListener.class);
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;
564 private static final long FAN_LISTENER_DELAY = 2L;
565 private boolean terminate;
567 private final Pattern messagePattern = Pattern.compile("[(](.*)");
569 private ConnectionManager conn;
571 private Runnable fanListenerRunnable = () -> {
574 } catch (RuntimeException e) {
575 logger.warn("FanListener for {} had unhandled exception: {}", thing.getUID(), e.getMessage(), e);
579 public FanListener(@Nullable String ipv4Address) {
580 conn = new ConnectionManager(ipv4Address);
583 public void startFanListener() {
585 conn.scheduleConnectionMonitorJob();
587 if (listenerJob == null) {
589 logger.debug("Starting listener in {} sec for {} at {}", FAN_LISTENER_DELAY, thing.getUID(), ipAddress);
590 listenerJob = scheduledExecutorService.schedule(fanListenerRunnable, FAN_LISTENER_DELAY,
595 public void stopFanListener() {
596 ScheduledFuture<?> localListenerJob = listenerJob;
597 if (localListenerJob != null) {
598 logger.debug("Stopping listener for {} at {}", thing.getUID(), ipAddress);
600 localListenerJob.cancel(true);
601 this.listenerJob = null;
604 conn.cancelConnectionMonitorJob();
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(),
613 if (!conn.isConnected()) {
618 logger.debug("Sending message to {} at {}: {}", thing.getUID(), ipAddress, command);
619 byte[] buffer = command.getBytes(StandardCharsets.US_ASCII);
622 } catch (IOException e) {
623 logger.warn("IO exception writing message to socket: {}", e.getMessage(), e);
628 private void listener() {
629 logger.debug("Fan listener thread is running for {} at {}", thing.getUID(), ipAddress);
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);
641 logger.debug("Fan listener thread is exiting for {} at {}", thing.getUID(), ipAddress);
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());
651 } catch (InterruptedException e) {
655 return readMessage();
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(),
668 private void processMessage(@Nullable String incomingMessage) {
669 if (incomingMessage == null || incomingMessage.isEmpty()) {
674 logger.debug("FanListener for {} received message from {}: {}", thing.getUID(), macAddress,
676 Matcher matcher = messagePattern.matcher(incomingMessage);
677 if (!matcher.find()) {
678 logger.debug("Unable to process message from {}, not in expected format: {}", thing.getUID(),
683 String message = matcher.group(1);
684 String[] messageParts = message.split(";");
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);
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);
737 logger.trace("Received unsupported message from {}: {}", thing.getUID(), message);
741 private boolean isMe(String idFromDevice) {
742 // Check match on MAC address
743 if (macAddress.equalsIgnoreCase(idFromDevice)) {
746 // Didn't match MAC address, check match for label
747 return label.equalsIgnoreCase(idFromDevice);
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));
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);
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));
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);
774 private void updateFanDirection(String[] messageParts) {
775 if (messageParts.length != 4) {
776 logger.debug("FAN;DIR has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
785 private void updateFanAuto(String[] messageParts) {
786 if (messageParts.length != 4) {
787 logger.debug("FAN;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
796 private void updateFanWhoosh(String[] messageParts) {
797 if (messageParts.length != 5) {
798 logger.debug("FAN;WHOOSH has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
807 private void updateFanWintermode(String[] messageParts) {
808 if (messageParts.length != 4) {
809 logger.debug("WINTERMODE;STATE has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
818 private void updateFanSmartmode(String[] messageParts) {
819 if (messageParts.length != 4) {
820 logger.debug("Smartmode has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
829 private void updateFanSpeedMin(String[] messageParts) {
830 if (messageParts.length != 5) {
831 logger.debug("FanSpeedMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
840 private void updateFanSpeedMax(String[] messageParts) {
841 if (messageParts.length != 5) {
842 logger.debug("FanSpeedMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
851 private void updateFanSleepMode(String[] messageParts) {
852 if (messageParts.length != 4) {
853 logger.debug("SLEEP;STATE; has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
862 private void updateFanLearnMinSpeed(String[] messageParts) {
863 if (messageParts.length != 4) {
864 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
873 private void updateFanLearnMaxSpeed(String[] messageParts) {
874 if (messageParts.length != 4) {
875 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
884 private void updateLightPower(String[] messageParts) {
885 if (messageParts.length != 4) {
886 logger.debug("LIGHT;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
895 private void updateLightLevel(String[] messageParts) {
896 if (messageParts.length != 5) {
897 logger.debug("LIGHT;LEVEL has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
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));
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);
918 private void updateLightAuto(String[] messageParts) {
919 if (messageParts.length != 4) {
920 logger.debug("LIGHT;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
929 private void updateLightLevelMin(String[] messageParts) {
930 if (messageParts.length != 5) {
931 logger.debug("LightLevelMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
940 private void updateLightLevelMax(String[] messageParts) {
941 if (messageParts.length != 5) {
942 logger.debug("LightLevelMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
951 private void updateLightPresent(String[] messageParts) {
952 if (messageParts.length < 4) {
953 logger.debug("LightPresent has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
968 private void updateMotion(String[] messageParts) {
969 if (messageParts.length != 4) {
970 logger.debug("SNSROCC has unexpected number of parameters: {}", Arrays.toString(messageParts));
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);
979 private void updateTime(String[] messageParts) {
980 if (messageParts.length != 4) {
981 logger.debug("TIME has unexpected number of parameters: {}", Arrays.toString(messageParts));
984 logger.debug("Process time update for {}: {}", thing.getUID(), messageParts[3]);
985 // (mac|name;TIME;VALUE;2017-03-26T14:06:27Z)
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]);
998 * The {@link ConnectionManager} class is responsible for managing the state of the TCP connection to the
1001 * @author Mark Hilbush - Initial contribution
1003 private class ConnectionManager {
1004 private Logger logger = LoggerFactory.getLogger(ConnectionManager.class);
1006 private boolean deviceIsConnected;
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;
1014 private @Nullable ScheduledFuture<?> connectionMonitorJob;
1015 private static final long CONNECTION_MONITOR_FREQ = 120L;
1016 private static final long CONNECTION_MONITOR_DELAY = 30L;
1018 Runnable connectionMonitorRunnable = () -> {
1019 logger.trace("Performing connection check for {} at IP {}", thing.getUID(), ipAddress);
1023 public ConnectionManager(@Nullable String ipv4Address) {
1024 deviceIsConnected = false;
1026 ifAddress = InetAddress.getByName(ipv4Address);
1028 logger.debug("Handler for {} using address {} on network interface {}", thing.getUID(), ipv4Address,
1029 NetworkInterface.getByInetAddress(ifAddress).getName());
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");
1042 * Connect to the command and serial port(s) on the device. The serial connections are established only for
1043 * devices that support serial.
1045 protected synchronized void connect() {
1046 if (isConnected()) {
1049 logger.trace("Connecting to {} at {}", thing.getUID(), ipAddress);
1051 Socket localFanSocket = new Socket();
1052 fanSocket = localFanSocket;
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,
1060 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
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());
1077 logger.info("Connected to {} at {}", thing.getUID(), ipAddress);
1078 deviceIsConnected = true;
1082 protected synchronized void disconnect() {
1083 if (!isConnected()) {
1086 logger.debug("Disconnecting from {} at {}", thing.getUID(), ipAddress);
1089 DataOutputStream localFanWriter = fanWriter;
1090 if (localFanWriter != null) {
1091 localFanWriter.close();
1094 Scanner localFanScanner = fanScanner;
1095 if (localFanScanner != null) {
1096 localFanScanner.close();
1098 Socket localFanSocket = fanSocket;
1099 if (localFanSocket != null) {
1100 localFanSocket.close();
1103 } catch (IllegalStateException | IOException e) {
1104 logger.warn("Exception closing connection to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1107 deviceIsConnected = false;
1114 public @Nullable String read() {
1115 if (fanScanner == null) {
1116 logger.warn("Scanner for {} is null when trying to scan from {}!", thing.getUID(), ipAddress);
1120 String nextToken = null;
1122 Scanner localFanScanner = fanScanner;
1123 if (localFanScanner != null) {
1124 nextToken = localFanScanner.next();
1126 } catch (NoSuchElementException e) {
1127 logger.debug("Scanner for {} threw NoSuchElementException; stream possibly closed", thing.getUID());
1128 // Force a reconnect to the device
1131 } catch (IllegalStateException e) {
1132 logger.debug("Scanner for {} threw IllegalStateException; scanner possibly closed", thing.getUID());
1134 } catch (BufferOverflowException e) {
1135 logger.debug("Scanner for {} threw BufferOverflowException", thing.getUID());
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);
1147 localFanWriter.write(buffer, 0, buffer.length);
1151 private boolean isConnected() {
1152 return deviceIsConnected;
1156 * Periodically validate the command connection to the device by executing a getversion command.
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);
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;
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);
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");