2 * Copyright (c) 2010-2021 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.io.UnsupportedEncodingException;
20 import java.net.InetAddress;
21 import java.net.InetSocketAddress;
22 import java.net.NetworkInterface;
23 import java.net.Socket;
24 import java.net.SocketException;
25 import java.net.UnknownHostException;
26 import java.nio.BufferOverflowException;
27 import java.time.Instant;
28 import java.time.ZoneId;
29 import java.time.ZonedDateTime;
30 import java.time.format.DateTimeParseException;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.HashMap;
35 import java.util.NoSuchElementException;
36 import java.util.Scanner;
37 import java.util.concurrent.ScheduledExecutorService;
38 import java.util.concurrent.ScheduledFuture;
39 import java.util.concurrent.TimeUnit;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
43 import org.apache.commons.lang.StringUtils;
44 import org.openhab.binding.bigassfan.internal.BigAssFanConfig;
45 import org.openhab.binding.bigassfan.internal.utils.BigAssFanConverter;
46 import org.openhab.core.common.ThreadPoolManager;
47 import org.openhab.core.library.types.DateTimeType;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.library.types.PercentType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.State;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
64 * The {@link BigAssFanHandler} is responsible for handling commands, which are
65 * sent to one of the channels.
67 * @author Mark Hilbush - Initial contribution
69 public class BigAssFanHandler extends BaseThingHandler {
70 private final Logger logger = LoggerFactory.getLogger(BigAssFanHandler.class);
72 private static final StringType LIGHT_COLOR = new StringType("COLOR");
73 private static final StringType LIGHT_PRESENT = new StringType("PRESENT");
75 private static final StringType OFF = new StringType("OFF");
76 private static final StringType COOLING = new StringType("COOLING");
77 private static final StringType HEATING = new StringType("HEATING");
79 private BigAssFanConfig config;
80 private String label = null;
81 private String ipAddress = null;
82 private String macAddress = null;
84 private FanListener fanListener;
86 protected Map<String, State> fanStateMap = Collections.synchronizedMap(new HashMap<>());
88 public BigAssFanHandler(Thing thing, String ipv4Address) {
92 logger.debug("Creating FanListener object for {}", thing.getUID());
93 fanListener = new FanListener(ipv4Address);
97 public void initialize() {
98 logger.debug("BigAssFanHandler for {} is initializing", thing.getUID());
100 config = getConfig().as(BigAssFanConfig.class);
101 logger.debug("BigAssFanHandler config for {} is {}", thing.getUID(), config);
103 if (!config.isValid()) {
104 logger.debug("BigAssFanHandler config of {} is invalid. Check configuration", thing.getUID());
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
106 "Invalid BigAssFan config. Check configuration.");
109 label = config.getLabel();
110 ipAddress = config.getIpAddress();
111 macAddress = config.getMacAddress();
113 fanListener.startFanListener();
117 public void dispose() {
118 logger.debug("BigAssFanHandler for {} is disposing", thing.getUID());
119 fanListener.stopFanListener();
123 public void handleCommand(ChannelUID channelUID, Command command) {
124 if (command instanceof RefreshType) {
128 logger.debug("Handle command for {} on channel {}: {}", thing.getUID(), channelUID, command);
129 if (channelUID.getId().equals(CHANNEL_FAN_POWER)) {
130 handleFanPower(command);
131 } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED)) {
132 handleFanSpeed(command);
133 } else if (channelUID.getId().equals(CHANNEL_FAN_AUTO)) {
134 handleFanAuto(command);
135 } else if (channelUID.getId().equals(CHANNEL_FAN_WHOOSH)) {
136 handleFanWhoosh(command);
137 } else if (channelUID.getId().equals(CHANNEL_FAN_SMARTMODE)) {
138 handleFanSmartmode(command);
139 } else if (channelUID.getId().equals(CHANNEL_FAN_LEARN_MINSPEED)) {
140 handleFanLearnSpeedMin(command);
141 } else if (channelUID.getId().equals(CHANNEL_FAN_LEARN_MAXSPEED)) {
142 handleFanLearnSpeedMax(command);
143 } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED_MIN)) {
144 handleFanSpeedMin(command);
145 } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED_MAX)) {
146 handleFanSpeedMax(command);
147 } else if (channelUID.getId().equals(CHANNEL_FAN_WINTERMODE)) {
148 handleFanWintermode(command);
149 } else if (channelUID.getId().equals(CHANNEL_LIGHT_POWER)) {
150 handleLightPower(command);
151 } else if (channelUID.getId().equals(CHANNEL_LIGHT_LEVEL)) {
152 handleLightLevel(command);
153 } else if (channelUID.getId().equals(CHANNEL_LIGHT_HUE)) {
154 handleLightHue(command);
155 } else if (channelUID.getId().equals(CHANNEL_LIGHT_AUTO)) {
156 handleLightAuto(command);
157 } else if (channelUID.getId().equals(CHANNEL_LIGHT_SMARTER)) {
158 handleLightSmarter(command);
159 } else if (channelUID.getId().equals(CHANNEL_LIGHT_LEVEL_MIN)) {
160 handleLightLevelMin(command);
161 } else if (channelUID.getId().equals(CHANNEL_LIGHT_LEVEL_MAX)) {
162 handleLightLevelMax(command);
163 } else if (channelUID.getId().equals(CHANNEL_FAN_SLEEP)) {
164 handleSleep(command);
166 logger.debug("Received command for {} on unknown channel {}", thing.getUID(), channelUID.getId());
170 private void handleFanPower(Command command) {
171 logger.debug("Handling fan power command for {}: {}", thing.getUID(), command);
173 // <mac;FAN;PWR;ON|OFF>
174 if (command instanceof OnOffType) {
175 if (command.equals(OnOffType.OFF)) {
176 sendCommand(macAddress, ";FAN;PWR;OFF");
177 } else if (command.equals(OnOffType.ON)) {
178 sendCommand(macAddress, ";FAN;PWR;ON");
183 private void handleFanSpeed(Command command) {
184 logger.debug("Handling fan speed command for {}: {}", thing.getUID(), command);
186 // <mac;FAN;SPD;SET;0..7>
187 if (command instanceof PercentType) {
188 sendCommand(macAddress, ";FAN;SPD;SET;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
192 private void handleFanAuto(Command command) {
193 logger.debug("Handling fan auto command {}", command);
195 // <mac;FAN;AUTO;ON|OFF>
196 if (command instanceof OnOffType) {
197 if (command.equals(OnOffType.OFF)) {
198 sendCommand(macAddress, ";FAN;AUTO;OFF");
199 } else if (command.equals(OnOffType.ON)) {
200 sendCommand(macAddress, ";FAN;AUTO;ON");
205 private void handleFanWhoosh(Command command) {
206 logger.debug("Handling fan whoosh command {}", command);
208 // <mac;FAN;WHOOSH;ON|OFF>
209 if (command instanceof OnOffType) {
210 if (command.equals(OnOffType.OFF)) {
211 sendCommand(macAddress, ";FAN;WHOOSH;OFF");
212 } else if (command.equals(OnOffType.ON)) {
213 sendCommand(macAddress, ";FAN;WHOOSH;ON");
218 private void handleFanSmartmode(Command command) {
219 logger.debug("Handling fan smartmode command {}", command);
221 // <mac;SMARTMODE;SET;OFF/COOLING/HEATING>
222 if (command instanceof StringType) {
223 if (command.equals(OFF)) {
224 sendCommand(macAddress, ";SMARTMODE;STATE;SET;OFF");
225 } else if (command.equals(COOLING)) {
226 sendCommand(macAddress, ";SMARTMODE;STATE;SET;COOLING");
227 } else if (command.equals(HEATING)) {
228 sendCommand(macAddress, ";SMARTMODE;STATE;SET;HEATING");
230 logger.debug("Unknown fan smartmode command: {}", command);
235 private void handleFanLearnSpeedMin(Command command) {
236 logger.debug("Handling fan learn speed minimum command {}", command);
237 // <mac;FAN;SPD;SET;MIN;0..7>
238 if (command instanceof PercentType) {
239 // Send min speed set command
240 sendCommand(macAddress,
241 ";LEARN;MINSPEED;SET;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
242 fanStateMap.put(CHANNEL_FAN_LEARN_MINSPEED, (PercentType) command);
243 // Don't let max be less than min
244 adjustMaxSpeed((PercentType) command, CHANNEL_FAN_LEARN_MAXSPEED, ";LEARN;MAXSPEED;");
248 private void handleFanLearnSpeedMax(Command command) {
249 logger.debug("Handling fan learn speed maximum command {}", command);
250 // <mac;FAN;SPD;SET;MAX;0..7>
251 if (command instanceof PercentType) {
252 // Send max speed set command
253 sendCommand(macAddress,
254 ";LEARN;MAXSPEED;SET;;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
255 fanStateMap.put(CHANNEL_FAN_LEARN_MAXSPEED, (PercentType) command);
256 // Don't let min be greater than max
257 adjustMinSpeed((PercentType) command, CHANNEL_FAN_LEARN_MINSPEED, ";LEARN;MINSPEED;");
261 private void handleFanSpeedMin(Command command) {
262 logger.debug("Handling fan speed minimum command {}", command);
263 // <mac;FAN;SPD;SET;MIN;0..7>
264 if (command instanceof PercentType) {
265 // Send min speed set command
266 sendCommand(macAddress,
267 ";FAN;SPD;SET;MIN;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
268 fanStateMap.put(CHANNEL_FAN_SPEED_MIN, (PercentType) command);
269 // Don't let max be less than min
270 adjustMaxSpeed((PercentType) command, 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) {
278 // Send max speed set command
279 sendCommand(macAddress,
280 ";FAN;SPD;SET;MAX;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
281 fanStateMap.put(CHANNEL_FAN_SPEED_MAX, (PercentType) command);
282 // Don't let min be greater than max
283 adjustMinSpeed((PercentType) command, CHANNEL_FAN_SPEED_MIN, ";FAN;SPD;SET;MIN;");
287 private void handleFanWintermode(Command command) {
288 logger.debug("Handling fan wintermode command {}", command);
290 // <mac;FAN;WINTERMODE;ON|OFF>
291 if (command instanceof OnOffType) {
292 if (command.equals(OnOffType.OFF)) {
293 sendCommand(macAddress, ";FAN;WINTERMODE;OFF");
294 } else if (command.equals(OnOffType.ON)) {
295 sendCommand(macAddress, ";FAN;WINTERMODE;ON");
300 private void handleSleep(Command command) {
301 logger.debug("Handling fan sleep command {}", command);
303 // <mac;SLEEP;STATE;ON|OFF>
304 if (command instanceof OnOffType) {
305 if (command.equals(OnOffType.OFF)) {
306 sendCommand(macAddress, ";SLEEP;STATE;OFF");
307 } else if (command.equals(OnOffType.ON)) {
308 sendCommand(macAddress, ";SLEEP;STATE;ON");
313 private void adjustMaxSpeed(PercentType command, String channelId, String commandFragment) {
314 int newMin = command.intValue();
315 int currentMax = PercentType.ZERO.intValue();
316 if (fanStateMap.get(channelId) != null) {
317 currentMax = ((PercentType) fanStateMap.get(channelId)).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 if (fanStateMap.get(channelId) != null) {
329 currentMin = ((PercentType) fanStateMap.get(channelId)).intValue();
331 if (newMax < currentMin) {
332 updateState(channelId, command);
333 sendCommand(macAddress, commandFragment.concat(BigAssFanConverter.percentToSpeed(command)));
337 private void handleLightPower(Command command) {
338 if (!isLightPresent()) {
339 logger.debug("Fan does not have light installed for command {}", command);
343 logger.debug("Handling light power command {}", command);
344 // <mac;LIGHT;PWR;ON|OFF>
345 if (command instanceof OnOffType) {
346 if (command.equals(OnOffType.OFF)) {
347 sendCommand(macAddress, ";LIGHT;PWR;OFF");
348 } else if (command.equals(OnOffType.ON)) {
349 sendCommand(macAddress, ";LIGHT;PWR;ON");
354 private void handleLightLevel(Command command) {
355 if (!isLightPresent()) {
356 logger.debug("Fan does not have light installed for command {}", command);
360 logger.debug("Handling light level command {}", command);
361 // <mac;LIGHT;LEVEL;SET;0..16>
362 if (command instanceof PercentType) {
363 sendCommand(macAddress,
364 ";LIGHT;LEVEL;SET;".concat(BigAssFanConverter.percentToLevel((PercentType) command)));
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) {
377 sendCommand(macAddress,
378 ";LIGHT;COLOR;TEMP;VALUE;SET;".concat(BigAssFanConverter.percentToHue((PercentType) command)));
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) {
425 // Send min light level set command
426 sendCommand(macAddress,
427 ";LIGHT;LEVEL;MIN;".concat(BigAssFanConverter.percentToLevel((PercentType) command)));
428 // Don't let max be less than min
429 adjustMaxLevel((PercentType) command);
433 private void handleLightLevelMax(Command command) {
434 if (!isLightPresent()) {
435 logger.debug("Fan does not have light installed for command {}", command);
439 logger.debug("Handling light level maximum command {}", command);
440 // <mac;LIGHT;LEVEL;MAX;0-16>
441 if (command instanceof PercentType) {
442 // Send max light level set command
443 sendCommand(macAddress,
444 ";LIGHT;LEVEL;MAX;".concat(BigAssFanConverter.percentToLevel((PercentType) command)));
445 // Don't let min be greater than max
446 adjustMinLevel((PercentType) command);
450 private void adjustMaxLevel(PercentType command) {
451 int newMin = command.intValue();
452 int currentMax = PercentType.ZERO.intValue();
453 if (fanStateMap.get(CHANNEL_LIGHT_LEVEL_MAX) != null) {
454 currentMax = ((PercentType) fanStateMap.get(CHANNEL_LIGHT_LEVEL_MAX)).intValue();
456 if (newMin > currentMax) {
457 updateState(CHANNEL_LIGHT_LEVEL_MAX, command);
458 sendCommand(macAddress, ";LIGHT;LEVEL;MAX;".concat(BigAssFanConverter.percentToLevel(command)));
462 private void adjustMinLevel(PercentType command) {
463 int newMax = command.intValue();
464 int currentMin = PercentType.HUNDRED.intValue();
465 if (fanStateMap.get(CHANNEL_LIGHT_LEVEL_MIN) != null) {
466 currentMin = ((PercentType) fanStateMap.get(CHANNEL_LIGHT_LEVEL_MIN)).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 if (fanListener == null) {
488 logger.error("Unable to send message to {} because fanListener object is null!", thing.getUID());
492 StringBuilder sb = new StringBuilder();
493 sb.append("<").append(mac).append(commandFragment).append(">");
494 String message = sb.toString();
495 logger.trace("Sending message to {} at {}: {}", thing.getUID(), ipAddress, message);
496 fanListener.send(message);
499 private void updateChannel(String channelName, State state) {
500 Channel channel = thing.getChannel(channelName);
501 if (channel != null) {
502 updateState(channel.getUID(), state);
507 * Manage the ONLINE/OFFLINE status of the thing
509 private void markOnline() {
511 logger.debug("Changing status of {} from {}({}) to ONLINE", thing.getUID(), getStatus(), getDetail());
512 updateStatus(ThingStatus.ONLINE);
516 private void markOffline() {
518 logger.debug("Changing status of {} from {}({}) to OFFLINE", thing.getUID(), getStatus(), getDetail());
519 updateStatus(ThingStatus.OFFLINE);
523 private void markOfflineWithMessage(ThingStatusDetail statusDetail, String statusMessage) {
524 // If it's offline with no detail or if it's not offline, mark it offline with detailed status
525 if ((isOffline() && getDetail() == ThingStatusDetail.NONE) || !isOffline()) {
526 logger.debug("Changing status of {} from {}({}) to OFFLINE({})", thing.getUID(), getStatus(), getDetail(),
528 updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage);
533 private boolean isOnline() {
534 return thing.getStatus().equals(ThingStatus.ONLINE);
537 private boolean isOffline() {
538 return thing.getStatus().equals(ThingStatus.OFFLINE);
541 private ThingStatus getStatus() {
542 return thing.getStatus();
545 private ThingStatusDetail getDetail() {
546 return thing.getStatusInfo().getStatusDetail();
550 * The {@link FanListener} is responsible for sending and receiving messages to a fan.
552 * @author Mark Hilbush - Initial contribution
554 public class FanListener {
555 private final Logger logger = LoggerFactory.getLogger(FanListener.class);
557 // Our own thread pool for the long-running listener job
558 private ScheduledExecutorService scheduledExecutorService = ThreadPoolManager
559 .getScheduledPool("bigassfanHandler" + "-" + thing.getUID());
560 private ScheduledFuture<?> listenerJob;
562 private final long FAN_LISTENER_DELAY = 2L;
563 private boolean terminate;
565 private final Pattern messagePattern = Pattern.compile("[(](.*)");
567 private ConnectionManager conn;
569 private Runnable fanListenerRunnable = () -> {
572 } catch (RuntimeException e) {
573 logger.warn("FanListener for {} had unhandled exception: {}", thing.getUID(), e.getMessage(), e);
577 public FanListener(String ipv4Address) {
578 conn = new ConnectionManager(ipv4Address);
581 public void startFanListener() {
583 conn.scheduleConnectionMonitorJob();
585 if (listenerJob == null) {
587 logger.debug("Starting listener in {} sec for {} at {}", FAN_LISTENER_DELAY, thing.getUID(), ipAddress);
588 listenerJob = scheduledExecutorService.schedule(fanListenerRunnable, FAN_LISTENER_DELAY,
593 public void stopFanListener() {
594 if (listenerJob != null) {
595 logger.debug("Stopping listener for {} at {}", thing.getUID(), ipAddress);
597 listenerJob.cancel(true);
600 conn.cancelConnectionMonitorJob();
604 public void send(String command) {
605 if (!conn.isConnected()) {
606 logger.debug("Unable to send message; no connection to {}. Trying to reconnect: {}", thing.getUID(),
609 if (!conn.isConnected()) {
614 logger.debug("Sending message to {} at {}: {}", thing.getUID(), ipAddress, command);
617 buffer = command.getBytes(CHARSET);
618 } catch (UnsupportedEncodingException e) {
619 logger.warn("Unable to convert to string using {} charset: {}", CHARSET, e.getMessage(), e);
624 } catch (IOException e) {
625 logger.warn("IO exception writing message to socket: {}", e.getMessage(), e);
630 private void listener() {
631 logger.debug("Fan listener thread is running for {} at {}", thing.getUID(), ipAddress);
635 // Wait for a message
636 processMessage(waitForMessage());
637 } catch (IOException ioe) {
638 logger.warn("Listener for {} got IO exception waiting for message: {}", thing.getUID(),
639 ioe.getMessage(), ioe);
643 logger.debug("Fan listener thread is exiting for {} at {}", thing.getUID(), ipAddress);
646 private String waitForMessage() throws IOException {
647 if (!conn.isConnected()) {
648 if (logger.isTraceEnabled()) {
649 logger.trace("FanListener for {} can't receive message. No connection to fan", thing.getUID());
653 } catch (InterruptedException e) {
657 return readMessage();
660 private String readMessage() {
661 logger.trace("Waiting for message from {} at {}", thing.getUID(), ipAddress);
662 String message = conn.read();
663 if (message != null) {
664 logger.trace("FanListener for {} received message of length {}: {}", thing.getUID(), message.length(),
670 private void processMessage(String incomingMessage) {
671 if (StringUtils.isEmpty(incomingMessage)) {
676 logger.debug("FanListener for {} received message from {}: {}", thing.getUID(), macAddress,
678 Matcher matcher = messagePattern.matcher(incomingMessage);
679 if (!matcher.find()) {
680 logger.debug("Unable to process message from {}, not in expected format: {}", thing.getUID(),
685 String message = matcher.group(1);
686 String[] messageParts = message.split(";");
688 // Check to make sure it is my MAC address or my label
689 if (!isMe(messageParts[0])) {
690 logger.trace("Message not for me ({}): {}", messageParts[0], macAddress);
694 logger.trace("Message is for me ({}): {}", messageParts[0], macAddress);
695 String messageUpperCase = message.toUpperCase();
696 if (messageUpperCase.contains(";FAN;PWR;")) {
697 updateFanPower(messageParts);
698 } else if (messageUpperCase.contains(";FAN;SPD;ACTUAL;")) {
699 updateFanSpeed(messageParts);
700 } else if (messageUpperCase.contains(";FAN;DIR;")) {
701 updateFanDirection(messageParts);
702 } else if (messageUpperCase.contains(";FAN;AUTO;")) {
703 updateFanAuto(messageParts);
704 } else if (messageUpperCase.contains(";FAN;WHOOSH;STATUS;")) {
705 updateFanWhoosh(messageParts);
706 } else if (messageUpperCase.contains(";WINTERMODE;STATE;")) {
707 updateFanWintermode(messageParts);
708 } else if (messageUpperCase.contains(";SMARTMODE;STATE;")) {
709 updateFanSmartmode(messageParts);
710 } else if (messageUpperCase.contains(";FAN;SPD;MIN;")) {
711 updateFanSpeedMin(messageParts);
712 } else if (messageUpperCase.contains(";FAN;SPD;MAX;")) {
713 updateFanSpeedMax(messageParts);
714 } else if (messageUpperCase.contains(";SLEEP;STATE")) {
715 updateFanSleepMode(messageParts);
716 } else if (messageUpperCase.contains(";LEARN;MINSPEED;")) {
717 updateFanLearnMinSpeed(messageParts);
718 } else if (messageUpperCase.contains(";LEARN;MAXSPEED;")) {
719 updateFanLearnMaxSpeed(messageParts);
720 } else if (messageUpperCase.contains(";LIGHT;PWR;")) {
721 updateLightPower(messageParts);
722 } else if (messageUpperCase.contains(";LIGHT;LEVEL;ACTUAL;")) {
723 updateLightLevel(messageParts);
724 } else if (messageUpperCase.contains(";LIGHT;COLOR;TEMP;VALUE;")) {
725 updateLightHue(messageParts);
726 } else if (messageUpperCase.contains(";LIGHT;AUTO;")) {
727 updateLightAuto(messageParts);
728 } else if (messageUpperCase.contains(";LIGHT;LEVEL;MIN;")) {
729 updateLightLevelMin(messageParts);
730 } else if (messageUpperCase.contains(";LIGHT;LEVEL;MAX;")) {
731 updateLightLevelMax(messageParts);
732 } else if (messageUpperCase.contains(";DEVICE;LIGHT;")) {
733 updateLightPresent(messageParts);
734 } else if (messageUpperCase.contains(";SNSROCC;STATUS;")) {
735 updateMotion(messageParts);
736 } else if (messageUpperCase.contains(";TIME;VALUE;")) {
737 updateTime(messageParts);
739 logger.trace("Received unsupported message from {}: {}", thing.getUID(), message);
743 private boolean isMe(String idFromDevice) {
744 // Check match on MAC address
745 if (StringUtils.equalsIgnoreCase(idFromDevice, macAddress)) {
748 // Didn't match MAC address, check match for label
749 if (StringUtils.equalsIgnoreCase(idFromDevice, label)) {
755 private void updateFanPower(String[] messageParts) {
756 if (messageParts.length != 4) {
757 if (logger.isDebugEnabled()) {
758 logger.debug("FAN;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
762 logger.debug("Process fan power update for {}: {}", thing.getUID(), messageParts[3]);
763 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
764 updateChannel(CHANNEL_FAN_POWER, state);
765 fanStateMap.put(CHANNEL_FAN_POWER, state);
768 private void updateFanSpeed(String[] messageParts) {
769 if (messageParts.length != 5) {
770 logger.debug("FAN;SPD;ACTUAL has unexpected number of parameters: {}", Arrays.toString(messageParts));
773 logger.debug("Process fan speed update for {}: {}", thing.getUID(), messageParts[4]);
774 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
775 updateChannel(CHANNEL_FAN_SPEED, state);
776 fanStateMap.put(CHANNEL_FAN_SPEED, state);
779 private void updateFanDirection(String[] messageParts) {
780 if (messageParts.length != 4) {
781 logger.debug("FAN;DIR has unexpected number of parameters: {}", Arrays.toString(messageParts));
784 logger.debug("Process fan direction update for {}: {}", thing.getUID(), messageParts[3]);
785 StringType state = new StringType(messageParts[3]);
786 updateChannel(CHANNEL_FAN_DIRECTION, state);
787 fanStateMap.put(CHANNEL_FAN_DIRECTION, state);
790 private void updateFanAuto(String[] messageParts) {
791 if (messageParts.length != 4) {
792 logger.debug("FAN;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
795 logger.debug("Process fan auto update for {}: {}", thing.getUID(), messageParts[3]);
796 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
797 updateChannel(CHANNEL_FAN_AUTO, state);
798 fanStateMap.put(CHANNEL_FAN_AUTO, state);
801 private void updateFanWhoosh(String[] messageParts) {
802 if (messageParts.length != 5) {
803 logger.debug("FAN;WHOOSH has unexpected number of parameters: {}", Arrays.toString(messageParts));
806 logger.debug("Process fan whoosh update for {}: {}", thing.getUID(), messageParts[4]);
807 OnOffType state = "ON".equalsIgnoreCase(messageParts[4]) ? OnOffType.ON : OnOffType.OFF;
808 updateChannel(CHANNEL_FAN_WHOOSH, state);
809 fanStateMap.put(CHANNEL_FAN_WHOOSH, state);
812 private void updateFanWintermode(String[] messageParts) {
813 if (messageParts.length != 4) {
814 logger.debug("WINTERMODE;STATE has unexpected number of parameters: {}", Arrays.toString(messageParts));
817 logger.debug("Process fan wintermode update for {}: {}", thing.getUID(), messageParts[3]);
818 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
819 updateChannel(CHANNEL_FAN_WINTERMODE, state);
820 fanStateMap.put(CHANNEL_FAN_WINTERMODE, state);
823 private void updateFanSmartmode(String[] messageParts) {
824 if (messageParts.length != 4) {
825 logger.debug("Smartmode has unexpected number of parameters: {}", Arrays.toString(messageParts));
828 logger.debug("Process fan smartmode update for {}: {}", thing.getUID(), messageParts[3]);
829 StringType state = new StringType(messageParts[3]);
830 updateChannel(CHANNEL_FAN_SMARTMODE, state);
831 fanStateMap.put(CHANNEL_FAN_SMARTMODE, state);
834 private void updateFanSpeedMin(String[] messageParts) {
835 if (messageParts.length != 5) {
836 logger.debug("FanSpeedMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
839 logger.debug("Process fan min speed update for {}: {}", thing.getUID(), messageParts[4]);
840 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
841 updateChannel(CHANNEL_FAN_SPEED_MIN, state);
842 fanStateMap.put(CHANNEL_FAN_SPEED_MIN, state);
845 private void updateFanSpeedMax(String[] messageParts) {
846 if (messageParts.length != 5) {
847 logger.debug("FanSpeedMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
850 logger.debug("Process fan speed max update for {}: {}", thing.getUID(), messageParts[4]);
851 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
852 updateChannel(CHANNEL_FAN_SPEED_MAX, state);
853 fanStateMap.put(CHANNEL_FAN_SPEED_MAX, state);
856 private void updateFanSleepMode(String[] messageParts) {
857 if (messageParts.length != 4) {
858 logger.debug("SLEEP;STATE; has unexpected number of parameters: {}", Arrays.toString(messageParts));
861 logger.debug("Process fan sleep mode for {}: {}", thing.getUID(), messageParts[3]);
862 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
863 updateChannel(CHANNEL_FAN_SLEEP, state);
864 fanStateMap.put(CHANNEL_FAN_SLEEP, state);
867 private void updateFanLearnMinSpeed(String[] messageParts) {
868 if (messageParts.length != 4) {
869 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
872 logger.debug("Process fan learn min speed update for {}: {}", thing.getUID(), messageParts[3]);
873 PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
874 updateChannel(CHANNEL_FAN_LEARN_MINSPEED, state);
875 fanStateMap.put(CHANNEL_FAN_LEARN_MINSPEED, state);
878 private void updateFanLearnMaxSpeed(String[] messageParts) {
879 if (messageParts.length != 4) {
880 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
883 logger.debug("Process fan learn max speed update for {}: {}", thing.getUID(), messageParts[3]);
884 PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
885 updateChannel(CHANNEL_FAN_LEARN_MAXSPEED, state);
886 fanStateMap.put(CHANNEL_FAN_LEARN_MAXSPEED, state);
889 private void updateLightPower(String[] messageParts) {
890 if (messageParts.length != 4) {
891 logger.debug("LIGHT;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
894 logger.debug("Process light power update for {}: {}", thing.getUID(), messageParts[3]);
895 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
896 updateChannel(CHANNEL_LIGHT_POWER, state);
897 fanStateMap.put(CHANNEL_LIGHT_POWER, state);
900 private void updateLightLevel(String[] messageParts) {
901 if (messageParts.length != 5) {
902 logger.debug("LIGHT;LEVEL has unexpected number of parameters: {}", Arrays.toString(messageParts));
905 logger.debug("Process light level update for {}: {}", thing.getUID(), messageParts[4]);
906 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
907 updateChannel(CHANNEL_LIGHT_LEVEL, state);
908 fanStateMap.put(CHANNEL_LIGHT_LEVEL, state);
911 private void updateLightHue(String[] messageParts) {
912 if (messageParts.length != 6) {
913 logger.debug("LIGHT;COLOR;TEMP;VALUE has unexpected number of parameters: {}",
914 Arrays.toString(messageParts));
917 logger.debug("Process light hue update for {}: {}", thing.getUID(), messageParts[4]);
918 PercentType state = BigAssFanConverter.hueToPercent(messageParts[5]);
919 updateChannel(CHANNEL_LIGHT_HUE, state);
920 fanStateMap.put(CHANNEL_LIGHT_HUE, state);
923 private void updateLightAuto(String[] messageParts) {
924 if (messageParts.length != 4) {
925 logger.debug("LIGHT;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
928 logger.debug("Process light auto update for {}: {}", thing.getUID(), messageParts[3]);
929 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
930 updateChannel(CHANNEL_LIGHT_AUTO, state);
931 fanStateMap.put(CHANNEL_LIGHT_AUTO, state);
934 private void updateLightLevelMin(String[] messageParts) {
935 if (messageParts.length != 5) {
936 logger.debug("LightLevelMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
939 logger.debug("Process light level min update for {}: {}", thing.getUID(), messageParts[4]);
940 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
941 updateChannel(CHANNEL_LIGHT_LEVEL_MIN, state);
942 fanStateMap.put(CHANNEL_LIGHT_LEVEL_MIN, state);
945 private void updateLightLevelMax(String[] messageParts) {
946 if (messageParts.length != 5) {
947 logger.debug("LightLevelMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
950 logger.debug("Process light level max update for {}: {}", thing.getUID(), messageParts[4]);
951 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
952 updateChannel(CHANNEL_LIGHT_LEVEL_MAX, state);
953 fanStateMap.put(CHANNEL_LIGHT_LEVEL_MAX, state);
956 private void updateLightPresent(String[] messageParts) {
957 if (messageParts.length < 4) {
958 logger.debug("LightPresent has unexpected number of parameters: {}", Arrays.toString(messageParts));
961 logger.debug("Process light present update for {}: {}", thing.getUID(), messageParts[3]);
962 StringType lightPresent = new StringType(messageParts[3]);
963 updateChannel(CHANNEL_LIGHT_PRESENT, lightPresent);
964 fanStateMap.put(CHANNEL_LIGHT_PRESENT, lightPresent);
965 if (messageParts.length == 5) {
966 logger.debug("Light supports hue adjustment");
967 StringType lightColor = new StringType(messageParts[4]);
968 updateChannel(CHANNEL_LIGHT_COLOR, lightColor);
969 fanStateMap.put(CHANNEL_LIGHT_COLOR, lightColor);
973 private void updateMotion(String[] messageParts) {
974 if (messageParts.length != 4) {
975 logger.debug("SNSROCC has unexpected number of parameters: {}", Arrays.toString(messageParts));
978 logger.debug("Process motion sensor update for {}: {}", thing.getUID(), messageParts[3]);
979 OnOffType state = "OCCUPIED".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
980 updateChannel(CHANNEL_MOTION, state);
981 fanStateMap.put(CHANNEL_MOTION, state);
984 private void updateTime(String[] messageParts) {
985 if (messageParts.length != 4) {
986 logger.debug("TIME has unexpected number of parameters: {}", Arrays.toString(messageParts));
989 logger.debug("Process time update for {}: {}", thing.getUID(), messageParts[3]);
990 // (mac|name;TIME;VALUE;2017-03-26T14:06:27Z)
992 Instant instant = Instant.parse(messageParts[3]);
993 DateTimeType state = new DateTimeType(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()));
994 updateChannel(CHANNEL_TIME, state);
995 fanStateMap.put(CHANNEL_TIME, state);
996 } catch (DateTimeParseException e) {
997 logger.info("Failed to parse date received from {}: {}", thing.getUID(), messageParts[3]);
1003 * The {@link ConnectionManager} class is responsible for managing the state of the TCP connection to the
1006 * @author Mark Hilbush - Initial contribution
1008 private class ConnectionManager {
1009 private Logger logger = LoggerFactory.getLogger(ConnectionManager.class);
1011 private boolean deviceIsConnected;
1013 private InetAddress ifAddress;
1014 private Socket fanSocket;
1015 private Scanner fanScanner;
1016 private DataOutputStream fanWriter;
1017 private final int SOCKET_CONNECT_TIMEOUT = 1500;
1019 ScheduledFuture<?> connectionMonitorJob;
1020 private final long CONNECTION_MONITOR_FREQ = 120L;
1021 private final long CONNECTION_MONITOR_DELAY = 30L;
1023 Runnable connectionMonitorRunnable = () -> {
1024 logger.trace("Performing connection check for {} at IP {}", thing.getUID(), ipAddress);
1028 public ConnectionManager(String ipv4Address) {
1029 deviceIsConnected = false;
1031 ifAddress = InetAddress.getByName(ipv4Address);
1032 logger.debug("Handler for {} using address {} on network interface {}", thing.getUID(),
1033 ifAddress.getHostAddress(), NetworkInterface.getByInetAddress(ifAddress).getName());
1034 } catch (UnknownHostException e) {
1035 logger.warn("Handler for {} got UnknownHostException getting local IPv4 net interface: {}",
1036 thing.getUID(), e.getMessage(), e);
1037 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1038 } catch (SocketException e) {
1039 logger.warn("Handler for {} got SocketException getting local IPv4 network interface: {}",
1040 thing.getUID(), e.getMessage(), e);
1041 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1046 * Connect to the command and serial port(s) on the device. The serial connections are established only for
1047 * devices that support serial.
1049 protected synchronized void connect() {
1050 if (isConnected()) {
1053 logger.trace("Connecting to {} at {}", thing.getUID(), ipAddress);
1057 fanSocket = new Socket();
1058 fanSocket.bind(new InetSocketAddress(ifAddress, 0));
1059 fanSocket.connect(new InetSocketAddress(ipAddress, BAF_PORT), SOCKET_CONNECT_TIMEOUT);
1060 } catch (IOException e) {
1061 logger.debug("IOException connecting to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage());
1062 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1069 fanWriter = new DataOutputStream(fanSocket.getOutputStream());
1070 fanScanner = new Scanner(fanSocket.getInputStream());
1071 fanScanner.useDelimiter("[)]");
1072 } catch (IOException e) {
1073 logger.warn("IOException getting streams for {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1075 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1079 logger.info("Connected to {} at {}", thing.getUID(), ipAddress);
1080 deviceIsConnected = true;
1084 protected synchronized void disconnect() {
1085 if (!isConnected()) {
1088 logger.debug("Disconnecting from {} at {}", thing.getUID(), ipAddress);
1091 if (fanWriter != null) {
1094 if (fanScanner != null) {
1097 if (fanSocket != null) {
1100 } catch (IOException e) {
1101 logger.warn("IOException closing connection to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1104 deviceIsConnected = false;
1111 public String read() {
1112 if (fanScanner == null) {
1113 logger.warn("Scanner for {} is null when trying to scan from {}!", thing.getUID(), ipAddress);
1119 nextToken = fanScanner.next();
1120 } catch (NoSuchElementException e) {
1121 logger.debug("Scanner for {} threw NoSuchElementException; stream possibly closed", thing.getUID());
1122 // Force a reconnect to the device
1125 } catch (IllegalStateException e) {
1126 logger.debug("Scanner for {} threw IllegalStateException; scanner possibly closed", thing.getUID());
1128 } catch (BufferOverflowException e) {
1129 logger.debug("Scanner for {} threw BufferOverflowException", thing.getUID());
1135 public void write(byte[] buffer) throws IOException {
1136 if (fanWriter == null) {
1137 logger.warn("fanWriter for {} is null when trying to write to {}!!!", thing.getUID(), ipAddress);
1140 fanWriter.write(buffer, 0, buffer.length);
1143 private boolean isConnected() {
1144 return deviceIsConnected;
1148 * Periodically validate the command connection to the device by executing a getversion command.
1150 private void scheduleConnectionMonitorJob() {
1151 if (connectionMonitorJob == null) {
1152 logger.debug("Starting connection monitor job in {} seconds for {} at {}", CONNECTION_MONITOR_DELAY,
1153 thing.getUID(), ipAddress);
1154 connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable,
1155 CONNECTION_MONITOR_DELAY, CONNECTION_MONITOR_FREQ, TimeUnit.SECONDS);
1159 private void cancelConnectionMonitorJob() {
1160 if (connectionMonitorJob != null) {
1161 logger.debug("Canceling connection monitor job for {} at {}", thing.getUID(), ipAddress);
1162 connectionMonitorJob.cancel(true);
1163 connectionMonitorJob = null;
1167 private void checkConnection() {
1168 logger.trace("Checking status of connection for {} at {}", thing.getUID(), ipAddress);
1169 if (!isConnected()) {
1170 logger.debug("Connection check FAILED for {} at {}", thing.getUID(), ipAddress);
1173 logger.debug("Connection check OK for {} at {}", thing.getUID(), ipAddress);
1174 logger.debug("Requesting status update from {} at {}", thing.getUID(), ipAddress);
1175 sendCommand(macAddress, ";GETALL");
1176 sendCommand(macAddress, ";SNSROCC;STATUS;GET");