2 * Copyright (c) 2010-2022 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.charset.StandardCharsets;
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);
614 byte[] buffer = command.getBytes(StandardCharsets.US_ASCII);
617 } catch (IOException e) {
618 logger.warn("IO exception writing message to socket: {}", e.getMessage(), e);
623 private void listener() {
624 logger.debug("Fan listener thread is running for {} at {}", thing.getUID(), ipAddress);
628 // Wait for a message
629 processMessage(waitForMessage());
630 } catch (IOException ioe) {
631 logger.warn("Listener for {} got IO exception waiting for message: {}", thing.getUID(),
632 ioe.getMessage(), ioe);
636 logger.debug("Fan listener thread is exiting for {} at {}", thing.getUID(), ipAddress);
639 private String waitForMessage() throws IOException {
640 if (!conn.isConnected()) {
641 if (logger.isTraceEnabled()) {
642 logger.trace("FanListener for {} can't receive message. No connection to fan", thing.getUID());
646 } catch (InterruptedException e) {
650 return readMessage();
653 private String readMessage() {
654 logger.trace("Waiting for message from {} at {}", thing.getUID(), ipAddress);
655 String message = conn.read();
656 if (message != null) {
657 logger.trace("FanListener for {} received message of length {}: {}", thing.getUID(), message.length(),
663 private void processMessage(String incomingMessage) {
664 if (incomingMessage == null || incomingMessage.isEmpty()) {
669 logger.debug("FanListener for {} received message from {}: {}", thing.getUID(), macAddress,
671 Matcher matcher = messagePattern.matcher(incomingMessage);
672 if (!matcher.find()) {
673 logger.debug("Unable to process message from {}, not in expected format: {}", thing.getUID(),
678 String message = matcher.group(1);
679 String[] messageParts = message.split(";");
681 // Check to make sure it is my MAC address or my label
682 if (!isMe(messageParts[0])) {
683 logger.trace("Message not for me ({}): {}", messageParts[0], macAddress);
687 logger.trace("Message is for me ({}): {}", messageParts[0], macAddress);
688 String messageUpperCase = message.toUpperCase();
689 if (messageUpperCase.contains(";FAN;PWR;")) {
690 updateFanPower(messageParts);
691 } else if (messageUpperCase.contains(";FAN;SPD;ACTUAL;")) {
692 updateFanSpeed(messageParts);
693 } else if (messageUpperCase.contains(";FAN;DIR;")) {
694 updateFanDirection(messageParts);
695 } else if (messageUpperCase.contains(";FAN;AUTO;")) {
696 updateFanAuto(messageParts);
697 } else if (messageUpperCase.contains(";FAN;WHOOSH;STATUS;")) {
698 updateFanWhoosh(messageParts);
699 } else if (messageUpperCase.contains(";WINTERMODE;STATE;")) {
700 updateFanWintermode(messageParts);
701 } else if (messageUpperCase.contains(";SMARTMODE;STATE;")) {
702 updateFanSmartmode(messageParts);
703 } else if (messageUpperCase.contains(";FAN;SPD;MIN;")) {
704 updateFanSpeedMin(messageParts);
705 } else if (messageUpperCase.contains(";FAN;SPD;MAX;")) {
706 updateFanSpeedMax(messageParts);
707 } else if (messageUpperCase.contains(";SLEEP;STATE")) {
708 updateFanSleepMode(messageParts);
709 } else if (messageUpperCase.contains(";LEARN;MINSPEED;")) {
710 updateFanLearnMinSpeed(messageParts);
711 } else if (messageUpperCase.contains(";LEARN;MAXSPEED;")) {
712 updateFanLearnMaxSpeed(messageParts);
713 } else if (messageUpperCase.contains(";LIGHT;PWR;")) {
714 updateLightPower(messageParts);
715 } else if (messageUpperCase.contains(";LIGHT;LEVEL;ACTUAL;")) {
716 updateLightLevel(messageParts);
717 } else if (messageUpperCase.contains(";LIGHT;COLOR;TEMP;VALUE;")) {
718 updateLightHue(messageParts);
719 } else if (messageUpperCase.contains(";LIGHT;AUTO;")) {
720 updateLightAuto(messageParts);
721 } else if (messageUpperCase.contains(";LIGHT;LEVEL;MIN;")) {
722 updateLightLevelMin(messageParts);
723 } else if (messageUpperCase.contains(";LIGHT;LEVEL;MAX;")) {
724 updateLightLevelMax(messageParts);
725 } else if (messageUpperCase.contains(";DEVICE;LIGHT;")) {
726 updateLightPresent(messageParts);
727 } else if (messageUpperCase.contains(";SNSROCC;STATUS;")) {
728 updateMotion(messageParts);
729 } else if (messageUpperCase.contains(";TIME;VALUE;")) {
730 updateTime(messageParts);
732 logger.trace("Received unsupported message from {}: {}", thing.getUID(), message);
736 private boolean isMe(String idFromDevice) {
737 // Check match on MAC address
738 if (macAddress.equalsIgnoreCase(idFromDevice)) {
741 // Didn't match MAC address, check match for label
742 if (label.equalsIgnoreCase(idFromDevice)) {
748 private void updateFanPower(String[] messageParts) {
749 if (messageParts.length != 4) {
750 if (logger.isDebugEnabled()) {
751 logger.debug("FAN;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
755 logger.debug("Process fan power update for {}: {}", thing.getUID(), messageParts[3]);
756 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
757 updateChannel(CHANNEL_FAN_POWER, state);
758 fanStateMap.put(CHANNEL_FAN_POWER, state);
761 private void updateFanSpeed(String[] messageParts) {
762 if (messageParts.length != 5) {
763 logger.debug("FAN;SPD;ACTUAL has unexpected number of parameters: {}", Arrays.toString(messageParts));
766 logger.debug("Process fan speed update for {}: {}", thing.getUID(), messageParts[4]);
767 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
768 updateChannel(CHANNEL_FAN_SPEED, state);
769 fanStateMap.put(CHANNEL_FAN_SPEED, state);
772 private void updateFanDirection(String[] messageParts) {
773 if (messageParts.length != 4) {
774 logger.debug("FAN;DIR has unexpected number of parameters: {}", Arrays.toString(messageParts));
777 logger.debug("Process fan direction update for {}: {}", thing.getUID(), messageParts[3]);
778 StringType state = new StringType(messageParts[3]);
779 updateChannel(CHANNEL_FAN_DIRECTION, state);
780 fanStateMap.put(CHANNEL_FAN_DIRECTION, state);
783 private void updateFanAuto(String[] messageParts) {
784 if (messageParts.length != 4) {
785 logger.debug("FAN;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
788 logger.debug("Process fan auto update for {}: {}", thing.getUID(), messageParts[3]);
789 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
790 updateChannel(CHANNEL_FAN_AUTO, state);
791 fanStateMap.put(CHANNEL_FAN_AUTO, state);
794 private void updateFanWhoosh(String[] messageParts) {
795 if (messageParts.length != 5) {
796 logger.debug("FAN;WHOOSH has unexpected number of parameters: {}", Arrays.toString(messageParts));
799 logger.debug("Process fan whoosh update for {}: {}", thing.getUID(), messageParts[4]);
800 OnOffType state = "ON".equalsIgnoreCase(messageParts[4]) ? OnOffType.ON : OnOffType.OFF;
801 updateChannel(CHANNEL_FAN_WHOOSH, state);
802 fanStateMap.put(CHANNEL_FAN_WHOOSH, state);
805 private void updateFanWintermode(String[] messageParts) {
806 if (messageParts.length != 4) {
807 logger.debug("WINTERMODE;STATE has unexpected number of parameters: {}", Arrays.toString(messageParts));
810 logger.debug("Process fan wintermode update for {}: {}", thing.getUID(), messageParts[3]);
811 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
812 updateChannel(CHANNEL_FAN_WINTERMODE, state);
813 fanStateMap.put(CHANNEL_FAN_WINTERMODE, state);
816 private void updateFanSmartmode(String[] messageParts) {
817 if (messageParts.length != 4) {
818 logger.debug("Smartmode has unexpected number of parameters: {}", Arrays.toString(messageParts));
821 logger.debug("Process fan smartmode update for {}: {}", thing.getUID(), messageParts[3]);
822 StringType state = new StringType(messageParts[3]);
823 updateChannel(CHANNEL_FAN_SMARTMODE, state);
824 fanStateMap.put(CHANNEL_FAN_SMARTMODE, state);
827 private void updateFanSpeedMin(String[] messageParts) {
828 if (messageParts.length != 5) {
829 logger.debug("FanSpeedMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
832 logger.debug("Process fan min speed update for {}: {}", thing.getUID(), messageParts[4]);
833 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
834 updateChannel(CHANNEL_FAN_SPEED_MIN, state);
835 fanStateMap.put(CHANNEL_FAN_SPEED_MIN, state);
838 private void updateFanSpeedMax(String[] messageParts) {
839 if (messageParts.length != 5) {
840 logger.debug("FanSpeedMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
843 logger.debug("Process fan speed max update for {}: {}", thing.getUID(), messageParts[4]);
844 PercentType state = BigAssFanConverter.speedToPercent(messageParts[4]);
845 updateChannel(CHANNEL_FAN_SPEED_MAX, state);
846 fanStateMap.put(CHANNEL_FAN_SPEED_MAX, state);
849 private void updateFanSleepMode(String[] messageParts) {
850 if (messageParts.length != 4) {
851 logger.debug("SLEEP;STATE; has unexpected number of parameters: {}", Arrays.toString(messageParts));
854 logger.debug("Process fan sleep mode for {}: {}", thing.getUID(), messageParts[3]);
855 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
856 updateChannel(CHANNEL_FAN_SLEEP, state);
857 fanStateMap.put(CHANNEL_FAN_SLEEP, state);
860 private void updateFanLearnMinSpeed(String[] messageParts) {
861 if (messageParts.length != 4) {
862 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
865 logger.debug("Process fan learn min speed update for {}: {}", thing.getUID(), messageParts[3]);
866 PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
867 updateChannel(CHANNEL_FAN_LEARN_MINSPEED, state);
868 fanStateMap.put(CHANNEL_FAN_LEARN_MINSPEED, state);
871 private void updateFanLearnMaxSpeed(String[] messageParts) {
872 if (messageParts.length != 4) {
873 logger.debug("FanLearnMaxSpeed has unexpected number of parameters: {}", Arrays.toString(messageParts));
876 logger.debug("Process fan learn max speed update for {}: {}", thing.getUID(), messageParts[3]);
877 PercentType state = BigAssFanConverter.speedToPercent(messageParts[3]);
878 updateChannel(CHANNEL_FAN_LEARN_MAXSPEED, state);
879 fanStateMap.put(CHANNEL_FAN_LEARN_MAXSPEED, state);
882 private void updateLightPower(String[] messageParts) {
883 if (messageParts.length != 4) {
884 logger.debug("LIGHT;PWR has unexpected number of parameters: {}", Arrays.toString(messageParts));
887 logger.debug("Process light power update for {}: {}", thing.getUID(), messageParts[3]);
888 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
889 updateChannel(CHANNEL_LIGHT_POWER, state);
890 fanStateMap.put(CHANNEL_LIGHT_POWER, state);
893 private void updateLightLevel(String[] messageParts) {
894 if (messageParts.length != 5) {
895 logger.debug("LIGHT;LEVEL has unexpected number of parameters: {}", Arrays.toString(messageParts));
898 logger.debug("Process light level update for {}: {}", thing.getUID(), messageParts[4]);
899 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
900 updateChannel(CHANNEL_LIGHT_LEVEL, state);
901 fanStateMap.put(CHANNEL_LIGHT_LEVEL, state);
904 private void updateLightHue(String[] messageParts) {
905 if (messageParts.length != 6) {
906 logger.debug("LIGHT;COLOR;TEMP;VALUE has unexpected number of parameters: {}",
907 Arrays.toString(messageParts));
910 logger.debug("Process light hue update for {}: {}", thing.getUID(), messageParts[4]);
911 PercentType state = BigAssFanConverter.hueToPercent(messageParts[5]);
912 updateChannel(CHANNEL_LIGHT_HUE, state);
913 fanStateMap.put(CHANNEL_LIGHT_HUE, state);
916 private void updateLightAuto(String[] messageParts) {
917 if (messageParts.length != 4) {
918 logger.debug("LIGHT;AUTO has unexpected number of parameters: {}", Arrays.toString(messageParts));
921 logger.debug("Process light auto update for {}: {}", thing.getUID(), messageParts[3]);
922 OnOffType state = "ON".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
923 updateChannel(CHANNEL_LIGHT_AUTO, state);
924 fanStateMap.put(CHANNEL_LIGHT_AUTO, state);
927 private void updateLightLevelMin(String[] messageParts) {
928 if (messageParts.length != 5) {
929 logger.debug("LightLevelMin has unexpected number of parameters: {}", Arrays.toString(messageParts));
932 logger.debug("Process light level min update for {}: {}", thing.getUID(), messageParts[4]);
933 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
934 updateChannel(CHANNEL_LIGHT_LEVEL_MIN, state);
935 fanStateMap.put(CHANNEL_LIGHT_LEVEL_MIN, state);
938 private void updateLightLevelMax(String[] messageParts) {
939 if (messageParts.length != 5) {
940 logger.debug("LightLevelMax has unexpected number of parameters: {}", Arrays.toString(messageParts));
943 logger.debug("Process light level max update for {}: {}", thing.getUID(), messageParts[4]);
944 PercentType state = BigAssFanConverter.levelToPercent(messageParts[4]);
945 updateChannel(CHANNEL_LIGHT_LEVEL_MAX, state);
946 fanStateMap.put(CHANNEL_LIGHT_LEVEL_MAX, state);
949 private void updateLightPresent(String[] messageParts) {
950 if (messageParts.length < 4) {
951 logger.debug("LightPresent has unexpected number of parameters: {}", Arrays.toString(messageParts));
954 logger.debug("Process light present update for {}: {}", thing.getUID(), messageParts[3]);
955 StringType lightPresent = new StringType(messageParts[3]);
956 updateChannel(CHANNEL_LIGHT_PRESENT, lightPresent);
957 fanStateMap.put(CHANNEL_LIGHT_PRESENT, lightPresent);
958 if (messageParts.length == 5) {
959 logger.debug("Light supports hue adjustment");
960 StringType lightColor = new StringType(messageParts[4]);
961 updateChannel(CHANNEL_LIGHT_COLOR, lightColor);
962 fanStateMap.put(CHANNEL_LIGHT_COLOR, lightColor);
966 private void updateMotion(String[] messageParts) {
967 if (messageParts.length != 4) {
968 logger.debug("SNSROCC has unexpected number of parameters: {}", Arrays.toString(messageParts));
971 logger.debug("Process motion sensor update for {}: {}", thing.getUID(), messageParts[3]);
972 OnOffType state = "OCCUPIED".equalsIgnoreCase(messageParts[3]) ? OnOffType.ON : OnOffType.OFF;
973 updateChannel(CHANNEL_MOTION, state);
974 fanStateMap.put(CHANNEL_MOTION, state);
977 private void updateTime(String[] messageParts) {
978 if (messageParts.length != 4) {
979 logger.debug("TIME has unexpected number of parameters: {}", Arrays.toString(messageParts));
982 logger.debug("Process time update for {}: {}", thing.getUID(), messageParts[3]);
983 // (mac|name;TIME;VALUE;2017-03-26T14:06:27Z)
985 Instant instant = Instant.parse(messageParts[3]);
986 DateTimeType state = new DateTimeType(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()));
987 updateChannel(CHANNEL_TIME, state);
988 fanStateMap.put(CHANNEL_TIME, state);
989 } catch (DateTimeParseException e) {
990 logger.info("Failed to parse date received from {}: {}", thing.getUID(), messageParts[3]);
996 * The {@link ConnectionManager} class is responsible for managing the state of the TCP connection to the
999 * @author Mark Hilbush - Initial contribution
1001 private class ConnectionManager {
1002 private Logger logger = LoggerFactory.getLogger(ConnectionManager.class);
1004 private boolean deviceIsConnected;
1006 private InetAddress ifAddress;
1007 private Socket fanSocket;
1008 private Scanner fanScanner;
1009 private DataOutputStream fanWriter;
1010 private final int SOCKET_CONNECT_TIMEOUT = 1500;
1012 ScheduledFuture<?> connectionMonitorJob;
1013 private final long CONNECTION_MONITOR_FREQ = 120L;
1014 private final long CONNECTION_MONITOR_DELAY = 30L;
1016 Runnable connectionMonitorRunnable = () -> {
1017 logger.trace("Performing connection check for {} at IP {}", thing.getUID(), ipAddress);
1021 public ConnectionManager(String ipv4Address) {
1022 deviceIsConnected = false;
1024 ifAddress = InetAddress.getByName(ipv4Address);
1025 logger.debug("Handler for {} using address {} on network interface {}", thing.getUID(),
1026 ifAddress.getHostAddress(), NetworkInterface.getByInetAddress(ifAddress).getName());
1027 } catch (UnknownHostException e) {
1028 logger.warn("Handler for {} got UnknownHostException getting local IPv4 net interface: {}",
1029 thing.getUID(), e.getMessage(), e);
1030 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1031 } catch (SocketException e) {
1032 logger.warn("Handler for {} got SocketException getting local IPv4 network interface: {}",
1033 thing.getUID(), e.getMessage(), e);
1034 markOfflineWithMessage(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
1039 * Connect to the command and serial port(s) on the device. The serial connections are established only for
1040 * devices that support serial.
1042 protected synchronized void connect() {
1043 if (isConnected()) {
1046 logger.trace("Connecting to {} at {}", thing.getUID(), ipAddress);
1050 fanSocket = new Socket();
1051 fanSocket.bind(new InetSocketAddress(ifAddress, 0));
1052 fanSocket.connect(new InetSocketAddress(ipAddress, BAF_PORT), SOCKET_CONNECT_TIMEOUT);
1053 } catch (IOException e) {
1054 logger.debug("IOException connecting to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage());
1055 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1062 fanWriter = new DataOutputStream(fanSocket.getOutputStream());
1063 fanScanner = new Scanner(fanSocket.getInputStream());
1064 fanScanner.useDelimiter("[)]");
1065 } catch (IOException e) {
1066 logger.warn("IOException getting streams for {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1068 markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
1072 logger.info("Connected to {} at {}", thing.getUID(), ipAddress);
1073 deviceIsConnected = true;
1077 protected synchronized void disconnect() {
1078 if (!isConnected()) {
1081 logger.debug("Disconnecting from {} at {}", thing.getUID(), ipAddress);
1084 if (fanWriter != null) {
1087 if (fanScanner != null) {
1090 if (fanSocket != null) {
1093 } catch (IOException e) {
1094 logger.warn("IOException closing connection to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(),
1097 deviceIsConnected = false;
1104 public String read() {
1105 if (fanScanner == null) {
1106 logger.warn("Scanner for {} is null when trying to scan from {}!", thing.getUID(), ipAddress);
1112 nextToken = fanScanner.next();
1113 } catch (NoSuchElementException e) {
1114 logger.debug("Scanner for {} threw NoSuchElementException; stream possibly closed", thing.getUID());
1115 // Force a reconnect to the device
1118 } catch (IllegalStateException e) {
1119 logger.debug("Scanner for {} threw IllegalStateException; scanner possibly closed", thing.getUID());
1121 } catch (BufferOverflowException e) {
1122 logger.debug("Scanner for {} threw BufferOverflowException", thing.getUID());
1128 public void write(byte[] buffer) throws IOException {
1129 if (fanWriter == null) {
1130 logger.warn("fanWriter for {} is null when trying to write to {}!!!", thing.getUID(), ipAddress);
1133 fanWriter.write(buffer, 0, buffer.length);
1136 private boolean isConnected() {
1137 return deviceIsConnected;
1141 * Periodically validate the command connection to the device by executing a getversion command.
1143 private void scheduleConnectionMonitorJob() {
1144 if (connectionMonitorJob == null) {
1145 logger.debug("Starting connection monitor job in {} seconds for {} at {}", CONNECTION_MONITOR_DELAY,
1146 thing.getUID(), ipAddress);
1147 connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable,
1148 CONNECTION_MONITOR_DELAY, CONNECTION_MONITOR_FREQ, TimeUnit.SECONDS);
1152 private void cancelConnectionMonitorJob() {
1153 if (connectionMonitorJob != null) {
1154 logger.debug("Canceling connection monitor job for {} at {}", thing.getUID(), ipAddress);
1155 connectionMonitorJob.cancel(true);
1156 connectionMonitorJob = null;
1160 private void checkConnection() {
1161 logger.trace("Checking status of connection for {} at {}", thing.getUID(), ipAddress);
1162 if (!isConnected()) {
1163 logger.debug("Connection check FAILED for {} at {}", thing.getUID(), ipAddress);
1166 logger.debug("Connection check OK for {} at {}", thing.getUID(), ipAddress);
1167 logger.debug("Requesting status update from {} at {}", thing.getUID(), ipAddress);
1168 sendCommand(macAddress, ";GETALL");
1169 sendCommand(macAddress, ";SNSROCC;STATUS;GET");