]> git.basschouten.com Git - openhab-addons.git/blob
6aa63391708067ecf076b2213946132a98f9b595
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.wifiled.internal.handler;
14
15 import java.io.DataOutputStream;
16 import java.io.IOException;
17 import java.net.Socket;
18 import java.util.concurrent.ExecutionException;
19 import java.util.concurrent.Executors;
20 import java.util.concurrent.Future;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.Semaphore;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.locks.Lock;
25 import java.util.concurrent.locks.ReentrantLock;
26 import java.util.function.BiFunction;
27 import java.util.function.Function;
28
29 import org.openhab.core.library.types.HSBType;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.PercentType;
32 import org.openhab.core.library.types.StringType;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 /**
37  * The {@link FadingWiFiLEDDriver} class is responsible for the communication with the WiFi LED controller.
38  * It utilizes color fading when changing colors or turning the light on of off.
39  *
40  * @author Stefan Endrullis - Initial contribution
41  * @author Ries van Twisk
42  */
43 public class FadingWiFiLEDDriver extends AbstractWiFiLEDDriver {
44
45     public static final int DEFAULT_FADE_DURATION_IN_MS = 1000;
46     public static final int DEFAULT_FADE_STEPS = 100;
47     public static final int KEEP_COMMUNICATION_OPEN_FOR_MS = 1000;
48
49     private static final InternalLedState BLACK_STATE = new InternalLedState();
50
51     private boolean power = false;
52     private InternalLedState currentState = new InternalLedState(); // Use to not update the controller with the same
53                                                                     // value
54     private InternalLedState currentFaderState = new InternalLedState();
55     private InternalLedState targetState = new InternalLedState();
56     private LEDStateDTO dtoState = LEDStateDTO.valueOf(0, 0, 0, 0, 0, 0, 0, 0);
57     private final ScheduledExecutorService waiterExecutor = Executors.newSingleThreadScheduledExecutor();
58     private final ScheduledExecutorService faderExecutor = Executors.newSingleThreadScheduledExecutor();
59     private LEDFaderRunner ledfaderThread = null;
60     private final Semaphore ledUpdateSyncSemaphore = new Semaphore(1, false);
61     private final int fadeDurationInMs;
62     private final int totalFadingSteps;
63
64     public FadingWiFiLEDDriver(String host, int port, AbstractWiFiLEDDriver.Protocol protocol, int fadeDurationInMs,
65             int totalFadingSteps) {
66         super(host, port, protocol);
67         this.fadeDurationInMs = fadeDurationInMs < 10 ? 10 : fadeDurationInMs;
68         this.totalFadingSteps = totalFadingSteps < 1 ? 1 : totalFadingSteps;
69     }
70
71     @Override
72     public void init() throws IOException {
73         try {
74             LEDState s = getLEDState();
75             dtoState = LEDStateDTO.valueOf(s.state, s.program, s.programSpeed, s.red, s.green, s.blue, s.white,
76                     s.white2);
77             power = (s.state & 0x01) != 0;
78             currentState = InternalLedState.fromRGBW(s.red, s.green, s.blue, s.white, s.white2);
79             currentFaderState = currentState;
80         } catch (IOException e) {
81             logger.warn("IOException", e);
82         }
83     }
84
85     @Override
86     public void shutdown() {
87         waiterExecutor.shutdown();
88         faderExecutor.shutdown();
89         try {
90             if (!waiterExecutor.awaitTermination((fadeDurationInMs / totalFadingSteps) * 2, TimeUnit.MILLISECONDS)) {
91                 waiterExecutor.shutdownNow();
92             }
93             if (!faderExecutor.awaitTermination((fadeDurationInMs / totalFadingSteps) * 2, TimeUnit.MILLISECONDS)) {
94                 faderExecutor.shutdownNow();
95             }
96         } catch (InterruptedException e) {
97             // Ignored
98         }
99     }
100
101     @Override
102     public void setColor(HSBType color) throws IOException {
103         dtoState = dtoState.withColor(color);
104         changeState(targetState.withColor(color));
105     }
106
107     @Override
108     public void setBrightness(PercentType brightness) throws IOException {
109         dtoState = dtoState.withBrightness(brightness);
110         changeState(targetState.withBrightness(brightness.doubleValue() / 100));
111     }
112
113     @Override
114     public void incBrightness(int step) throws IOException {
115         dtoState = dtoState.withIncrementedBrightness(step);
116         changeState(targetState.withBrightness(targetState.getBrightness() + ((double) step / 100)));
117     }
118
119     @Override
120     public void decBrightness(int step) throws IOException {
121         dtoState = dtoState.withIncrementedBrightness(-step);
122         changeState(targetState.withBrightness(targetState.getBrightness() - ((double) step / 100)));
123     }
124
125     @Override
126     public void setWhite(PercentType white) throws IOException {
127         dtoState = dtoState.withWhite(white);
128         changeState(targetState.withWhite(white.doubleValue() / 100));
129     }
130
131     @Override
132     public void incWhite(int step) throws IOException {
133         dtoState = dtoState.withIncrementedWhite(step);
134         changeState(targetState.withWhite(targetState.getWhite() + ((double) step / 100)));
135     }
136
137     @Override
138     public void setWhite2(PercentType white2) throws IOException {
139         dtoState = dtoState.withWhite2(white2);
140         changeState(targetState.withWhite2(white2.doubleValue() / 100));
141     }
142
143     @Override
144     public void incWhite2(int step) throws IOException {
145         dtoState = dtoState.withIncrementedWhite2(step);
146         changeState(targetState.withWhite2(targetState.getWhite2() + ((double) step / 100)));
147     }
148
149     @Override
150     public void setProgram(StringType program) throws IOException {
151     }
152
153     @Override
154     public void setProgramSpeed(PercentType speed) throws IOException {
155     }
156
157     @Override
158     public void incProgramSpeed(int step) throws IOException {
159     }
160
161     @Override
162     public void setPower(OnOffType command) throws IOException {
163         dtoState = dtoState.withPower(command);
164         power = command == OnOffType.ON;
165         fadeToState(power ? targetState : BLACK_STATE);
166     }
167
168     @Override
169     public LEDStateDTO getLEDStateDTO() throws IOException {
170         return dtoState;
171     }
172
173     private void changeState(final InternalLedState newState) throws IOException {
174         targetState = newState;
175         if (power) {
176             fadeToState(targetState);
177         }
178     }
179
180     /**
181      * Runnable that takes care of fading of the LED's
182      */
183     static final class LEDFaderRunner implements Runnable {
184         private final Logger logger = LoggerFactory.getLogger(LEDFaderRunner.class);
185
186         private String host;
187         private int port;
188         private InternalLedState fromState;
189         private InternalLedState toState;
190         private final int totalFadingSteps;
191         private final long keepCommOpenForMS;
192         private final Function<DataOutputStream, Boolean> powerOnFunc;
193         private final BiFunction<DataOutputStream, InternalLedState, Boolean> ledSender;
194
195         private long lastCommunicationTime = 0;
196
197         private int currentFadingStep = 1;
198
199         private final Lock lock = new ReentrantLock();
200
201         private InternalLedState currentFadeState;
202         private Socket socket;
203         private DataOutputStream outputStream;
204
205         public LEDFaderRunner(String host, int port, InternalLedState fromState, InternalLedState toState,
206                 int totalFadingSteps, int keepCommOpenForMS, Function<DataOutputStream, Boolean> powerOnFunc,
207                 BiFunction<DataOutputStream, InternalLedState, Boolean> ledSender) {
208             this.host = host;
209             this.port = port;
210             this.fromState = fromState;
211             this.toState = toState;
212             this.totalFadingSteps = totalFadingSteps;
213             this.keepCommOpenForMS = keepCommOpenForMS;
214             this.powerOnFunc = powerOnFunc;
215             this.ledSender = ledSender;
216         }
217
218         /**
219          * Call before starting a thre`ad, it will initialise a socket and power on the LEDs
220          *
221          * @throws IOException
222          */
223         public void init() throws IOException {
224             socket = new Socket(host, port);
225             socket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT);
226             outputStream = new DataOutputStream(socket.getOutputStream());
227             logger.debug("Connected to '{}'", socket);
228             powerOnFunc.apply(outputStream);
229             currentFadeState = fromState;
230         }
231
232         public void setToState(InternalLedState toState) {
233             lock.lock();
234             this.fromState = currentFadeState;
235             this.toState = toState;
236             this.currentFadingStep = 1;
237             lock.unlock();
238         }
239
240         @Override
241         public void run() {
242             lock.lock();
243             if (currentFadingStep <= totalFadingSteps) {
244                 currentFadeState = fromState.fade(toState, (double) currentFadingStep / totalFadingSteps);
245                 lock.unlock();
246
247                 logger.debug("currentFadeState: {}", currentFadeState);
248                 if (!ledSender.apply(outputStream, currentFadeState)) {
249                     logger.warn("Failed sending at step {}", currentFadingStep);
250                     throw new IllegalStateException("Failed sending at step " + currentFadingStep);
251                 }
252                 lastCommunicationTime = System.currentTimeMillis();
253             } else {
254                 lock.unlock();
255                 if (lastCommunicationTime < (System.currentTimeMillis() - keepCommOpenForMS)) {
256                     throw new IllegalStateException("Reached end step");
257                 }
258             }
259             currentFadingStep++;
260         }
261
262         public void shutdown() {
263             if (socket != null) {
264                 try {
265                     socket.close();
266                 } catch (IOException e) {
267                     // Ignored
268                 }
269             }
270         }
271     }
272
273     private synchronized void fadeToState(final InternalLedState newTargetState) throws IOException {
274         if (ledUpdateSyncSemaphore.tryAcquire(1)) {
275             // Create and Execute a new LED Fader
276             ledfaderThread = new LEDFaderRunner(host, port, currentFaderState, newTargetState, totalFadingSteps,
277                     KEEP_COMMUNICATION_OPEN_FOR_MS, (outputStream) -> {
278                         try {
279                             sendRaw(getBytesForPower(true), outputStream);
280                             return true;
281                         } catch (IOException e) {
282                             logger.warn("IOException", e);
283                             return false;
284                         }
285                     }, (outputStream, fs) -> {
286                         try {
287                             sendLEDData(fs, outputStream);
288                             currentFaderState = fs;
289                             logger.trace("Current: {} {} {} {}", fs.getR(), fs.getG(), fs.getB(), fs.getWhite());
290                             return true;
291                         } catch (IOException e) {
292                             logger.warn("IOException", e);
293                             return false;
294                         }
295                     });
296             ledfaderThread.init();
297             final int period = fadeDurationInMs / totalFadingSteps;
298             final Future<?> future = faderExecutor.scheduleAtFixedRate(ledfaderThread, 0, period < 1 ? 1 : period,
299                     TimeUnit.MILLISECONDS);
300
301             // Wait untill LED Thread has finished, when so shutdown fader
302             waiterExecutor.schedule(() -> {
303                 try {
304                     future.get();
305                 } catch (InterruptedException | ExecutionException e) {
306                     // Ignored
307                 } catch (Exception e) {
308                     logger.warn("Exception", e);
309                 }
310                 ledfaderThread.shutdown();
311                 ledfaderThread = null;
312                 ledUpdateSyncSemaphore.release(1);
313             }, 0, TimeUnit.MILLISECONDS);
314         } else {
315             ledfaderThread.setToState(newTargetState);
316         }
317     }
318
319     private void sendLEDData(InternalLedState ledState, DataOutputStream out) throws IOException {
320         logger.debug("Setting LED State to {}", ledState);
321
322         if (!ledState.equals(currentState)) {
323             // "normal" program: set color etc.
324             byte r = (byte) (ledState.getR() & 0xFF);
325             byte g = (byte) (ledState.getG() & 0xFF);
326             byte b = (byte) (ledState.getB() & 0xFF);
327             byte w = (byte) (ledState.getW() & 0xFF);
328             byte w2 = (byte) (ledState.getW2() & 0xFF);
329
330             logger.debug("RGBW: {}, {}, {}, {}, {}", r, g, b, w, w2);
331
332             byte[] bytes = getBytesForColor(r, g, b, w, w2);
333             sendRaw(bytes, out);
334         }
335
336         currentState = ledState;
337     }
338 }