]> git.basschouten.com Git - openhab-addons.git/blob
8676b5f49dceddd52a65e9f7147c6abfb8fd7ff9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.lifx.internal;
14
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.*;
18
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;
24 import java.util.Map;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.function.Supplier;
28
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;
53
54 /**
55  * The {@link LifxLightDiscovery} provides support for auto-discovery of LIFX
56  * lights.
57  *
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
61  */
62 @Component(service = DiscoveryService.class, configurationPid = "discovery.lifx")
63 @NonNullByDefault
64 public class LifxLightDiscovery extends AbstractDiscoveryService {
65
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);
69
70     private final Logger logger = LoggerFactory.getLogger(LifxLightDiscovery.class);
71
72     private final Map<MACAddress, @Nullable DiscoveredLight> discoveredLights = new HashMap<>();
73     private final long sourceId = randomSourceId();
74     private final Supplier<Integer> sequenceNumberSupplier = new LifxSequenceNumberSupplier();
75
76     private @Nullable Selector selector;
77     private @Nullable SelectionKey broadcastKey;
78
79     private @Nullable ScheduledFuture<?> discoveryJob;
80     private @Nullable ScheduledFuture<?> networkJob;
81
82     private boolean isScanning = false;
83
84     private class DiscoveredLight {
85
86         private MACAddress macAddress;
87         private InetSocketAddress socketAddress;
88         private String logId;
89         private @Nullable String label;
90         private @Nullable Product product;
91         private long productVersion;
92         private boolean supportedProduct = true;
93         private LifxSelectorContext selectorContext;
94
95         private long lastRequestTimeMillis;
96
97         public DiscoveredLight(Selector lightSelector, MACAddress macAddress, InetSocketAddress socketAddress,
98                 String logId, @Nullable SelectionKey unicastKey) {
99             this.macAddress = macAddress;
100             this.logId = logId;
101             this.socketAddress = socketAddress;
102             this.selectorContext = new LifxSelectorContext(lightSelector, sourceId, sequenceNumberSupplier, logId,
103                     socketAddress, macAddress, broadcastKey, unicastKey);
104         }
105
106         public boolean isDataComplete() {
107             return label != null && product != null;
108         }
109
110         public void cancelUnicastKey() {
111             SelectionKey unicastKey = selectorContext.getUnicastKey();
112             if (unicastKey != null) {
113                 cancelKey(unicastKey, selectorContext.getLogId());
114             }
115         }
116     }
117
118     public LifxLightDiscovery() throws IllegalArgumentException {
119         super(LifxBindingConstants.SUPPORTED_THING_TYPES, 1, true);
120     }
121
122     @Activate
123     @Override
124     protected void activate(@Nullable Map<String, @Nullable Object> configProperties) {
125         super.activate(configProperties);
126     }
127
128     @Modified
129     @Override
130     protected void modified(@Nullable Map<String, @Nullable Object> configProperties) {
131         super.modified(configProperties);
132     }
133
134     @Deactivate
135     @Override
136     protected void deactivate() {
137         super.deactivate();
138     }
139
140     @Override
141     protected void startBackgroundDiscovery() {
142         logger.debug("Starting the LIFX device background discovery");
143
144         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
145         if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
146             discoveryJob = scheduler.scheduleWithFixedDelay(this::doScan, 0, REFRESH_INTERVAL, TimeUnit.SECONDS);
147         }
148     }
149
150     @Override
151     protected void stopBackgroundDiscovery() {
152         logger.debug("Stopping LIFX device background discovery");
153
154         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
155         if (localDiscoveryJob != null && !localDiscoveryJob.isCancelled()) {
156             localDiscoveryJob.cancel(true);
157             discoveryJob = null;
158         }
159
160         ScheduledFuture<?> localNetworkJob = networkJob;
161         if (localNetworkJob != null && !localNetworkJob.isCancelled()) {
162             localNetworkJob.cancel(true);
163             networkJob = null;
164         }
165     }
166
167     @Override
168     protected void startScan() {
169         doScan();
170     }
171
172     @Override
173     protected synchronized void stopScan() {
174         super.stopScan();
175         removeOlderResults(getTimestampOfLastScan());
176     }
177
178     protected void doScan() {
179         try {
180             if (!isScanning) {
181                 isScanning = true;
182                 if (selector != null) {
183                     closeSelector(selector, LOG_ID);
184                 }
185
186                 logger.debug("The LIFX discovery service will use '{}' as source identifier",
187                         Long.toString(sourceId, 16));
188
189                 Selector localSelector = Selector.open();
190                 selector = localSelector;
191
192                 broadcastKey = openBroadcastChannel(localSelector, LOG_ID, BROADCAST_PORT);
193                 networkJob = scheduler.schedule(this::receiveAndHandlePackets, 0, TimeUnit.MILLISECONDS);
194
195                 LifxSelectorContext selectorContext = new LifxSelectorContext(localSelector, sourceId,
196                         sequenceNumberSupplier, LOG_ID, broadcastKey);
197                 broadcastPacket(selectorContext, new GetServiceRequest());
198             } else {
199                 logger.info("A discovery scan for LIFX lights is already underway");
200             }
201         } catch (IOException e) {
202             logger.debug("{} while discovering LIFX lights : {}", e.getClass().getSimpleName(), e.getMessage());
203             isScanning = false;
204         }
205     }
206
207     public void receiveAndHandlePackets() {
208         Selector localSelector = selector;
209
210         try {
211             if (localSelector == null || !localSelector.isOpen()) {
212                 logger.debug("Unable to receive and handle packets with null or closed selector");
213                 return;
214             }
215
216             discoveredLights.clear();
217             logger.trace("Entering read loop");
218             long startStamp = System.currentTimeMillis();
219
220             while (System.currentTimeMillis() - startStamp < SELECTOR_TIMEOUT) {
221                 int lightCount = discoveredLights.size();
222                 long selectStamp = System.currentTimeMillis();
223
224                 LifxSelectorUtil.receiveAndHandlePackets(localSelector, LOG_ID,
225                         (packet, address) -> handlePacket(packet, address));
226                 requestAdditionalLightData();
227
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);
233                     }
234                 }
235             }
236             logger.trace("Exited read loop");
237         } catch (Exception e) {
238             logger.debug("{} while receiving and handling discovery packets: {}", e.getClass().getSimpleName(),
239                     e.getMessage(), e);
240         } finally {
241             LifxSelectorUtil.closeSelector(localSelector, LOG_ID);
242             selector = null;
243             isScanning = false;
244         }
245     }
246
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()) {
251             if (light == null) {
252                 continue;
253             }
254             boolean waitingForLightResponse = System.currentTimeMillis() - light.lastRequestTimeMillis < 200;
255
256             if (light.supportedProduct && !light.isDataComplete() && !waitingForLightResponse) {
257                 if (light.product == null) {
258                     sendPacket(light.selectorContext, new GetVersionRequest());
259                 }
260                 if (light.label == null) {
261                     sendPacket(light.selectorContext, new GetLabelRequest());
262                 }
263                 light.lastRequestTimeMillis = System.currentTimeMillis();
264             }
265         }
266     }
267
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) });
272
273         if (packet.getSource() == sourceId || packet.getSource() == 0) {
274             MACAddress macAddress = packet.getTarget();
275             DiscoveredLight light = discoveredLights.get(macAddress);
276
277             if (packet instanceof StateServiceResponse) {
278                 int port = (int) ((StateServiceResponse) packet).getPort();
279                 if (port != 0) {
280                     try {
281                         InetSocketAddress socketAddress = new InetSocketAddress(address.getAddress(), port);
282                         if (light == null || (!socketAddress.equals(light.socketAddress))) {
283                             if (light != null) {
284                                 light.cancelUnicastKey();
285                             }
286
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);
293                             }
294                         }
295                     } catch (Exception e) {
296                         logger.warn("{} while connecting to IP address: {}", e.getClass().getSimpleName(),
297                                 e.getMessage());
298                         return;
299                     }
300                 }
301             } else if (light != null) {
302                 if (packet instanceof StateLabelResponse) {
303                     light.label = ((StateLabelResponse) packet).getLabel().trim();
304                 } else if (packet instanceof StateVersionResponse) {
305                     try {
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(),
310                                 e.getMessage());
311                         light.supportedProduct = false;
312                     }
313                 }
314             }
315
316             if (light != null && light.isDataComplete()) {
317                 try {
318                     thingDiscovered(createDiscoveryResult(light));
319                 } catch (IllegalArgumentException e) {
320                     logger.trace("{} while creating discovery result of light ({})", e.getClass().getSimpleName(),
321                             light.logId, e);
322                 }
323             }
324         }
325     }
326
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");
331         }
332
333         String macAsLabel = light.macAddress.getAsLabel();
334         ThingUID thingUID = new ThingUID(product.getThingTypeUID(), macAsLabel);
335
336         String label = light.label;
337         if (StringUtils.isBlank(label)) {
338             label = product.getName();
339         }
340
341         logger.trace("Discovered a LIFX light: {}", label);
342
343         DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUID);
344         builder.withRepresentationProperty(LifxBindingConstants.PROPERTY_MAC_ADDRESS);
345         builder.withLabel(label);
346
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());
354
355         return builder.build();
356     }
357 }