2 * Copyright (c) 2010-2024 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.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.binding.wled.internal.handlers.WLedBridgeHandler;
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.types.StateOption;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 import com.google.gson.Gson;
53 import com.google.gson.JsonSyntaxException;
56 * The {@link WledApiV084} is the json Api methods for firmware version 0.8.4 and newer
57 * as newer firmwares come out with breaking changes, extend this class into a newer firmware version class.
59 * @author Matthew Skinner - Initial contribution
62 public class WledApiV084 implements WledApi {
63 protected final Logger logger = LoggerFactory.getLogger(this.getClass());
64 protected final Gson gson = new Gson();
65 protected final HttpClient httpClient;
66 protected final WLedBridgeHandler handler;
67 protected final String address;
68 protected WledState state = new WledState();
69 private int version = 0;
71 public WledApiV084(WLedBridgeHandler handler, HttpClient httpClient) {
72 this.handler = handler;
73 this.address = handler.config.address;
74 this.httpClient = httpClient;
78 public void initialize() throws ApiException {
79 state.jsonResponse = getJson();
80 state.infoResponse = getInfo();
82 LedInfo localLedInfo = gson.fromJson(state.infoResponse.leds.toString(), LedInfo.class);
83 if (localLedInfo != null) {
84 state.ledInfo = localLedInfo;
86 handler.hasWhite = state.ledInfo.rgbw;
90 public String sendGetRequest(String url) throws ApiException {
91 Request request = httpClient.newRequest(address + url);
92 request.timeout(3, TimeUnit.SECONDS);
93 request.method(HttpMethod.GET);
94 request.header(HttpHeader.ACCEPT_ENCODING, "gzip");
95 logger.trace("Sending WLED GET:{}", url);
96 String errorReason = "";
98 ContentResponse contentResponse = request.send();
99 if (contentResponse.getStatus() == 200) {
100 return contentResponse.getContentAsString();
102 errorReason = String.format("WLED request failed with %d: %s", contentResponse.getStatus(),
103 contentResponse.getReason());
105 } catch (TimeoutException e) {
106 errorReason = "TimeoutException: WLED was not reachable on your network";
107 } catch (ExecutionException e) {
108 errorReason = String.format("ExecutionException: %s", e.getMessage());
109 } catch (InterruptedException e) {
110 Thread.currentThread().interrupt();
111 errorReason = String.format("InterruptedException: %s", e.getMessage());
113 throw new ApiException(errorReason);
116 protected String postState(String json) throws ApiException {
117 return sendPostRequest("/json/state", json);
120 protected String sendPostRequest(String url, String json) throws ApiException {
121 logger.debug("Sending WLED POST:{} Message:{}", url, json);
122 Request request = httpClient.POST(address + url);
123 request.timeout(3, TimeUnit.SECONDS);
124 request.header(HttpHeader.CONTENT_TYPE, "application/json");
125 request.content(new StringContentProvider(json), "application/json");
126 String errorReason = "";
128 ContentResponse contentResponse = request.send();
129 if (contentResponse.getStatus() == 200) {
130 return contentResponse.getContentAsString();
132 errorReason = String.format("WLED request failed with %d: %s", contentResponse.getStatus(),
133 contentResponse.getReason());
135 } catch (InterruptedException e) {
136 errorReason = String.format("InterruptedException: %s", e.getMessage());
137 } catch (TimeoutException e) {
138 errorReason = "TimeoutException: WLED was not reachable on your network";
139 } catch (ExecutionException e) {
140 errorReason = String.format("ExecutionException: %s", e.getMessage());
142 throw new ApiException(errorReason);
145 protected void updateStateFromReply(String jsonState) {
147 StateResponse response = gson.fromJson(jsonState, StateResponse.class);
148 if (response == null) {
149 throw new ApiException("Reply back from WLED when command was made is not valid JSON");
151 state.stateResponse = response;
152 state.unpackJsonObjects();
153 processBridgeStates();
154 for (int count = 0; count < state.stateResponse.seg.length; count++) {
157 } catch (JsonSyntaxException | ApiException e) {
158 logger.debug("Reply back when a command was sent triggered an exception:{}", jsonState);
162 protected StateResponse getState() throws ApiException {
164 String returnContent = sendGetRequest("/json/state");
165 StateResponse response = gson.fromJson(returnContent, StateResponse.class);
166 if (response == null) {
167 throw new ApiException("Could not GET:/json/state");
169 logger.trace("json/state:{}", returnContent);
171 } catch (JsonSyntaxException e) {
172 throw new ApiException("JsonSyntaxException:{}", e);
176 protected InfoResponse getInfo() throws ApiException {
178 String returnContent = sendGetRequest("/json/info");
179 InfoResponse response = gson.fromJson(returnContent, InfoResponse.class);
180 if (response == null) {
181 throw new ApiException("Could not GET:/json/info");
183 logger.trace("/json/info:{}", returnContent);
185 } catch (JsonSyntaxException e) {
186 throw new ApiException("JsonSyntaxException:{}", e);
190 protected JsonResponse getJson() throws ApiException {
192 String returnContent = sendGetRequest("/json");
193 JsonResponse response = gson.fromJson(returnContent, JsonResponse.class);
194 if (response == null) {
195 throw new ApiException("Could not GET:/json");
198 } catch (JsonSyntaxException e) {
199 throw new ApiException("JsonSyntaxException:{}", e);
204 public void update() throws ApiException {
205 state.stateResponse = getState();
206 state.unpackJsonObjects();
207 processBridgeStates();
208 for (int count = 0; count < state.stateResponse.seg.length; count++) {
214 public List<StateOption> getUpdatedFxList() {
215 List<StateOption> fxOptions = new ArrayList<>();
217 for (String value : state.jsonResponse.effects) {
218 fxOptions.add(new StateOption(Integer.toString(counter++), value));
220 if (handler.config.sortEffects) {
221 fxOptions.sort(Comparator.comparing(o -> "0".equals(o.getValue()) ? "" : o.getLabel()));
227 public List<StateOption> getUpdatedPaletteList() {
228 List<StateOption> palleteOptions = new ArrayList<>();
230 for (String value : state.jsonResponse.palettes) {
231 palleteOptions.add(new StateOption(Integer.toString(counter++), value));
233 if (handler.config.sortPalettes) {
234 palleteOptions.sort(Comparator.comparing(o -> "0".equals(o.getValue()) ? "" : o.getLabel()));
236 return palleteOptions;
240 public int getFirmwareVersion() throws ApiException {
241 state.infoResponse = getInfo();
242 String temp = state.infoResponse.ver;
243 logger.debug("Firmware for WLED is ver:{}", temp);
244 temp = temp.replace(".", "");
245 if (temp.length() > 4) {
246 temp = temp.substring(0, 4);
248 version = Integer.parseInt(temp);
252 protected void processBridgeStates() throws ApiException {
253 if (!state.stateResponse.on) {
254 handler.update(CHANNEL_GLOBAL_BRIGHTNESS, OnOffType.OFF);
256 handler.update(CHANNEL_GLOBAL_BRIGHTNESS, new PercentType(
257 new BigDecimal(state.stateResponse.bri).divide(BIG_DECIMAL_2_55, RoundingMode.HALF_UP)));
259 handler.update(CHANNEL_LIVE_OVERRIDE, new StringType(Integer.toString(state.stateResponse.lor)));
260 handler.update(CHANNEL_PRESETS, new StringType(Integer.toString(state.stateResponse.ps)));
261 if (state.nightLightState.on) {
262 handler.update(CHANNEL_SLEEP, OnOffType.ON);
264 handler.update(CHANNEL_SLEEP, OnOffType.OFF);
266 if (state.udpnState.recv) {
267 handler.update(CHANNEL_SYNC_RECEIVE, OnOffType.ON);
269 handler.update(CHANNEL_SYNC_RECEIVE, OnOffType.OFF);
271 if (state.udpnState.send) {
272 handler.update(CHANNEL_SYNC_SEND, OnOffType.ON);
274 handler.update(CHANNEL_SYNC_SEND, OnOffType.OFF);
276 if (state.stateResponse.pl == 0) {
277 handler.update(CHANNEL_PRESET_CYCLE, OnOffType.ON);
279 handler.update(CHANNEL_PRESET_CYCLE, OnOffType.OFF);
281 handler.update(CHANNEL_TRANS_TIME, new QuantityType<>(
282 new BigDecimal(state.stateResponse.transition).divide(BigDecimal.TEN), Units.SECOND));
283 handler.update(CHANNEL_SLEEP_DURATION,
284 new QuantityType<>(new BigDecimal(state.nightLightState.dur), Units.MINUTE));
285 handler.update(CHANNEL_SLEEP_BRIGHTNESS, new PercentType(
286 new BigDecimal(state.nightLightState.tbri).divide(BIG_DECIMAL_2_55, RoundingMode.HALF_UP)));
287 handler.update(CHANNEL_SLEEP_MODE, new StringType(Integer.toString(state.nightLightState.mode)));
290 protected void processState(int segmentIndex) throws ApiException {
291 if (state.stateResponse.seg.length <= segmentIndex) {
292 throw new ApiException(
293 "Segment " + segmentIndex + " is not currently setup correctly in the WLED firmware");
295 if (handler.handlerMissing(segmentIndex)) {
296 // There is no thing setup for this segmentIndex.
299 HSBType tempHSB = WLedHelper.parseToHSBType(state.stateResponse.seg[segmentIndex].col[0].toString());
300 handler.update(segmentIndex, CHANNEL_PRIMARY_COLOR, tempHSB);
301 handler.update(segmentIndex, CHANNEL_SECONDARY_COLOR,
302 WLedHelper.parseToHSBType(state.stateResponse.seg[segmentIndex].col[1].toString()));
303 handler.update(segmentIndex, CHANNEL_THIRD_COLOR,
304 WLedHelper.parseToHSBType(state.stateResponse.seg[segmentIndex].col[2].toString()));
305 if (state.ledInfo.rgbw) {
306 handler.update(segmentIndex, CHANNEL_PRIMARY_WHITE,
307 WLedHelper.parseWhitePercent(state.stateResponse.seg[segmentIndex].col[0].toString()));
308 handler.update(segmentIndex, CHANNEL_SECONDARY_WHITE,
309 WLedHelper.parseWhitePercent(state.stateResponse.seg[segmentIndex].col[1].toString()));
310 handler.update(segmentIndex, CHANNEL_THIRD_WHITE,
311 WLedHelper.parseWhitePercent(state.stateResponse.seg[segmentIndex].col[2].toString()));
313 // Global OFF or Segment OFF needs to be treated as OFF
314 if (!state.stateResponse.seg[segmentIndex].on || !state.stateResponse.on) {
315 handler.update(segmentIndex, CHANNEL_MASTER_CONTROLS, OnOffType.OFF);
316 handler.update(segmentIndex, CHANNEL_SEGMENT_BRIGHTNESS, OnOffType.OFF);
318 handler.update(segmentIndex, CHANNEL_MASTER_CONTROLS, tempHSB);
319 handler.update(segmentIndex, CHANNEL_SEGMENT_BRIGHTNESS,
320 new PercentType(new BigDecimal(state.stateResponse.seg[segmentIndex].bri).divide(BIG_DECIMAL_2_55,
321 RoundingMode.HALF_UP)));
323 if (state.stateResponse.seg[segmentIndex].mi) {
324 handler.update(segmentIndex, CHANNEL_MIRROR, OnOffType.ON);
326 handler.update(segmentIndex, CHANNEL_MIRROR, OnOffType.OFF);
328 if (state.stateResponse.seg[segmentIndex].rev) {
329 handler.update(segmentIndex, CHANNEL_REVERSE, OnOffType.ON);
331 handler.update(segmentIndex, CHANNEL_REVERSE, OnOffType.OFF);
333 handler.update(segmentIndex, CHANNEL_FX,
334 new StringType(Integer.toString(state.stateResponse.seg[segmentIndex].fx)));
335 handler.update(segmentIndex, CHANNEL_PALETTES,
336 new StringType(Integer.toString(state.stateResponse.seg[segmentIndex].pal)));
337 handler.update(segmentIndex, CHANNEL_SPEED,
338 new PercentType(new BigDecimal(state.stateResponse.seg[segmentIndex].sx).divide(BIG_DECIMAL_2_55,
339 RoundingMode.HALF_UP)));
340 handler.update(segmentIndex, CHANNEL_INTENSITY,
341 new PercentType(new BigDecimal(state.stateResponse.seg[segmentIndex].ix).divide(BIG_DECIMAL_2_55,
342 RoundingMode.HALF_UP)));
343 handler.update(segmentIndex, CHANNEL_GROUPING, new DecimalType(state.stateResponse.seg[segmentIndex].grp));
344 handler.update(segmentIndex, CHANNEL_SPACING, new DecimalType(state.stateResponse.seg[segmentIndex].spc));
348 public void setGlobalOn(boolean bool) throws ApiException {
349 updateStateFromReply(postState("{\"on\":" + bool + ",\"v\":true,\"tt\":2}"));
353 public void setMasterOn(boolean bool, int segmentIndex) throws ApiException {
354 updateStateFromReply(
355 postState("{\"v\":true,\"tt\":2,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":" + bool + "}]}"));
359 public void setGlobalBrightness(PercentType percent) throws ApiException {
360 if (percent.equals(PercentType.ZERO)) {
361 updateStateFromReply(postState("{\"on\":false,\"v\":true}"));
364 updateStateFromReply(postState("{\"on\":true,\"v\":true,\"tt\":2,\"bri\":"
365 + percent.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}"));
369 public void setMasterBrightness(PercentType percent, int segmentIndex) throws ApiException {
370 if (percent.equals(PercentType.ZERO)) {
371 updateStateFromReply(postState("{\"v\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":false}]}"));
374 updateStateFromReply(postState("{\"tt\":2,\"v\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"on\":true,\"bri\":"
375 + percent.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}]}"));
379 public void setMasterHSB(HSBType hsbType, int segmentIndex) throws ApiException {
380 if (hsbType.getBrightness().toBigDecimal().equals(BigDecimal.ZERO)) {
381 updateStateFromReply(postState("{\"tt\":2,\"v\":true,\"seg\":[{\"on\":false,\"id\":" + segmentIndex
382 + ",\"fx\":0,\"col\":[[" + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue()
383 + "," + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
384 + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "]]}]}"));
387 updateStateFromReply(postState("{\"tt\":2,\"v\":true,\"seg\":[{\"on\":true,\"id\":" + segmentIndex
388 + ",\"fx\":0,\"col\":[[" + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
389 + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
390 + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "]]}]}"));
394 public void setEffect(String string, int segmentIndex) throws ApiException {
395 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"fx\":" + string + "}]}");
399 public void setPreset(String string) throws ApiException {
400 updateStateFromReply(postState("{\"ps\":" + string + ",\"v\":true}"));
404 public void setPalette(String string, int segmentIndex) throws ApiException {
405 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"pal\":" + string + "}]}");
409 public void setFxIntencity(PercentType percentType, int segmentIndex) throws ApiException {
410 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"ix\":"
411 + percentType.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}]}");
415 public void setFxSpeed(PercentType percentType, int segmentIndex) throws ApiException {
416 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"sx\":"
417 + percentType.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}]}");
421 public void setSleep(boolean bool) throws ApiException {
422 postState("{\"nl\":{\"on\":" + bool + "}}");
426 public void setUdpSend(boolean bool) throws ApiException {
427 postState("{\"udpn\":{\"send\":" + bool + "}}");
431 public void setUdpRecieve(boolean bool) throws ApiException {
432 postState("{\"udpn\":{\"recv\":" + bool + "}}");
436 public void setTransitionTime(BigDecimal time) throws ApiException {
437 postState("{\"transition\":" + time + "}");
441 public void setPresetCycle(boolean bool) throws ApiException {
443 postState("{\"pl\":0}");
445 postState("{\"pl\":-1}");
450 public void setPrimaryColor(HSBType hsbType, int segmentIndex) throws ApiException {
451 postState("{\"on\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"col\":[["
452 + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
453 + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
454 + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "],[],[]]}]}");
458 public void setSecondaryColor(HSBType hsbType, int segmentIndex) throws ApiException {
459 postState("{\"on\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"col\":[[],["
460 + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
461 + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
462 + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "],[]]}]}");
466 public void setTertiaryColor(HSBType hsbType, int segmentIndex) throws ApiException {
467 postState("{\"on\":true,\"seg\":[{\"id\":" + segmentIndex + ",\"col\":[[],[],["
468 + hsbType.getRed().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
469 + hsbType.getGreen().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + ","
470 + hsbType.getBlue().toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "]]}]}");
474 public void setWhiteOnly(PercentType percentType, int segmentIndex) throws ApiException {
475 postState("{\"seg\":[{\"on\":true,\"id\":" + segmentIndex + ",\"fx\":0,\"col\":[[0,0,0,"
476 + percentType.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "]]}]}");
480 public void setMirror(boolean bool, int segmentIndex) throws ApiException {
481 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"mi\":" + bool + "}]}");
485 public void setReverse(boolean bool, int segmentIndex) throws ApiException {
486 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"rev\":" + bool + "}]}");
490 public void savePreset(int position, String presetName) throws ApiException {
491 // named presets not supported in older firmwares, and max of 16.
492 if (position > 16 || position < 1) {
493 logger.warn("Preset position {} is not supported in this firmware version", position);
497 sendGetRequest("/win&PS=" + position);
498 } catch (ApiException e) {
499 logger.warn("Preset failed to save:{}", e.getMessage());
504 public void setLiveOverride(String value) throws ApiException {
505 postState("{\"lor\":" + value + "}");
509 public void setGrouping(int value, int segmentIndex) throws ApiException {
510 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"grp\":" + value + "}]}");
514 public void setSpacing(int value, int segmentIndex) throws ApiException {
515 postState("{\"seg\":[{\"id\":" + segmentIndex + ",\"spc\":" + value + "}]}");
519 public List<String> getSegmentNames() {
520 List<String> segmentNames = new ArrayList<>(state.stateResponse.seg.length);
521 for (int count = 0; count < state.stateResponse.seg.length; count++) {
522 segmentNames.add("Segment " + count);
528 public void setSleepMode(String value) throws ApiException {
529 // Binding requires firmware 0.11.0 and newer
533 public void setSleepDuration(BigDecimal time) throws ApiException {
534 postState("{\"nl\":{\"dur\":" + time + "}}");
538 public void setSleepTargetBrightness(PercentType percent) throws ApiException {
539 postState("{\"nl\":{\"tbri\":" + percent.toBigDecimal().multiply(BIG_DECIMAL_2_55).intValue() + "}}");