2 * Copyright (c) 2010-2020 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.lifx.internal;
15 import static org.openhab.binding.lifx.internal.LifxBindingConstants.*;
16 import static org.openhab.binding.lifx.internal.util.LifxMessageUtil.randomSourceId;
17 import static org.openhab.binding.lifx.internal.util.LifxSelectorUtil.*;
19 import java.io.IOException;
20 import java.net.InetSocketAddress;
21 import java.nio.channels.SelectionKey;
22 import java.nio.channels.Selector;
23 import java.util.HashMap;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.function.Supplier;
29 import org.apache.commons.lang.StringUtils;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.lifx.internal.fields.MACAddress;
33 import org.openhab.binding.lifx.internal.protocol.GetLabelRequest;
34 import org.openhab.binding.lifx.internal.protocol.GetServiceRequest;
35 import org.openhab.binding.lifx.internal.protocol.GetVersionRequest;
36 import org.openhab.binding.lifx.internal.protocol.Packet;
37 import org.openhab.binding.lifx.internal.protocol.Product;
38 import org.openhab.binding.lifx.internal.protocol.StateLabelResponse;
39 import org.openhab.binding.lifx.internal.protocol.StateServiceResponse;
40 import org.openhab.binding.lifx.internal.protocol.StateVersionResponse;
41 import org.openhab.binding.lifx.internal.util.LifxSelectorUtil;
42 import org.openhab.core.config.discovery.AbstractDiscoveryService;
43 import org.openhab.core.config.discovery.DiscoveryResult;
44 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
45 import org.openhab.core.config.discovery.DiscoveryService;
46 import org.openhab.core.thing.ThingUID;
47 import org.osgi.service.component.annotations.Activate;
48 import org.osgi.service.component.annotations.Component;
49 import org.osgi.service.component.annotations.Deactivate;
50 import org.osgi.service.component.annotations.Modified;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * The {@link LifxLightDiscovery} provides support for auto-discovery of LIFX
58 * @author Dennis Nobel - Initial contribution
59 * @author Karel Goderis - Rewrite for Firmware V2, and remove dependency on external libraries
60 * @author Wouter Born - Discover light labels, improve locking, optimize packet handling
62 @Component(immediate = true, service = DiscoveryService.class, configurationPid = "discovery.lifx")
64 public class LifxLightDiscovery extends AbstractDiscoveryService {
66 private static final String LOG_ID = "Discovery";
67 private static final long REFRESH_INTERVAL = TimeUnit.MINUTES.toSeconds(1);
68 private static final long SELECTOR_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
70 private final Logger logger = LoggerFactory.getLogger(LifxLightDiscovery.class);
72 private final Map<MACAddress, @Nullable DiscoveredLight> discoveredLights = new HashMap<>();
73 private final long sourceId = randomSourceId();
74 private final Supplier<Integer> sequenceNumberSupplier = new LifxSequenceNumberSupplier();
76 private @Nullable Selector selector;
77 private @Nullable SelectionKey broadcastKey;
79 private @Nullable ScheduledFuture<?> discoveryJob;
80 private @Nullable ScheduledFuture<?> networkJob;
82 private boolean isScanning = false;
84 private class DiscoveredLight {
86 private MACAddress macAddress;
87 private InetSocketAddress socketAddress;
89 private @Nullable String label;
90 private @Nullable Product product;
91 private long productVersion;
92 private boolean supportedProduct = true;
93 private LifxSelectorContext selectorContext;
95 private long lastRequestTimeMillis;
97 public DiscoveredLight(Selector lightSelector, MACAddress macAddress, InetSocketAddress socketAddress,
98 String logId, @Nullable SelectionKey unicastKey) {
99 this.macAddress = macAddress;
101 this.socketAddress = socketAddress;
102 this.selectorContext = new LifxSelectorContext(lightSelector, sourceId, sequenceNumberSupplier, logId,
103 socketAddress, macAddress, broadcastKey, unicastKey);
106 public boolean isDataComplete() {
107 return label != null && product != null;
110 public void cancelUnicastKey() {
111 SelectionKey unicastKey = selectorContext.getUnicastKey();
112 if (unicastKey != null) {
113 cancelKey(unicastKey, selectorContext.getLogId());
118 public LifxLightDiscovery() throws IllegalArgumentException {
119 super(LifxBindingConstants.SUPPORTED_THING_TYPES, 1, true);
124 protected void activate(@Nullable Map<String, @Nullable Object> configProperties) {
125 super.activate(configProperties);
130 protected void modified(@Nullable Map<String, @Nullable Object> configProperties) {
131 super.modified(configProperties);
136 protected void deactivate() {
141 protected void startBackgroundDiscovery() {
142 logger.debug("Starting the LIFX device background discovery");
144 ScheduledFuture<?> localDiscoveryJob = discoveryJob;
145 if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
146 discoveryJob = scheduler.scheduleWithFixedDelay(this::doScan, 0, REFRESH_INTERVAL, TimeUnit.SECONDS);
151 protected void stopBackgroundDiscovery() {
152 logger.debug("Stopping LIFX device background discovery");
154 ScheduledFuture<?> localDiscoveryJob = discoveryJob;
155 if (localDiscoveryJob != null && !localDiscoveryJob.isCancelled()) {
156 localDiscoveryJob.cancel(true);
160 ScheduledFuture<?> localNetworkJob = networkJob;
161 if (localNetworkJob != null && !localNetworkJob.isCancelled()) {
162 localNetworkJob.cancel(true);
168 protected void startScan() {
173 protected synchronized void stopScan() {
175 removeOlderResults(getTimestampOfLastScan());
178 protected void doScan() {
182 if (selector != null) {
183 closeSelector(selector, LOG_ID);
186 logger.debug("The LIFX discovery service will use '{}' as source identifier",
187 Long.toString(sourceId, 16));
189 Selector localSelector = Selector.open();
190 selector = localSelector;
192 broadcastKey = openBroadcastChannel(localSelector, LOG_ID, BROADCAST_PORT);
193 networkJob = scheduler.schedule(this::receiveAndHandlePackets, 0, TimeUnit.MILLISECONDS);
195 LifxSelectorContext selectorContext = new LifxSelectorContext(localSelector, sourceId,
196 sequenceNumberSupplier, LOG_ID, broadcastKey);
197 broadcastPacket(selectorContext, new GetServiceRequest());
199 logger.info("A discovery scan for LIFX lights is already underway");
201 } catch (IOException e) {
202 logger.debug("{} while discovering LIFX lights : {}", e.getClass().getSimpleName(), e.getMessage());
207 public void receiveAndHandlePackets() {
208 Selector localSelector = selector;
211 if (localSelector == null || !localSelector.isOpen()) {
212 logger.debug("Unable to receive and handle packets with null or closed selector");
216 discoveredLights.clear();
217 logger.trace("Entering read loop");
218 long startStamp = System.currentTimeMillis();
220 while (System.currentTimeMillis() - startStamp < SELECTOR_TIMEOUT) {
221 int lightCount = discoveredLights.size();
222 long selectStamp = System.currentTimeMillis();
224 LifxSelectorUtil.receiveAndHandlePackets(localSelector, LOG_ID,
225 (packet, address) -> handlePacket(packet, address));
226 requestAdditionalLightData();
228 boolean discoveredNewLights = lightCount < discoveredLights.size();
229 if (!discoveredNewLights) {
230 boolean preventBusyWaiting = System.currentTimeMillis() - selectStamp < PACKET_INTERVAL;
231 if (preventBusyWaiting) {
232 Thread.sleep(PACKET_INTERVAL);
236 logger.trace("Exited read loop");
237 } catch (Exception e) {
238 logger.debug("{} while receiving and handling discovery packets: {}", e.getClass().getSimpleName(),
241 LifxSelectorUtil.closeSelector(localSelector, LOG_ID);
247 private void requestAdditionalLightData() {
248 // Iterate through the discovered lights that have to be set up, and the packets that have to be sent
249 // Workaround to avoid a ConcurrentModifictionException on the selector.SelectedKeys() Set
250 for (DiscoveredLight light : discoveredLights.values()) {
254 boolean waitingForLightResponse = System.currentTimeMillis() - light.lastRequestTimeMillis < 200;
256 if (light.supportedProduct && !light.isDataComplete() && !waitingForLightResponse) {
257 if (light.product == null) {
258 sendPacket(light.selectorContext, new GetVersionRequest());
260 if (light.label == null) {
261 sendPacket(light.selectorContext, new GetLabelRequest());
263 light.lastRequestTimeMillis = System.currentTimeMillis();
268 private void handlePacket(Packet packet, InetSocketAddress address) {
269 logger.trace("Discovery : Packet type '{}' received from '{}' for '{}' with sequence '{}' and source '{}'",
270 new Object[] { packet.getClass().getSimpleName(), address.toString(), packet.getTarget().getHex(),
271 packet.getSequence(), Long.toString(packet.getSource(), 16) });
273 if (packet.getSource() == sourceId || packet.getSource() == 0) {
274 MACAddress macAddress = packet.getTarget();
275 DiscoveredLight light = discoveredLights.get(macAddress);
277 if (packet instanceof StateServiceResponse) {
278 int port = (int) ((StateServiceResponse) packet).getPort();
281 InetSocketAddress socketAddress = new InetSocketAddress(address.getAddress(), port);
282 if (light == null || (!socketAddress.equals(light.socketAddress))) {
284 light.cancelUnicastKey();
287 Selector lightSelector = selector;
288 if (lightSelector != null) {
289 String logId = getLogId(macAddress, socketAddress);
290 light = new DiscoveredLight(lightSelector, macAddress, socketAddress, logId,
291 openUnicastChannel(lightSelector, logId, socketAddress));
292 discoveredLights.put(macAddress, light);
295 } catch (Exception e) {
296 logger.warn("{} while connecting to IP address: {}", e.getClass().getSimpleName(),
301 } else if (light != null) {
302 if (packet instanceof StateLabelResponse) {
303 light.label = ((StateLabelResponse) packet).getLabel().trim();
304 } else if (packet instanceof StateVersionResponse) {
306 light.product = Product.getProductFromProductID(((StateVersionResponse) packet).getProduct());
307 light.productVersion = ((StateVersionResponse) packet).getVersion();
308 } catch (IllegalArgumentException e) {
309 logger.debug("Discovered an unsupported light ({}): {}", light.macAddress.getAsLabel(),
311 light.supportedProduct = false;
316 if (light != null && light.isDataComplete()) {
318 thingDiscovered(createDiscoveryResult(light));
319 } catch (IllegalArgumentException e) {
320 logger.trace("{} while creating discovery result of light ({})", e.getClass().getSimpleName(),
327 private DiscoveryResult createDiscoveryResult(DiscoveredLight light) throws IllegalArgumentException {
328 Product product = light.product;
329 if (product == null) {
330 throw new IllegalArgumentException("Product of discovered light is null");
333 String macAsLabel = light.macAddress.getAsLabel();
334 ThingUID thingUID = new ThingUID(product.getThingTypeUID(), macAsLabel);
336 String label = light.label;
337 if (StringUtils.isBlank(label)) {
338 label = product.getName();
341 logger.trace("Discovered a LIFX light: {}", label);
343 DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUID);
344 builder.withRepresentationProperty(LifxBindingConstants.PROPERTY_MAC_ADDRESS);
345 builder.withLabel(label);
347 builder.withProperty(LifxBindingConstants.CONFIG_PROPERTY_DEVICE_ID, macAsLabel);
348 builder.withProperty(LifxBindingConstants.PROPERTY_MAC_ADDRESS, macAsLabel);
349 builder.withProperty(LifxBindingConstants.PROPERTY_PRODUCT_ID, product.getID());
350 builder.withProperty(LifxBindingConstants.PROPERTY_PRODUCT_NAME, product.getName());
351 builder.withProperty(LifxBindingConstants.PROPERTY_PRODUCT_VERSION, light.productVersion);
352 builder.withProperty(LifxBindingConstants.PROPERTY_VENDOR_ID, product.getVendor().getID());
353 builder.withProperty(LifxBindingConstants.PROPERTY_VENDOR_NAME, product.getVendor().getName());
355 return builder.build();