[WLed] Initial contribution - Binding for LED strings with a large range of built in FX. (#8669)

* V3
* Fix null compiler warnings.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
Matthew Skinner
2020-11-04 04:09:40 +11:00
committed by GitHub
parent 067a8f7953
commit c49eeb2528
17 changed files with 1223 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.wled-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${project.version}/xml/features</repository>
<feature name="openhab-binding-wled" description="WLED Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.wled/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wled.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WLedActions} is responsible for Actions.
*
* @author Matthew Skinner - Initial contribution
*/
@ThingActionsScope(name = "wled")
@NonNullByDefault
public class WLedActions implements ThingActions {
public final Logger logger = LoggerFactory.getLogger(getClass());
private @Nullable WLedHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (WLedHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@RuleAction(label = "save state to preset", description = "Save a WLED state to a preset slot")
public void savePreset(
@ActionInput(name = "presetNumber", label = "Preset Slot", description = "Number for the preset slot you wish to use") int presetNumber) {
WLedHandler localHandler = handler;
if (presetNumber > 0 && localHandler != null) {
localHandler.savePreset(presetNumber);
}
}
public static void savePreset(@Nullable ThingActions actions, int presetNumber) {
if (actions instanceof WLedActions) {
((WLedActions) actions).savePreset(presetNumber);
} else {
throw new IllegalArgumentException("Instance is not a WLED class.");
}
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wled.internal;
import java.math.BigDecimal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link WLedBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class WLedBindingConstants {
public static final String BINDING_ID = "wled";
public static final BigDecimal BIG_DECIMAL_2_55 = new BigDecimal(2.55);
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_WLED = new ThingTypeUID(BINDING_ID, "wled");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_WLED);
// Configs
public static final String CONFIG_ADDRESS = "address";
public static final String CONFIG_POLL_TIME = "pollTime";
public static final String CONFIG_SEGMENT_INDEX = "segmentIndex";
public static final String CONFIG_SAT_THRESHOLD = "saturationThreshold";
// Channels
public static final String CHANNEL_MASTER_CONTROLS = "masterControls";
public static final String CHANNEL_PRIMARY_COLOR = "primaryColor";
public static final String CHANNEL_SECONDARY_COLOR = "secondaryColor";
public static final String CHANNEL_PRIMARY_WHITE = "primaryWhite";
public static final String CHANNEL_SECONDARY_WHITE = "secondaryWhite";
public static final String CHANNEL_PALETTES = "palettes";
public static final String CHANNEL_PRESETS = "presets";
public static final String CHANNEL_PRESET_DURATION = "presetDuration";
public static final String CHANNEL_TRANS_TIME = "transformTime";
public static final String CHANNEL_PRESET_CYCLE = "presetCycle";
public static final String CHANNEL_FX = "fx";
public static final String CHANNEL_SPEED = "speed";
public static final String CHANNEL_INTENSITY = "intensity";
public static final String CHANNEL_SLEEP = "sleep";
public static final String CHANNEL_SYNC_SEND = "syncSend";
public static final String CHANNEL_SYNC_RECEIVE = "syncReceive";
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wled.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link WLedConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class WLedConfiguration {
public String address = "";
public int pollTime;
public int segmentIndex;
public int saturationThreshold;
}

View File

@@ -0,0 +1,128 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wled.internal;
import static org.openhab.binding.wled.internal.WLedBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WLedDiscoveryService} Discovers and adds any Wled devices found.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
@Component(service = MDNSDiscoveryParticipant.class)
public class WLedDiscoveryService implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(WLedDiscoveryService.class);
private final HttpClient httpClient;
@Activate
public WLedDiscoveryService(@Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
private String sendGetRequest(String address, String url) {
Request request = httpClient.newRequest(address + url);
request.timeout(3, TimeUnit.SECONDS);
request.method(HttpMethod.GET);
request.header(HttpHeader.ACCEPT_ENCODING, "gzip");
logger.trace("Sending WLED GET:{}", url);
try {
ContentResponse contentResponse = request.send();
if (contentResponse.getStatus() == 200) {
return contentResponse.getContentAsString();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException | ExecutionException e) {
logger.debug(
"WLED discovery hit a TimeoutException | ExecutionException which may have blocked a device from getting discovered:{}",
e.getMessage());
}
return "";
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
String name = service.getName().toLowerCase();
if (!name.contains("wled")) {
return null;
}
String address[] = service.getURLs();
if ((address == null) || address.length < 1) {
logger.debug("WLED discovered with empty IP address-{}", service);
return null;
}
String response = sendGetRequest(address[0], "/json");
// LinkedList<String> segmentIndexList = WLedHelper.listOfResults(response, "{\"id\":", ",");
// How to create multiple things from the returned list of segments?
String label = WLedHelper.getValue(response, "\"name\":\"", "\"");
if (label.isEmpty()) {
label = "WLED @ " + address[0];
}
String macAddress = WLedHelper.getValue(response, "\"mac\":\"", "\"");
String firmware = WLedHelper.getValue(response, "\"ver\":\"", "\"");
ThingTypeUID thingtypeuid = new ThingTypeUID("wled", "wled");
ThingUID thingUID = new ThingUID(thingtypeuid, macAddress);
Map<String, Object> properties = new HashMap<>();
properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress);
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmware);
return DiscoveryResultBuilder.create(thingUID).withProperty(CONFIG_ADDRESS, address[0])
.withProperty(CONFIG_SEGMENT_INDEX, -1).withLabel(label).withProperties(properties)
.withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build();
}
@Override
public @Nullable ThingUID getThingUID(ServiceInfo service) {
return null;
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES;
}
@Override
public String getServiceType() {
return "_http._tcp.local.";
}
}

View File

@@ -0,0 +1,451 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wled.internal;
import static org.openhab.binding.wled.internal.WLedBindingConstants.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WLedHandler} is responsible for handling commands and states, which are
* sent to one of the channels or http replies back.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class WLedHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final HttpClient httpClient;
private final WledDynamicStateDescriptionProvider stateDescriptionProvider;
private @Nullable ScheduledFuture<?> pollingFuture = null;
private BigDecimal masterBrightness255 = BigDecimal.ZERO;
private HSBType primaryColor = new HSBType();
private BigDecimal primaryWhite = BigDecimal.ZERO;
private HSBType secondaryColor = new HSBType();
private BigDecimal secondaryWhite = BigDecimal.ZERO;
private boolean hasWhite = false;
private WLedConfiguration config = new WLedConfiguration();
public WLedHandler(Thing thing, HttpClient httpClient,
WledDynamicStateDescriptionProvider stateDescriptionProvider) {
super(thing);
this.httpClient = httpClient;
this.stateDescriptionProvider = stateDescriptionProvider;
}
private void sendGetRequest(String url) {
Request request;
if (url.contains("json") || config.segmentIndex == -1) {
request = httpClient.newRequest(config.address + url);
} else {
request = httpClient.newRequest(config.address + url + "&SM=" + config.segmentIndex);
}
request.timeout(3, TimeUnit.SECONDS);
request.method(HttpMethod.GET);
request.header(HttpHeader.ACCEPT_ENCODING, "gzip");
logger.trace("Sending WLED GET:{}", url);
String errorReason = "";
try {
ContentResponse contentResponse = request.send();
if (contentResponse.getStatus() == 200) {
processState(contentResponse.getContentAsString());
return;
} else {
errorReason = String.format("WLED request failed with %d: %s", contentResponse.getStatus(),
contentResponse.getReason());
}
} catch (TimeoutException e) {
errorReason = "TimeoutException: WLED was not reachable on your network";
} catch (ExecutionException e) {
errorReason = String.format("ExecutionException: %s", e.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
errorReason = String.format("InterruptedException: %s", e.getMessage());
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorReason);
}
private HSBType parseToHSBType(String message, String element) {
int startIndex = message.indexOf(element);
if (startIndex == -1) {
return new HSBType();
}
int endIndex = message.indexOf("<", startIndex + element.length());
int r = 0, g = 0, b = 0;
try {
r = Integer.parseInt(message.substring(startIndex + element.length(), endIndex));
// look for second element
startIndex = message.indexOf(element, endIndex);
if (startIndex == -1) {
return new HSBType();
}
endIndex = message.indexOf("<", startIndex + element.length());
g = Integer.parseInt(message.substring(startIndex + element.length(), endIndex));
// look for third element called <cl>
startIndex = message.indexOf(element, endIndex);
if (startIndex == -1) {
return new HSBType();
}
endIndex = message.indexOf("<", startIndex + element.length());
b = Integer.parseInt(message.substring(startIndex + element.length(), endIndex));
} catch (NumberFormatException e) {
logger.warn("NumberFormatException when parsing the WLED color fields:{}", e.getMessage());
}
return HSBType.fromRGB(r, g, b);
}
private void parseColours(String message) {
primaryColor = parseToHSBType(message, "<cl>");
updateState(CHANNEL_PRIMARY_COLOR, primaryColor);
secondaryColor = parseToHSBType(message, "<cs>");
updateState(CHANNEL_SECONDARY_COLOR, secondaryColor);
try {
primaryWhite = new BigDecimal(WLedHelper.getValue(message, "<wv>", "<"));
if (primaryWhite.intValue() > -1) {
hasWhite = true;
updateState(CHANNEL_PRIMARY_WHITE,
new PercentType(primaryWhite.divide(BIG_DECIMAL_2_55, RoundingMode.HALF_UP)));
secondaryWhite = new BigDecimal(WLedHelper.getValue(message, "<ws>", "<"));
updateState(CHANNEL_SECONDARY_WHITE,
new PercentType(secondaryWhite.divide(BIG_DECIMAL_2_55, RoundingMode.HALF_UP)));
}
} catch (NumberFormatException e) {
logger.warn("NumberFormatException when parsing the WLED colour and white fields:{}", e.getMessage());
}
}
/**
*
* This function should prevent the need to keep updating the binding as more FX and Palettes are added to the
* firmware.
*/
private void scrapeChannelOptions(String message) {
List<StateOption> fxOptions = new ArrayList<>();
List<StateOption> palleteOptions = new ArrayList<>();
int counter = 0;
for (String value : WLedHelper.getValue(message, "\"effects\":[", "]").replace("\"", "").split(",")) {
fxOptions.add(new StateOption(Integer.toString(counter++), value));
}
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_FX), fxOptions);
counter = 0;
for (String value : (WLedHelper.getValue(message, "\"palettes\":[", "]").replace("\"", "")).split(",")) {
palleteOptions.add(new StateOption(Integer.toString(counter++), value));
}
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_PALETTES), palleteOptions);
}
private void processState(String message) {
logger.trace("WLED states are:{}", message);
if (thing.getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
sendGetRequest("/json"); // fetch FX and Pallete names
}
if (message.contains("\"effects\":[")) {// JSON API reply
scrapeChannelOptions(message);
return;
}
if (message.contains("<ac>0</ac>")) {
updateState(CHANNEL_MASTER_CONTROLS, OnOffType.OFF);
} else {
masterBrightness255 = new BigDecimal(WLedHelper.getValue(message, "<ac>", "<"));
updateState(CHANNEL_MASTER_CONTROLS,
new PercentType(masterBrightness255.divide(BIG_DECIMAL_2_55, RoundingMode.HALF_UP)));
}
if (message.contains("<ix>0</ix>")) {
updateState(CHANNEL_INTENSITY, OnOffType.OFF);
} else {
BigDecimal bigTemp = new BigDecimal(WLedHelper.getValue(message, "<ix>", "<")).divide(BIG_DECIMAL_2_55,
RoundingMode.HALF_UP);
updateState(CHANNEL_INTENSITY, new PercentType(bigTemp));
}
if (message.contains("<cy>1</cy>")) {
updateState(CHANNEL_PRESET_CYCLE, OnOffType.ON);
} else {
updateState(CHANNEL_PRESET_CYCLE, OnOffType.OFF);
}
if (message.contains("<nl>1</nl>")) {
updateState(CHANNEL_SLEEP, OnOffType.ON);
} else {
updateState(CHANNEL_SLEEP, OnOffType.OFF);
}
if (message.contains("<ns>1</ns>")) {
updateState(CHANNEL_SYNC_SEND, OnOffType.ON);
} else {
updateState(CHANNEL_SYNC_SEND, OnOffType.OFF);
}
if (message.contains("<nr>1</nr>")) {
updateState(CHANNEL_SYNC_RECEIVE, OnOffType.ON);
} else {
updateState(CHANNEL_SYNC_RECEIVE, OnOffType.OFF);
}
if (message.contains("<fx>")) {
updateState(CHANNEL_FX, new StringType(WLedHelper.getValue(message, "<fx>", "<")));
}
if (message.contains("<sx>")) {
BigDecimal bigTemp = new BigDecimal(WLedHelper.getValue(message, "<sx>", "<")).divide(BIG_DECIMAL_2_55,
RoundingMode.HALF_UP);
updateState(CHANNEL_SPEED, new PercentType(bigTemp));
}
if (message.contains("<fp>")) {
updateState(CHANNEL_PALETTES, new StringType(WLedHelper.getValue(message, "<fp>", "<")));
}
parseColours(message);
}
private void sendWhite() {
if (hasWhite) {
sendGetRequest("/win&TT=1000&FX=0&CY=0&CL=hFF000000" + "&A=" + masterBrightness255);
} else {
sendGetRequest("/win&TT=1000&FX=0&CY=0&CL=hFFFFFF" + "&A=" + masterBrightness255);
}
}
/**
*
* @param hsb
* @return WLED needs the letter h followed by 2 digit HEX code for RRGGBB
*/
private String createColorHex(HSBType hsb) {
return String.format("h%06X", hsb.getRGB());
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
BigDecimal bigTemp;
PercentType localPercentType;
if (command instanceof RefreshType) {
switch (channelUID.getId()) {
case CHANNEL_MASTER_CONTROLS:
sendGetRequest("/win");
}
return;// no need to check for refresh below
}
logger.debug("command {} sent to {}", command, channelUID.getId());
switch (channelUID.getId()) {
case CHANNEL_SYNC_SEND:
if (OnOffType.OFF.equals(command)) {
sendGetRequest("/win&NS=0");
} else {
sendGetRequest("/win&NS=1");
}
break;
case CHANNEL_SYNC_RECEIVE:
if (OnOffType.OFF.equals(command)) {
sendGetRequest("/win&NR=0");
} else {
sendGetRequest("/win&NR=1");
}
break;
case CHANNEL_PRIMARY_WHITE:
if (command instanceof PercentType) {
sendGetRequest("/win&W=" + ((PercentType) command).toBigDecimal().multiply(BIG_DECIMAL_2_55));
}
break;
case CHANNEL_SECONDARY_WHITE:
if (command instanceof PercentType) {
sendGetRequest("/win&W2=" + ((PercentType) command).toBigDecimal().multiply(BIG_DECIMAL_2_55));
}
break;
case CHANNEL_MASTER_CONTROLS:
if (command instanceof OnOffType) {
if (OnOffType.OFF.equals(command)) {
sendGetRequest("/win&TT=250&T=0");
} else {
sendGetRequest("/win&TT=1000&T=1");
}
} else if (command instanceof IncreaseDecreaseType) {
if (IncreaseDecreaseType.INCREASE.equals(command)) {
if (masterBrightness255.intValue() < 240) {
sendGetRequest("/win&TT=1000&A=~15"); // 255 divided by 15 = 17 different levels
} else {
sendGetRequest("/win&TT=1000&A=255");
}
} else {
if (masterBrightness255.intValue() > 15) {
sendGetRequest("/win&TT=1000&A=~-15");
} else {
sendGetRequest("/win&TT=1000&A=0");
}
}
} else if (command instanceof HSBType) {
if ((((HSBType) command).getBrightness()) == PercentType.ZERO) {
sendGetRequest("/win&TT=500&T=0");
}
primaryColor = (HSBType) command;
masterBrightness255 = primaryColor.getBrightness().toBigDecimal().multiply(BIG_DECIMAL_2_55);
if (primaryColor.getSaturation().intValue() < config.saturationThreshold) {
sendWhite();
} else if (primaryColor.getSaturation().intValue() == 32 && primaryColor.getHue().intValue() == 36
&& hasWhite) {
// Google sends this when it wants white
sendWhite();
} else {
sendGetRequest("/win&TT=1000&FX=0&CY=0&CL=" + createColorHex(primaryColor) + "&A="
+ masterBrightness255);
}
} else if (command instanceof PercentType) {
masterBrightness255 = ((PercentType) command).toBigDecimal().multiply(BIG_DECIMAL_2_55);
sendGetRequest("/win&TT=1000&A=" + masterBrightness255);
}
return;
case CHANNEL_PRIMARY_COLOR:
if (command instanceof HSBType) {
primaryColor = (HSBType) command;
sendGetRequest("/win&CL=" + createColorHex(primaryColor));
} else if (command instanceof PercentType) {
primaryColor = new HSBType(primaryColor.getHue(), primaryColor.getSaturation(),
((PercentType) command));
sendGetRequest("/win&CL=" + createColorHex(primaryColor));
}
return;
case CHANNEL_SECONDARY_COLOR:
if (command instanceof HSBType) {
secondaryColor = (HSBType) command;
sendGetRequest("/win&C2=" + createColorHex(secondaryColor));
} else if (command instanceof PercentType) {
secondaryColor = new HSBType(secondaryColor.getHue(), secondaryColor.getSaturation(),
((PercentType) command));
sendGetRequest("/win&C2=" + createColorHex(secondaryColor));
}
return;
case CHANNEL_PALETTES:
sendGetRequest("/win&FP=" + command);
break;
case CHANNEL_FX:
sendGetRequest("/win&FX=" + command);
break;
case CHANNEL_SPEED:
localPercentType = ((State) command).as(PercentType.class);
if (localPercentType != null) {
bigTemp = localPercentType.toBigDecimal().multiply(BIG_DECIMAL_2_55);
sendGetRequest("/win&SX=" + bigTemp);
}
break;
case CHANNEL_INTENSITY:
localPercentType = ((State) command).as(PercentType.class);
if (localPercentType != null) {
bigTemp = localPercentType.toBigDecimal().multiply(BIG_DECIMAL_2_55);
sendGetRequest("/win&IX=" + bigTemp);
}
break;
case CHANNEL_SLEEP:
if (OnOffType.ON.equals(command)) {
sendGetRequest("/win&NL=1");
} else {
sendGetRequest("/win&NL=0");
}
break;
case CHANNEL_PRESETS:
sendGetRequest("/win&PL=" + command);
break;
case CHANNEL_PRESET_DURATION:
if (command instanceof QuantityType) {
QuantityType<?> seconds = ((QuantityType<?>) command).toUnit(SmartHomeUnits.SECOND);
if (seconds != null) {
bigTemp = new BigDecimal(seconds.intValue()).multiply(new BigDecimal(1000));
sendGetRequest("/win&PT=" + bigTemp.intValue());
}
}
break;
case CHANNEL_TRANS_TIME:
if (command instanceof QuantityType) {
QuantityType<?> seconds = ((QuantityType<?>) command).toUnit(SmartHomeUnits.SECOND);
if (seconds != null) {
bigTemp = new BigDecimal(seconds.intValue()).multiply(new BigDecimal(1000));
sendGetRequest("/win&TT=" + bigTemp.intValue());
}
}
break;
case CHANNEL_PRESET_CYCLE:
if (OnOffType.ON.equals(command)) {
sendGetRequest("/win&CY=1");
} else {
sendGetRequest("/win&CY=0");
}
break;
}
}
public void savePreset(int presetIndex) {
if (presetIndex > 16) {
logger.warn("Presets above 16 do not exist, and the action sent {}", presetIndex);
return;
}
sendGetRequest("/win&PS=" + presetIndex);
}
private void pollLED() {
sendGetRequest("/win");
}
@Override
public void initialize() {
config = getConfigAs(WLedConfiguration.class);
if (!config.address.contains("://")) {
logger.debug("Address was not entered in correct format, it may be the raw IP so adding http:// to start");
config.address = "http://" + config.address;
}
pollingFuture = scheduler.scheduleWithFixedDelay(this::pollLED, 1, config.pollTime, TimeUnit.SECONDS);
}
@Override
public void dispose() {
Future<?> future = pollingFuture;
if (future != null) {
future.cancel(true);
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(WLedActions.class);
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wled.internal;
import static org.openhab.binding.wled.internal.WLedBindingConstants.SUPPORTED_THING_TYPES;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link WLedHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.wled", service = ThingHandlerFactory.class)
public class WLedHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
private final WledDynamicStateDescriptionProvider stateDescriptionProvider;
@Activate
public WLedHandlerFactory(@Reference HttpClientFactory httpClientFactory,
final @Reference WledDynamicStateDescriptionProvider stateDescriptionProvider) {
this.httpClient = httpClientFactory.getCommonHttpClient();
this.stateDescriptionProvider = stateDescriptionProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
if (SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return true;
}
return false;
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new WLedHandler(thing, httpClient, stateDescriptionProvider);
}
return null;
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wled.internal;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link WLedHelper} Provides helper classes that are used from multiple classes in the binding.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class WLedHelper {
/**
* @return A string that starts after finding the element and terminates when it finds the first occurrence of the
* end string after the element.
*/
static String getValue(String message, String element, String end) {
int startIndex = message.indexOf(element);
if (startIndex != -1) // -1 means "not found"
{
int endIndex = message.indexOf(end, startIndex + element.length());
if (endIndex != -1) {
return message.substring(startIndex + element.length(), endIndex);
}
}
return "";
}
/**
* @return A List that holds the values from a heading/element that re-occurs in a message multiple times.
*
*/
static List<String> listOfResults(String message, String element, String end) {
List<String> results = new LinkedList<>();
String temp = "";
for (int startLookingFromIndex = 0; startLookingFromIndex != -1;) {
startLookingFromIndex = message.indexOf(element, startLookingFromIndex);
if (startLookingFromIndex >= 0) {
temp = getValue(message.substring(startLookingFromIndex), element, end);
if (!temp.isEmpty()) {
results.add(temp);
} else {
return results;// end string must not exist so stop looking.
}
startLookingFromIndex += temp.length();
}
}
return results;
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.wled.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link WledDynamicStateDescriptionProvider} Allows the dynamic updating of the FX and Palletes that WLED keep
* changing between firmware versions.
*
* @author Matthew Skinner - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, WledDynamicStateDescriptionProvider.class })
@NonNullByDefault
public class WledDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
@Activate
public WledDynamicStateDescriptionProvider(
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="wled" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>WLED Binding</name>
<description>This is the binding for WLED</description>
<author>Matthew Skinner</author>
</binding:binding>

View File

@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="wled"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="wled">
<label>WLED String</label>
<description>A WLED string of LEDs</description>
<category>ColorLight</category>
<channels>
<channel id="masterControls" typeId="masterControls"/>
<channel id="primaryColor" typeId="primaryColor"/>
<channel id="primaryWhite" typeId="primaryWhite"/>
<channel id="secondaryColor" typeId="secondaryColor"/>
<channel id="secondaryWhite" typeId="secondaryWhite"/>
<channel id="presets" typeId="presets"/>
<channel id="presetDuration" typeId="presetDuration"/>
<channel id="transformTime" typeId="transformTime"/>
<channel id="presetCycle" typeId="presetCycle"/>
<channel id="palettes" typeId="palettes"/>
<channel id="fx" typeId="fx"/>
<channel id="speed" typeId="speed"/>
<channel id="intensity" typeId="intensity"/>
<channel id="sleep" typeId="sleep"/>
<channel id="syncSend" typeId="syncSend"/>
<channel id="syncReceive" typeId="syncReceive"/>
</channels>
<config-description>
<parameter name="address" type="text" required="true">
<label>Address</label>
<description>Use this format http://192.168.1.2:80</description>
</parameter>
<parameter name="pollTime" type="integer" required="true" min="1" unit="s">
<label>Poll States</label>
<description>Time in seconds of how often to fetch the state of the LEDs.</description>
<default>10</default>
</parameter>
<parameter name="segmentIndex" type="integer" required="true" min="-1">
<label>Segment Index</label>
<description>Leave this as -1 if you are not using segments, otherwise set this to the segment index number that you
wish to control.</description>
<default>-1</default>
</parameter>
<parameter name="saturationThreshold" type="integer" required="true" min="0" max="99">
<label>Saturation Threshold</label>
<description>This feature allows you to specify a number that if the saturation drops below, will trigger white.
</description>
<default>0</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="masterControls">
<item-type>Color</item-type>
<label>Master Controls</label>
<description>Allows you to exit FX mode and use the LEDS like a normal light</description>
<category>ColorLight</category>
<tags>
<tag>Lighting</tag>
</tags>
</channel-type>
<channel-type id="primaryColor" advanced="true">
<item-type>Color</item-type>
<label>Primary Color</label>
<description>Allows you to change the primary color used in FX</description>
<category>ColorLight</category>
</channel-type>
<channel-type id="primaryWhite" advanced="true">
<item-type>Dimmer</item-type>
<label>Primary White</label>
<description>Changes the brightness of the primary white LED</description>
<category>DimmableLight</category>
</channel-type>
<channel-type id="secondaryColor" advanced="true">
<item-type>Color</item-type>
<label>Secondary Color</label>
<description>Allows you to change the secondary color used in FX</description>
<category>ColorLight</category>
</channel-type>
<channel-type id="secondaryWhite" advanced="true">
<item-type>Dimmer</item-type>
<label>Secondary White</label>
<description>Changes the brightness of the secondary white LED</description>
<category>DimmableLight</category>
</channel-type>
<channel-type id="palettes">
<item-type>String</item-type>
<label>Palettes</label>
<description>Change the colours used by the FX</description>
</channel-type>
<channel-type id="fx">
<item-type>String</item-type>
<label>Effect</label>
<description>Use the built in FX</description>
</channel-type>
<channel-type id="presets">
<item-type>String</item-type>
<label>Presets</label>
<description>Auto rotate or change to a saved preset</description>
<state>
<options>
<option value="1">Preset 1</option>
<option value="2">Preset 2</option>
<option value="3">Preset 3</option>
<option value="4">Preset 4</option>
<option value="5">Preset 5</option>
<option value="6">Preset 6</option>
<option value="7">Preset 7</option>
<option value="8">Preset 8</option>
<option value="9">Preset 9</option>
<option value="10">Preset 10</option>
<option value="11">Preset 11</option>
<option value="12">Preset 12</option>
<option value="13">Preset 13</option>
<option value="14">Preset 14</option>
<option value="15">Preset 15</option>
<option value="16">Preset 16</option>
</options>
</state>
</channel-type>
<channel-type id="presetDuration" advanced="true">
<item-type>Number:Time</item-type>
<label>Preset Duration</label>
<description>Time for how long to show each preset for before moving to the next</description>
<category>Time</category>
<state min="0.1" max="65" step="0.1" pattern="%.1f %unit%" readOnly="false"/>
</channel-type>
<channel-type id="transformTime" advanced="true">
<item-type>Number:Time</item-type>
<label>Transform Time</label>
<description>Time it takes to change/fade from one look to the next.</description>
<category>Time</category>
<state min="0" max="65" step="0.1" pattern="%.1f %unit%" readOnly="false"/>
</channel-type>
<channel-type id="speed" advanced="true">
<item-type>Dimmer</item-type>
<label>FX Speed</label>
<description>Change the speed of the FX</description>
</channel-type>
<channel-type id="intensity" advanced="true">
<item-type>Dimmer</item-type>
<label>FX Intensity</label>
<description>Change the intensity of the FX</description>
</channel-type>
<channel-type id="sleep" advanced="true">
<item-type>Switch</item-type>
<label>Sleep Timer</label>
<description>Fade the level of light and turn off after set time</description>
<category>Time</category>
</channel-type>
<channel-type id="presetCycle">
<item-type>Switch</item-type>
<label>Preset Cycle</label>
<description>Cycle through the saved presets</description>
</channel-type>
<channel-type id="syncSend" advanced="true">
<item-type>Switch</item-type>
<label>Sync Send</label>
<description>Sends UDP packets that tell other WLED lights to follow this one.</description>
</channel-type>
<channel-type id="syncReceive" advanced="true">
<item-type>Switch</item-type>
<label>Sync Receive</label>
<description>Allows UDP packets from other WLED lights to control this one.</description>
</channel-type>
</thing:thing-descriptions>