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.List;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
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;
53 import com.google.gson.Gson;
54 import com.google.gson.JsonSyntaxException;
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.
60 * @author Matthew Skinner - Initial contribution
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;
72 public WledApiV084(WLedHandler handler, HttpClient httpClient) {
73 this.handler = handler;
74 this.address = handler.config.address;
75 this.httpClient = httpClient;
79 public void initialize() throws ApiException {
80 state.jsonResponse = getJson();
82 getUpdatedPaletteList();
85 LedInfo localLedInfo = gson.fromJson(state.infoResponse.leds.toString(), LedInfo.class);
86 if (localLedInfo != null) {
87 state.ledInfo = localLedInfo;
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);
98 channel = handler.getThing().getChannel(CHANNEL_SECONDARY_WHITE);
99 if (channel != null) {
100 removeChannels.add(channel);
102 channel = handler.getThing().getChannel(CHANNEL_THIRD_WHITE);
103 if (channel != null) {
104 removeChannels.add(channel);
107 handler.removeChannels(removeChannels);
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 = "";
119 ContentResponse contentResponse = request.send();
120 if (contentResponse.getStatus() == 200) {
121 return contentResponse.getContentAsString();
123 errorReason = String.format("WLED request failed with %d: %s", contentResponse.getStatus(),
124 contentResponse.getReason());
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());
134 throw new ApiException(errorReason);
137 protected String postState(String json) throws ApiException {
138 return sendPostRequest("/json/state", json);
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 = "";
149 ContentResponse contentResponse = request.send();
150 if (contentResponse.getStatus() == 200) {
151 return contentResponse.getContentAsString();
153 errorReason = String.format("WLED request failed with %d: %s", contentResponse.getStatus(),
154 contentResponse.getReason());
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());
163 throw new ApiException(errorReason);
166 protected void updateStateFromReply(String jsonState) {
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");
172 state.stateResponse = response;
173 state.unpackJsonObjects();
175 } catch (JsonSyntaxException | ApiException e) {
176 logger.debug("Reply back when a command was sent triggered an exception:{}", jsonState);
180 protected StateResponse getState() throws ApiException {
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");
187 logger.trace("json/state:{}", returnContent);
189 } catch (JsonSyntaxException e) {
190 throw new ApiException("JsonSyntaxException:{}", e);
194 protected InfoResponse getInfo() throws ApiException {
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");
202 } catch (JsonSyntaxException e) {
203 throw new ApiException("JsonSyntaxException:{}", e);
207 protected JsonResponse getJson() throws ApiException {
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");
215 } catch (JsonSyntaxException e) {
216 throw new ApiException("JsonSyntaxException:{}", e);
221 public void update() throws ApiException {
222 state.stateResponse = getState();
223 state.unpackJsonObjects();
227 protected void getUpdatedFxList() {
228 List<StateOption> fxOptions = new ArrayList<>();
230 for (String value : state.jsonResponse.effects) {
231 fxOptions.add(new StateOption(Integer.toString(counter++), value));
233 handler.stateDescriptionProvider.setStateOptions(new ChannelUID(handler.getThing().getUID(), CHANNEL_FX),
237 protected void getUpdatedPaletteList() {
238 List<StateOption> palleteOptions = new ArrayList<>();
240 for (String value : state.jsonResponse.palettes) {
241 palleteOptions.add(new StateOption(Integer.toString(counter++), value));
243 handler.stateDescriptionProvider.setStateOptions(new ChannelUID(handler.getThing().getUID(), CHANNEL_PALETTES),
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);
256 version = Integer.parseInt(temp);
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");
265 HSBType tempHSB = WLedHelper
266 .parseToHSBType(state.stateResponse.seg[handler.config.segmentIndex].col[0].toString());
267 handler.update(CHANNEL_PRIMARY_COLOR, tempHSB);
268 handler.update(CHANNEL_SECONDARY_COLOR,
269 WLedHelper.parseToHSBType(state.stateResponse.seg[handler.config.segmentIndex].col[1].toString()));
270 handler.update(CHANNEL_THIRD_COLOR,
271 WLedHelper.parseToHSBType(state.stateResponse.seg[handler.config.segmentIndex].col[2].toString()));
272 if (state.ledInfo.rgbw) {
273 handler.update(CHANNEL_PRIMARY_WHITE, WLedHelper
274 .parseWhitePercent(state.stateResponse.seg[handler.config.segmentIndex].col[0].toString()));
275 handler.update(CHANNEL_SECONDARY_WHITE, WLedHelper
276 .parseWhitePercent(state.stateResponse.seg[handler.config.segmentIndex].col[1].toString()));
277 handler.update(CHANNEL_THIRD_WHITE, WLedHelper
278 .parseWhitePercent(state.stateResponse.seg[handler.config.segmentIndex].col[2].toString()));
281 if (!state.stateResponse.seg[handler.config.segmentIndex].on) {
282 handler.update(CHANNEL_MASTER_CONTROLS, OnOffType.OFF);
283 handler.update(CHANNEL_SEGMENT_BRIGHTNESS, OnOffType.OFF);
285 handler.update(CHANNEL_MASTER_CONTROLS, tempHSB);
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)));
290 if (state.nightLightState.on) {
291 handler.update(CHANNEL_SLEEP, OnOffType.ON);
293 handler.update(CHANNEL_SLEEP, OnOffType.OFF);
295 if (state.stateResponse.pl == 0) {
296 handler.update(CHANNEL_PRESET_CYCLE, OnOffType.ON);
298 handler.update(CHANNEL_PRESET_CYCLE, OnOffType.OFF);
300 if (state.udpnState.recv) {
301 handler.update(CHANNEL_SYNC_RECEIVE, OnOffType.ON);
303 handler.update(CHANNEL_SYNC_RECEIVE, OnOffType.OFF);
305 if (state.udpnState.send) {
306 handler.update(CHANNEL_SYNC_SEND, OnOffType.ON);
308 handler.update(CHANNEL_SYNC_SEND, OnOffType.OFF);
310 if (state.stateResponse.seg[handler.config.segmentIndex].mi) {
311 handler.update(CHANNEL_MIRROR, OnOffType.ON);
313 handler.update(CHANNEL_MIRROR, OnOffType.OFF);
315 if (state.stateResponse.seg[handler.config.segmentIndex].rev) {
316 handler.update(CHANNEL_REVERSE, OnOffType.ON);
318 handler.update(CHANNEL_REVERSE, OnOffType.OFF);
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));
339 public void setGlobalOn(boolean bool) throws ApiException {
340 updateStateFromReply(postState("{\"on\":" + bool + ",\"v\":true,\"tt\":2}"));
344 public void setMasterOn(boolean bool, int segmentIndex) throws ApiException {
345 updateStateFromReply(
346 postState("{\"v\":true,\"tt\":2,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":" + bool + "}]}"));
350 public void setGlobalBrightness(PercentType percent) throws ApiException {
351 if (percent.equals(PercentType.ZERO)) {
352 updateStateFromReply(postState("{\"on\":false,\"v\":true}"));
355 updateStateFromReply(postState("{\"on\":true,\"v\":true,\"tt\":2,\"bri\":"
356 + percent.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}"));
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}]}"));
365 updateStateFromReply(postState("{\"tt\":2,\"v\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":true,\"bri\":"
366 + percent.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}]}"));
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() + "]]}]}"));
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() + "]]}]}"));
385 public void setEffect(String string, int segmentIndex) throws ApiException {
386 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"fx\":" + string + "}]}");
390 public void setPreset(String string) throws ApiException {
391 updateStateFromReply(postState("{\"ps\":" + string + ",\"v\":true}"));
395 public void setPalette(String string, int segmentIndex) throws ApiException {
396 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"pal\":" + string + "}]}");
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() + "}]}");
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() + "}]}");
412 public void setSleep(boolean bool) throws ApiException {
413 postState("{\"nl\":{\"on\":" + bool + "}}");
417 public void setUdpSend(boolean bool) throws ApiException {
418 postState("{\"udpn\":{\"send\":" + bool + "}}");
422 public void setUdpRecieve(boolean bool) throws ApiException {
423 postState("{\"udpn\":{\"recv\":" + bool + "}}");
427 public void setTransitionTime(BigDecimal time) throws ApiException {
428 postState("{\"transition\":" + time + "}");
432 public void setPresetCycle(boolean bool) throws ApiException {
434 postState("{\"pl\":0}");
436 postState("{\"pl\":-1}");
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() + "],[],[]]}]}");
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() + "],[]]}]}");
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() + "]]}]}");
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() + "]]}]}");
471 public void setMirror(boolean bool, int segmentIndex) throws ApiException {
472 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"mi\":" + bool + "}]}");
476 public void setReverse(boolean bool, int segmentIndex) throws ApiException {
477 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"rev\":" + bool + "}]}");
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);
488 sendGetRequest("/win&PS=" + position);
489 } catch (ApiException e) {
490 logger.warn("Preset failed to save:{}", e.getMessage());
495 public void setLiveOverride(String value) throws ApiException {
496 postState("{\"lor\":" + value + "}");
500 public void setGrouping(int value, int segmentIndex) throws ApiException {
501 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"grp\":" + value + "}]}");
505 public void setSpacing(int value, int segmentIndex) throws ApiException {
506 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"spc\":" + value + "}]}");