]> git.basschouten.com Git - openhab-addons.git/blob
dbccb2113b5d7f91627dcee6cf266fbca6d8331f
[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.LifxProduct.Features;
33 import org.openhab.binding.lifx.internal.dto.AcknowledgementResponse;
34 import org.openhab.binding.lifx.internal.dto.ApplicationRequest;
35 import org.openhab.binding.lifx.internal.dto.Effect;
36 import org.openhab.binding.lifx.internal.dto.GetColorZonesRequest;
37 import org.openhab.binding.lifx.internal.dto.GetLightInfraredRequest;
38 import org.openhab.binding.lifx.internal.dto.GetLightPowerRequest;
39 import org.openhab.binding.lifx.internal.dto.GetRequest;
40 import org.openhab.binding.lifx.internal.dto.Packet;
41 import org.openhab.binding.lifx.internal.dto.PowerState;
42 import org.openhab.binding.lifx.internal.dto.SetColorRequest;
43 import org.openhab.binding.lifx.internal.dto.SetColorZonesRequest;
44 import org.openhab.binding.lifx.internal.dto.SetLightInfraredRequest;
45 import org.openhab.binding.lifx.internal.dto.SetLightPowerRequest;
46 import org.openhab.binding.lifx.internal.dto.SetPowerRequest;
47 import org.openhab.binding.lifx.internal.dto.SetTileEffectRequest;
48 import org.openhab.binding.lifx.internal.dto.SignalStrength;
49 import org.openhab.binding.lifx.internal.fields.HSBK;
50 import org.openhab.binding.lifx.internal.listener.LifxLightStateListener;
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 - Initial contribution
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 Features features;
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, 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.features = context.getFeatures();
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             for (PendingPacket pendingPacket : pendingPackets) {
234                 if (pendingPacket.hasAcknowledgeIntervalElapsed()
235                         && (result == null || pendingPacket.lastSend < result.lastSend)) {
236                     result = pendingPacket;
237                 }
238             }
239         }
240
241         return result;
242     }
243
244     private void removePacketsByType(int packetType) {
245         try {
246             lock.lock();
247             pendingPacketsMap.remove(packetType);
248         } finally {
249             lock.unlock();
250         }
251     }
252
253     private void removeFailedPackets() {
254         for (List<PendingPacket> pendingPackets : pendingPacketsMap.values()) {
255             Iterator<PendingPacket> it = pendingPackets.iterator();
256             while (it.hasNext()) {
257                 PendingPacket pendingPacket = it.next();
258                 if (pendingPacket.sendCount > MAX_RETRIES && pendingPacket.hasAcknowledgeIntervalElapsed()) {
259                     logger.warn("{} failed (unacknowledged {} times to light {})",
260                             pendingPacket.packet.getClass().getSimpleName(), pendingPacket.sendCount, logId);
261                     it.remove();
262                 }
263             }
264         }
265     }
266
267     private @Nullable PendingPacket removeAcknowledgedPacket(int sequenceNumber) {
268         for (List<PendingPacket> pendingPackets : pendingPacketsMap.values()) {
269             Iterator<PendingPacket> it = pendingPackets.iterator();
270             while (it.hasNext()) {
271                 PendingPacket pendingPacket = it.next();
272                 if (pendingPacket.packet.getSequence() == sequenceNumber) {
273                     it.remove();
274                     return pendingPacket;
275                 }
276             }
277         }
278
279         return null;
280     }
281
282     @Override
283     public void handleColorsChange(HSBK[] oldColors, HSBK[] newColors) {
284         if (sameColors(newColors)) {
285             SetColorRequest packet = new SetColorRequest(pendingLightState.getColors()[0], fadeTime.toMillis());
286             removePacketsByType(SetColorZonesRequest.TYPE);
287             replacePacketsInMap(packet);
288         } else {
289             List<SetColorZonesRequest> packets = new ArrayList<>();
290             for (int i = 0; i < newColors.length; i++) {
291                 if (newColors[i] != null && !newColors[i].equals(oldColors[i])) {
292                     packets.add(
293                             new SetColorZonesRequest(i, newColors[i], fadeTime.toMillis(), ApplicationRequest.APPLY));
294                 }
295             }
296             if (!packets.isEmpty()) {
297                 removePacketsByType(SetColorRequest.TYPE);
298                 addPacketsToMap(packets.toArray(new SetColorZonesRequest[packets.size()]));
299             }
300         }
301     }
302
303     @Override
304     public void handlePowerStateChange(@Nullable PowerState oldPowerState, PowerState newPowerState) {
305         if (!newPowerState.equals(oldPowerState)) {
306             SetLightPowerRequest packet = new SetLightPowerRequest(pendingLightState.getPowerState());
307             replacePacketsInMap(packet);
308         }
309     }
310
311     @Override
312     public void handleInfraredChange(@Nullable PercentType oldInfrared, PercentType newInfrared) {
313         PercentType infrared = pendingLightState.getInfrared();
314         if (infrared != null) {
315             SetLightInfraredRequest packet = new SetLightInfraredRequest(percentTypeToInfrared(infrared));
316             replacePacketsInMap(packet);
317         }
318     }
319
320     @Override
321     public void handleSignalStrengthChange(@Nullable SignalStrength oldSignalStrength,
322             SignalStrength newSignalStrength) {
323         // Nothing to handle
324     }
325
326     @Override
327     public void handleTileEffectChange(@Nullable Effect oldEffect, Effect newEffect) {
328         if (oldEffect == null || !oldEffect.equals(newEffect)) {
329             SetTileEffectRequest packet = new SetTileEffectRequest(newEffect);
330             replacePacketsInMap(packet);
331         }
332     }
333
334     public void handleResponsePacket(Packet packet) {
335         if (packet instanceof AcknowledgementResponse) {
336             long ackTimestamp = System.currentTimeMillis();
337
338             PendingPacket pendingPacket;
339
340             try {
341                 lock.lock();
342                 pendingPacket = removeAcknowledgedPacket(packet.getSequence());
343             } finally {
344                 lock.unlock();
345             }
346
347             if (pendingPacket != null) {
348                 Packet sentPacket = pendingPacket.packet;
349                 logger.debug("{} : {} packet was acknowledged in {}ms", logId, sentPacket.getClass().getSimpleName(),
350                         ackTimestamp - pendingPacket.lastSend);
351
352                 // when these packets get lost the current state will still be updated by the
353                 // LifxLightCurrentStateUpdater
354                 if (sentPacket instanceof SetPowerRequest) {
355                     GetLightPowerRequest powerPacket = new GetLightPowerRequest();
356                     communicationHandler.sendPacket(powerPacket);
357                 } else if (sentPacket instanceof SetColorRequest) {
358                     GetRequest colorPacket = new GetRequest();
359                     communicationHandler.sendPacket(colorPacket);
360                     getZonesIfZonesAreSet();
361                 } else if (sentPacket instanceof SetColorZonesRequest) {
362                     getZonesIfZonesAreSet();
363                 } else if (sentPacket instanceof SetLightInfraredRequest) {
364                     GetLightInfraredRequest infraredPacket = new GetLightInfraredRequest();
365                     communicationHandler.sendPacket(infraredPacket);
366                 }
367             } else {
368                 logger.debug("{} : No pending packet found for ack with sequence number: {}", logId,
369                         packet.getSequence());
370             }
371         }
372     }
373
374     private void getZonesIfZonesAreSet() {
375         if (features.hasFeature(MULTIZONE)) {
376             List<PendingPacket> pending = pendingPacketsMap.get(SetColorZonesRequest.TYPE);
377             if (pending == null || pending.isEmpty()) {
378                 GetColorZonesRequest zoneColorPacket = new GetColorZonesRequest();
379                 communicationHandler.sendPacket(zoneColorPacket);
380             }
381         }
382     }
383 }