2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.lifx.internal;
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.*;
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;
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;
47 * The {@link LifxLightCommunicationHandler} is responsible for the communications with a light.
49 * @author Wouter Born - Initial contribution
52 public class LifxLightCommunicationHandler {
54 private final Logger logger = LoggerFactory.getLogger(LifxLightCommunicationHandler.class);
56 private final String logId;
57 private final CurrentLightState currentLightState;
58 private final ScheduledExecutorService scheduler;
60 private final ReentrantLock lock = new ReentrantLock();
61 private final long sourceId = randomSourceId();
62 private final Supplier<Integer> sequenceNumberSupplier = new LifxSequenceNumberSupplier();
65 private int unicastPort;
66 private final int broadcastPort = LifxNetworkUtil.getNewBroadcastPort();
68 private @Nullable ScheduledFuture<?> networkJob;
70 private @Nullable MACAddress macAddress;
71 private @Nullable InetSocketAddress host;
72 private boolean broadcastEnabled;
74 private @Nullable Selector selector;
75 private @Nullable SelectionKey broadcastKey;
76 private @Nullable SelectionKey unicastKey;
77 private @Nullable LifxSelectorContext selectorContext;
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;
88 private List<LifxResponsePacketListener> responsePacketListeners = new CopyOnWriteArrayList<>();
90 public void addResponsePacketListener(LifxResponsePacketListener listener) {
91 responsePacketListeners.add(listener);
94 public void removeResponsePacketListener(LifxResponsePacketListener listener) {
95 responsePacketListeners.remove(listener);
102 logger.debug("{} : Starting communication handler", logId);
103 logger.debug("{} : Using '{}' as source identifier", logId, Long.toString(sourceId, 16));
105 ScheduledFuture<?> localNetworkJob = networkJob;
106 if (localNetworkJob == null || localNetworkJob.isCancelled()) {
107 networkJob = scheduler.scheduleWithFixedDelay(this::receiveAndHandlePackets, 0, PACKET_INTERVAL,
108 TimeUnit.MILLISECONDS);
111 currentLightState.setOffline();
113 Selector localSelector = Selector.open();
114 selector = localSelector;
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());
122 unicastKey = openUnicastChannel(selector, logId, host);
123 selectorContext = new LifxSelectorContext(localSelector, sourceId, sequenceNumberSupplier, logId, host,
124 macAddress, broadcastKey, unicastKey);
125 sendPacket(new GetServiceRequest());
127 } catch (IOException e) {
128 logger.error("{} while starting LIFX communication handler for light '{}' : {}",
129 e.getClass().getSimpleName(), logId, e.getMessage(), e);
139 ScheduledFuture<?> localNetworkJob = networkJob;
140 if (localNetworkJob != null && !localNetworkJob.isCancelled()) {
141 localNetworkJob.cancel(true);
145 closeSelector(selector, logId);
149 selectorContext = null;
155 public @Nullable InetSocketAddress getIpAddress() {
159 public @Nullable MACAddress getMACAddress() {
163 public void receiveAndHandlePackets() {
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);
170 LifxSelectorUtil.receiveAndHandlePackets(localSelector, logId,
171 (packet, address) -> handlePacket(packet, address));
173 } catch (Exception e) {
174 logger.error("{} while receiving a packet from the light ({}): {}", e.getClass().getSimpleName(), logId,
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);
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) });
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);
199 LifxSelectorContext context = selectorContext;
200 if (context != null) {
201 context.setMACAddress(macAddress);
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();
209 if (newHost || newPort || newService || currentLightState.isOffline()) {
210 this.unicastPort = (int) response.getPort();
211 this.service = response.getService();
213 if (unicastPort == 0) {
214 logger.warn("Light ({}) service with ID '{}' is currently not available", logId, service);
215 currentLightState.setOfflineByCommunicationError();
217 this.host = new InetSocketAddress(address.getAddress(), unicastPort);
220 cancelKey(unicastKey, logId);
221 unicastKey = openUnicastChannel(selector, logId, host);
223 LifxSelectorContext context = selectorContext;
224 if (context != null) {
225 context.setHost(host);
226 context.setUnicastKey(unicastKey);
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();
235 currentLightState.setOnline();
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);
248 public boolean isBroadcastEnabled() {
249 return broadcastEnabled;
252 public void broadcastPacket(Packet packet) {
253 wrappedPacketSend((s, p) -> LifxSelectorUtil.broadcastPacket(s, p), packet);
256 public void sendPacket(Packet packet) {
258 wrappedPacketSend((s, p) -> LifxSelectorUtil.sendPacket(s, p), packet);
262 public void resendPacket(Packet packet) {
264 wrappedPacketSend((s, p) -> LifxSelectorUtil.resendPacket(s, p), packet);
268 private void wrappedPacketSend(BiFunction<LifxSelectorContext, Packet, Boolean> function, Packet packet) {
269 LifxSelectorContext localSelectorContext = selectorContext;
270 if (localSelectorContext != null) {
271 boolean result = false;
274 result = function.apply(localSelectorContext, packet);
278 currentLightState.setOfflineByCommunicationError();