2 * Copyright (c) 2010-2020 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.protocol.Product.Feature.MULTIZONE;
17 import static org.openhab.binding.lifx.internal.util.LifxMessageUtil.*;
19 import java.time.Duration;
20 import java.util.ArrayList;
21 import java.util.Iterator;
22 import java.util.List;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.locks.ReentrantLock;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.lifx.internal.fields.HSBK;
33 import org.openhab.binding.lifx.internal.listener.LifxLightStateListener;
34 import org.openhab.binding.lifx.internal.protocol.AcknowledgementResponse;
35 import org.openhab.binding.lifx.internal.protocol.ApplicationRequest;
36 import org.openhab.binding.lifx.internal.protocol.Effect;
37 import org.openhab.binding.lifx.internal.protocol.GetColorZonesRequest;
38 import org.openhab.binding.lifx.internal.protocol.GetLightInfraredRequest;
39 import org.openhab.binding.lifx.internal.protocol.GetLightPowerRequest;
40 import org.openhab.binding.lifx.internal.protocol.GetRequest;
41 import org.openhab.binding.lifx.internal.protocol.Packet;
42 import org.openhab.binding.lifx.internal.protocol.PowerState;
43 import org.openhab.binding.lifx.internal.protocol.Product;
44 import org.openhab.binding.lifx.internal.protocol.SetColorRequest;
45 import org.openhab.binding.lifx.internal.protocol.SetColorZonesRequest;
46 import org.openhab.binding.lifx.internal.protocol.SetLightInfraredRequest;
47 import org.openhab.binding.lifx.internal.protocol.SetLightPowerRequest;
48 import org.openhab.binding.lifx.internal.protocol.SetPowerRequest;
49 import org.openhab.binding.lifx.internal.protocol.SetTileEffectRequest;
50 import org.openhab.binding.lifx.internal.protocol.SignalStrength;
51 import org.openhab.core.library.types.PercentType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * The {@link LifxLightStateChanger} listens to state changes of the {@code pendingLightState}. It sends packets to a
57 * light so the change the actual light state to that of the {@code pendingLightState}. When the light does not
58 * acknowledge a packet, it resends it (max 3 times).
60 * @author Wouter Born - Extracted class from LifxLightHandler, added logic for handling packet loss
63 public class LifxLightStateChanger implements LifxLightStateListener {
66 * Milliseconds before a packet is considered to be lost (unacknowledged).
68 private static final int PACKET_ACKNOWLEDGE_INTERVAL = 250;
71 * The number of times a lost packet will be resent.
73 private static final int MAX_RETRIES = 3;
75 private final Logger logger = LoggerFactory.getLogger(LifxLightStateChanger.class);
77 private final String logId;
78 private final Product product;
79 private final Duration fadeTime;
80 private final LifxLightState pendingLightState;
81 private final ScheduledExecutorService scheduler;
82 private final LifxLightCommunicationHandler communicationHandler;
84 private final ReentrantLock lock = new ReentrantLock();
86 private @Nullable ScheduledFuture<?> sendJob;
88 private Map<Integer, @Nullable List<PendingPacket>> pendingPacketsMap = new ConcurrentHashMap<>();
90 private class PendingPacket {
96 private PendingPacket(Packet packet) {
100 private boolean hasAcknowledgeIntervalElapsed() {
101 long millisSinceLastSend = System.currentTimeMillis() - lastSend;
102 return millisSinceLastSend > PACKET_ACKNOWLEDGE_INTERVAL;
106 public LifxLightStateChanger(LifxLightContext context, LifxLightCommunicationHandler communicationHandler) {
107 this.logId = context.getLogId();
108 this.product = context.getProduct();
109 this.fadeTime = context.getConfiguration().getFadeTime();
110 this.pendingLightState = context.getPendingLightState();
111 this.scheduler = context.getScheduler();
112 this.communicationHandler = communicationHandler;
115 private void sendPendingPackets() {
119 removeFailedPackets();
120 PendingPacket pendingPacket = findPacketToSend();
122 if (pendingPacket != null) {
123 Packet packet = pendingPacket.packet;
125 if (pendingPacket.sendCount == 0) {
126 // sendPacket will set the sequence number
127 logger.debug("{} : Sending {} packet", logId, packet.getClass().getSimpleName());
128 communicationHandler.sendPacket(packet);
130 // resendPacket will reuse the sequence number
131 logger.debug("{} : Resending {} packet", logId, packet.getClass().getSimpleName());
132 communicationHandler.resendPacket(packet);
134 pendingPacket.lastSend = System.currentTimeMillis();
135 pendingPacket.sendCount++;
137 } catch (Exception e) {
138 logger.error("Error occurred while sending packet", e);
144 public void start() {
147 communicationHandler.addResponsePacketListener(this::handleResponsePacket);
148 pendingLightState.addListener(this);
149 ScheduledFuture<?> localSendJob = sendJob;
150 if (localSendJob == null || localSendJob.isCancelled()) {
151 sendJob = scheduler.scheduleWithFixedDelay(this::sendPendingPackets, 0, PACKET_INTERVAL,
152 TimeUnit.MILLISECONDS);
154 } catch (Exception e) {
155 logger.error("Error occurred while starting send packets job", e);
164 communicationHandler.removeResponsePacketListener(this::handleResponsePacket);
165 pendingLightState.removeListener(this);
166 ScheduledFuture<?> localSendJob = sendJob;
167 if (localSendJob != null && !localSendJob.isCancelled()) {
168 localSendJob.cancel(true);
171 pendingPacketsMap.clear();
172 } catch (Exception e) {
173 logger.error("Error occurred while stopping send packets job", e);
179 private List<PendingPacket> createPendingPackets(Packet... packets) {
180 Integer packetType = null;
181 List<PendingPacket> pendingPackets = new ArrayList<>();
183 for (Packet packet : packets) {
184 // the acknowledgement is used to resend the packet in case of packet loss
185 packet.setAckRequired(true);
186 // the LIFX LAN protocol spec indicates that the response returned for a request would be the
188 packet.setResponseRequired(false);
189 pendingPackets.add(new PendingPacket(packet));
191 if (packetType == null) {
192 packetType = packet.getPacketType();
193 } else if (packetType != packet.getPacketType()) {
194 throw new IllegalArgumentException("Packets should have same packet type");
198 return pendingPackets;
201 private void addPacketsToMap(Packet... packets) {
202 List<PendingPacket> newPendingPackets = createPendingPackets(packets);
203 int packetType = packets[0].getPacketType();
207 List<PendingPacket> pendingPackets = pendingPacketsMap.get(packetType);
208 if (pendingPackets == null) {
209 pendingPacketsMap.put(packetType, newPendingPackets);
211 pendingPackets.addAll(newPendingPackets);
218 private void replacePacketsInMap(Packet... packets) {
219 List<PendingPacket> pendingPackets = createPendingPackets(packets);
220 int packetType = packets[0].getPacketType();
224 pendingPacketsMap.put(packetType, pendingPackets);
230 private @Nullable PendingPacket findPacketToSend() {
231 PendingPacket result = null;
232 for (List<PendingPacket> pendingPackets : pendingPacketsMap.values()) {
233 if (pendingPackets != null) {
234 for (PendingPacket pendingPacket : pendingPackets) {
235 if (pendingPacket.hasAcknowledgeIntervalElapsed()
236 && (result == null || pendingPacket.lastSend < result.lastSend)) {
237 result = pendingPacket;
246 private void removePacketsByType(int packetType) {
249 pendingPacketsMap.remove(packetType);
255 private void removeFailedPackets() {
256 for (List<PendingPacket> pendingPackets : pendingPacketsMap.values()) {
257 if (pendingPackets != null) {
258 Iterator<PendingPacket> it = pendingPackets.iterator();
259 while (it.hasNext()) {
260 PendingPacket pendingPacket = it.next();
261 if (pendingPacket.sendCount > MAX_RETRIES && pendingPacket.hasAcknowledgeIntervalElapsed()) {
262 logger.warn("{} failed (unacknowledged {} times to light {})",
263 pendingPacket.packet.getClass().getSimpleName(), pendingPacket.sendCount, logId);
271 private @Nullable PendingPacket removeAcknowledgedPacket(int sequenceNumber) {
272 for (List<PendingPacket> pendingPackets : pendingPacketsMap.values()) {
273 if (pendingPackets != null) {
274 Iterator<PendingPacket> it = pendingPackets.iterator();
275 while (it.hasNext()) {
276 PendingPacket pendingPacket = it.next();
277 if (pendingPacket.packet.getSequence() == sequenceNumber) {
279 return pendingPacket;
289 public void handleColorsChange(HSBK[] oldColors, HSBK[] newColors) {
290 if (sameColors(newColors)) {
291 SetColorRequest packet = new SetColorRequest(pendingLightState.getColors()[0], fadeTime.toMillis());
292 removePacketsByType(SetColorZonesRequest.TYPE);
293 replacePacketsInMap(packet);
295 List<SetColorZonesRequest> packets = new ArrayList<>();
296 for (int i = 0; i < newColors.length; i++) {
297 if (newColors[i] != null && !newColors[i].equals(oldColors[i])) {
299 new SetColorZonesRequest(i, newColors[i], fadeTime.toMillis(), ApplicationRequest.APPLY));
302 if (!packets.isEmpty()) {
303 removePacketsByType(SetColorRequest.TYPE);
304 addPacketsToMap(packets.toArray(new SetColorZonesRequest[packets.size()]));
310 public void handlePowerStateChange(@Nullable PowerState oldPowerState, PowerState newPowerState) {
311 if (!newPowerState.equals(oldPowerState)) {
312 SetLightPowerRequest packet = new SetLightPowerRequest(pendingLightState.getPowerState());
313 replacePacketsInMap(packet);
318 public void handleInfraredChange(@Nullable PercentType oldInfrared, PercentType newInfrared) {
319 PercentType infrared = pendingLightState.getInfrared();
320 if (infrared != null) {
321 SetLightInfraredRequest packet = new SetLightInfraredRequest(percentTypeToInfrared(infrared));
322 replacePacketsInMap(packet);
327 public void handleSignalStrengthChange(@Nullable SignalStrength oldSignalStrength,
328 SignalStrength newSignalStrength) {
333 public void handleTileEffectChange(@Nullable Effect oldEffect, Effect newEffect) {
334 if (oldEffect == null || !oldEffect.equals(newEffect)) {
335 SetTileEffectRequest packet = new SetTileEffectRequest(newEffect);
336 replacePacketsInMap(packet);
340 public void handleResponsePacket(Packet packet) {
341 if (packet instanceof AcknowledgementResponse) {
342 long ackTimestamp = System.currentTimeMillis();
344 PendingPacket pendingPacket;
348 pendingPacket = removeAcknowledgedPacket(packet.getSequence());
353 if (pendingPacket != null) {
354 Packet sentPacket = pendingPacket.packet;
355 logger.debug("{} : {} packet was acknowledged in {}ms", logId, sentPacket.getClass().getSimpleName(),
356 ackTimestamp - pendingPacket.lastSend);
358 // when these packets get lost the current state will still be updated by the
359 // LifxLightCurrentStateUpdater
360 if (sentPacket instanceof SetPowerRequest) {
361 GetLightPowerRequest powerPacket = new GetLightPowerRequest();
362 communicationHandler.sendPacket(powerPacket);
363 } else if (sentPacket instanceof SetColorRequest) {
364 GetRequest colorPacket = new GetRequest();
365 communicationHandler.sendPacket(colorPacket);
366 getZonesIfZonesAreSet();
367 } else if (sentPacket instanceof SetColorZonesRequest) {
368 getZonesIfZonesAreSet();
369 } else if (sentPacket instanceof SetLightInfraredRequest) {
370 GetLightInfraredRequest infraredPacket = new GetLightInfraredRequest();
371 communicationHandler.sendPacket(infraredPacket);
374 logger.debug("{} : No pending packet found for ack with sequence number: {}", logId,
375 packet.getSequence());
380 private void getZonesIfZonesAreSet() {
381 if (product.hasFeature(MULTIZONE)) {
382 List<PendingPacket> pending = pendingPacketsMap.get(SetColorZonesRequest.TYPE);
383 if (pending == null || pending.isEmpty()) {
384 GetColorZonesRequest zoneColorPacket = new GetColorZonesRequest();
385 communicationHandler.sendPacket(zoneColorPacket);