/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.binding.lifx.internal; import static org.eclipse.smarthome.binding.lifx.LifxBindingConstants.PACKET_INTERVAL; import java.io.IOException; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.StandardProtocolFamily; import java.net.StandardSocketOptions; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.ConcurrentModificationException; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import org.eclipse.smarthome.binding.lifx.LifxBindingConstants; import org.eclipse.smarthome.binding.lifx.handler.LifxLightHandler.CurrentLightState; import org.eclipse.smarthome.binding.lifx.internal.fields.MACAddress; import org.eclipse.smarthome.binding.lifx.internal.listener.LifxResponsePacketListener; import org.eclipse.smarthome.binding.lifx.internal.protocol.GetServiceRequest; import org.eclipse.smarthome.binding.lifx.internal.protocol.Packet; import org.eclipse.smarthome.binding.lifx.internal.protocol.PacketFactory; import org.eclipse.smarthome.binding.lifx.internal.protocol.PacketHandler; import org.eclipse.smarthome.binding.lifx.internal.protocol.StateServiceResponse; import org.eclipse.smarthome.core.common.ThreadPoolManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link LifxLightCommunicationHandler} is responsible for the communications with a light. * * @author Wouter Born - Extracted class from LifxLightHandler */ public class LifxLightCommunicationHandler { private Logger logger = LoggerFactory.getLogger(LifxLightCommunicationHandler.class); private final int BROADCAST_PORT = 56700; private CurrentLightState currentLightState; private static AtomicInteger lightCounter = new AtomicInteger(1); private long source; private int service; private int port; private MACAddress macAddress; private String macAsHex; private MACAddress broadcastAddress = new MACAddress("000000000000", true); private AtomicInteger sequenceNumber = new AtomicInteger(1); private Selector selector; private ScheduledFuture<?> networkJob; private ReentrantLock lock = new ReentrantLock(); private InetSocketAddress ipAddress; private DatagramChannel unicastChannel; private SelectionKey unicastKey; private SelectionKey broadcastKey; private List<InetSocketAddress> broadcastAddresses; private List<InetAddress> interfaceAddresses; private int bufferSize = 0; private final ScheduledExecutorService scheduler = ThreadPoolManager .getScheduledPool(LifxBindingConstants.THREADPOOL_NAME); public LifxLightCommunicationHandler(MACAddress macAddress, CurrentLightState currentLightState) { this.macAddress = macAddress; this.macAsHex = macAddress.getHex(); this.currentLightState = currentLightState; } private List<LifxResponsePacketListener> responsePacketListeners = new CopyOnWriteArrayList<>(); public void addResponsePacketListener(LifxResponsePacketListener listener) { responsePacketListeners.add(listener); } public void removeResponsePacketListener(LifxResponsePacketListener listener) { responsePacketListeners.remove(listener); } public void start() { try { lock.lock(); logger.debug("Starting LIFX communication handler for light '{}'.", macAsHex); if (networkJob == null || networkJob.isCancelled()) { networkJob = scheduler.scheduleWithFixedDelay(networkRunnable, 0, PACKET_INTERVAL, TimeUnit.MILLISECONDS); } source = UUID.randomUUID().getLeastSignificantBits() & (-1L >>> 32); logger.debug("The LIFX handler will use '{}' as source identifier", Long.toString(source, 16)); broadcastAddresses = new ArrayList<InetSocketAddress>(); interfaceAddresses = new ArrayList<InetAddress>(); Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); while (networkInterfaces.hasMoreElements()) { NetworkInterface iface = networkInterfaces.nextElement(); if (iface.isUp() && !iface.isLoopback()) { for (InterfaceAddress ifaceAddr : iface.getInterfaceAddresses()) { if (ifaceAddr.getAddress() instanceof Inet4Address) { logger.debug("Adding '{}' as interface address with MTU {}", ifaceAddr.getAddress(), iface.getMTU()); if (iface.getMTU() > bufferSize) { bufferSize = iface.getMTU(); } interfaceAddresses.add(ifaceAddr.getAddress()); if (ifaceAddr.getBroadcast() != null) { logger.debug("Adding '{}' as broadcast address", ifaceAddr.getBroadcast()); broadcastAddresses.add(new InetSocketAddress(ifaceAddr.getBroadcast(), BROADCAST_PORT)); } } } } } selector = Selector.open(); DatagramChannel broadcastChannel = DatagramChannel.open(StandardProtocolFamily.INET) .setOption(StandardSocketOptions.SO_REUSEADDR, true) .setOption(StandardSocketOptions.SO_BROADCAST, true); broadcastChannel.configureBlocking(false); int offset = lightCounter.getAndIncrement(); logger.debug("Binding the broadcast channel on port {}", BROADCAST_PORT + offset); broadcastChannel.bind(new InetSocketAddress(BROADCAST_PORT + offset)); broadcastKey = broadcastChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); currentLightState.setOffline(); // look for lights on the network GetServiceRequest packet = new GetServiceRequest(); broadcastPacket(packet); } catch (Exception ex) { logger.error("Error occurred while initializing LIFX handler: {}", ex.getMessage(), ex); } finally { lock.unlock(); } } public void stop() { try { lock.lock(); if (networkJob != null && !networkJob.isCancelled()) { networkJob.cancel(true); networkJob = null; } try { if (selector != null) { selector.wakeup(); boolean isContinue = true; while (isContinue) { try { for (SelectionKey selectionKey : selector.keys()) { selectionKey.channel().close(); selectionKey.cancel(); } isContinue = false; // continue till all keys are cancelled } catch (ConcurrentModificationException e) { logger.warn("An exception occurred while closing a selector key : '{}'", e.getMessage()); } } selector.close(); } } catch (IOException e) { logger.warn("An exception occurred while closing the selector : '{}'", e.getMessage()); } if (broadcastKey != null) { try { broadcastKey.channel().close(); } catch (IOException e) { logger.warn("An exception occurred while closing the broadcast channel : '{}'", e.getMessage()); } } if (unicastKey != null) { try { unicastKey.channel().close(); } catch (IOException e) { logger.warn("An exception occurred while closing the unicast channel : '{}'", e.getMessage()); } } ipAddress = null; } finally { lock.unlock(); } } private Runnable networkRunnable = new Runnable() { @Override public void run() { try { lock.lock(); if (selector != null) { try { selector.selectNow(); } catch (IOException e) { logger.error("An exception occurred while selecting: {}", e.getMessage()); } Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { SelectionKey key; try { key = keyIterator.next(); } catch (ConcurrentModificationException e) { // when a StateServiceResponse packet is handled a new unicastChannel may be registered // in the selector which causes this exception, recover from it by restarting the iteration logger.debug("{} : Restarting iteration after ConcurrentModificationException", macAsHex); keyIterator = selector.selectedKeys().iterator(); continue; } if (key.isValid() && key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. // block of code only for completeness purposes } else if (key.isValid() && key.isConnectable()) { // a connection was established with a remote server. // block of code only for completeness purposes } else if (key.isValid() && key.isReadable()) { // a channel is ready for reading SelectableChannel channel = key.channel(); InetSocketAddress address = null; int messageLength = 0; ByteBuffer readBuffer = ByteBuffer.allocate(bufferSize); try { if (channel instanceof DatagramChannel) { address = (InetSocketAddress) ((DatagramChannel) channel).receive(readBuffer); } else if (channel instanceof SocketChannel) { address = (InetSocketAddress) ((SocketChannel) channel).getRemoteAddress(); ((SocketChannel) channel).read(readBuffer); } messageLength = readBuffer.position(); } catch (Exception e) { logger.warn("An exception occurred while reading data : '{}'", e.getMessage()); } if (address != null) { if (!interfaceAddresses.contains(address.getAddress())) { readBuffer.rewind(); ByteBuffer packetSize = readBuffer.slice(); packetSize.position(0); packetSize.limit(2); int size = Packet.FIELD_SIZE.value(packetSize); if (messageLength == size) { ByteBuffer packetType = readBuffer.slice(); packetType.position(32); packetType.limit(34); int type = Packet.FIELD_PACKET_TYPE.value(packetType); PacketHandler<?> handler = PacketFactory.createHandler(type); if (handler == null) { logger.trace("Unknown packet type: {} (source: {})", String.format("0x%02X", type), address.toString()); continue; } Packet packet = handler.handle(readBuffer); if (packet == null) { logger.warn("Handler {} was unable to handle packet", handler.getClass().getName()); } else { handlePacket(packet, address); } } } } } else if (key.isValid() && key.isWritable()) { // a channel is ready for writing // block of code only for completeness purposes } } } } catch (Exception e) { logger.error("An exception occurred while receiving a packet from the light : '{}'", e.getMessage()); } finally { lock.unlock(); } } }; private void handlePacket(final Packet packet, InetSocketAddress address) { if ((packet.getTarget().equals(macAddress) || packet.getTarget().equals(broadcastAddress)) && (packet.getSource() == source || packet.getSource() == 0)) { logger.trace("{} : Packet type '{}' received from '{}' for '{}' with sequence '{}' and source '{}'", new Object[] { macAsHex, packet.getClass().getSimpleName(), address.toString(), packet.getTarget().getHex(), packet.getSequence(), Long.toString(packet.getSource(), 16) }); if (packet instanceof StateServiceResponse) { MACAddress discoveredAddress = ((StateServiceResponse) packet).getTarget(); if (macAddress.equals(discoveredAddress)) { if (!address.equals(ipAddress) && port != (int) ((StateServiceResponse) packet).getPort() && service != ((StateServiceResponse) packet).getService() || currentLightState.isOffline()) { this.port = (int) ((StateServiceResponse) packet).getPort(); this.service = ((StateServiceResponse) packet).getService(); if (port == 0) { logger.warn("The service with ID '{}' is currently not available", service); currentLightState.setOfflineByCommunicationError(); } else { if (unicastChannel != null && unicastKey != null) { try { unicastChannel.close(); } catch (IOException e) { logger.error("An exception occurred while closing the channel : '{}'", e.getMessage()); } unicastKey.cancel(); } try { ipAddress = new InetSocketAddress(address.getAddress(), port); unicastChannel = DatagramChannel.open(StandardProtocolFamily.INET) .setOption(StandardSocketOptions.SO_REUSEADDR, true); unicastChannel.configureBlocking(false); unicastKey = unicastChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); unicastChannel.connect(ipAddress); logger.trace("Connected to a light via {}", unicastChannel.getLocalAddress().toString()); } catch (Exception e) { logger.warn("An exception occurred while connecting to the light's IP address : '{}'", e.getMessage()); currentLightState.setOfflineByCommunicationError(); return; } currentLightState.setOnline(); } } } } // Listeners are notified in a separate thread for better concurrency and to prevent deadlock. Runnable notifyListenersRunnable = new Runnable() { @Override public void run() { for (LifxResponsePacketListener listener : responsePacketListeners) { listener.handleResponsePacket(packet); } } }; scheduler.schedule(notifyListenersRunnable, 0, TimeUnit.MILLISECONDS); } } /** * Atomically increases the sequence number. When Java 8 is available this can be replaced by using the * {@code getAndUpdate} method. */ private int getAndIncreaseSequenceNumber() { int prev, next; do { prev = sequenceNumber.get(); next = prev + 1; if (next > 255) { next = 1; } } while (!sequenceNumber.compareAndSet(prev, next)); return prev; } public void sendPacket(Packet packet) { if (ipAddress != null) { packet.setSource(source); packet.setTarget(macAddress); packet.setSequence(getAndIncreaseSequenceNumber()); sendPacket(packet, ipAddress, unicastKey); } } public void resendPacket(Packet packet) { if (ipAddress != null) { packet.setSource(source); packet.setTarget(macAddress); sendPacket(packet, ipAddress, unicastKey); } } public void broadcastPacket(Packet packet) { packet.setSource(source); packet.setSequence(getAndIncreaseSequenceNumber()); for (InetSocketAddress address : broadcastAddresses) { sendPacket(packet, address, broadcastKey); } } private boolean sendPacket(Packet packet, InetSocketAddress address, SelectionKey selectedKey) { boolean result = false; try { lock.lock(); if (selectedKey == unicastKey) { LifxNetworkThrottler.lock(macAddress); } else { LifxNetworkThrottler.lock(); } while (!result) { selector.selectNow(); Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (!result && keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isValid() && key.isWritable() && key.equals(selectedKey)) { SelectableChannel channel = key.channel(); if (channel instanceof DatagramChannel) { logger.trace( "{} : Sending packet type '{}' from '{}' to '{}' for '{}' with sequence '{}' and source '{}'", new Object[] { macAsHex, packet.getClass().getSimpleName(), ((InetSocketAddress) ((DatagramChannel) channel).getLocalAddress()) .toString(), address.toString(), packet.getTarget().getHex(), packet.getSequence(), Long.toString(packet.getSource(), 16) }); ((DatagramChannel) channel).send(packet.bytes(), address); result = true; } else if (channel instanceof SocketChannel) { ((SocketChannel) channel).write(packet.bytes()); result = true; } } } } } catch (Exception e) { logger.debug("An exception occurred while sending a packet to the light : '{}'", e.getMessage()); currentLightState.setOfflineByCommunicationError(); } finally { if (selectedKey == unicastKey) { LifxNetworkThrottler.unlock(macAddress); } else { LifxNetworkThrottler.unlock(); } lock.unlock(); } return result; } }