]> git.basschouten.com Git - openhab-addons.git/blob
3f4721a1db432be2c5ffdbdf529b7152c2f8661a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.protocol.Product.Feature.MULTIZONE;
17 import static org.openhab.binding.lifx.internal.util.LifxMessageUtil.*;
18
19 import java.time.Duration;
20 import java.util.ArrayList;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Map;
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;
29
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;
54
55 /**
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).
59  *
60  * @author Wouter Born - Extracted class from LifxLightHandler, added logic for handling packet loss
61  */
62 @NonNullByDefault
63 public class LifxLightStateChanger implements LifxLightStateListener {
64
65     /**
66      * Milliseconds before a packet is considered to be lost (unacknowledged).
67      */
68     private static final int PACKET_ACKNOWLEDGE_INTERVAL = 250;
69
70     /**
71      * The number of times a lost packet will be resent.
72      */
73     private static final int MAX_RETRIES = 3;
74
75     private final Logger logger = LoggerFactory.getLogger(LifxLightStateChanger.class);
76
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;
83
84     private final ReentrantLock lock = new ReentrantLock();
85
86     private @Nullable ScheduledFuture<?> sendJob;
87
88     private Map<Integer, @Nullable List<PendingPacket>> pendingPacketsMap = new ConcurrentHashMap<>();
89
90     private class PendingPacket {
91
92         long lastSend;
93         int sendCount;
94         final Packet packet;
95
96         private PendingPacket(Packet packet) {
97             this.packet = packet;
98         }
99
100         private boolean hasAcknowledgeIntervalElapsed() {
101             long millisSinceLastSend = System.currentTimeMillis() - lastSend;
102             return millisSinceLastSend > PACKET_ACKNOWLEDGE_INTERVAL;
103         }
104     }
105
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;
113     }
114
115     private void sendPendingPackets() {
116         try {
117             lock.lock();
118
119             removeFailedPackets();
120             PendingPacket pendingPacket = findPacketToSend();
121
122             if (pendingPacket != null) {
123                 Packet packet = pendingPacket.packet;
124
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);
129                 } else {
130                     // resendPacket will reuse the sequence number
131                     logger.debug("{} : Resending {} packet", logId, packet.getClass().getSimpleName());
132                     communicationHandler.resendPacket(packet);
133                 }
134                 pendingPacket.lastSend = System.currentTimeMillis();
135                 pendingPacket.sendCount++;
136             }
137         } catch (Exception e) {
138             logger.error("Error occurred while sending packet", e);
139         } finally {
140             lock.unlock();
141         }
142     }
143
144     public void start() {
145         try {
146             lock.lock();
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);
153             }
154         } catch (Exception e) {
155             logger.error("Error occurred while starting send packets job", e);
156         } finally {
157             lock.unlock();
158         }
159     }
160
161     public void stop() {
162         try {
163             lock.lock();
164             communicationHandler.removeResponsePacketListener(this::handleResponsePacket);
165             pendingLightState.removeListener(this);
166             ScheduledFuture<?> localSendJob = sendJob;
167             if (localSendJob != null && !localSendJob.isCancelled()) {
168                 localSendJob.cancel(true);
169                 sendJob = null;
170             }
171             pendingPacketsMap.clear();
172         } catch (Exception e) {
173             logger.error("Error occurred while stopping send packets job", e);
174         } finally {
175             lock.unlock();
176         }
177     }
178
179     private List<PendingPacket> createPendingPackets(Packet... packets) {
180         Integer packetType = null;
181         List<PendingPacket> pendingPackets = new ArrayList<>();
182
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
187             // previous value
188             packet.setResponseRequired(false);
189             pendingPackets.add(new PendingPacket(packet));
190
191             if (packetType == null) {
192                 packetType = packet.getPacketType();
193             } else if (packetType != packet.getPacketType()) {
194                 throw new IllegalArgumentException("Packets should have same packet type");
195             }
196         }
197
198         return pendingPackets;
199     }
200
201     private void addPacketsToMap(Packet... packets) {
202         List<PendingPacket> newPendingPackets = createPendingPackets(packets);
203         int packetType = packets[0].getPacketType();
204
205         try {
206             lock.lock();
207             List<PendingPacket> pendingPackets = pendingPacketsMap.get(packetType);
208             if (pendingPackets == null) {
209                 pendingPacketsMap.put(packetType, newPendingPackets);
210             } else {
211                 pendingPackets.addAll(newPendingPackets);
212             }
213         } finally {
214             lock.unlock();
215         }
216     }
217
218     private void replacePacketsInMap(Packet... packets) {
219         List<PendingPacket> pendingPackets = createPendingPackets(packets);
220         int packetType = packets[0].getPacketType();
221
222         try {
223             lock.lock();
224             pendingPacketsMap.put(packetType, pendingPackets);
225         } finally {
226             lock.unlock();
227         }
228     }
229
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;
238                     }
239                 }
240             }
241         }
242
243         return result;
244     }
245
246     private void removePacketsByType(int packetType) {
247         try {
248             lock.lock();
249             pendingPacketsMap.remove(packetType);
250         } finally {
251             lock.unlock();
252         }
253     }
254
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);
264                         it.remove();
265                     }
266                 }
267             }
268         }
269     }
270
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) {
278                         it.remove();
279                         return pendingPacket;
280                     }
281                 }
282             }
283         }
284
285         return null;
286     }
287
288     @Override
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);
294         } else {
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])) {
298                     packets.add(
299                             new SetColorZonesRequest(i, newColors[i], fadeTime.toMillis(), ApplicationRequest.APPLY));
300                 }
301             }
302             if (!packets.isEmpty()) {
303                 removePacketsByType(SetColorRequest.TYPE);
304                 addPacketsToMap(packets.toArray(new SetColorZonesRequest[packets.size()]));
305             }
306         }
307     }
308
309     @Override
310     public void handlePowerStateChange(@Nullable PowerState oldPowerState, PowerState newPowerState) {
311         if (!newPowerState.equals(oldPowerState)) {
312             SetLightPowerRequest packet = new SetLightPowerRequest(pendingLightState.getPowerState());
313             replacePacketsInMap(packet);
314         }
315     }
316
317     @Override
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);
323         }
324     }
325
326     @Override
327     public void handleSignalStrengthChange(@Nullable SignalStrength oldSignalStrength,
328             SignalStrength newSignalStrength) {
329         // Nothing to handle
330     }
331
332     @Override
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);
337         }
338     }
339
340     public void handleResponsePacket(Packet packet) {
341         if (packet instanceof AcknowledgementResponse) {
342             long ackTimestamp = System.currentTimeMillis();
343
344             PendingPacket pendingPacket;
345
346             try {
347                 lock.lock();
348                 pendingPacket = removeAcknowledgedPacket(packet.getSequence());
349             } finally {
350                 lock.unlock();
351             }
352
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);
357
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);
372                 }
373             } else {
374                 logger.debug("{} : No pending packet found for ack with sequence number: {}", logId,
375                         packet.getSequence());
376             }
377         }
378     }
379
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);
386             }
387         }
388     }
389 }