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