]> git.basschouten.com Git - openhab-addons.git/blob
e54cfa9ca8ba6589b2a000c43ff6fec7b8553b6f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.lifx.internal.dto.GetLabelRequest;
32 import org.openhab.binding.lifx.internal.dto.GetServiceRequest;
33 import org.openhab.binding.lifx.internal.dto.GetVersionRequest;
34 import org.openhab.binding.lifx.internal.dto.Packet;
35 import org.openhab.binding.lifx.internal.dto.StateLabelResponse;
36 import org.openhab.binding.lifx.internal.dto.StateServiceResponse;
37 import org.openhab.binding.lifx.internal.dto.StateVersionResponse;
38 import org.openhab.binding.lifx.internal.fields.MACAddress;
39 import org.openhab.binding.lifx.internal.util.LifxSelectorUtil;
40 import org.openhab.core.config.discovery.AbstractDiscoveryService;
41 import org.openhab.core.config.discovery.DiscoveryResult;
42 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
43 import org.openhab.core.config.discovery.DiscoveryService;
44 import org.openhab.core.thing.ThingUID;
45 import org.osgi.service.component.annotations.Activate;
46 import org.osgi.service.component.annotations.Component;
47 import org.osgi.service.component.annotations.Deactivate;
48 import org.osgi.service.component.annotations.Modified;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 /**
53  * The {@link LifxLightDiscovery} provides support for auto-discovery of LIFX
54  * lights.
55  *
56  * @author Dennis Nobel - Initial contribution
57  * @author Karel Goderis - Rewrite for Firmware V2, and remove dependency on external libraries
58  * @author Wouter Born - Discover light labels, improve locking, optimize packet handling
59  */
60 @Component(service = DiscoveryService.class, configurationPid = "discovery.lifx")
61 @NonNullByDefault
62 public class LifxLightDiscovery extends AbstractDiscoveryService {
63
64     private static final String LOG_ID = "Discovery";
65     private static final long REFRESH_INTERVAL = TimeUnit.MINUTES.toSeconds(1);
66     private static final long SELECTOR_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
67
68     private final Logger logger = LoggerFactory.getLogger(LifxLightDiscovery.class);
69
70     private final Map<MACAddress, DiscoveredLight> discoveredLights = new HashMap<>();
71     private final long sourceId = randomSourceId();
72     private final Supplier<Integer> sequenceNumberSupplier = new LifxSequenceNumberSupplier();
73
74     private @Nullable Selector selector;
75     private @Nullable SelectionKey broadcastKey;
76
77     private @Nullable ScheduledFuture<?> discoveryJob;
78     private @Nullable ScheduledFuture<?> networkJob;
79
80     private boolean isScanning = false;
81
82     private class DiscoveredLight {
83
84         private MACAddress macAddress;
85         private InetSocketAddress socketAddress;
86         private String logId;
87         private @Nullable String label;
88         private @Nullable LifxProduct product;
89         private long productVersion;
90         private boolean supportedProduct = true;
91         private LifxSelectorContext selectorContext;
92
93         private long lastRequestTimeMillis;
94
95         public DiscoveredLight(Selector lightSelector, MACAddress macAddress, InetSocketAddress socketAddress,
96                 String logId, @Nullable SelectionKey unicastKey) {
97             this.macAddress = macAddress;
98             this.logId = logId;
99             this.socketAddress = socketAddress;
100             this.selectorContext = new LifxSelectorContext(lightSelector, sourceId, sequenceNumberSupplier, logId,
101                     socketAddress, macAddress, broadcastKey, unicastKey);
102         }
103
104         public boolean isDataComplete() {
105             return label != null && product != null;
106         }
107
108         public void cancelUnicastKey() {
109             SelectionKey unicastKey = selectorContext.getUnicastKey();
110             if (unicastKey != null) {
111                 cancelKey(unicastKey, selectorContext.getLogId());
112             }
113         }
114     }
115
116     public LifxLightDiscovery() throws IllegalArgumentException {
117         super(LifxBindingConstants.SUPPORTED_THING_TYPES, 1, true);
118     }
119
120     @Activate
121     @Override
122     protected void activate(@Nullable Map<String, Object> configProperties) {
123         super.activate(configProperties);
124     }
125
126     @Modified
127     @Override
128     protected void modified(@Nullable Map<String, Object> configProperties) {
129         super.modified(configProperties);
130     }
131
132     @Deactivate
133     @Override
134     protected void deactivate() {
135         super.deactivate();
136     }
137
138     @Override
139     protected void startBackgroundDiscovery() {
140         logger.debug("Starting the LIFX device background discovery");
141
142         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
143         if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
144             discoveryJob = scheduler.scheduleWithFixedDelay(this::doScan, 0, REFRESH_INTERVAL, TimeUnit.SECONDS);
145         }
146     }
147
148     @Override
149     protected void stopBackgroundDiscovery() {
150         logger.debug("Stopping LIFX device background discovery");
151
152         ScheduledFuture<?> localDiscoveryJob = discoveryJob;
153         if (localDiscoveryJob != null && !localDiscoveryJob.isCancelled()) {
154             localDiscoveryJob.cancel(true);
155             discoveryJob = null;
156         }
157
158         ScheduledFuture<?> localNetworkJob = networkJob;
159         if (localNetworkJob != null && !localNetworkJob.isCancelled()) {
160             localNetworkJob.cancel(true);
161             networkJob = null;
162         }
163     }
164
165     @Override
166     protected void startScan() {
167         doScan();
168     }
169
170     @Override
171     protected synchronized void stopScan() {
172         super.stopScan();
173         removeOlderResults(getTimestampOfLastScan());
174     }
175
176     protected void doScan() {
177         try {
178             if (!isScanning) {
179                 isScanning = true;
180                 if (selector != null) {
181                     closeSelector(selector, LOG_ID);
182                 }
183
184                 logger.debug("The LIFX discovery service will use '{}' as source identifier",
185                         Long.toString(sourceId, 16));
186
187                 Selector localSelector = Selector.open();
188                 selector = localSelector;
189
190                 broadcastKey = openBroadcastChannel(localSelector, LOG_ID, BROADCAST_PORT);
191                 networkJob = scheduler.schedule(this::receiveAndHandlePackets, 0, TimeUnit.MILLISECONDS);
192
193                 LifxSelectorContext selectorContext = new LifxSelectorContext(localSelector, sourceId,
194                         sequenceNumberSupplier, LOG_ID, broadcastKey);
195                 broadcastPacket(selectorContext, new GetServiceRequest());
196             } else {
197                 logger.info("A discovery scan for LIFX lights is already underway");
198             }
199         } catch (IOException e) {
200             logger.debug("{} while discovering LIFX lights : {}", e.getClass().getSimpleName(), e.getMessage());
201             isScanning = false;
202         }
203     }
204
205     public void receiveAndHandlePackets() {
206         Selector localSelector = selector;
207
208         try {
209             if (localSelector == null || !localSelector.isOpen()) {
210                 logger.debug("Unable to receive and handle packets with null or closed selector");
211                 return;
212             }
213
214             discoveredLights.clear();
215             logger.trace("Entering read loop");
216             long startStamp = System.currentTimeMillis();
217
218             while (System.currentTimeMillis() - startStamp < SELECTOR_TIMEOUT) {
219                 int lightCount = discoveredLights.size();
220                 long selectStamp = System.currentTimeMillis();
221
222                 LifxSelectorUtil.receiveAndHandlePackets(localSelector, LOG_ID,
223                         (packet, address) -> handlePacket(packet, address));
224                 requestAdditionalLightData();
225
226                 boolean discoveredNewLights = lightCount < discoveredLights.size();
227                 if (!discoveredNewLights) {
228                     boolean preventBusyWaiting = System.currentTimeMillis() - selectStamp < PACKET_INTERVAL;
229                     if (preventBusyWaiting) {
230                         Thread.sleep(PACKET_INTERVAL);
231                     }
232                 }
233             }
234             logger.trace("Exited read loop");
235         } catch (Exception e) {
236             logger.debug("{} while receiving and handling discovery packets: {}", e.getClass().getSimpleName(),
237                     e.getMessage(), e);
238         } finally {
239             LifxSelectorUtil.closeSelector(localSelector, LOG_ID);
240             selector = null;
241             isScanning = false;
242         }
243     }
244
245     private void requestAdditionalLightData() {
246         // Iterate through the discovered lights that have to be set up, and the packets that have to be sent
247         // Workaround to avoid a ConcurrentModifictionException on the selector.SelectedKeys() Set
248         for (DiscoveredLight light : discoveredLights.values()) {
249             boolean waitingForLightResponse = System.currentTimeMillis() - light.lastRequestTimeMillis < 200;
250
251             if (light.supportedProduct && !light.isDataComplete() && !waitingForLightResponse) {
252                 if (light.product == null) {
253                     sendPacket(light.selectorContext, new GetVersionRequest());
254                 }
255                 if (light.label == null) {
256                     sendPacket(light.selectorContext, new GetLabelRequest());
257                 }
258                 light.lastRequestTimeMillis = System.currentTimeMillis();
259             }
260         }
261     }
262
263     private void handlePacket(Packet packet, InetSocketAddress address) {
264         logger.trace("Discovery : Packet type '{}' received from '{}' for '{}' with sequence '{}' and source '{}'",
265                 new Object[] { packet.getClass().getSimpleName(), address.toString(), packet.getTarget().getHex(),
266                         packet.getSequence(), Long.toString(packet.getSource(), 16) });
267
268         if (packet.getSource() == sourceId || packet.getSource() == 0) {
269             MACAddress macAddress = packet.getTarget();
270             DiscoveredLight light = discoveredLights.get(macAddress);
271
272             if (packet instanceof StateServiceResponse response) {
273                 int port = (int) response.getPort();
274                 if (port != 0) {
275                     try {
276                         InetSocketAddress socketAddress = new InetSocketAddress(address.getAddress(), port);
277                         if (light == null || (!socketAddress.equals(light.socketAddress))) {
278                             if (light != null) {
279                                 light.cancelUnicastKey();
280                             }
281
282                             Selector lightSelector = selector;
283                             if (lightSelector != null) {
284                                 String logId = getLogId(macAddress, socketAddress);
285                                 light = new DiscoveredLight(lightSelector, macAddress, socketAddress, logId,
286                                         openUnicastChannel(lightSelector, logId, socketAddress));
287                                 discoveredLights.put(macAddress, light);
288                             }
289                         }
290                     } catch (Exception e) {
291                         logger.warn("{} while connecting to IP address: {}", e.getClass().getSimpleName(),
292                                 e.getMessage());
293                         return;
294                     }
295                 }
296             } else if (light != null) {
297                 if (packet instanceof StateLabelResponse response) {
298                     light.label = response.getLabel().trim();
299                 } else if (packet instanceof StateVersionResponse response) {
300                     try {
301                         LifxProduct product = LifxProduct.getProductFromProductID(response.getProduct());
302                         light.product = product;
303                         light.productVersion = response.getVersion();
304                         light.supportedProduct = product.isLight();
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.supportedProduct && 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         LifxProduct 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 (label == null || label.isBlank()) {
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 }