2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.wled.internal.api;
15 import static org.openhab.binding.wled.internal.WLedBindingConstants.*;
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;
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;
54 import com.google.gson.Gson;
55 import com.google.gson.JsonSyntaxException;
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.
61 * @author Matthew Skinner - Initial contribution
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;
73 public WledApiV084(WLedHandler handler, HttpClient httpClient) {
74 this.handler = handler;
75 this.address = handler.config.address;
76 this.httpClient = httpClient;
80 public void initialize() throws ApiException {
81 state.jsonResponse = getJson();
83 getUpdatedPaletteList();
86 LedInfo localLedInfo = gson.fromJson(state.infoResponse.leds.toString(), LedInfo.class);
87 if (localLedInfo != null) {
88 state.ledInfo = localLedInfo;
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);
99 channel = handler.getThing().getChannel(CHANNEL_SECONDARY_WHITE);
100 if (channel != null) {
101 removeChannels.add(channel);
103 channel = handler.getThing().getChannel(CHANNEL_THIRD_WHITE);
104 if (channel != null) {
105 removeChannels.add(channel);
108 handler.removeChannels(removeChannels);
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 = "";
120 ContentResponse contentResponse = request.send();
121 if (contentResponse.getStatus() == 200) {
122 return contentResponse.getContentAsString();
124 errorReason = String.format("WLED request failed with %d: %s", contentResponse.getStatus(),
125 contentResponse.getReason());
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());
135 throw new ApiException(errorReason);
138 protected String postState(String json) throws ApiException {
139 return sendPostRequest("/json/state", json);
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 = "";
150 ContentResponse contentResponse = request.send();
151 if (contentResponse.getStatus() == 200) {
152 return contentResponse.getContentAsString();
154 errorReason = String.format("WLED request failed with %d: %s", contentResponse.getStatus(),
155 contentResponse.getReason());
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());
164 throw new ApiException(errorReason);
167 protected void updateStateFromReply(String jsonState) {
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");
173 state.stateResponse = response;
174 state.unpackJsonObjects();
176 } catch (JsonSyntaxException | ApiException e) {
177 logger.debug("Reply back when a command was sent triggered an exception:{}", jsonState);
181 protected StateResponse getState() throws ApiException {
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");
188 logger.trace("json/state:{}", returnContent);
190 } catch (JsonSyntaxException e) {
191 throw new ApiException("JsonSyntaxException:{}", e);
195 protected InfoResponse getInfo() throws ApiException {
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");
203 } catch (JsonSyntaxException e) {
204 throw new ApiException("JsonSyntaxException:{}", e);
208 protected JsonResponse getJson() throws ApiException {
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");
216 } catch (JsonSyntaxException e) {
217 throw new ApiException("JsonSyntaxException:{}", e);
222 public void update() throws ApiException {
223 state.stateResponse = getState();
224 state.unpackJsonObjects();
228 protected void getUpdatedFxList() {
229 List<StateOption> fxOptions = new ArrayList<>();
231 for (String value : state.jsonResponse.effects) {
232 fxOptions.add(new StateOption(Integer.toString(counter++), value));
234 if (handler.config.sortEffects) {
235 fxOptions.sort(Comparator.comparing(o -> o.getValue().equals("0") ? "" : o.getLabel()));
237 handler.stateDescriptionProvider.setStateOptions(new ChannelUID(handler.getThing().getUID(), CHANNEL_FX),
241 protected void getUpdatedPaletteList() {
242 List<StateOption> palleteOptions = new ArrayList<>();
244 for (String value : state.jsonResponse.palettes) {
245 palleteOptions.add(new StateOption(Integer.toString(counter++), value));
247 if (handler.config.sortPalettes) {
248 palleteOptions.sort(Comparator.comparing(o -> o.getValue().equals("0") ? "" : o.getLabel()));
250 handler.stateDescriptionProvider.setStateOptions(new ChannelUID(handler.getThing().getUID(), CHANNEL_PALETTES),
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);
263 version = Integer.parseInt(temp);
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");
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()));
288 if (!state.stateResponse.seg[handler.config.segmentIndex].on) {
289 handler.update(CHANNEL_MASTER_CONTROLS, OnOffType.OFF);
290 handler.update(CHANNEL_SEGMENT_BRIGHTNESS, OnOffType.OFF);
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)));
297 if (state.nightLightState.on) {
298 handler.update(CHANNEL_SLEEP, OnOffType.ON);
300 handler.update(CHANNEL_SLEEP, OnOffType.OFF);
302 if (state.stateResponse.pl == 0) {
303 handler.update(CHANNEL_PRESET_CYCLE, OnOffType.ON);
305 handler.update(CHANNEL_PRESET_CYCLE, OnOffType.OFF);
307 if (state.udpnState.recv) {
308 handler.update(CHANNEL_SYNC_RECEIVE, OnOffType.ON);
310 handler.update(CHANNEL_SYNC_RECEIVE, OnOffType.OFF);
312 if (state.udpnState.send) {
313 handler.update(CHANNEL_SYNC_SEND, OnOffType.ON);
315 handler.update(CHANNEL_SYNC_SEND, OnOffType.OFF);
317 if (state.stateResponse.seg[handler.config.segmentIndex].mi) {
318 handler.update(CHANNEL_MIRROR, OnOffType.ON);
320 handler.update(CHANNEL_MIRROR, OnOffType.OFF);
322 if (state.stateResponse.seg[handler.config.segmentIndex].rev) {
323 handler.update(CHANNEL_REVERSE, OnOffType.ON);
325 handler.update(CHANNEL_REVERSE, OnOffType.OFF);
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));
346 public void setGlobalOn(boolean bool) throws ApiException {
347 updateStateFromReply(postState("{\"on\":" + bool + ",\"v\":true,\"tt\":2}"));
351 public void setMasterOn(boolean bool, int segmentIndex) throws ApiException {
352 updateStateFromReply(
353 postState("{\"v\":true,\"tt\":2,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":" + bool + "}]}"));
357 public void setGlobalBrightness(PercentType percent) throws ApiException {
358 if (percent.equals(PercentType.ZERO)) {
359 updateStateFromReply(postState("{\"on\":false,\"v\":true}"));
362 updateStateFromReply(postState("{\"on\":true,\"v\":true,\"tt\":2,\"bri\":"
363 + percent.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}"));
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}]}"));
372 updateStateFromReply(postState("{\"tt\":2,\"v\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":true,\"bri\":"
373 + percent.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}]}"));
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() + "]]}]}"));
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() + "]]}]}"));
392 public void setEffect(String string, int segmentIndex) throws ApiException {
393 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"fx\":" + string + "}]}");
397 public void setPreset(String string) throws ApiException {
398 updateStateFromReply(postState("{\"ps\":" + string + ",\"v\":true}"));
402 public void setPalette(String string, int segmentIndex) throws ApiException {
403 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"pal\":" + string + "}]}");
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() + "}]}");
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() + "}]}");
419 public void setSleep(boolean bool) throws ApiException {
420 postState("{\"nl\":{\"on\":" + bool + "}}");
424 public void setUdpSend(boolean bool) throws ApiException {
425 postState("{\"udpn\":{\"send\":" + bool + "}}");
429 public void setUdpRecieve(boolean bool) throws ApiException {
430 postState("{\"udpn\":{\"recv\":" + bool + "}}");
434 public void setTransitionTime(BigDecimal time) throws ApiException {
435 postState("{\"transition\":" + time + "}");
439 public void setPresetCycle(boolean bool) throws ApiException {
441 postState("{\"pl\":0}");
443 postState("{\"pl\":-1}");
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() + "],[],[]]}]}");
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() + "],[]]}]}");
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() + "]]}]}");
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() + "]]}]}");
478 public void setMirror(boolean bool, int segmentIndex) throws ApiException {
479 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"mi\":" + bool + "}]}");
483 public void setReverse(boolean bool, int segmentIndex) throws ApiException {
484 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"rev\":" + bool + "}]}");
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);
495 sendGetRequest("/win&PS=" + position);
496 } catch (ApiException e) {
497 logger.warn("Preset failed to save:{}", e.getMessage());
502 public void setLiveOverride(String value) throws ApiException {
503 postState("{\"lor\":" + value + "}");
507 public void setGrouping(int value, int segmentIndex) throws ApiException {
508 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"grp\":" + value + "}]}");
512 public void setSpacing(int value, int segmentIndex) throws ApiException {
513 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"spc\":" + value + "}]}");