]> git.basschouten.com Git - openhab-addons.git/blob
f1a8282f81b97693de7068294604b8d2a66ae3d7
[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.PACKET_INTERVAL;
16 import static org.openhab.binding.lifx.internal.fields.MACAddress.BROADCAST_ADDRESS;
17 import static org.openhab.binding.lifx.internal.util.LifxMessageUtil.randomSourceId;
18 import static org.openhab.binding.lifx.internal.util.LifxSelectorUtil.*;
19
20 import java.io.IOException;
21 import java.net.InetSocketAddress;
22 import java.nio.channels.SelectionKey;
23 import java.nio.channels.Selector;
24 import java.util.List;
25 import java.util.concurrent.CopyOnWriteArrayList;
26 import java.util.concurrent.ScheduledExecutorService;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.locks.ReentrantLock;
30 import java.util.function.BiFunction;
31 import java.util.function.Supplier;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.lifx.internal.dto.GetServiceRequest;
36 import org.openhab.binding.lifx.internal.dto.Packet;
37 import org.openhab.binding.lifx.internal.dto.StateServiceResponse;
38 import org.openhab.binding.lifx.internal.fields.MACAddress;
39 import org.openhab.binding.lifx.internal.handler.LifxLightHandler.CurrentLightState;
40 import org.openhab.binding.lifx.internal.listener.LifxResponsePacketListener;
41 import org.openhab.binding.lifx.internal.util.LifxNetworkUtil;
42 import org.openhab.binding.lifx.internal.util.LifxSelectorUtil;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 /**
47  * The {@link LifxLightCommunicationHandler} is responsible for the communications with a light.
48  *
49  * @author Wouter Born - Initial contribution
50  */
51 @NonNullByDefault
52 public class LifxLightCommunicationHandler {
53
54     private final Logger logger = LoggerFactory.getLogger(LifxLightCommunicationHandler.class);
55
56     private final String logId;
57     private final CurrentLightState currentLightState;
58     private final ScheduledExecutorService scheduler;
59
60     private final ReentrantLock lock = new ReentrantLock();
61     private final long sourceId = randomSourceId();
62     private final Supplier<Integer> sequenceNumberSupplier = new LifxSequenceNumberSupplier();
63
64     private int service;
65     private int unicastPort;
66     private final int broadcastPort = LifxNetworkUtil.getNewBroadcastPort();
67
68     private @Nullable ScheduledFuture<?> networkJob;
69
70     private @Nullable MACAddress macAddress;
71     private @Nullable InetSocketAddress host;
72     private boolean broadcastEnabled;
73
74     private @Nullable Selector selector;
75     private @Nullable SelectionKey broadcastKey;
76     private @Nullable SelectionKey unicastKey;
77     private @Nullable LifxSelectorContext selectorContext;
78
79     public LifxLightCommunicationHandler(LifxLightContext context) {
80         this.logId = context.getLogId();
81         this.macAddress = context.getConfiguration().getMACAddress();
82         this.host = context.getConfiguration().getHost();
83         this.currentLightState = context.getCurrentLightState();
84         this.scheduler = context.getScheduler();
85         this.broadcastEnabled = context.getConfiguration().getHost() == null;
86     }
87
88     private List<LifxResponsePacketListener> responsePacketListeners = new CopyOnWriteArrayList<>();
89
90     public void addResponsePacketListener(LifxResponsePacketListener listener) {
91         responsePacketListeners.add(listener);
92     }
93
94     public void removeResponsePacketListener(LifxResponsePacketListener listener) {
95         responsePacketListeners.remove(listener);
96     }
97
98     public void start() {
99         try {
100             lock.lock();
101
102             logger.debug("{} : Starting communication handler", logId);
103             logger.debug("{} : Using '{}' as source identifier", logId, Long.toString(sourceId, 16));
104
105             ScheduledFuture<?> localNetworkJob = networkJob;
106             if (localNetworkJob == null || localNetworkJob.isCancelled()) {
107                 networkJob = scheduler.scheduleWithFixedDelay(this::receiveAndHandlePackets, 0, PACKET_INTERVAL,
108                         TimeUnit.MILLISECONDS);
109             }
110
111             currentLightState.setOffline();
112
113             Selector localSelector = Selector.open();
114             selector = localSelector;
115
116             if (isBroadcastEnabled()) {
117                 broadcastKey = openBroadcastChannel(selector, logId, broadcastPort);
118                 selectorContext = new LifxSelectorContext(localSelector, sourceId, sequenceNumberSupplier, logId, host,
119                         macAddress, broadcastKey, unicastKey);
120                 broadcastPacket(new GetServiceRequest());
121             } else {
122                 unicastKey = openUnicastChannel(selector, logId, host);
123                 selectorContext = new LifxSelectorContext(localSelector, sourceId, sequenceNumberSupplier, logId, host,
124                         macAddress, broadcastKey, unicastKey);
125                 sendPacket(new GetServiceRequest());
126             }
127         } catch (IOException e) {
128             logger.error("{} while starting LIFX communication handler for light '{}' : {}",
129                     e.getClass().getSimpleName(), logId, e.getMessage(), e);
130         } finally {
131             lock.unlock();
132         }
133     }
134
135     public void stop() {
136         try {
137             lock.lock();
138
139             ScheduledFuture<?> localNetworkJob = networkJob;
140             if (localNetworkJob != null && !localNetworkJob.isCancelled()) {
141                 localNetworkJob.cancel(true);
142                 networkJob = null;
143             }
144
145             closeSelector(selector, logId);
146             selector = null;
147             broadcastKey = null;
148             unicastKey = null;
149             selectorContext = null;
150         } finally {
151             lock.unlock();
152         }
153     }
154
155     public @Nullable InetSocketAddress getIpAddress() {
156         return host;
157     }
158
159     public @Nullable MACAddress getMACAddress() {
160         return macAddress;
161     }
162
163     public void receiveAndHandlePackets() {
164         try {
165             lock.lock();
166             Selector localSelector = selector;
167             if (localSelector == null || !localSelector.isOpen()) {
168                 logger.debug("{} : Unable to receive and handle packets with null or closed selector", logId);
169             } else {
170                 LifxSelectorUtil.receiveAndHandlePackets(localSelector, logId,
171                         (packet, address) -> handlePacket(packet, address));
172             }
173         } catch (Exception e) {
174             logger.error("{} while receiving a packet from the light ({}): {}", e.getClass().getSimpleName(), logId,
175                     e.getMessage());
176         } finally {
177             lock.unlock();
178         }
179     }
180
181     private void handlePacket(Packet packet, InetSocketAddress address) {
182         boolean packetFromConfiguredMAC = macAddress != null && (packet.getTarget().equals(macAddress));
183         boolean packetFromConfiguredHost = host != null && (address.equals(host));
184         boolean broadcastPacket = packet.getTarget().equals(BROADCAST_ADDRESS);
185         boolean packetSourceIsHandler = (packet.getSource() == sourceId || packet.getSource() == 0);
186
187         if ((packetFromConfiguredMAC || packetFromConfiguredHost || broadcastPacket) && packetSourceIsHandler) {
188             logger.trace("{} : Packet type '{}' received from '{}' for '{}' with sequence '{}' and source '{}'",
189                     new Object[] { logId, packet.getClass().getSimpleName(), address.toString(),
190                             packet.getTarget().getHex(), packet.getSequence(), Long.toString(packet.getSource(), 16) });
191
192             if (packet instanceof StateServiceResponse response) {
193                 MACAddress discoveredAddress = response.getTarget();
194                 if (packetFromConfiguredHost && macAddress == null) {
195                     macAddress = discoveredAddress;
196                     currentLightState.setOnline(discoveredAddress);
197
198                     LifxSelectorContext context = selectorContext;
199                     if (context != null) {
200                         context.setMACAddress(macAddress);
201                     }
202                     return;
203                 } else if (macAddress != null && macAddress.equals(discoveredAddress)) {
204                     boolean newHost = host == null || !address.equals(host);
205                     boolean newPort = unicastPort != (int) response.getPort();
206                     boolean newService = service != response.getService();
207
208                     if (newHost || newPort || newService || currentLightState.isOffline()) {
209                         this.unicastPort = (int) response.getPort();
210                         this.service = response.getService();
211
212                         if (unicastPort == 0) {
213                             logger.warn("Light ({}) service with ID '{}' is currently not available", logId, service);
214                             currentLightState.setOfflineByCommunicationError();
215                         } else {
216                             this.host = new InetSocketAddress(address.getAddress(), unicastPort);
217
218                             try {
219                                 cancelKey(unicastKey, logId);
220                                 unicastKey = openUnicastChannel(selector, logId, host);
221
222                                 LifxSelectorContext context = selectorContext;
223                                 if (context != null) {
224                                     context.setHost(host);
225                                     context.setUnicastKey(unicastKey);
226                                 }
227                             } catch (IOException e) {
228                                 logger.warn("{} while opening the unicast channel of the light ({}): {}",
229                                         e.getClass().getSimpleName(), logId, e.getMessage());
230                                 currentLightState.setOfflineByCommunicationError();
231                                 return;
232                             }
233
234                             currentLightState.setOnline();
235                         }
236                     }
237                 }
238             }
239
240             // Listeners are notified in a separate thread for better concurrency and to prevent deadlock.
241             scheduler.schedule(() -> {
242                 responsePacketListeners.forEach(listener -> listener.handleResponsePacket(packet));
243             }, 0, TimeUnit.MILLISECONDS);
244         }
245     }
246
247     public boolean isBroadcastEnabled() {
248         return broadcastEnabled;
249     }
250
251     public void broadcastPacket(Packet packet) {
252         wrappedPacketSend((s, p) -> LifxSelectorUtil.broadcastPacket(s, p), packet);
253     }
254
255     public void sendPacket(Packet packet) {
256         if (host != null) {
257             wrappedPacketSend((s, p) -> LifxSelectorUtil.sendPacket(s, p), packet);
258         }
259     }
260
261     public void resendPacket(Packet packet) {
262         if (host != null) {
263             wrappedPacketSend((s, p) -> LifxSelectorUtil.resendPacket(s, p), packet);
264         }
265     }
266
267     private void wrappedPacketSend(BiFunction<LifxSelectorContext, Packet, Boolean> function, Packet packet) {
268         LifxSelectorContext localSelectorContext = selectorContext;
269         if (localSelectorContext != null) {
270             boolean result = false;
271             try {
272                 lock.lock();
273                 result = function.apply(localSelectorContext, packet);
274             } finally {
275                 lock.unlock();
276                 if (!result) {
277                     currentLightState.setOfflineByCommunicationError();
278                 }
279             }
280         }
281     }
282 }