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