]> git.basschouten.com Git - openhab-addons.git/blob
00350626f7ff83a5d314d2b6094399b993503615
[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) {
193                 StateServiceResponse response = (StateServiceResponse) packet;
194                 MACAddress discoveredAddress = response.getTarget();
195                 if (packetFromConfiguredHost && macAddress == null) {
196                     macAddress = discoveredAddress;
197                     currentLightState.setOnline(discoveredAddress);
198
199                     LifxSelectorContext context = selectorContext;
200                     if (context != null) {
201                         context.setMACAddress(macAddress);
202                     }
203                     return;
204                 } else if (macAddress != null && macAddress.equals(discoveredAddress)) {
205                     boolean newHost = host == null || !address.equals(host);
206                     boolean newPort = unicastPort != (int) response.getPort();
207                     boolean newService = service != response.getService();
208
209                     if (newHost || newPort || newService || currentLightState.isOffline()) {
210                         this.unicastPort = (int) response.getPort();
211                         this.service = response.getService();
212
213                         if (unicastPort == 0) {
214                             logger.warn("Light ({}) service with ID '{}' is currently not available", logId, service);
215                             currentLightState.setOfflineByCommunicationError();
216                         } else {
217                             this.host = new InetSocketAddress(address.getAddress(), unicastPort);
218
219                             try {
220                                 cancelKey(unicastKey, logId);
221                                 unicastKey = openUnicastChannel(selector, logId, host);
222
223                                 LifxSelectorContext context = selectorContext;
224                                 if (context != null) {
225                                     context.setHost(host);
226                                     context.setUnicastKey(unicastKey);
227                                 }
228                             } catch (IOException e) {
229                                 logger.warn("{} while opening the unicast channel of the light ({}): {}",
230                                         e.getClass().getSimpleName(), logId, e.getMessage());
231                                 currentLightState.setOfflineByCommunicationError();
232                                 return;
233                             }
234
235                             currentLightState.setOnline();
236                         }
237                     }
238                 }
239             }
240
241             // Listeners are notified in a separate thread for better concurrency and to prevent deadlock.
242             scheduler.schedule(() -> {
243                 responsePacketListeners.forEach(listener -> listener.handleResponsePacket(packet));
244             }, 0, TimeUnit.MILLISECONDS);
245         }
246     }
247
248     public boolean isBroadcastEnabled() {
249         return broadcastEnabled;
250     }
251
252     public void broadcastPacket(Packet packet) {
253         wrappedPacketSend((s, p) -> LifxSelectorUtil.broadcastPacket(s, p), packet);
254     }
255
256     public void sendPacket(Packet packet) {
257         if (host != null) {
258             wrappedPacketSend((s, p) -> LifxSelectorUtil.sendPacket(s, p), packet);
259         }
260     }
261
262     public void resendPacket(Packet packet) {
263         if (host != null) {
264             wrappedPacketSend((s, p) -> LifxSelectorUtil.resendPacket(s, p), packet);
265         }
266     }
267
268     private void wrappedPacketSend(BiFunction<LifxSelectorContext, Packet, Boolean> function, Packet packet) {
269         LifxSelectorContext localSelectorContext = selectorContext;
270         if (localSelectorContext != null) {
271             boolean result = false;
272             try {
273                 lock.lock();
274                 result = function.apply(localSelectorContext, packet);
275             } finally {
276                 lock.unlock();
277                 if (!result) {
278                     currentLightState.setOfflineByCommunicationError();
279                 }
280             }
281         }
282     }
283 }