]> git.basschouten.com Git - openhab-addons.git/blob
21011010dbe4870f94289b73c69670233c0f43c6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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, 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, Object> configProperties) {
125         super.activate(configProperties);
126     }
127
128     @Modified
129     @Override
130     protected void modified(@Nullable Map<String, 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             boolean waitingForLightResponse = System.currentTimeMillis() - light.lastRequestTimeMillis < 200;
252
253             if (light.supportedProduct && !light.isDataComplete() && !waitingForLightResponse) {
254                 if (light.product == null) {
255                     sendPacket(light.selectorContext, new GetVersionRequest());
256                 }
257                 if (light.label == null) {
258                     sendPacket(light.selectorContext, new GetLabelRequest());
259                 }
260                 light.lastRequestTimeMillis = System.currentTimeMillis();
261             }
262         }
263     }
264
265     private void handlePacket(Packet packet, InetSocketAddress address) {
266         logger.trace("Discovery : Packet type '{}' received from '{}' for '{}' with sequence '{}' and source '{}'",
267                 new Object[] { packet.getClass().getSimpleName(), address.toString(), packet.getTarget().getHex(),
268                         packet.getSequence(), Long.toString(packet.getSource(), 16) });
269
270         if (packet.getSource() == sourceId || packet.getSource() == 0) {
271             MACAddress macAddress = packet.getTarget();
272             DiscoveredLight light = discoveredLights.get(macAddress);
273
274             if (packet instanceof StateServiceResponse) {
275                 int port = (int) ((StateServiceResponse) packet).getPort();
276                 if (port != 0) {
277                     try {
278                         InetSocketAddress socketAddress = new InetSocketAddress(address.getAddress(), port);
279                         if (light == null || (!socketAddress.equals(light.socketAddress))) {
280                             if (light != null) {
281                                 light.cancelUnicastKey();
282                             }
283
284                             Selector lightSelector = selector;
285                             if (lightSelector != null) {
286                                 String logId = getLogId(macAddress, socketAddress);
287                                 light = new DiscoveredLight(lightSelector, macAddress, socketAddress, logId,
288                                         openUnicastChannel(lightSelector, logId, socketAddress));
289                                 discoveredLights.put(macAddress, light);
290                             }
291                         }
292                     } catch (Exception e) {
293                         logger.warn("{} while connecting to IP address: {}", e.getClass().getSimpleName(),
294                                 e.getMessage());
295                         return;
296                     }
297                 }
298             } else if (light != null) {
299                 if (packet instanceof StateLabelResponse) {
300                     light.label = ((StateLabelResponse) packet).getLabel().trim();
301                 } else if (packet instanceof StateVersionResponse) {
302                     try {
303                         light.product = Product.getProductFromProductID(((StateVersionResponse) packet).getProduct());
304                         light.productVersion = ((StateVersionResponse) packet).getVersion();
305                     } catch (IllegalArgumentException e) {
306                         logger.debug("Discovered an unsupported light ({}): {}", light.macAddress.getAsLabel(),
307                                 e.getMessage());
308                         light.supportedProduct = false;
309                     }
310                 }
311             }
312
313             if (light != null && light.isDataComplete()) {
314                 try {
315                     thingDiscovered(createDiscoveryResult(light));
316                 } catch (IllegalArgumentException e) {
317                     logger.trace("{} while creating discovery result of light ({})", e.getClass().getSimpleName(),
318                             light.logId, e);
319                 }
320             }
321         }
322     }
323
324     private DiscoveryResult createDiscoveryResult(DiscoveredLight light) throws IllegalArgumentException {
325         Product product = light.product;
326         if (product == null) {
327             throw new IllegalArgumentException("Product of discovered light is null");
328         }
329
330         String macAsLabel = light.macAddress.getAsLabel();
331         ThingUID thingUID = new ThingUID(product.getThingTypeUID(), macAsLabel);
332
333         String label = light.label;
334         if (StringUtils.isBlank(label)) {
335             label = product.getName();
336         }
337
338         logger.trace("Discovered a LIFX light: {}", label);
339
340         DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUID);
341         builder.withRepresentationProperty(LifxBindingConstants.PROPERTY_MAC_ADDRESS);
342         builder.withLabel(label);
343
344         builder.withProperty(LifxBindingConstants.CONFIG_PROPERTY_DEVICE_ID, macAsLabel);
345         builder.withProperty(LifxBindingConstants.PROPERTY_MAC_ADDRESS, macAsLabel);
346         builder.withProperty(LifxBindingConstants.PROPERTY_PRODUCT_ID, product.getID());
347         builder.withProperty(LifxBindingConstants.PROPERTY_PRODUCT_NAME, product.getName());
348         builder.withProperty(LifxBindingConstants.PROPERTY_PRODUCT_VERSION, light.productVersion);
349         builder.withProperty(LifxBindingConstants.PROPERTY_VENDOR_ID, product.getVendor().getID());
350         builder.withProperty(LifxBindingConstants.PROPERTY_VENDOR_NAME, product.getVendor().getName());
351
352         return builder.build();
353     }
354 }