]> git.basschouten.com Git - openhab-addons.git/blob
270940544db534a4da65f1b1550a37da24b98eb5
[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.wled.internal.api;
14
15 import static org.openhab.binding.wled.internal.WLedBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.math.RoundingMode;
19 import java.util.ArrayList;
20 import java.util.Comparator;
21 import java.util.List;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentResponse;
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.client.util.StringContentProvider;
32 import org.eclipse.jetty.http.HttpHeader;
33 import org.eclipse.jetty.http.HttpMethod;
34 import org.openhab.binding.wled.internal.WLedHandler;
35 import org.openhab.binding.wled.internal.WLedHelper;
36 import org.openhab.binding.wled.internal.WledState;
37 import org.openhab.binding.wled.internal.WledState.InfoResponse;
38 import org.openhab.binding.wled.internal.WledState.JsonResponse;
39 import org.openhab.binding.wled.internal.WledState.LedInfo;
40 import org.openhab.binding.wled.internal.WledState.StateResponse;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.HSBType;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.library.types.PercentType;
45 import org.openhab.core.library.types.QuantityType;
46 import org.openhab.core.library.types.StringType;
47 import org.openhab.core.library.unit.Units;
48 import org.openhab.core.thing.Channel;
49 import org.openhab.core.thing.ChannelUID;
50 import org.openhab.core.types.StateOption;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import com.google.gson.Gson;
55 import com.google.gson.JsonSyntaxException;
56
57 /**
58  * The {@link WledApiV084} is the json Api methods for firmware version 0.8.4 and newer
59  * as newer firmwares come out with breaking changes, extend this class into a newer firmware version class.
60  *
61  * @author Matthew Skinner - Initial contribution
62  */
63 @NonNullByDefault
64 public class WledApiV084 implements WledApi {
65     protected final Logger logger = LoggerFactory.getLogger(this.getClass());
66     protected final Gson gson = new Gson();
67     protected final HttpClient httpClient;
68     protected final WLedHandler handler;
69     protected final String address;
70     protected WledState state = new WledState();
71     private int version = 0;
72
73     public WledApiV084(WLedHandler handler, HttpClient httpClient) {
74         this.handler = handler;
75         this.address = handler.config.address;
76         this.httpClient = httpClient;
77     }
78
79     @Override
80     public void initialize() throws ApiException {
81         state.jsonResponse = getJson();
82         getUpdatedFxList();
83         getUpdatedPaletteList();
84
85         @Nullable
86         LedInfo localLedInfo = gson.fromJson(state.infoResponse.leds.toString(), LedInfo.class);
87         if (localLedInfo != null) {
88             state.ledInfo = localLedInfo;
89         }
90
91         handler.hasWhite = state.ledInfo.rgbw;
92         ArrayList<Channel> removeChannels = new ArrayList<>();
93         if (!state.ledInfo.rgbw) {
94             logger.debug("WLED is not setup to use RGBW, so removing un-needed white channels");
95             Channel channel = handler.getThing().getChannel(CHANNEL_PRIMARY_WHITE);
96             if (channel != null) {
97                 removeChannels.add(channel);
98             }
99             channel = handler.getThing().getChannel(CHANNEL_SECONDARY_WHITE);
100             if (channel != null) {
101                 removeChannels.add(channel);
102             }
103             channel = handler.getThing().getChannel(CHANNEL_THIRD_WHITE);
104             if (channel != null) {
105                 removeChannels.add(channel);
106             }
107         }
108         handler.removeChannels(removeChannels);
109     }
110
111     @Override
112     public String sendGetRequest(String url) throws ApiException {
113         Request request = httpClient.newRequest(address + url);
114         request.timeout(3, TimeUnit.SECONDS);
115         request.method(HttpMethod.GET);
116         request.header(HttpHeader.ACCEPT_ENCODING, "gzip");
117         logger.trace("Sending WLED GET:{}", url);
118         String errorReason = "";
119         try {
120             ContentResponse contentResponse = request.send();
121             if (contentResponse.getStatus() == 200) {
122                 return contentResponse.getContentAsString();
123             } else {
124                 errorReason = String.format("WLED request failed with %d: %s", contentResponse.getStatus(),
125                         contentResponse.getReason());
126             }
127         } catch (TimeoutException e) {
128             errorReason = "TimeoutException: WLED was not reachable on your network";
129         } catch (ExecutionException e) {
130             errorReason = String.format("ExecutionException: %s", e.getMessage());
131         } catch (InterruptedException e) {
132             Thread.currentThread().interrupt();
133             errorReason = String.format("InterruptedException: %s", e.getMessage());
134         }
135         throw new ApiException(errorReason);
136     }
137
138     protected String postState(String json) throws ApiException {
139         return sendPostRequest("/json/state", json);
140     }
141
142     protected String sendPostRequest(String url, String json) throws ApiException {
143         logger.debug("Sending WLED POST:{} Message:{}", url, json);
144         Request request = httpClient.POST(address + url);
145         request.timeout(3, TimeUnit.SECONDS);
146         request.header(HttpHeader.CONTENT_TYPE, "application/json");
147         request.content(new StringContentProvider(json), "application/json");
148         String errorReason = "";
149         try {
150             ContentResponse contentResponse = request.send();
151             if (contentResponse.getStatus() == 200) {
152                 return contentResponse.getContentAsString();
153             } else {
154                 errorReason = String.format("WLED request failed with %d: %s", contentResponse.getStatus(),
155                         contentResponse.getReason());
156             }
157         } catch (InterruptedException e) {
158             errorReason = String.format("InterruptedException: %s", e.getMessage());
159         } catch (TimeoutException e) {
160             errorReason = "TimeoutException: WLED was not reachable on your network";
161         } catch (ExecutionException e) {
162             errorReason = String.format("ExecutionException: %s", e.getMessage());
163         }
164         throw new ApiException(errorReason);
165     }
166
167     protected void updateStateFromReply(String jsonState) {
168         try {
169             StateResponse response = gson.fromJson(jsonState, StateResponse.class);
170             if (response == null) {
171                 throw new ApiException("Reply back from WLED when command was made is not valid JSON");
172             }
173             state.stateResponse = response;
174             state.unpackJsonObjects();
175             processState();
176         } catch (JsonSyntaxException | ApiException e) {
177             logger.debug("Reply back when a command was sent triggered an exception:{}", jsonState);
178         }
179     }
180
181     protected StateResponse getState() throws ApiException {
182         try {
183             String returnContent = sendGetRequest("/json/state");
184             StateResponse response = gson.fromJson(returnContent, StateResponse.class);
185             if (response == null) {
186                 throw new ApiException("Could not GET:/json/state");
187             }
188             logger.trace("json/state:{}", returnContent);
189             return response;
190         } catch (JsonSyntaxException e) {
191             throw new ApiException("JsonSyntaxException:{}", e);
192         }
193     }
194
195     protected InfoResponse getInfo() throws ApiException {
196         try {
197             String returnContent = sendGetRequest("/json/info");
198             InfoResponse response = gson.fromJson(returnContent, InfoResponse.class);
199             if (response == null) {
200                 throw new ApiException("Could not GET:/json/info");
201             }
202             return response;
203         } catch (JsonSyntaxException e) {
204             throw new ApiException("JsonSyntaxException:{}", e);
205         }
206     }
207
208     protected JsonResponse getJson() throws ApiException {
209         try {
210             String returnContent = sendGetRequest("/json");
211             JsonResponse response = gson.fromJson(returnContent, JsonResponse.class);
212             if (response == null) {
213                 throw new ApiException("Could not GET:/json");
214             }
215             return response;
216         } catch (JsonSyntaxException e) {
217             throw new ApiException("JsonSyntaxException:{}", e);
218         }
219     }
220
221     @Override
222     public void update() throws ApiException {
223         state.stateResponse = getState();
224         state.unpackJsonObjects();
225         processState();
226     }
227
228     protected void getUpdatedFxList() {
229         List<StateOption> fxOptions = new ArrayList<>();
230         int counter = 0;
231         for (String value : state.jsonResponse.effects) {
232             fxOptions.add(new StateOption(Integer.toString(counter++), value));
233         }
234         if (handler.config.sortEffects) {
235             fxOptions.sort(Comparator.comparing(o -> o.getValue().equals("0") ? "" : o.getLabel()));
236         }
237         handler.stateDescriptionProvider.setStateOptions(new ChannelUID(handler.getThing().getUID(), CHANNEL_FX),
238                 fxOptions);
239     }
240
241     protected void getUpdatedPaletteList() {
242         List<StateOption> palleteOptions = new ArrayList<>();
243         int counter = 0;
244         for (String value : state.jsonResponse.palettes) {
245             palleteOptions.add(new StateOption(Integer.toString(counter++), value));
246         }
247         if (handler.config.sortPalettes) {
248             palleteOptions.sort(Comparator.comparing(o -> o.getValue().equals("0") ? "" : o.getLabel()));
249         }
250         handler.stateDescriptionProvider.setStateOptions(new ChannelUID(handler.getThing().getUID(), CHANNEL_PALETTES),
251                 palleteOptions);
252     }
253
254     @Override
255     public int getFirmwareVersion() throws ApiException {
256         state.infoResponse = getInfo();
257         String temp = state.infoResponse.ver;
258         logger.debug("Firmware for WLED is ver:{}", temp);
259         temp = temp.replaceAll("\\.", "");
260         if (temp.length() > 4) {
261             temp = temp.substring(0, 4);
262         }
263         version = Integer.parseInt(temp);
264         return version;
265     }
266
267     protected void processState() throws ApiException {
268         if (state.stateResponse.seg.length <= handler.config.segmentIndex) {
269             throw new ApiException("Segment " + handler.config.segmentIndex
270                     + " is not currently setup correctly in the WLED firmware");
271         }
272         HSBType tempHSB = WLedHelper
273                 .parseToHSBType(state.stateResponse.seg[handler.config.segmentIndex].col[0].toString());
274         handler.update(CHANNEL_PRIMARY_COLOR, tempHSB);
275         handler.update(CHANNEL_SECONDARY_COLOR,
276                 WLedHelper.parseToHSBType(state.stateResponse.seg[handler.config.segmentIndex].col[1].toString()));
277         handler.update(CHANNEL_THIRD_COLOR,
278                 WLedHelper.parseToHSBType(state.stateResponse.seg[handler.config.segmentIndex].col[2].toString()));
279         if (state.ledInfo.rgbw) {
280             handler.update(CHANNEL_PRIMARY_WHITE, WLedHelper
281                     .parseWhitePercent(state.stateResponse.seg[handler.config.segmentIndex].col[0].toString()));
282             handler.update(CHANNEL_SECONDARY_WHITE, WLedHelper
283                     .parseWhitePercent(state.stateResponse.seg[handler.config.segmentIndex].col[1].toString()));
284             handler.update(CHANNEL_THIRD_WHITE, WLedHelper
285                     .parseWhitePercent(state.stateResponse.seg[handler.config.segmentIndex].col[2].toString()));
286         }
287         // Global OFF or Segment OFF needs to be treated as OFF
288         if (!state.stateResponse.seg[handler.config.segmentIndex].on || !state.stateResponse.on) {
289             handler.update(CHANNEL_MASTER_CONTROLS, OnOffType.OFF);
290             handler.update(CHANNEL_SEGMENT_BRIGHTNESS, OnOffType.OFF);
291         } else {
292             handler.update(CHANNEL_MASTER_CONTROLS, tempHSB);
293             handler.update(CHANNEL_SEGMENT_BRIGHTNESS,
294                     new PercentType(new BigDecimal(state.stateResponse.seg[handler.config.segmentIndex].bri)
295                             .divide(BIG_DECIMAL_2_55, RoundingMode.HALF_UP)));
296         }
297         if (state.nightLightState.on) {
298             handler.update(CHANNEL_SLEEP, OnOffType.ON);
299         } else {
300             handler.update(CHANNEL_SLEEP, OnOffType.OFF);
301         }
302         if (state.stateResponse.pl == 0) {
303             handler.update(CHANNEL_PRESET_CYCLE, OnOffType.ON);
304         } else {
305             handler.update(CHANNEL_PRESET_CYCLE, OnOffType.OFF);
306         }
307         if (state.udpnState.recv) {
308             handler.update(CHANNEL_SYNC_RECEIVE, OnOffType.ON);
309         } else {
310             handler.update(CHANNEL_SYNC_RECEIVE, OnOffType.OFF);
311         }
312         if (state.udpnState.send) {
313             handler.update(CHANNEL_SYNC_SEND, OnOffType.ON);
314         } else {
315             handler.update(CHANNEL_SYNC_SEND, OnOffType.OFF);
316         }
317         if (state.stateResponse.seg[handler.config.segmentIndex].mi) {
318             handler.update(CHANNEL_MIRROR, OnOffType.ON);
319         } else {
320             handler.update(CHANNEL_MIRROR, OnOffType.OFF);
321         }
322         if (state.stateResponse.seg[handler.config.segmentIndex].rev) {
323             handler.update(CHANNEL_REVERSE, OnOffType.ON);
324         } else {
325             handler.update(CHANNEL_REVERSE, OnOffType.OFF);
326         }
327         handler.update(CHANNEL_TRANS_TIME, new QuantityType<>(
328                 new BigDecimal(state.stateResponse.transition).divide(BigDecimal.TEN), Units.SECOND));
329         handler.update(CHANNEL_PRESETS, new StringType(Integer.toString(state.stateResponse.ps)));
330         handler.update(CHANNEL_FX,
331                 new StringType(Integer.toString(state.stateResponse.seg[handler.config.segmentIndex].fx)));
332         handler.update(CHANNEL_PALETTES,
333                 new StringType(Integer.toString(state.stateResponse.seg[handler.config.segmentIndex].pal)));
334         handler.update(CHANNEL_SPEED,
335                 new PercentType(new BigDecimal(state.stateResponse.seg[handler.config.segmentIndex].sx)
336                         .divide(BIG_DECIMAL_2_55, RoundingMode.HALF_UP)));
337         handler.update(CHANNEL_INTENSITY,
338                 new PercentType(new BigDecimal(state.stateResponse.seg[handler.config.segmentIndex].ix)
339                         .divide(BIG_DECIMAL_2_55, RoundingMode.HALF_UP)));
340         handler.update(CHANNEL_LIVE_OVERRIDE, new StringType(Integer.toString(state.stateResponse.lor)));
341         handler.update(CHANNEL_GROUPING, new DecimalType(state.stateResponse.seg[handler.config.segmentIndex].grp));
342         handler.update(CHANNEL_SPACING, new DecimalType(state.stateResponse.seg[handler.config.segmentIndex].spc));
343     }
344
345     @Override
346     public void setGlobalOn(boolean bool) throws ApiException {
347         updateStateFromReply(postState("{\"on\":" + bool + ",\"v\":true,\"tt\":2}"));
348     }
349
350     @Override
351     public void setMasterOn(boolean bool, int segmentIndex) throws ApiException {
352         updateStateFromReply(
353                 postState("{\"v\":true,\"tt\":2,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":" + bool + "}]}"));
354     }
355
356     @Override
357     public void setGlobalBrightness(PercentType percent) throws ApiException {
358         if (percent.equals(PercentType.ZERO)) {
359             updateStateFromReply(postState("{\"on\":false,\"v\":true}"));
360             return;
361         }
362         updateStateFromReply(postState("{\"on\":true,\"v\":true,\"tt\":2,\"bri\":"
363                 + percent.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}"));
364     }
365
366     @Override
367     public void setMasterBrightness(PercentType percent, int segmentIndex) throws ApiException {
368         if (percent.equals(PercentType.ZERO)) {
369             updateStateFromReply(postState("{\"v\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":false}]}"));
370             return;
371         }
372         updateStateFromReply(postState("{\"tt\":2,\"v\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":true,\"bri\":"
373                 + percent.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}]}"));
374     }
375
376     @Override
377     public void setMasterHSB(HSBType hsbType, int segmentIndex) throws ApiException {
378         if (hsbType.getBrightness().toBigDecimal().equals(BigDecimal.ZERO)) {
379             updateStateFromReply(postState("{\"tt\":2,\"v\":true,\"seg\":[{\"on\":false,\"id\":" + segmentIndex
380                     + ",\"fx\":0,\"col\":[[" + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue()
381                     + "," + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
382                     + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "]]}]}"));
383             return;
384         }
385         updateStateFromReply(postState("{\"tt\":2,\"v\":true,\"seg\":[{\"on\":true,\"id\":" + segmentIndex
386                 + ",\"fx\":0,\"col\":[[" + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
387                 + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
388                 + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "]]}]}"));
389     }
390
391     @Override
392     public void setEffect(String string, int segmentIndex) throws ApiException {
393         postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"fx\":" + string + "}]}");
394     }
395
396     @Override
397     public void setPreset(String string) throws ApiException {
398         updateStateFromReply(postState("{\"ps\":" + string + ",\"v\":true}"));
399     }
400
401     @Override
402     public void setPalette(String string, int segmentIndex) throws ApiException {
403         postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"pal\":" + string + "}]}");
404     }
405
406     @Override
407     public void setFxIntencity(PercentType percentType, int segmentIndex) throws ApiException {
408         postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"ix\":"
409                 + percentType.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}]}");
410     }
411
412     @Override
413     public void setFxSpeed(PercentType percentType, int segmentIndex) throws ApiException {
414         postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"sx\":"
415                 + percentType.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}]}");
416     }
417
418     @Override
419     public void setSleep(boolean bool) throws ApiException {
420         postState("{\"nl\":{\"on\":" + bool + "}}");
421     }
422
423     @Override
424     public void setUdpSend(boolean bool) throws ApiException {
425         postState("{\"udpn\":{\"send\":" + bool + "}}");
426     }
427
428     @Override
429     public void setUdpRecieve(boolean bool) throws ApiException {
430         postState("{\"udpn\":{\"recv\":" + bool + "}}");
431     }
432
433     @Override
434     public void setTransitionTime(BigDecimal time) throws ApiException {
435         postState("{\"transition\":" + time + "}");
436     }
437
438     @Override
439     public void setPresetCycle(boolean bool) throws ApiException {
440         if (bool) {
441             postState("{\"pl\":0}");
442         } else {
443             postState("{\"pl\":-1}");
444         }
445     }
446
447     @Override
448     public void setPrimaryColor(HSBType hsbType, int segmentIndex) throws ApiException {
449         postState("{\"on\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"col\":[["
450                 + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
451                 + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
452                 + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "],[],[]]}]}");
453     }
454
455     @Override
456     public void setSecondaryColor(HSBType hsbType, int segmentIndex) throws ApiException {
457         postState("{\"on\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"col\":[[],["
458                 + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
459                 + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
460                 + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "],[]]}]}");
461     }
462
463     @Override
464     public void setTertiaryColor(HSBType hsbType, int segmentIndex) throws ApiException {
465         postState("{\"on\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"col\":[[],[],["
466                 + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
467                 + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
468                 + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "]]}]}");
469     }
470
471     @Override
472     public void setWhiteOnly(PercentType percentType, int segmentIndex) throws ApiException {
473         postState("{\"seg\":[{\"on\":true,\"id\":" + segmentIndex + ",\"fx\":0,\"col\":[[0,0,0,"
474                 + percentType.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "]]}]}");
475     }
476
477     @Override
478     public void setMirror(boolean bool, int segmentIndex) throws ApiException {
479         postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"mi\":" + bool + "}]}");
480     }
481
482     @Override
483     public void setReverse(boolean bool, int segmentIndex) throws ApiException {
484         postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"rev\":" + bool + "}]}");
485     }
486
487     @Override
488     public void savePreset(int position, String presetName) throws ApiException {
489         // named presets not supported in older firmwares, and max of 16.
490         if (position > 16 || position < 1) {
491             logger.warn("Preset position {} is not supported in this firmware version", position);
492             return;
493         }
494         try {
495             sendGetRequest("/win&PS=" + position);
496         } catch (ApiException e) {
497             logger.warn("Preset failed to save:{}", e.getMessage());
498         }
499     }
500
501     @Override
502     public void setLiveOverride(String value) throws ApiException {
503         postState("{\"lor\":" + value + "}");
504     }
505
506     @Override
507     public void setGrouping(int value, int segmentIndex) throws ApiException {
508         postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"grp\":" + value + "}]}");
509     }
510
511     @Override
512     public void setSpacing(int value, int segmentIndex) throws ApiException {
513         postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"spc\":" + value + "}]}");
514     }
515 }