2 * Copyright (c) 2010-2021 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.util;
15 import static org.openhab.binding.lifx.internal.LifxBindingConstants.PACKET_INTERVAL;
17 import java.util.List;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.CopyOnWriteArrayList;
21 import java.util.concurrent.locks.ReentrantLock;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.lifx.internal.fields.MACAddress;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
30 * The {@link LifxThrottlingUtil} is a utility class that regulates the frequency at which messages/packets are
31 * sent to LIFX lights. The LIFX LAN Protocol Specification states that lights can process up to 20 messages per second,
34 * @author Karel Goderis - Initial contribution
35 * @author Wouter Born - Deadlock fix
38 public final class LifxThrottlingUtil {
40 private static final Logger LOGGER = LoggerFactory.getLogger(LifxThrottlingUtil.class);
42 private LifxThrottlingUtil() {
43 // hidden utility class constructor
47 * Tracks when the last packet was sent to a LIFX light. The packet is sent after obtaining the lock and before
50 private static class LifxLightCommunicationTracker {
52 private long timestamp;
54 private ReentrantLock lock = new ReentrantLock();
60 public void unlock() {
61 // When iterating over all trackers another thread may have inserted this object so this thread may not
62 // have a lock on it. When the thread does not have the lock, it also did not send a packet.
63 if (lock.isHeldByCurrentThread()) {
64 timestamp = System.currentTimeMillis();
69 public long getTimestamp() {
75 * A separate list of trackers is maintained when locking all lights in case of a broadcast. Iterators of
76 * {@link ConcurrentHashMap}s may behave non-linear when inserts take place to obtain more concurrency. When the
77 * iterator of {@code values()} of {@link #macTrackerMapping} is used for locking all lights, it could sometimes
80 private static List<LifxLightCommunicationTracker> trackers = new CopyOnWriteArrayList<>();
82 private static Map<MACAddress, LifxLightCommunicationTracker> macTrackerMapping = new ConcurrentHashMap<>();
84 public static void lock(@Nullable MACAddress mac) throws InterruptedException {
86 LifxLightCommunicationTracker tracker = getOrCreateTracker(mac);
88 waitForNextPacketInterval(tracker.getTimestamp());
94 private static LifxLightCommunicationTracker getOrCreateTracker(MACAddress mac) {
95 LifxLightCommunicationTracker tracker = macTrackerMapping.get(mac);
96 if (tracker == null) {
97 // for better performance only synchronize when necessary
98 synchronized (trackers) {
99 // another thread may just have added a tracker in this synchronized block, so reevaluate
100 tracker = macTrackerMapping.get(mac);
101 if (tracker == null) {
102 tracker = new LifxLightCommunicationTracker();
103 trackers.add(tracker);
104 macTrackerMapping.put(mac, tracker);
111 private static void waitForNextPacketInterval(long timestamp) throws InterruptedException {
112 long timeToWait = Math.max(PACKET_INTERVAL - (System.currentTimeMillis() - timestamp), 0);
113 if (timeToWait > 0) {
114 Thread.sleep(timeToWait);
118 public static void unlock(@Nullable MACAddress mac) {
120 LifxLightCommunicationTracker tracker = macTrackerMapping.get(mac);
121 if (tracker != null) {
129 public static void lock() throws InterruptedException {
131 for (LifxLightCommunicationTracker tracker : trackers) {
133 lastStamp = Math.max(lastStamp, tracker.getTimestamp());
135 waitForNextPacketInterval(lastStamp);
138 public static void unlock() {
139 for (LifxLightCommunicationTracker tracker : trackers) {