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 response) {
193 MACAddress discoveredAddress = response.getTarget();
194 if (packetFromConfiguredHost && macAddress == null) {
195 macAddress = discoveredAddress;
196 currentLightState.setOnline(discoveredAddress);
198 LifxSelectorContext context = selectorContext;
199 if (context != null) {
200 context.setMACAddress(macAddress);
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();
208 if (newHost || newPort || newService || currentLightState.isOffline()) {
209 this.unicastPort = (int) response.getPort();
210 this.service = response.getService();
212 if (unicastPort == 0) {
213 logger.warn("Light ({}) service with ID '{}' is currently not available", logId, service);
214 currentLightState.setOfflineByCommunicationError();
216 this.host = new InetSocketAddress(address.getAddress(), unicastPort);
219 cancelKey(unicastKey, logId);
220 unicastKey = openUnicastChannel(selector, logId, host);
222 LifxSelectorContext context = selectorContext;
223 if (context != null) {
224 context.setHost(host);
225 context.setUnicastKey(unicastKey);
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();
234 currentLightState.setOnline();
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);
247 public boolean isBroadcastEnabled() {
248 return broadcastEnabled;
251 public void broadcastPacket(Packet packet) {
252 wrappedPacketSend((s, p) -> LifxSelectorUtil.broadcastPacket(s, p), packet);
255 public void sendPacket(Packet packet) {
257 wrappedPacketSend((s, p) -> LifxSelectorUtil.sendPacket(s, p), packet);
261 public void resendPacket(Packet packet) {
263 wrappedPacketSend((s, p) -> LifxSelectorUtil.resendPacket(s, p), packet);
267 private void wrappedPacketSend(BiFunction<LifxSelectorContext, Packet, Boolean> function, Packet packet) {
268 LifxSelectorContext localSelectorContext = selectorContext;
269 if (localSelectorContext != null) {
270 boolean result = false;
273 result = function.apply(localSelectorContext, packet);
277 currentLightState.setOfflineByCommunicationError();