]> git.basschouten.com Git - openhab-addons.git/blob
c2ee8d1c9ba423687d0ee721ebf6993851363993
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.LifxProduct.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.dto.AcknowledgementResponse;
33 import org.openhab.binding.lifx.internal.dto.ApplicationRequest;
34 import org.openhab.binding.lifx.internal.dto.Effect;
35 import org.openhab.binding.lifx.internal.dto.GetColorZonesRequest;
36 import org.openhab.binding.lifx.internal.dto.GetLightInfraredRequest;
37 import org.openhab.binding.lifx.internal.dto.GetLightPowerRequest;
38 import org.openhab.binding.lifx.internal.dto.GetRequest;
39 import org.openhab.binding.lifx.internal.dto.Packet;
40 import org.openhab.binding.lifx.internal.dto.PowerState;
41 import org.openhab.binding.lifx.internal.dto.SetColorRequest;
42 import org.openhab.binding.lifx.internal.dto.SetColorZonesRequest;
43 import org.openhab.binding.lifx.internal.dto.SetLightInfraredRequest;
44 import org.openhab.binding.lifx.internal.dto.SetLightPowerRequest;
45 import org.openhab.binding.lifx.internal.dto.SetPowerRequest;
46 import org.openhab.binding.lifx.internal.dto.SetTileEffectRequest;
47 import org.openhab.binding.lifx.internal.dto.SignalStrength;
48 import org.openhab.binding.lifx.internal.fields.HSBK;
49 import org.openhab.binding.lifx.internal.listener.LifxLightStateListener;
50 import org.openhab.core.library.types.PercentType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 /**
55  * The {@link LifxLightStateChanger} listens to state changes of the {@code pendingLightState}. It sends packets to a
56  * light so the change the actual light state to that of the {@code pendingLightState}. When the light does not
57  * acknowledge a packet, it resends it (max 3 times).
58  *
59  * @author Wouter Born - Initial contribution
60  */
61 @NonNullByDefault
62 public class LifxLightStateChanger implements LifxLightStateListener {
63
64     /**
65      * Milliseconds before a packet is considered to be lost (unacknowledged).
66      */
67     private static final int PACKET_ACKNOWLEDGE_INTERVAL = 250;
68
69     /**
70      * The number of times a lost packet will be resent.
71      */
72     private static final int MAX_RETRIES = 3;
73
74     private final Logger logger = LoggerFactory.getLogger(LifxLightStateChanger.class);
75
76     private final String logId;
77     private final LifxProduct product;
78     private final Duration fadeTime;
79     private final LifxLightState pendingLightState;
80     private final ScheduledExecutorService scheduler;
81     private final LifxLightCommunicationHandler communicationHandler;
82
83     private final ReentrantLock lock = new ReentrantLock();
84
85     private @Nullable ScheduledFuture<?> sendJob;
86
87     private Map<Integer, List<PendingPacket>> pendingPacketsMap = new ConcurrentHashMap<>();
88
89     private class PendingPacket {
90
91         long lastSend;
92         int sendCount;
93         final Packet packet;
94
95         private PendingPacket(Packet packet) {
96             this.packet = packet;
97         }
98
99         private boolean hasAcknowledgeIntervalElapsed() {
100             long millisSinceLastSend = System.currentTimeMillis() - lastSend;
101             return millisSinceLastSend > PACKET_ACKNOWLEDGE_INTERVAL;
102         }
103     }
104
105     public LifxLightStateChanger(LifxLightContext context, LifxLightCommunicationHandler communicationHandler) {
106         this.logId = context.getLogId();
107         this.product = context.getProduct();
108         this.fadeTime = context.getConfiguration().getFadeTime();
109         this.pendingLightState = context.getPendingLightState();
110         this.scheduler = context.getScheduler();
111         this.communicationHandler = communicationHandler;
112     }
113
114     private void sendPendingPackets() {
115         try {
116             lock.lock();
117
118             removeFailedPackets();
119             PendingPacket pendingPacket = findPacketToSend();
120
121             if (pendingPacket != null) {
122                 Packet packet = pendingPacket.packet;
123
124                 if (pendingPacket.sendCount == 0) {
125                     // sendPacket will set the sequence number
126                     logger.debug("{} : Sending {} packet", logId, packet.getClass().getSimpleName());
127                     communicationHandler.sendPacket(packet);
128                 } else {
129                     // resendPacket will reuse the sequence number
130                     logger.debug("{} : Resending {} packet", logId, packet.getClass().getSimpleName());
131                     communicationHandler.resendPacket(packet);
132                 }
133                 pendingPacket.lastSend = System.currentTimeMillis();
134                 pendingPacket.sendCount++;
135             }
136         } catch (Exception e) {
137             logger.error("Error occurred while sending packet", e);
138         } finally {
139             lock.unlock();
140         }
141     }
142
143     public void start() {
144         try {
145             lock.lock();
146             communicationHandler.addResponsePacketListener(this::handleResponsePacket);
147             pendingLightState.addListener(this);
148             ScheduledFuture<?> localSendJob = sendJob;
149             if (localSendJob == null || localSendJob.isCancelled()) {
150                 sendJob = scheduler.scheduleWithFixedDelay(this::sendPendingPackets, 0, PACKET_INTERVAL,
151                         TimeUnit.MILLISECONDS);
152             }
153         } catch (Exception e) {
154             logger.error("Error occurred while starting send packets job", e);
155         } finally {
156             lock.unlock();
157         }
158     }
159
160     public void stop() {
161         try {
162             lock.lock();
163             communicationHandler.removeResponsePacketListener(this::handleResponsePacket);
164             pendingLightState.removeListener(this);
165             ScheduledFuture<?> localSendJob = sendJob;
166             if (localSendJob != null && !localSendJob.isCancelled()) {
167                 localSendJob.cancel(true);
168                 sendJob = null;
169             }
170             pendingPacketsMap.clear();
171         } catch (Exception e) {
172             logger.error("Error occurred while stopping send packets job", e);
173         } finally {
174             lock.unlock();
175         }
176     }
177
178     private List<PendingPacket> createPendingPackets(Packet... packets) {
179         Integer packetType = null;
180         List<PendingPacket> pendingPackets = new ArrayList<>();
181
182         for (Packet packet : packets) {
183             // the acknowledgement is used to resend the packet in case of packet loss
184             packet.setAckRequired(true);
185             // the LIFX LAN protocol spec indicates that the response returned for a request would be the
186             // previous value
187             packet.setResponseRequired(false);
188             pendingPackets.add(new PendingPacket(packet));
189
190             if (packetType == null) {
191                 packetType = packet.getPacketType();
192             } else if (packetType != packet.getPacketType()) {
193                 throw new IllegalArgumentException("Packets should have same packet type");
194             }
195         }
196
197         return pendingPackets;
198     }
199
200     private void addPacketsToMap(Packet... packets) {
201         List<PendingPacket> newPendingPackets = createPendingPackets(packets);
202         int packetType = packets[0].getPacketType();
203
204         try {
205             lock.lock();
206             List<PendingPacket> pendingPackets = pendingPacketsMap.get(packetType);
207             if (pendingPackets == null) {
208                 pendingPacketsMap.put(packetType, newPendingPackets);
209             } else {
210                 pendingPackets.addAll(newPendingPackets);
211             }
212         } finally {
213             lock.unlock();
214         }
215     }
216
217     private void replacePacketsInMap(Packet... packets) {
218         List<PendingPacket> pendingPackets = createPendingPackets(packets);
219         int packetType = packets[0].getPacketType();
220
221         try {
222             lock.lock();
223             pendingPacketsMap.put(packetType, pendingPackets);
224         } finally {
225             lock.unlock();
226         }
227     }
228
229     private @Nullable PendingPacket findPacketToSend() {
230         PendingPacket result = null;
231         for (List<PendingPacket> pendingPackets : pendingPacketsMap.values()) {
232             for (PendingPacket pendingPacket : pendingPackets) {
233                 if (pendingPacket.hasAcknowledgeIntervalElapsed()
234                         && (result == null || pendingPacket.lastSend < result.lastSend)) {
235                     result = pendingPacket;
236                 }
237             }
238         }
239
240         return result;
241     }
242
243     private void removePacketsByType(int packetType) {
244         try {
245             lock.lock();
246             pendingPacketsMap.remove(packetType);
247         } finally {
248             lock.unlock();
249         }
250     }
251
252     private void removeFailedPackets() {
253         for (List<PendingPacket> pendingPackets : pendingPacketsMap.values()) {
254             Iterator<PendingPacket> it = pendingPackets.iterator();
255             while (it.hasNext()) {
256                 PendingPacket pendingPacket = it.next();
257                 if (pendingPacket.sendCount > MAX_RETRIES && pendingPacket.hasAcknowledgeIntervalElapsed()) {
258                     logger.warn("{} failed (unacknowledged {} times to light {})",
259                             pendingPacket.packet.getClass().getSimpleName(), pendingPacket.sendCount, logId);
260                     it.remove();
261                 }
262             }
263         }
264     }
265
266     private @Nullable PendingPacket removeAcknowledgedPacket(int sequenceNumber) {
267         for (List<PendingPacket> pendingPackets : pendingPacketsMap.values()) {
268             Iterator<PendingPacket> it = pendingPackets.iterator();
269             while (it.hasNext()) {
270                 PendingPacket pendingPacket = it.next();
271                 if (pendingPacket.packet.getSequence() == sequenceNumber) {
272                     it.remove();
273                     return pendingPacket;
274                 }
275             }
276         }
277
278         return null;
279     }
280
281     @Override
282     public void handleColorsChange(HSBK[] oldColors, HSBK[] newColors) {
283         if (sameColors(newColors)) {
284             SetColorRequest packet = new SetColorRequest(pendingLightState.getColors()[0], fadeTime.toMillis());
285             removePacketsByType(SetColorZonesRequest.TYPE);
286             replacePacketsInMap(packet);
287         } else {
288             List<SetColorZonesRequest> packets = new ArrayList<>();
289             for (int i = 0; i < newColors.length; i++) {
290                 if (newColors[i] != null && !newColors[i].equals(oldColors[i])) {
291                     packets.add(
292                             new SetColorZonesRequest(i, newColors[i], fadeTime.toMillis(), ApplicationRequest.APPLY));
293                 }
294             }
295             if (!packets.isEmpty()) {
296                 removePacketsByType(SetColorRequest.TYPE);
297                 addPacketsToMap(packets.toArray(new SetColorZonesRequest[packets.size()]));
298             }
299         }
300     }
301
302     @Override
303     public void handlePowerStateChange(@Nullable PowerState oldPowerState, PowerState newPowerState) {
304         if (!newPowerState.equals(oldPowerState)) {
305             SetLightPowerRequest packet = new SetLightPowerRequest(pendingLightState.getPowerState());
306             replacePacketsInMap(packet);
307         }
308     }
309
310     @Override
311     public void handleInfraredChange(@Nullable PercentType oldInfrared, PercentType newInfrared) {
312         PercentType infrared = pendingLightState.getInfrared();
313         if (infrared != null) {
314             SetLightInfraredRequest packet = new SetLightInfraredRequest(percentTypeToInfrared(infrared));
315             replacePacketsInMap(packet);
316         }
317     }
318
319     @Override
320     public void handleSignalStrengthChange(@Nullable SignalStrength oldSignalStrength,
321             SignalStrength newSignalStrength) {
322         // Nothing to handle
323     }
324
325     @Override
326     public void handleTileEffectChange(@Nullable Effect oldEffect, Effect newEffect) {
327         if (oldEffect == null || !oldEffect.equals(newEffect)) {
328             SetTileEffectRequest packet = new SetTileEffectRequest(newEffect);
329             replacePacketsInMap(packet);
330         }
331     }
332
333     public void handleResponsePacket(Packet packet) {
334         if (packet instanceof AcknowledgementResponse) {
335             long ackTimestamp = System.currentTimeMillis();
336
337             PendingPacket pendingPacket;
338
339             try {
340                 lock.lock();
341                 pendingPacket = removeAcknowledgedPacket(packet.getSequence());
342             } finally {
343                 lock.unlock();
344             }
345
346             if (pendingPacket != null) {
347                 Packet sentPacket = pendingPacket.packet;
348                 logger.debug("{} : {} packet was acknowledged in {}ms", logId, sentPacket.getClass().getSimpleName(),
349                         ackTimestamp - pendingPacket.lastSend);
350
351                 // when these packets get lost the current state will still be updated by the
352                 // LifxLightCurrentStateUpdater
353                 if (sentPacket instanceof SetPowerRequest) {
354                     GetLightPowerRequest powerPacket = new GetLightPowerRequest();
355                     communicationHandler.sendPacket(powerPacket);
356                 } else if (sentPacket instanceof SetColorRequest) {
357                     GetRequest colorPacket = new GetRequest();
358                     communicationHandler.sendPacket(colorPacket);
359                     getZonesIfZonesAreSet();
360                 } else if (sentPacket instanceof SetColorZonesRequest) {
361                     getZonesIfZonesAreSet();
362                 } else if (sentPacket instanceof SetLightInfraredRequest) {
363                     GetLightInfraredRequest infraredPacket = new GetLightInfraredRequest();
364                     communicationHandler.sendPacket(infraredPacket);
365                 }
366             } else {
367                 logger.debug("{} : No pending packet found for ack with sequence number: {}", logId,
368                         packet.getSequence());
369             }
370         }
371     }
372
373     private void getZonesIfZonesAreSet() {
374         if (product.hasFeature(MULTIZONE)) {
375             List<PendingPacket> pending = pendingPacketsMap.get(SetColorZonesRequest.TYPE);
376             if (pending == null || pending.isEmpty()) {
377                 GetColorZonesRequest zoneColorPacket = new GetColorZonesRequest();
378                 communicationHandler.sendPacket(zoneColorPacket);
379             }
380         }
381     }
382 }