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 percentCommand) {
191 sendCommand(macAddress, ";FAN;SPD;SET;".concat(BigAssFanConverter.percentToSpeed(percentCommand)));
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 percentCommand) {
242 // Send min speed set command
243 sendCommand(macAddress, ";LEARN;MINSPEED;SET;".concat(BigAssFanConverter.percentToSpeed(percentCommand)));
244 fanStateMap.put(CHANNEL_FAN_LEARN_MINSPEED, percentCommand);
245 // Don't let max be less than min
246 adjustMaxSpeed(percentCommand, CHANNEL_FAN_LEARN_MAXSPEED, ";LEARN;MAXSPEED;");
250 private void handleFanLearnSpeedMax(Command command) {
251 logger.debug("Handling fan learn speed maximum command {}", command);
252 // <mac;FAN;SPD;SET;MAX;0..7>
253 if (command instanceof PercentType percentCommand) {
254 // Send max speed set command
255 sendCommand(macAddress, ";LEARN;MAXSPEED;SET;;".concat(BigAssFanConverter.percentToSpeed(percentCommand)));
256 fanStateMap.put(CHANNEL_FAN_LEARN_MAXSPEED, percentCommand);
257 // Don't let min be greater than max
258 adjustMinSpeed(percentCommand, CHANNEL_FAN_LEARN_MINSPEED, ";LEARN;MINSPEED;");
262 private void handleFanSpeedMin(Command command) {
263 logger.debug("Handling fan speed minimum command {}", command);
264 // <mac;FAN;SPD;SET;MIN;0..7>
265 if (command instanceof PercentType percentCommand) {
266 // Send min speed set command
267 sendCommand(macAddress, ";FAN;SPD;SET;MIN;".concat(BigAssFanConverter.percentToSpeed(percentCommand)));
268 fanStateMap.put(CHANNEL_FAN_SPEED_MIN, percentCommand);
269 // Don't let max be less than min
270 adjustMaxSpeed(percentCommand, CHANNEL_FAN_SPEED_MAX, ";FAN;SPD;SET;MAX;");
274 private void handleFanSpeedMax(Command command) {
275 logger.debug("Handling fan speed maximum command {}", command);
276 // <mac;FAN;SPD;SET;MAX;0..7>
277 if (command instanceof PercentType percentCommand) {
278 // Send max speed set command
279 sendCommand(macAddress, ";FAN;SPD;SET;MAX;".concat(BigAssFanConverter.percentToSpeed(percentCommand)));
280 fanStateMap.put(CHANNEL_FAN_SPEED_MAX, percentCommand);
281 // Don't let min be greater than max
282 adjustMinSpeed(percentCommand, CHANNEL_FAN_SPEED_MIN, ";FAN;SPD;SET;MIN;");
286 private void handleFanWintermode(Command command) {
287 logger.debug("Handling fan wintermode command {}", command);
289 // <mac;FAN;WINTERMODE;ON|OFF>
290 if (command instanceof OnOffType) {
291 if (command.equals(OnOffType.OFF)) {
292 sendCommand(macAddress, ";FAN;WINTERMODE;OFF");
293 } else if (command.equals(OnOffType.ON)) {
294 sendCommand(macAddress, ";FAN;WINTERMODE;ON");
299 private void handleSleep(Command command) {
300 logger.debug("Handling fan sleep command {}", command);
302 // <mac;SLEEP;STATE;ON|OFF>
303 if (command instanceof OnOffType) {
304 if (command.equals(OnOffType.OFF)) {
305 sendCommand(macAddress, ";SLEEP;STATE;OFF");
306 } else if (command.equals(OnOffType.ON)) {
307 sendCommand(macAddress, ";SLEEP;STATE;ON");
312 private void adjustMaxSpeed(PercentType command, String channelId, String commandFragment) {
313 int newMin = command.intValue();
314 int currentMax = PercentType.ZERO.intValue();
315 State fanState = fanStateMap.get(channelId);
316 if (fanState != null) {
317 currentMax = ((PercentType) fanState).intValue();
319 if (newMin > currentMax) {
320 updateState(CHANNEL_FAN_SPEED_MAX, command);
321 sendCommand(macAddress, commandFragment.concat(BigAssFanConverter.percentToSpeed(command)));
325 private void adjustMinSpeed(PercentType command, String channelId, String commandFragment) {
326 int newMax = command.intValue();
327 int currentMin = PercentType.HUNDRED.intValue();
328 State fanSate = fanStateMap.get(channelId);
329 if (fanSate != null) {
330 currentMin = ((PercentType) fanSate).intValue();
332 if (newMax < currentMin) {
333 updateState(channelId, command);
334 sendCommand(macAddress, commandFragment.concat(BigAssFanConverter.percentToSpeed(command)));
338 private void handleLightPower(Command command) {
339 if (!isLightPresent()) {
340 logger.debug("Fan does not have light installed for command {}", command);
344 logger.debug("Handling light power command {}", command);
345 // <mac;LIGHT;PWR;ON|OFF>
346 if (command instanceof OnOffType) {
347 if (command.equals(OnOffType.OFF)) {
348 sendCommand(macAddress, ";LIGHT;PWR;OFF");
349 } else if (command.equals(OnOffType.ON)) {
350 sendCommand(macAddress, ";LIGHT;PWR;ON");
355 private void handleLightLevel(Command command) {
356 if (!isLightPresent()) {
357 logger.debug("Fan does not have light installed for command {}", command);
361 logger.debug("Handling light level command {}", command);
362 // <mac;LIGHT;LEVEL;SET;0..16>
363 if (command instanceof PercentType percentCommand) {
364 sendCommand(macAddress, ";LIGHT;LEVEL;SET;".concat(BigAssFanConverter.percentToLevel(percentCommand)));
368 private void handleLightHue(Command command) {
369 if (!isLightPresent() || !isLightColor()) {
370 logger.debug("Fan does not have light installed or does not support hue for command {}", command);
374 logger.debug("Handling light hue command {}", command);
375 // <mac;LIGHT;COLOR;TEMP;SET;2200..5000>
376 if (command instanceof PercentType percentCommand) {
377 sendCommand(macAddress,
378 ";LIGHT;COLOR;TEMP;VALUE;SET;".concat(BigAssFanConverter.percentToHue(percentCommand)));
382 private void handleLightAuto(Command command) {
383 if (!isLightPresent()) {
384 logger.debug("Fan does not have light installed for command {}", command);
388 logger.debug("Handling light auto command {}", command);
389 // <mac;LIGHT;AUTO;ON|OFF>
390 if (command instanceof OnOffType) {
391 if (command.equals(OnOffType.OFF)) {
392 sendCommand(macAddress, ";LIGHT;AUTO;OFF");
393 } else if (command.equals(OnOffType.ON)) {
394 sendCommand(macAddress, ";LIGHT;AUTO;ON");
399 private void handleLightSmarter(Command command) {
400 if (!isLightPresent()) {
401 logger.debug("Fan does not have light installed for command {}", command);
405 logger.debug("Handling light smartmode command {}", command);
406 // <mac;LIGHT;SMART;ON/OFF>
407 if (command instanceof OnOffType) {
408 if (command.equals(OnOffType.OFF)) {
409 sendCommand(macAddress, ";LIGHT;SMART;OFF");
410 } else if (command.equals(OnOffType.ON)) {
411 sendCommand(macAddress, ";LIGHT;SMART;ON");
416 private void handleLightLevelMin(Command command) {
417 if (!isLightPresent()) {
418 logger.debug("Fan does not have light installed for command {}", command);
422 logger.debug("Handling light level minimum command {}", command);
423 // <mac;LIGHT;LEVEL;MIN;0-16>
424 if (command instanceof PercentType percentCommand) {
425 // Send min light level set command
426 sendCommand(macAddress, ";LIGHT;LEVEL;MIN;".concat(BigAssFanConverter.percentToLevel(percentCommand)));
427 // Don't let max be less than min
428 adjustMaxLevel(percentCommand);
432 private void handleLightLevelMax(Command command) {
433 if (!isLightPresent()) {
434 logger.debug("Fan does not have light installed for command {}", command);
438 logger.debug("Handling light level maximum command {}", command);
439 // <mac;LIGHT;LEVEL;MAX;0-16>
440 if (command instanceof PercentType percentCommand) {
441 // Send max light level set command
442 sendCommand(macAddress, ";LIGHT;LEVEL;MAX;".concat(BigAssFanConverter.percentToLevel(percentCommand)));
443 // Don't let min be greater than max
444 adjustMinLevel(percentCommand);
448 private void adjustMaxLevel(PercentType command) {
449 int newMin = command.intValue();
450 int currentMax = PercentType.ZERO.intValue();
451 State fanState = fanStateMap.get(CHANNEL_LIGHT_LEVEL_MAX);
452 if (fanState != null) {
453 currentMax = ((PercentType) fanState).intValue();
455 if (newMin > currentMax) {
456 updateState(CHANNEL_LIGHT_LEVEL_MAX, command);
457 sendCommand(macAddress, ";LIGHT;LEVEL;MAX;".concat(BigAssFanConverter.percentToLevel(command)));
461 private void adjustMinLevel(PercentType command) {
462 int newMax = command.intValue();
463 int currentMin = PercentType.HUNDRED.intValue();
464 State fanState = fanStateMap.get(CHANNEL_LIGHT_LEVEL_MIN);
465 if (fanState != null) {
466 currentMin = ((PercentType) fanState).intValue();
468 if (newMax < currentMin) {
469 updateState(CHANNEL_LIGHT_LEVEL_MIN, command);
470 sendCommand(macAddress, ";LIGHT;LEVEL;MIN;".concat(BigAssFanConverter.percentToLevel(command)));
474 private boolean isLightPresent() {
475 return fanStateMap.containsKey(CHANNEL_LIGHT_PRESENT)
476 && LIGHT_PRESENT.equals(fanStateMap.get(CHANNEL_LIGHT_PRESENT));
479 private boolean isLightColor() {
480 return fanStateMap.containsKey(CHANNEL_LIGHT_COLOR) && LIGHT_COLOR.equals(fanStateMap.get(CHANNEL_LIGHT_COLOR));
484 * Send a command to the fan
486 private void sendCommand(String mac, String commandFragment) {
487 StringBuilder sb = new StringBuilder();
488 sb.append("<").append(mac).append(commandFragment).append(">");
489 String message = sb.toString();
490 logger.trace("Sending message to {} at {}: {}", thing.getUID(), ipAddress, message);
491 fanListener.send(message);
494 private void updateChannel(String channelName, State state) {
495 Channel channel = thing.getChannel(channelName);
496 if (channel != null) {
497 updateState(channel.getUID(), state);
502 * Manage the ONLINE/OFFLINE status of the thing
504 private void markOnline() {
506 logger.debug("Changing status of {} from {}({}) to ONLINE", thing.getUID(), getStatus(), getDetail());
507 updateStatus(ThingStatus.ONLINE);
511 private void markOffline() {
513 logger.debug("Changing status of {} from {}({}) to OFFLINE", thing.getUID(), getStatus(), getDetail());
514 updateStatus(ThingStatus.OFFLINE);
518 private void markOfflineWithMessage(ThingStatusDetail statusDetail, @Nullable String statusMessage) {
519 // If it's offline with no detail or if it's not offline, mark it offline with detailed status
520 if ((isOffline() && getDetail() == ThingStatusDetail.NONE) || !isOffline()) {
521 logger.debug("Changing status of {} from {}({}) to OFFLINE({})", thing.getUID(), getStatus(), getDetail(),
523 updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage);
528 private boolean isOnline() {
529 return thing.getStatus().equals(ThingStatus.ONLINE);
532 private boolean isOffline() {
533 return thing.getStatus().equals(ThingStatus.OFFLINE);
536 private ThingStatus getStatus() {
537 return thing.getStatus();
540 private ThingStatusDetail getDetail() {
541 return thing.getStatusInfo().getStatusDetail();
545 * The {@link FanListener} is responsible for sending and receiving messages to a fan.
547 * @author Mark Hilbush - Initial contribution
549 public class FanListener {
550 private final Logger logger = LoggerFactory.getLogger(FanListener.class);
552 // Our own thread pool for the long-running listener job
553 private ScheduledExecutorService scheduledExecutorService = ThreadPoolManager
554 .getScheduledPool("bigassfanHandler" + "-" + thing.getUID());
555 private @Nullable ScheduledFuture<?> listenerJob;
557 private static final long FAN_LISTENER_DELAY = 2L;
558 private boolean terminate;
560 private final Pattern messagePattern = Pattern.compile("[(](.*)");
562 private ConnectionManager conn;
564 private Runnable fanListenerRunnable = () -> {
567 } catch (RuntimeException e) {
568 logger.warn("FanListener for {} had unhandled exception: {}", thing.getUID(), e.getMessage(), e);
572 public FanListener(@Nullable String ipv4Address) {
573 conn = new ConnectionManager(ipv4Address);
576 public void startFanListener() {
578 conn.scheduleConnectionMonitorJob();
580 if (listenerJob == null) {
582 logger.debug("Starting listener in {} sec for {} at {}", FAN_LISTENER_DELAY, thing.getUID(), ipAddress);
583 listenerJob = scheduledExecutorService.schedule(fanListenerRunnable, FAN_LISTENER_DELAY,
588 public void stopFanListener() {
589 ScheduledFuture<?> localListenerJob = listenerJob;
590 if (localListenerJob != null) {
591 logger.debug("Stopping listener for {} at {}", thing.getUID(), ipAddress);
593 localListenerJob.cancel(true);
594 this.listenerJob = null;
597 conn.cancelConnectionMonitorJob();
601 public void send(String command) {
602 if (!conn.isConnected()) {
603 logger.debug("Unable to send message; no connection to {}. Trying to reconnect: {}", thing.getUID(),
606 if (!conn.isConnected()) {
611 logger.debug("Sending message to {} at {}: {}", thing.getUID(), ipAddress, command);
612 byte[] buffer = command.getBytes(StandardCharsets.US_ASCII);
615 } catch (IOException e) {
616 logger.warn("IO exception writing message to socket: {}", e.getMessage(), e);
621 private void listener() {
622 logger.debug("Fan listener thread is running for {} at {}", thing.getUID(), ipAddress);
626 // Wait for a message
627 processMessage(waitForMessage());
628 } catch (IOException ioe) {
629 logger.warn("Listener for {} got IO exception waiting for message: {}", thing.getUID(),
630 ioe.getMessage(), ioe);
634 logger.debug("Fan listener thread is exiting for {} at {}", thing.getUID(), ipAddress);
637 private @Nullable String waitForMessage() throws IOException {
638 if (!conn.isConnected()) {
639 if (logger.isTraceEnabled()) {
640 logger.trace("FanListener for {} can't receive message. No connection to fan", thing.getUID());
644 } catch (InterruptedException e) {
648 return readMessage();
651 private @Nullable String readMessage() {
652 logger.trace("Waiting for message from {} at {}", thing.getUID(), ipAddress);
653 String message = conn.read();
654 if (message != null) {
655 logger.trace("FanListener for {} received message of length {}: {}", thing.getUID(), message.length(),
661 private void processMessage(@Nullable String incomingMessage) {
662 if (incomingMessage == null || incomingMessage.isEmpty()) {
667 logger.debug("FanListener for {} received message from {}: {}", thing.getUID(), macAddress,
669 Matcher matcher = messagePattern.matcher(incomingMessage);
670 if (!matcher.find()) {
671 logger.debug("Unable to process message from {}, not in expected format: {}", thing.getUID(),
676 String message = matcher.group(1);
677 String[] messageParts = message.split(";");
679 // Check to make sure it is my MAC address or my label
680 if (!isMe(messageParts[0])) {
681 logger.trace("Message not for me ({}): {}", messageParts[0], macAddress);
685 logger.trace("Message is for me ({}): {}", messageParts[0], macAddress);
686 String messageUpperCase = message.toUpperCase();
687 if (messageUpperCase.contains(";FAN;PWR;")) {
688 updateFanPower(messageParts);
689 } else if (messageUpperCase.contains(";FAN;SPD;ACTUAL;")) {
690 updateFanSpeed(messageParts);
691 } else if (messageUpperCase.contains(";FAN;DIR;")) {
692 updateFanDirection(messageParts);
693 } else if (messageUpperCase.contains(";FAN;AUTO;")) {
694 updateFanAuto(messageParts);
695 } else if (messageUpperCase.contains(";FAN;WHOOSH;STATUS;")) {
696 updateFanWhoosh(messageParts);
697 } else if (messageUpperCase.contains(";WINTERMODE;STATE;")) {
698 updateFanWintermode(messageParts);
699 } else if (messageUpperCase.contains(";SMARTMODE;STATE;")) {
700 updateFanSmartmode(messageParts);
701 } else if (messageUpperCase.contains(";FAN;SPD;MIN;")) {
702 updateFanSpeedMin(messageParts);
703 } else if (messageUpperCase.contains(";FAN;SPD;MAX;")) {
704 updateFanSpeedMax(messageParts);
705 } else if (messageUpperCase.contains(";SLEEP;STATE")) {
706 updateFanSleepMode(messageParts);
707 } else if (messageUpperCase.contains(";LEARN;MINSPEED;")) {
708 updateFanLearnMinSpeed(messageParts);
709 } else if (messageUpperCase.contains(";LEARN;MAXSPEED;")) {
710 updateFanLearnMaxSpeed(messageParts);
711 } else if (messageUpperCase.contains(";LIGHT;PWR;")) {
712 updateLightPower(messageParts);
713 } else if (messageUpperCase.contains(";LIGHT;LEVEL;ACTUAL;")) {
714 updateLightLevel(messageParts);
715 } else if (messageUpperCase.contains(";LIGHT;COLOR;TEMP;VALUE;")) {
716 updateLightHue(messageParts);
717 } else if (messageUpperCase.contains(";LIGHT;AUTO;")) {
718 updateLightAuto(messageParts);
719 } else if (messageUpperCase.contains(";LIGHT;LEVEL;MIN;")) {
720 updateLightLevelMin(messageParts);
721 } else if (messageUpperCase.contains(";LIGHT;LEVEL;MAX;")) {
722 updateLightLevelMax(messageParts);
723 } else if (messageUpperCase.contains(";DEVICE;LIGHT;")) {
724 updateLightPresent(messageParts);
725 } else if (messageUpperCase.contains(";SNSROCC;STATUS;")) {
726 updateMotion(messageParts);
727 } else if (messageUpperCase.contains(";TIME;VALUE;")) {
728 updateTime(messageParts);
730 logger.trace("Received unsupported message from {}: {}", thing.getUID(), message);
734 private boolean isMe(String idFromDevice) {
735 // Check match on MAC address
736 if (macAddress.equalsIgnoreCase(idFromDevice)) {
739 // Didn't match MAC address, check match for label
740 return label.equalsIgnoreCase(idFromDevice);
743 private void updateFanPower(String[] messageParts) {
744 if (messageParts.length != 4) {
745 if (logger.isDebugEnabled()) {
746 logger.debug("FAN;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
750 logger.debug("Process fan power update for {}: {}", thing.getUID(), messageParts[3]);
751 OnOffType state = OnOffType.from("ON".equalsIgnoreCase(messageParts[3]));
752 updateChannel(CHANNEL_FAN_POWER, state);
753 fanStateMap.put(CHANNEL_FAN_POWER, state);
756 private void updateFanSpeed(String[] messageParts) {
757 if (messageParts.length != 5) {
758 logger.debug("FAN;SPD;ACTUAL has unexpected number of parameters: {}", Arrays.toString(messageParts));
761 logger.debug("Process fan speed update for {}: {}", thing.getUID(), messageParts[4]);
762 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
763 updateChannel(CHANNEL_FAN_SPEED, state);
764 fanStateMap.put(CHANNEL_FAN_SPEED, state);
767 private void updateFanDirection(String[] messageParts) {
768 if (messageParts.length != 4) {
769 logger.debug("FAN;DIR has unexpected number of parameters: {}", Arrays.toString(messageParts));
772 logger.debug("Process fan direction update for {}: {}", thing.getUID(), messageParts[3]);
773 StringType state = new StringType(messageParts[3]);
774 updateChannel(CHANNEL_FAN_DIRECTION, state);
775 fanStateMap.put(CHANNEL_FAN_DIRECTION, state);
778 private void updateFanAuto(String[] messageParts) {
779 if (messageParts.length != 4) {
780 logger.debug("FAN;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
783 logger.debug("Process fan auto update for {}: {}", thing.getUID(), messageParts[3]);
784 OnOffType state = OnOffType.from("ON".equalsIgnoreCase(messageParts[3]));
785 updateChannel(CHANNEL_FAN_AUTO, state);
786 fanStateMap.put(CHANNEL_FAN_AUTO, state);
789 private void updateFanWhoosh(String[] messageParts) {
790 if (messageParts.length != 5) {
791 logger.debug("FAN;WHOOSH has unexpected number of parameters: {}", Arrays.toString(messageParts));
794 logger.debug("Process fan whoosh update for {}: {}", thing.getUID(), messageParts[4]);
795 OnOffType state = OnOffType.from("ON".equalsIgnoreCase(messageParts[4]));
796 updateChannel(CHANNEL_FAN_WHOOSH, state);
797 fanStateMap.put(CHANNEL_FAN_WHOOSH, state);
800 private void updateFanWintermode(String[] messageParts) {
801 if (messageParts.length != 4) {
802 logger.debug("WINTERMODE;STATE has unexpected number of parameters: {}", Arrays.toString(messageParts));
805 logger.debug("Process fan wintermode update for {}: {}", thing.getUID(), messageParts[3]);
806 OnOffType state = OnOffType.from("ON".equalsIgnoreCase(messageParts[3]));
807 updateChannel(CHANNEL_FAN_WINTERMODE, state);
808 fanStateMap.put(CHANNEL_FAN_WINTERMODE, state);
811 private void updateFanSmartmode(String[] messageParts) {
812 if (messageParts.length != 4) {
813 logger.debug("Smartmode has unexpected number of parameters: {}", Arrays.toString(messageParts));
816 logger.debug("Process fan smartmode update for {}: {}", thing.getUID(), messageParts[3]);
817 StringType state = new StringType(messageParts[3]);
818 updateChannel(CHANNEL_FAN_SMARTMODE, state);
819 fanStateMap.put(CHANNEL_FAN_SMARTMODE, state);
822 private void updateFanSpeedMin(String[] messageParts) {
823 if (messageParts.length != 5) {
824 logger.debug("FanSpeedMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
827 logger.debug("Process fan min speed update for {}: {}", thing.getUID(), messageParts[4]);
828 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
829 updateChannel(CHANNEL_FAN_SPEED_MIN, state);
830 fanStateMap.put(CHANNEL_FAN_SPEED_MIN, state);
833 private void updateFanSpeedMax(String[] messageParts) {
834 if (messageParts.length != 5) {
835 logger.debug("FanSpeedMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
838 logger.debug("Process fan speed max update for {}: {}", thing.getUID(), messageParts[4]);
839 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
840 updateChannel(CHANNEL_FAN_SPEED_MAX, state);
841 fanStateMap.put(CHANNEL_FAN_SPEED_MAX, state);
844 private void updateFanSleepMode(String[] messageParts) {
845 if (messageParts.length != 4) {
846 logger.debug("SLEEP;STATE; has unexpected number of parameters: {}", Arrays.toString(messageParts));
849 logger.debug("Process fan sleep mode for {}: {}", thing.getUID(), messageParts[3]);
850 OnOffType state = OnOffType.from("ON".equalsIgnoreCase(messageParts[3]));
851 updateChannel(CHANNEL_FAN_SLEEP, state);
852 fanStateMap.put(CHANNEL_FAN_SLEEP, state);
855 private void updateFanLearnMinSpeed(String[] messageParts) {
856 if (messageParts.length != 4) {
857 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
860 logger.debug("Process fan learn min speed update for {}: {}", thing.getUID(), messageParts[3]);
861 PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
862 updateChannel(CHANNEL_FAN_LEARN_MINSPEED, state);
863 fanStateMap.put(CHANNEL_FAN_LEARN_MINSPEED, state);
866 private void updateFanLearnMaxSpeed(String[] messageParts) {
867 if (messageParts.length != 4) {
868 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
871 logger.debug("Process fan learn max speed update for {}: {}", thing.getUID(), messageParts[3]);
872 PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
873 updateChannel(CHANNEL_FAN_LEARN_MAXSPEED, state);
874 fanStateMap.put(CHANNEL_FAN_LEARN_MAXSPEED, state);
877 private void updateLightPower(String[] messageParts) {
878 if (messageParts.length != 4) {
879 logger.debug("LIGHT;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
882 logger.debug("Process light power update for {}: {}", thing.getUID(), messageParts[3]);
883 OnOffType state = OnOffType.from("ON".equalsIgnoreCase(messageParts[3]));
884 updateChannel(CHANNEL_LIGHT_POWER, state);
885 fanStateMap.put(CHANNEL_LIGHT_POWER, state);
888 private void updateLightLevel(String[] messageParts) {
889 if (messageParts.length != 5) {
890 logger.debug("LIGHT;LEVEL has unexpected number of parameters: {}", Arrays.toString(messageParts));
893 logger.debug("Process light level update for {}: {}", thing.getUID(), messageParts[4]);
894 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
895 updateChannel(CHANNEL_LIGHT_LEVEL, state);
896 fanStateMap.put(CHANNEL_LIGHT_LEVEL, state);
899 private void updateLightHue(String[] messageParts) {
900 if (messageParts.length != 6) {
901 logger.debug("LIGHT;COLOR;TEMP;VALUE has unexpected number of parameters: {}",
902 Arrays.toString(messageParts));
905 logger.debug("Process light hue update for {}: {}", thing.getUID(), messageParts[4]);
906 PercentType state = BigAssFanConverter.hueToPercent(messageParts[5]);
907 updateChannel(CHANNEL_LIGHT_HUE, state);
908 fanStateMap.put(CHANNEL_LIGHT_HUE, state);
911 private void updateLightAuto(String[] messageParts) {
912 if (messageParts.length != 4) {
913 logger.debug("LIGHT;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
916 logger.debug("Process light auto update for {}: {}", thing.getUID(), messageParts[3]);
917 OnOffType state = OnOffType.from("ON".equalsIgnoreCase(messageParts[3]));
918 updateChannel(CHANNEL_LIGHT_AUTO, state);
919 fanStateMap.put(CHANNEL_LIGHT_AUTO, state);
922 private void updateLightLevelMin(String[] messageParts) {
923 if (messageParts.length != 5) {
924 logger.debug("LightLevelMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
927 logger.debug("Process light level min update for {}: {}", thing.getUID(), messageParts[4]);
928 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
929 updateChannel(CHANNEL_LIGHT_LEVEL_MIN, state);
930 fanStateMap.put(CHANNEL_LIGHT_LEVEL_MIN, state);
933 private void updateLightLevelMax(String[] messageParts) {
934 if (messageParts.length != 5) {
935 logger.debug("LightLevelMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
938 logger.debug("Process light level max update for {}: {}", thing.getUID(), messageParts[4]);
939 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
940 updateChannel(CHANNEL_LIGHT_LEVEL_MAX, state);
941 fanStateMap.put(CHANNEL_LIGHT_LEVEL_MAX, state);
944 private void updateLightPresent(String[] messageParts) {
945 if (messageParts.length < 4) {
946 logger.debug("LightPresent has unexpected number of parameters: {}", Arrays.toString(messageParts));
949 logger.debug("Process light present update for {}: {}", thing.getUID(), messageParts[3]);
950 StringType lightPresent = new StringType(messageParts[3]);
951 updateChannel(CHANNEL_LIGHT_PRESENT, lightPresent);
952 fanStateMap.put(CHANNEL_LIGHT_PRESENT, lightPresent);
953 if (messageParts.length == 5) {
954 logger.debug("Light supports hue adjustment");
955 StringType lightColor = new StringType(messageParts[4]);
956 updateChannel(CHANNEL_LIGHT_COLOR, lightColor);
957 fanStateMap.put(CHANNEL_LIGHT_COLOR, lightColor);
961 private void updateMotion(String[] messageParts) {
962 if (messageParts.length != 4) {
963 logger.debug("SNSROCC has unexpected number of parameters: {}", Arrays.toString(messageParts));
966 logger.debug("Process motion sensor update for {}: {}", thing.getUID(), messageParts[3]);
967 OnOffType state = OnOffType.from("OCCUPIED".equalsIgnoreCase(messageParts[3]));
968 updateChannel(CHANNEL_MOTION, state);
969 fanStateMap.put(CHANNEL_MOTION, state);
972 private void updateTime(String[] messageParts) {
973 if (messageParts.length != 4) {
974 logger.debug("TIME has unexpected number of parameters: {}", Arrays.toString(messageParts));
977 logger.debug("Process time update for {}: {}", thing.getUID(), messageParts[3]);
978 // (mac|name;TIME;VALUE;2017-03-26T14:06:27Z)
980 Instant instant = Instant.parse(messageParts[3]);
981 DateTimeType state = new DateTimeType(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()));
982 updateChannel(CHANNEL_TIME, state);
983 fanStateMap.put(CHANNEL_TIME, state);
984 } catch (DateTimeParseException e) {
985 logger.info("Failed to parse date received from {}: {}", thing.getUID(), messageParts[3]);
991 * The {@link ConnectionManager} class is responsible for managing the state of the TCP connection to the
994 * @author Mark Hilbush - Initial contribution
996 private class ConnectionManager {
997 private Logger logger = LoggerFactory.getLogger(ConnectionManager.class);
999 private boolean deviceIsConnected;
1001 private @Nullable InetAddress ifAddress;
1002 private @Nullable Socket fanSocket;
1003 private @Nullable Scanner fanScanner;
1004 private @Nullable DataOutputStream fanWriter;
1005 private static final int SOCKET_CONNECT_TIMEOUT = 1500;
1007 private @Nullable ScheduledFuture<?> connectionMonitorJob;
1008 private static final long CONNECTION_MONITOR_FREQ = 120L;
1009 private static final long CONNECTION_MONITOR_DELAY = 30L;
1011 Runnable connectionMonitorRunnable = () -> {
1012 logger.trace("Performing connection check for {} at IP {}", thing.getUID(), ipAddress);
1016 public ConnectionManager(@Nullable String ipv4Address) {
1017 deviceIsConnected = false;
1019 ifAddress = InetAddress.getByName(ipv4Address);
1020 NetworkInterface netIF = NetworkInterface.getByInetAddress(ifAddress);
1021 logger.debug("Handler for {} using address {} on network interface {}", thing.getUID(), ipv4Address,
1022 netIF != null ? netIF.getName() : "UNKNOWN");
1023 } catch (UnknownHostException e) {
1024 logger.warn("Handler for {} got UnknownHostException getting local IPv4 net interface: {}",
1025 thing.getUID(), e.getMessage(), e);
1026 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1027 } catch (SocketException e) {
1028 logger.warn("Handler for {} got SocketException getting local IPv4 network interface: {}",
1029 thing.getUID(), e.getMessage(), e);
1030 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1035 * Connect to the command and serial port(s) on the device. The serial connections are established only for
1036 * devices that support serial.
1038 protected synchronized void connect() {
1039 if (isConnected()) {
1042 logger.trace("Connecting to {} at {}", thing.getUID(), ipAddress);
1044 Socket localFanSocket = new Socket();
1045 fanSocket = localFanSocket;
1048 localFanSocket.bind(new InetSocketAddress(ifAddress, 0));
1049 localFanSocket.connect(new InetSocketAddress(ipAddress, BAF_PORT), SOCKET_CONNECT_TIMEOUT);
1050 } catch (SecurityException | IllegalArgumentException | IOException e) {
1051 logger.debug("Unexpected exception connecting to {} at {}: {}", thing.getUID(), ipAddress,
1053 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1060 fanWriter = new DataOutputStream(localFanSocket.getOutputStream());
1061 Scanner localFanScanner = new Scanner(localFanSocket.getInputStream());
1062 localFanScanner.useDelimiter("[)]");
1063 fanScanner = localFanScanner;
1064 } catch (IllegalBlockingModeException | IOException e) {
1065 logger.warn("Exception getting streams for {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(), e);
1066 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1070 logger.info("Connected to {} at {}", thing.getUID(), ipAddress);
1071 deviceIsConnected = true;
1075 protected synchronized void disconnect() {
1076 if (!isConnected()) {
1079 logger.debug("Disconnecting from {} at {}", thing.getUID(), ipAddress);
1082 DataOutputStream localFanWriter = fanWriter;
1083 if (localFanWriter != null) {
1084 localFanWriter.close();
1087 Scanner localFanScanner = fanScanner;
1088 if (localFanScanner != null) {
1089 localFanScanner.close();
1091 Socket localFanSocket = fanSocket;
1092 if (localFanSocket != null) {
1093 localFanSocket.close();
1096 } catch (IllegalStateException | IOException e) {
1097 logger.warn("Exception closing connection to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1100 deviceIsConnected = false;
1107 public @Nullable String read() {
1108 if (fanScanner == null) {
1109 logger.warn("Scanner for {} is null when trying to scan from {}!", thing.getUID(), ipAddress);
1113 String nextToken = null;
1115 Scanner localFanScanner = fanScanner;
1116 if (localFanScanner != null) {
1117 nextToken = localFanScanner.next();
1119 } catch (NoSuchElementException e) {
1120 logger.debug("Scanner for {} threw NoSuchElementException; stream possibly closed", thing.getUID());
1121 // Force a reconnect to the device
1124 } catch (IllegalStateException e) {
1125 logger.debug("Scanner for {} threw IllegalStateException; scanner possibly closed", thing.getUID());
1127 } catch (BufferOverflowException e) {
1128 logger.debug("Scanner for {} threw BufferOverflowException", thing.getUID());
1134 public void write(byte[] buffer) throws IOException {
1135 DataOutputStream localFanWriter = fanWriter;
1136 if (localFanWriter == null) {
1137 logger.warn("fanWriter for {} is null when trying to write to {}!!!", thing.getUID(), ipAddress);
1140 localFanWriter.write(buffer, 0, buffer.length);
1144 private boolean isConnected() {
1145 return deviceIsConnected;
1149 * Periodically validate the command connection to the device by executing a getversion command.
1151 private synchronized void scheduleConnectionMonitorJob() {
1152 if (connectionMonitorJob == null) {
1153 logger.debug("Starting connection monitor job in {} seconds for {} at {}", CONNECTION_MONITOR_DELAY,
1154 thing.getUID(), ipAddress);
1155 connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable,
1156 CONNECTION_MONITOR_DELAY, CONNECTION_MONITOR_FREQ, TimeUnit.SECONDS);
1160 private void cancelConnectionMonitorJob() {
1161 ScheduledFuture<?> localConnectionMonitorJob = connectionMonitorJob;
1162 if (localConnectionMonitorJob != null) {
1163 logger.debug("Canceling connection monitor job for {} at {}", thing.getUID(), ipAddress);
1164 localConnectionMonitorJob.cancel(true);
1165 connectionMonitorJob = null;
1169 private void checkConnection() {
1170 logger.trace("Checking status of connection for {} at {}", thing.getUID(), ipAddress);
1171 if (!isConnected()) {
1172 logger.debug("Connection check FAILED for {} at {}", thing.getUID(), ipAddress);
1175 logger.debug("Connection check OK for {} at {}", thing.getUID(), ipAddress);
1176 logger.debug("Requesting status update from {} at {}", thing.getUID(), ipAddress);
1177 sendCommand(macAddress, ";GETALL");
1178 sendCommand(macAddress, ";SNSROCC;STATUS;GET");