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.openhab.binding.bigassfan.internal.BigAssFanConfig;
44 import org.openhab.binding.bigassfan.internal.utils.BigAssFanConverter;
45 import org.openhab.core.common.ThreadPoolManager;
46 import org.openhab.core.library.types.DateTimeType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.PercentType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.thing.Channel;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.binding.BaseThingHandler;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.openhab.core.types.State;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
63 * The {@link BigAssFanHandler} is responsible for handling commands, which are
64 * sent to one of the channels.
66 * @author Mark Hilbush - Initial contribution
68 public class BigAssFanHandler extends BaseThingHandler {
69 private final Logger logger = LoggerFactory.getLogger(BigAssFanHandler.class);
71 private static final StringType LIGHT_COLOR = new StringType("COLOR");
72 private static final StringType LIGHT_PRESENT = new StringType("PRESENT");
74 private static final StringType OFF = new StringType("OFF");
75 private static final StringType COOLING = new StringType("COOLING");
76 private static final StringType HEATING = new StringType("HEATING");
78 private BigAssFanConfig config;
79 private String label = null;
80 private String ipAddress = null;
81 private String macAddress = null;
83 private FanListener fanListener;
85 protected Map<String, State> fanStateMap = Collections.synchronizedMap(new HashMap<>());
87 public BigAssFanHandler(Thing thing, String ipv4Address) {
91 logger.debug("Creating FanListener object for {}", thing.getUID());
92 fanListener = new FanListener(ipv4Address);
96 public void initialize() {
97 logger.debug("BigAssFanHandler for {} is initializing", thing.getUID());
99 config = getConfig().as(BigAssFanConfig.class);
100 logger.debug("BigAssFanHandler config for {} is {}", thing.getUID(), config);
102 if (!config.isValid()) {
103 logger.debug("BigAssFanHandler config of {} is invalid. Check configuration", thing.getUID());
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
105 "Invalid BigAssFan config. Check configuration.");
108 label = config.getLabel();
109 ipAddress = config.getIpAddress();
110 macAddress = config.getMacAddress();
112 fanListener.startFanListener();
116 public void dispose() {
117 logger.debug("BigAssFanHandler for {} is disposing", thing.getUID());
118 fanListener.stopFanListener();
122 public void handleCommand(ChannelUID channelUID, Command command) {
123 if (command instanceof RefreshType) {
127 logger.debug("Handle command for {} on channel {}: {}", thing.getUID(), channelUID, command);
128 if (channelUID.getId().equals(CHANNEL_FAN_POWER)) {
129 handleFanPower(command);
130 } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED)) {
131 handleFanSpeed(command);
132 } else if (channelUID.getId().equals(CHANNEL_FAN_AUTO)) {
133 handleFanAuto(command);
134 } else if (channelUID.getId().equals(CHANNEL_FAN_WHOOSH)) {
135 handleFanWhoosh(command);
136 } else if (channelUID.getId().equals(CHANNEL_FAN_SMARTMODE)) {
137 handleFanSmartmode(command);
138 } else if (channelUID.getId().equals(CHANNEL_FAN_LEARN_MINSPEED)) {
139 handleFanLearnSpeedMin(command);
140 } else if (channelUID.getId().equals(CHANNEL_FAN_LEARN_MAXSPEED)) {
141 handleFanLearnSpeedMax(command);
142 } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED_MIN)) {
143 handleFanSpeedMin(command);
144 } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED_MAX)) {
145 handleFanSpeedMax(command);
146 } else if (channelUID.getId().equals(CHANNEL_FAN_WINTERMODE)) {
147 handleFanWintermode(command);
148 } else if (channelUID.getId().equals(CHANNEL_LIGHT_POWER)) {
149 handleLightPower(command);
150 } else if (channelUID.getId().equals(CHANNEL_LIGHT_LEVEL)) {
151 handleLightLevel(command);
152 } else if (channelUID.getId().equals(CHANNEL_LIGHT_HUE)) {
153 handleLightHue(command);
154 } else if (channelUID.getId().equals(CHANNEL_LIGHT_AUTO)) {
155 handleLightAuto(command);
156 } else if (channelUID.getId().equals(CHANNEL_LIGHT_SMARTER)) {
157 handleLightSmarter(command);
158 } else if (channelUID.getId().equals(CHANNEL_LIGHT_LEVEL_MIN)) {
159 handleLightLevelMin(command);
160 } else if (channelUID.getId().equals(CHANNEL_LIGHT_LEVEL_MAX)) {
161 handleLightLevelMax(command);
162 } else if (channelUID.getId().equals(CHANNEL_FAN_SLEEP)) {
163 handleSleep(command);
165 logger.debug("Received command for {} on unknown channel {}", thing.getUID(), channelUID.getId());
169 private void handleFanPower(Command command) {
170 logger.debug("Handling fan power command for {}: {}", thing.getUID(), command);
172 // <mac;FAN;PWR;ON|OFF>
173 if (command instanceof OnOffType) {
174 if (command.equals(OnOffType.OFF)) {
175 sendCommand(macAddress, ";FAN;PWR;OFF");
176 } else if (command.equals(OnOffType.ON)) {
177 sendCommand(macAddress, ";FAN;PWR;ON");
182 private void handleFanSpeed(Command command) {
183 logger.debug("Handling fan speed command for {}: {}", thing.getUID(), command);
185 // <mac;FAN;SPD;SET;0..7>
186 if (command instanceof PercentType) {
187 sendCommand(macAddress, ";FAN;SPD;SET;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
191 private void handleFanAuto(Command command) {
192 logger.debug("Handling fan auto command {}", command);
194 // <mac;FAN;AUTO;ON|OFF>
195 if (command instanceof OnOffType) {
196 if (command.equals(OnOffType.OFF)) {
197 sendCommand(macAddress, ";FAN;AUTO;OFF");
198 } else if (command.equals(OnOffType.ON)) {
199 sendCommand(macAddress, ";FAN;AUTO;ON");
204 private void handleFanWhoosh(Command command) {
205 logger.debug("Handling fan whoosh command {}", command);
207 // <mac;FAN;WHOOSH;ON|OFF>
208 if (command instanceof OnOffType) {
209 if (command.equals(OnOffType.OFF)) {
210 sendCommand(macAddress, ";FAN;WHOOSH;OFF");
211 } else if (command.equals(OnOffType.ON)) {
212 sendCommand(macAddress, ";FAN;WHOOSH;ON");
217 private void handleFanSmartmode(Command command) {
218 logger.debug("Handling fan smartmode command {}", command);
220 // <mac;SMARTMODE;SET;OFF/COOLING/HEATING>
221 if (command instanceof StringType) {
222 if (command.equals(OFF)) {
223 sendCommand(macAddress, ";SMARTMODE;STATE;SET;OFF");
224 } else if (command.equals(COOLING)) {
225 sendCommand(macAddress, ";SMARTMODE;STATE;SET;COOLING");
226 } else if (command.equals(HEATING)) {
227 sendCommand(macAddress, ";SMARTMODE;STATE;SET;HEATING");
229 logger.debug("Unknown fan smartmode command: {}", command);
234 private void handleFanLearnSpeedMin(Command command) {
235 logger.debug("Handling fan learn speed minimum command {}", command);
236 // <mac;FAN;SPD;SET;MIN;0..7>
237 if (command instanceof PercentType) {
238 // Send min speed set command
239 sendCommand(macAddress,
240 ";LEARN;MINSPEED;SET;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
241 fanStateMap.put(CHANNEL_FAN_LEARN_MINSPEED, (PercentType) command);
242 // Don't let max be less than min
243 adjustMaxSpeed((PercentType) command, CHANNEL_FAN_LEARN_MAXSPEED, ";LEARN;MAXSPEED;");
247 private void handleFanLearnSpeedMax(Command command) {
248 logger.debug("Handling fan learn speed maximum command {}", command);
249 // <mac;FAN;SPD;SET;MAX;0..7>
250 if (command instanceof PercentType) {
251 // Send max speed set command
252 sendCommand(macAddress,
253 ";LEARN;MAXSPEED;SET;;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
254 fanStateMap.put(CHANNEL_FAN_LEARN_MAXSPEED, (PercentType) command);
255 // Don't let min be greater than max
256 adjustMinSpeed((PercentType) command, CHANNEL_FAN_LEARN_MINSPEED, ";LEARN;MINSPEED;");
260 private void handleFanSpeedMin(Command command) {
261 logger.debug("Handling fan speed minimum command {}", command);
262 // <mac;FAN;SPD;SET;MIN;0..7>
263 if (command instanceof PercentType) {
264 // Send min speed set command
265 sendCommand(macAddress,
266 ";FAN;SPD;SET;MIN;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
267 fanStateMap.put(CHANNEL_FAN_SPEED_MIN, (PercentType) command);
268 // Don't let max be less than min
269 adjustMaxSpeed((PercentType) command, CHANNEL_FAN_SPEED_MAX, ";FAN;SPD;SET;MAX;");
273 private void handleFanSpeedMax(Command command) {
274 logger.debug("Handling fan speed maximum command {}", command);
275 // <mac;FAN;SPD;SET;MAX;0..7>
276 if (command instanceof PercentType) {
277 // Send max speed set command
278 sendCommand(macAddress,
279 ";FAN;SPD;SET;MAX;".concat(BigAssFanConverter.percentToSpeed((PercentType) command)));
280 fanStateMap.put(CHANNEL_FAN_SPEED_MAX, (PercentType) command);
281 // Don't let min be greater than max
282 adjustMinSpeed((PercentType) command, CHANNEL_FAN_SPEED_MIN, ";FAN;SPD;SET;MIN;");
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 if (fanStateMap.get(channelId) != null) {
316 currentMax = ((PercentType) fanStateMap.get(channelId)).intValue();
318 if (newMin > currentMax) {
319 updateState(CHANNEL_FAN_SPEED_MAX, command);
320 sendCommand(macAddress, commandFragment.concat(BigAssFanConverter.percentToSpeed(command)));
324 private void adjustMinSpeed(PercentType command, String channelId, String commandFragment) {
325 int newMax = command.intValue();
326 int currentMin = PercentType.HUNDRED.intValue();
327 if (fanStateMap.get(channelId) != null) {
328 currentMin = ((PercentType) fanStateMap.get(channelId)).intValue();
330 if (newMax < currentMin) {
331 updateState(channelId, command);
332 sendCommand(macAddress, commandFragment.concat(BigAssFanConverter.percentToSpeed(command)));
336 private void handleLightPower(Command command) {
337 if (!isLightPresent()) {
338 logger.debug("Fan does not have light installed for command {}", command);
342 logger.debug("Handling light power command {}", command);
343 // <mac;LIGHT;PWR;ON|OFF>
344 if (command instanceof OnOffType) {
345 if (command.equals(OnOffType.OFF)) {
346 sendCommand(macAddress, ";LIGHT;PWR;OFF");
347 } else if (command.equals(OnOffType.ON)) {
348 sendCommand(macAddress, ";LIGHT;PWR;ON");
353 private void handleLightLevel(Command command) {
354 if (!isLightPresent()) {
355 logger.debug("Fan does not have light installed for command {}", command);
359 logger.debug("Handling light level command {}", command);
360 // <mac;LIGHT;LEVEL;SET;0..16>
361 if (command instanceof PercentType) {
362 sendCommand(macAddress,
363 ";LIGHT;LEVEL;SET;".concat(BigAssFanConverter.percentToLevel((PercentType) command)));
367 private void handleLightHue(Command command) {
368 if (!isLightPresent() || !isLightColor()) {
369 logger.debug("Fan does not have light installed or does not support hue for command {}", command);
373 logger.debug("Handling light hue command {}", command);
374 // <mac;LIGHT;COLOR;TEMP;SET;2200..5000>
375 if (command instanceof PercentType) {
376 sendCommand(macAddress,
377 ";LIGHT;COLOR;TEMP;VALUE;SET;".concat(BigAssFanConverter.percentToHue((PercentType) command)));
381 private void handleLightAuto(Command command) {
382 if (!isLightPresent()) {
383 logger.debug("Fan does not have light installed for command {}", command);
387 logger.debug("Handling light auto command {}", command);
388 // <mac;LIGHT;AUTO;ON|OFF>
389 if (command instanceof OnOffType) {
390 if (command.equals(OnOffType.OFF)) {
391 sendCommand(macAddress, ";LIGHT;AUTO;OFF");
392 } else if (command.equals(OnOffType.ON)) {
393 sendCommand(macAddress, ";LIGHT;AUTO;ON");
398 private void handleLightSmarter(Command command) {
399 if (!isLightPresent()) {
400 logger.debug("Fan does not have light installed for command {}", command);
404 logger.debug("Handling light smartmode command {}", command);
405 // <mac;LIGHT;SMART;ON/OFF>
406 if (command instanceof OnOffType) {
407 if (command.equals(OnOffType.OFF)) {
408 sendCommand(macAddress, ";LIGHT;SMART;OFF");
409 } else if (command.equals(OnOffType.ON)) {
410 sendCommand(macAddress, ";LIGHT;SMART;ON");
415 private void handleLightLevelMin(Command command) {
416 if (!isLightPresent()) {
417 logger.debug("Fan does not have light installed for command {}", command);
421 logger.debug("Handling light level minimum command {}", command);
422 // <mac;LIGHT;LEVEL;MIN;0-16>
423 if (command instanceof PercentType) {
424 // Send min light level set command
425 sendCommand(macAddress,
426 ";LIGHT;LEVEL;MIN;".concat(BigAssFanConverter.percentToLevel((PercentType) command)));
427 // Don't let max be less than min
428 adjustMaxLevel((PercentType) command);
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) {
441 // Send max light level set command
442 sendCommand(macAddress,
443 ";LIGHT;LEVEL;MAX;".concat(BigAssFanConverter.percentToLevel((PercentType) command)));
444 // Don't let min be greater than max
445 adjustMinLevel((PercentType) command);
449 private void adjustMaxLevel(PercentType command) {
450 int newMin = command.intValue();
451 int currentMax = PercentType.ZERO.intValue();
452 if (fanStateMap.get(CHANNEL_LIGHT_LEVEL_MAX) != null) {
453 currentMax = ((PercentType) fanStateMap.get(CHANNEL_LIGHT_LEVEL_MAX)).intValue();
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 if (fanStateMap.get(CHANNEL_LIGHT_LEVEL_MIN) != null) {
465 currentMin = ((PercentType) fanStateMap.get(CHANNEL_LIGHT_LEVEL_MIN)).intValue();
467 if (newMax < currentMin) {
468 updateState(CHANNEL_LIGHT_LEVEL_MIN, command);
469 sendCommand(macAddress, ";LIGHT;LEVEL;MIN;".concat(BigAssFanConverter.percentToLevel(command)));
473 private boolean isLightPresent() {
474 return fanStateMap.containsKey(CHANNEL_LIGHT_PRESENT)
475 && LIGHT_PRESENT.equals(fanStateMap.get(CHANNEL_LIGHT_PRESENT));
478 private boolean isLightColor() {
479 return fanStateMap.containsKey(CHANNEL_LIGHT_COLOR) && LIGHT_COLOR.equals(fanStateMap.get(CHANNEL_LIGHT_COLOR));
483 * Send a command to the fan
485 private void sendCommand(String mac, String commandFragment) {
486 if (fanListener == null) {
487 logger.error("Unable to send message to {} because fanListener object is null!", thing.getUID());
491 StringBuilder sb = new StringBuilder();
492 sb.append("<").append(mac).append(commandFragment).append(">");
493 String message = sb.toString();
494 logger.trace("Sending message to {} at {}: {}", thing.getUID(), ipAddress, message);
495 fanListener.send(message);
498 private void updateChannel(String channelName, State state) {
499 Channel channel = thing.getChannel(channelName);
500 if (channel != null) {
501 updateState(channel.getUID(), state);
506 * Manage the ONLINE/OFFLINE status of the thing
508 private void markOnline() {
510 logger.debug("Changing status of {} from {}({}) to ONLINE", thing.getUID(), getStatus(), getDetail());
511 updateStatus(ThingStatus.ONLINE);
515 private void markOffline() {
517 logger.debug("Changing status of {} from {}({}) to OFFLINE", thing.getUID(), getStatus(), getDetail());
518 updateStatus(ThingStatus.OFFLINE);
522 private void markOfflineWithMessage(ThingStatusDetail statusDetail, String statusMessage) {
523 // If it's offline with no detail or if it's not offline, mark it offline with detailed status
524 if ((isOffline() && getDetail() == ThingStatusDetail.NONE) || !isOffline()) {
525 logger.debug("Changing status of {} from {}({}) to OFFLINE({})", thing.getUID(), getStatus(), getDetail(),
527 updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage);
532 private boolean isOnline() {
533 return thing.getStatus().equals(ThingStatus.ONLINE);
536 private boolean isOffline() {
537 return thing.getStatus().equals(ThingStatus.OFFLINE);
540 private ThingStatus getStatus() {
541 return thing.getStatus();
544 private ThingStatusDetail getDetail() {
545 return thing.getStatusInfo().getStatusDetail();
549 * The {@link FanListener} is responsible for sending and receiving messages to a fan.
551 * @author Mark Hilbush - Initial contribution
553 public class FanListener {
554 private final Logger logger = LoggerFactory.getLogger(FanListener.class);
556 // Our own thread pool for the long-running listener job
557 private ScheduledExecutorService scheduledExecutorService = ThreadPoolManager
558 .getScheduledPool("bigassfanHandler" + "-" + thing.getUID());
559 private ScheduledFuture<?> listenerJob;
561 private final long FAN_LISTENER_DELAY = 2L;
562 private boolean terminate;
564 private final Pattern messagePattern = Pattern.compile("[(](.*)");
566 private ConnectionManager conn;
568 private Runnable fanListenerRunnable = () -> {
571 } catch (RuntimeException e) {
572 logger.warn("FanListener for {} had unhandled exception: {}", thing.getUID(), e.getMessage(), e);
576 public FanListener(String ipv4Address) {
577 conn = new ConnectionManager(ipv4Address);
580 public void startFanListener() {
582 conn.scheduleConnectionMonitorJob();
584 if (listenerJob == null) {
586 logger.debug("Starting listener in {} sec for {} at {}", FAN_LISTENER_DELAY, thing.getUID(), ipAddress);
587 listenerJob = scheduledExecutorService.schedule(fanListenerRunnable, FAN_LISTENER_DELAY,
592 public void stopFanListener() {
593 if (listenerJob != null) {
594 logger.debug("Stopping listener for {} at {}", thing.getUID(), ipAddress);
596 listenerJob.cancel(true);
599 conn.cancelConnectionMonitorJob();
603 public void send(String command) {
604 if (!conn.isConnected()) {
605 logger.debug("Unable to send message; no connection to {}. Trying to reconnect: {}", thing.getUID(),
608 if (!conn.isConnected()) {
613 logger.debug("Sending message to {} at {}: {}", thing.getUID(), ipAddress, command);
616 buffer = command.getBytes(CHARSET);
617 } catch (UnsupportedEncodingException e) {
618 logger.warn("Unable to convert to string using {} charset: {}", CHARSET, e.getMessage(), e);
623 } catch (IOException e) {
624 logger.warn("IO exception writing message to socket: {}", e.getMessage(), e);
629 private void listener() {
630 logger.debug("Fan listener thread is running for {} at {}", thing.getUID(), ipAddress);
634 // Wait for a message
635 processMessage(waitForMessage());
636 } catch (IOException ioe) {
637 logger.warn("Listener for {} got IO exception waiting for message: {}", thing.getUID(),
638 ioe.getMessage(), ioe);
642 logger.debug("Fan listener thread is exiting for {} at {}", thing.getUID(), ipAddress);
645 private String waitForMessage() throws IOException {
646 if (!conn.isConnected()) {
647 if (logger.isTraceEnabled()) {
648 logger.trace("FanListener for {} can't receive message. No connection to fan", thing.getUID());
652 } catch (InterruptedException e) {
656 return readMessage();
659 private String readMessage() {
660 logger.trace("Waiting for message from {} at {}", thing.getUID(), ipAddress);
661 String message = conn.read();
662 if (message != null) {
663 logger.trace("FanListener for {} received message of length {}: {}", thing.getUID(), message.length(),
669 private void processMessage(String incomingMessage) {
670 if (incomingMessage == null || incomingMessage.isEmpty()) {
675 logger.debug("FanListener for {} received message from {}: {}", thing.getUID(), macAddress,
677 Matcher matcher = messagePattern.matcher(incomingMessage);
678 if (!matcher.find()) {
679 logger.debug("Unable to process message from {}, not in expected format: {}", thing.getUID(),
684 String message = matcher.group(1);
685 String[] messageParts = message.split(";");
687 // Check to make sure it is my MAC address or my label
688 if (!isMe(messageParts[0])) {
689 logger.trace("Message not for me ({}): {}", messageParts[0], macAddress);
693 logger.trace("Message is for me ({}): {}", messageParts[0], macAddress);
694 String messageUpperCase = message.toUpperCase();
695 if (messageUpperCase.contains(";FAN;PWR;")) {
696 updateFanPower(messageParts);
697 } else if (messageUpperCase.contains(";FAN;SPD;ACTUAL;")) {
698 updateFanSpeed(messageParts);
699 } else if (messageUpperCase.contains(";FAN;DIR;")) {
700 updateFanDirection(messageParts);
701 } else if (messageUpperCase.contains(";FAN;AUTO;")) {
702 updateFanAuto(messageParts);
703 } else if (messageUpperCase.contains(";FAN;WHOOSH;STATUS;")) {
704 updateFanWhoosh(messageParts);
705 } else if (messageUpperCase.contains(";WINTERMODE;STATE;")) {
706 updateFanWintermode(messageParts);
707 } else if (messageUpperCase.contains(";SMARTMODE;STATE;")) {
708 updateFanSmartmode(messageParts);
709 } else if (messageUpperCase.contains(";FAN;SPD;MIN;")) {
710 updateFanSpeedMin(messageParts);
711 } else if (messageUpperCase.contains(";FAN;SPD;MAX;")) {
712 updateFanSpeedMax(messageParts);
713 } else if (messageUpperCase.contains(";SLEEP;STATE")) {
714 updateFanSleepMode(messageParts);
715 } else if (messageUpperCase.contains(";LEARN;MINSPEED;")) {
716 updateFanLearnMinSpeed(messageParts);
717 } else if (messageUpperCase.contains(";LEARN;MAXSPEED;")) {
718 updateFanLearnMaxSpeed(messageParts);
719 } else if (messageUpperCase.contains(";LIGHT;PWR;")) {
720 updateLightPower(messageParts);
721 } else if (messageUpperCase.contains(";LIGHT;LEVEL;ACTUAL;")) {
722 updateLightLevel(messageParts);
723 } else if (messageUpperCase.contains(";LIGHT;COLOR;TEMP;VALUE;")) {
724 updateLightHue(messageParts);
725 } else if (messageUpperCase.contains(";LIGHT;AUTO;")) {
726 updateLightAuto(messageParts);
727 } else if (messageUpperCase.contains(";LIGHT;LEVEL;MIN;")) {
728 updateLightLevelMin(messageParts);
729 } else if (messageUpperCase.contains(";LIGHT;LEVEL;MAX;")) {
730 updateLightLevelMax(messageParts);
731 } else if (messageUpperCase.contains(";DEVICE;LIGHT;")) {
732 updateLightPresent(messageParts);
733 } else if (messageUpperCase.contains(";SNSROCC;STATUS;")) {
734 updateMotion(messageParts);
735 } else if (messageUpperCase.contains(";TIME;VALUE;")) {
736 updateTime(messageParts);
738 logger.trace("Received unsupported message from {}: {}", thing.getUID(), message);
742 private boolean isMe(String idFromDevice) {
743 // Check match on MAC address
744 if (macAddress.equalsIgnoreCase(idFromDevice)) {
747 // Didn't match MAC address, check match for label
748 if (label.equalsIgnoreCase(idFromDevice)) {
754 private void updateFanPower(String[] messageParts) {
755 if (messageParts.length != 4) {
756 if (logger.isDebugEnabled()) {
757 logger.debug("FAN;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
761 logger.debug("Process fan power update for {}: {}", thing.getUID(), messageParts[3]);
762 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
763 updateChannel(CHANNEL_FAN_POWER, state);
764 fanStateMap.put(CHANNEL_FAN_POWER, state);
767 private void updateFanSpeed(String[] messageParts) {
768 if (messageParts.length != 5) {
769 logger.debug("FAN;SPD;ACTUAL has unexpected number of parameters: {}", Arrays.toString(messageParts));
772 logger.debug("Process fan speed update for {}: {}", thing.getUID(), messageParts[4]);
773 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
774 updateChannel(CHANNEL_FAN_SPEED, state);
775 fanStateMap.put(CHANNEL_FAN_SPEED, state);
778 private void updateFanDirection(String[] messageParts) {
779 if (messageParts.length != 4) {
780 logger.debug("FAN;DIR has unexpected number of parameters: {}", Arrays.toString(messageParts));
783 logger.debug("Process fan direction update for {}: {}", thing.getUID(), messageParts[3]);
784 StringType state = new StringType(messageParts[3]);
785 updateChannel(CHANNEL_FAN_DIRECTION, state);
786 fanStateMap.put(CHANNEL_FAN_DIRECTION, state);
789 private void updateFanAuto(String[] messageParts) {
790 if (messageParts.length != 4) {
791 logger.debug("FAN;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
794 logger.debug("Process fan auto update for {}: {}", thing.getUID(), messageParts[3]);
795 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
796 updateChannel(CHANNEL_FAN_AUTO, state);
797 fanStateMap.put(CHANNEL_FAN_AUTO, state);
800 private void updateFanWhoosh(String[] messageParts) {
801 if (messageParts.length != 5) {
802 logger.debug("FAN;WHOOSH has unexpected number of parameters: {}", Arrays.toString(messageParts));
805 logger.debug("Process fan whoosh update for {}: {}", thing.getUID(), messageParts[4]);
806 OnOffType state = "ON".equalsIgnoreCase(messageParts[4]) ? OnOffType.ON : OnOffType.OFF;
807 updateChannel(CHANNEL_FAN_WHOOSH, state);
808 fanStateMap.put(CHANNEL_FAN_WHOOSH, state);
811 private void updateFanWintermode(String[] messageParts) {
812 if (messageParts.length != 4) {
813 logger.debug("WINTERMODE;STATE has unexpected number of parameters: {}", Arrays.toString(messageParts));
816 logger.debug("Process fan wintermode update for {}: {}", thing.getUID(), messageParts[3]);
817 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
818 updateChannel(CHANNEL_FAN_WINTERMODE, state);
819 fanStateMap.put(CHANNEL_FAN_WINTERMODE, state);
822 private void updateFanSmartmode(String[] messageParts) {
823 if (messageParts.length != 4) {
824 logger.debug("Smartmode has unexpected number of parameters: {}", Arrays.toString(messageParts));
827 logger.debug("Process fan smartmode update for {}: {}", thing.getUID(), messageParts[3]);
828 StringType state = new StringType(messageParts[3]);
829 updateChannel(CHANNEL_FAN_SMARTMODE, state);
830 fanStateMap.put(CHANNEL_FAN_SMARTMODE, state);
833 private void updateFanSpeedMin(String[] messageParts) {
834 if (messageParts.length != 5) {
835 logger.debug("FanSpeedMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
838 logger.debug("Process fan min speed update for {}: {}", thing.getUID(), messageParts[4]);
839 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
840 updateChannel(CHANNEL_FAN_SPEED_MIN, state);
841 fanStateMap.put(CHANNEL_FAN_SPEED_MIN, state);
844 private void updateFanSpeedMax(String[] messageParts) {
845 if (messageParts.length != 5) {
846 logger.debug("FanSpeedMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
849 logger.debug("Process fan speed max update for {}: {}", thing.getUID(), messageParts[4]);
850 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
851 updateChannel(CHANNEL_FAN_SPEED_MAX, state);
852 fanStateMap.put(CHANNEL_FAN_SPEED_MAX, state);
855 private void updateFanSleepMode(String[] messageParts) {
856 if (messageParts.length != 4) {
857 logger.debug("SLEEP;STATE; has unexpected number of parameters: {}", Arrays.toString(messageParts));
860 logger.debug("Process fan sleep mode for {}: {}", thing.getUID(), messageParts[3]);
861 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
862 updateChannel(CHANNEL_FAN_SLEEP, state);
863 fanStateMap.put(CHANNEL_FAN_SLEEP, state);
866 private void updateFanLearnMinSpeed(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 min speed update for {}: {}", thing.getUID(), messageParts[3]);
872 PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
873 updateChannel(CHANNEL_FAN_LEARN_MINSPEED, state);
874 fanStateMap.put(CHANNEL_FAN_LEARN_MINSPEED, state);
877 private void updateFanLearnMaxSpeed(String[] messageParts) {
878 if (messageParts.length != 4) {
879 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
882 logger.debug("Process fan learn max speed update for {}: {}", thing.getUID(), messageParts[3]);
883 PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
884 updateChannel(CHANNEL_FAN_LEARN_MAXSPEED, state);
885 fanStateMap.put(CHANNEL_FAN_LEARN_MAXSPEED, state);
888 private void updateLightPower(String[] messageParts) {
889 if (messageParts.length != 4) {
890 logger.debug("LIGHT;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
893 logger.debug("Process light power update for {}: {}", thing.getUID(), messageParts[3]);
894 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
895 updateChannel(CHANNEL_LIGHT_POWER, state);
896 fanStateMap.put(CHANNEL_LIGHT_POWER, state);
899 private void updateLightLevel(String[] messageParts) {
900 if (messageParts.length != 5) {
901 logger.debug("LIGHT;LEVEL has unexpected number of parameters: {}", Arrays.toString(messageParts));
904 logger.debug("Process light level update for {}: {}", thing.getUID(), messageParts[4]);
905 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
906 updateChannel(CHANNEL_LIGHT_LEVEL, state);
907 fanStateMap.put(CHANNEL_LIGHT_LEVEL, state);
910 private void updateLightHue(String[] messageParts) {
911 if (messageParts.length != 6) {
912 logger.debug("LIGHT;COLOR;TEMP;VALUE has unexpected number of parameters: {}",
913 Arrays.toString(messageParts));
916 logger.debug("Process light hue update for {}: {}", thing.getUID(), messageParts[4]);
917 PercentType state = BigAssFanConverter.hueToPercent(messageParts[5]);
918 updateChannel(CHANNEL_LIGHT_HUE, state);
919 fanStateMap.put(CHANNEL_LIGHT_HUE, state);
922 private void updateLightAuto(String[] messageParts) {
923 if (messageParts.length != 4) {
924 logger.debug("LIGHT;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
927 logger.debug("Process light auto update for {}: {}", thing.getUID(), messageParts[3]);
928 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
929 updateChannel(CHANNEL_LIGHT_AUTO, state);
930 fanStateMap.put(CHANNEL_LIGHT_AUTO, state);
933 private void updateLightLevelMin(String[] messageParts) {
934 if (messageParts.length != 5) {
935 logger.debug("LightLevelMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
938 logger.debug("Process light level min update for {}: {}", thing.getUID(), messageParts[4]);
939 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
940 updateChannel(CHANNEL_LIGHT_LEVEL_MIN, state);
941 fanStateMap.put(CHANNEL_LIGHT_LEVEL_MIN, state);
944 private void updateLightLevelMax(String[] messageParts) {
945 if (messageParts.length != 5) {
946 logger.debug("LightLevelMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
949 logger.debug("Process light level max update for {}: {}", thing.getUID(), messageParts[4]);
950 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
951 updateChannel(CHANNEL_LIGHT_LEVEL_MAX, state);
952 fanStateMap.put(CHANNEL_LIGHT_LEVEL_MAX, state);
955 private void updateLightPresent(String[] messageParts) {
956 if (messageParts.length < 4) {
957 logger.debug("LightPresent has unexpected number of parameters: {}", Arrays.toString(messageParts));
960 logger.debug("Process light present update for {}: {}", thing.getUID(), messageParts[3]);
961 StringType lightPresent = new StringType(messageParts[3]);
962 updateChannel(CHANNEL_LIGHT_PRESENT, lightPresent);
963 fanStateMap.put(CHANNEL_LIGHT_PRESENT, lightPresent);
964 if (messageParts.length == 5) {
965 logger.debug("Light supports hue adjustment");
966 StringType lightColor = new StringType(messageParts[4]);
967 updateChannel(CHANNEL_LIGHT_COLOR, lightColor);
968 fanStateMap.put(CHANNEL_LIGHT_COLOR, lightColor);
972 private void updateMotion(String[] messageParts) {
973 if (messageParts.length != 4) {
974 logger.debug("SNSROCC has unexpected number of parameters: {}", Arrays.toString(messageParts));
977 logger.debug("Process motion sensor update for {}: {}", thing.getUID(), messageParts[3]);
978 OnOffType state = "OCCUPIED".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
979 updateChannel(CHANNEL_MOTION, state);
980 fanStateMap.put(CHANNEL_MOTION, state);
983 private void updateTime(String[] messageParts) {
984 if (messageParts.length != 4) {
985 logger.debug("TIME has unexpected number of parameters: {}", Arrays.toString(messageParts));
988 logger.debug("Process time update for {}: {}", thing.getUID(), messageParts[3]);
989 // (mac|name;TIME;VALUE;2017-03-26T14:06:27Z)
991 Instant instant = Instant.parse(messageParts[3]);
992 DateTimeType state = new DateTimeType(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()));
993 updateChannel(CHANNEL_TIME, state);
994 fanStateMap.put(CHANNEL_TIME, state);
995 } catch (DateTimeParseException e) {
996 logger.info("Failed to parse date received from {}: {}", thing.getUID(), messageParts[3]);
1002 * The {@link ConnectionManager} class is responsible for managing the state of the TCP connection to the
1005 * @author Mark Hilbush - Initial contribution
1007 private class ConnectionManager {
1008 private Logger logger = LoggerFactory.getLogger(ConnectionManager.class);
1010 private boolean deviceIsConnected;
1012 private InetAddress ifAddress;
1013 private Socket fanSocket;
1014 private Scanner fanScanner;
1015 private DataOutputStream fanWriter;
1016 private final int SOCKET_CONNECT_TIMEOUT = 1500;
1018 ScheduledFuture<?> connectionMonitorJob;
1019 private final long CONNECTION_MONITOR_FREQ = 120L;
1020 private final long CONNECTION_MONITOR_DELAY = 30L;
1022 Runnable connectionMonitorRunnable = () -> {
1023 logger.trace("Performing connection check for {} at IP {}", thing.getUID(), ipAddress);
1027 public ConnectionManager(String ipv4Address) {
1028 deviceIsConnected = false;
1030 ifAddress = InetAddress.getByName(ipv4Address);
1031 logger.debug("Handler for {} using address {} on network interface {}", thing.getUID(),
1032 ifAddress.getHostAddress(), NetworkInterface.getByInetAddress(ifAddress).getName());
1033 } catch (UnknownHostException e) {
1034 logger.warn("Handler for {} got UnknownHostException getting local IPv4 net interface: {}",
1035 thing.getUID(), e.getMessage(), e);
1036 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1037 } catch (SocketException e) {
1038 logger.warn("Handler for {} got SocketException getting local IPv4 network interface: {}",
1039 thing.getUID(), e.getMessage(), e);
1040 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1045 * Connect to the command and serial port(s) on the device. The serial connections are established only for
1046 * devices that support serial.
1048 protected synchronized void connect() {
1049 if (isConnected()) {
1052 logger.trace("Connecting to {} at {}", thing.getUID(), ipAddress);
1056 fanSocket = new Socket();
1057 fanSocket.bind(new InetSocketAddress(ifAddress, 0));
1058 fanSocket.connect(new InetSocketAddress(ipAddress, BAF_PORT), SOCKET_CONNECT_TIMEOUT);
1059 } catch (IOException e) {
1060 logger.debug("IOException connecting to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage());
1061 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1068 fanWriter = new DataOutputStream(fanSocket.getOutputStream());
1069 fanScanner = new Scanner(fanSocket.getInputStream());
1070 fanScanner.useDelimiter("[)]");
1071 } catch (IOException e) {
1072 logger.warn("IOException getting streams for {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1074 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1078 logger.info("Connected to {} at {}", thing.getUID(), ipAddress);
1079 deviceIsConnected = true;
1083 protected synchronized void disconnect() {
1084 if (!isConnected()) {
1087 logger.debug("Disconnecting from {} at {}", thing.getUID(), ipAddress);
1090 if (fanWriter != null) {
1093 if (fanScanner != null) {
1096 if (fanSocket != null) {
1099 } catch (IOException e) {
1100 logger.warn("IOException closing connection to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1103 deviceIsConnected = false;
1110 public String read() {
1111 if (fanScanner == null) {
1112 logger.warn("Scanner for {} is null when trying to scan from {}!", thing.getUID(), ipAddress);
1118 nextToken = fanScanner.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 if (fanWriter == null) {
1136 logger.warn("fanWriter for {} is null when trying to write to {}!!!", thing.getUID(), ipAddress);
1139 fanWriter.write(buffer, 0, buffer.length);
1142 private boolean isConnected() {
1143 return deviceIsConnected;
1147 * Periodically validate the command connection to the device by executing a getversion command.
1149 private void scheduleConnectionMonitorJob() {
1150 if (connectionMonitorJob == null) {
1151 logger.debug("Starting connection monitor job in {} seconds for {} at {}", CONNECTION_MONITOR_DELAY,
1152 thing.getUID(), ipAddress);
1153 connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable,
1154 CONNECTION_MONITOR_DELAY, CONNECTION_MONITOR_FREQ, TimeUnit.SECONDS);
1158 private void cancelConnectionMonitorJob() {
1159 if (connectionMonitorJob != null) {
1160 logger.debug("Canceling connection monitor job for {} at {}", thing.getUID(), ipAddress);
1161 connectionMonitorJob.cancel(true);
1162 connectionMonitorJob = null;
1166 private void checkConnection() {
1167 logger.trace("Checking status of connection for {} at {}", thing.getUID(), ipAddress);
1168 if (!isConnected()) {
1169 logger.debug("Connection check FAILED for {} at {}", thing.getUID(), ipAddress);
1172 logger.debug("Connection check OK for {} at {}", thing.getUID(), ipAddress);
1173 logger.debug("Requesting status update from {} at {}", thing.getUID(), ipAddress);
1174 sendCommand(macAddress, ";GETALL");
1175 sendCommand(macAddress, ";SNSROCC;STATUS;GET");