[nanoleaf] Refactored code to use core features and more (#10101)
This is a bigger refactoring bringing these (breaking) changes: - System channel types are used where applicable - Obsolete channels (such as power) were removed - Some channel types were marked "advanced" - "Tap" channels were converted to a trigger channel type providing a "system button" behavior - Layout can now be requested by a console command - Command options for effect channel are dynamically provided - Log level has been reduced where appropriate - HTTP request timeouts were reduced - handleRemoval now returns quickly as expected - Fixed hanging thread / infinite loop when requesting layouts for non-square panels - Various other smaller enhancements and fixes - Documentation has been adapted accordingly Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -27,7 +27,7 @@ import org.openhab.core.thing.ThingTypeUID;
|
||||
@NonNullByDefault
|
||||
public class NanoleafBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "nanoleaf";
|
||||
public static final String BINDING_ID = "nanoleaf";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller");
|
||||
@@ -44,7 +44,6 @@ public class NanoleafBindingConstants {
|
||||
public static final String CONFIG_PANEL_ID = "id";
|
||||
|
||||
// List of controller channels
|
||||
public static final String CHANNEL_POWER = "power";
|
||||
public static final String CHANNEL_COLOR = "color";
|
||||
public static final String CHANNEL_COLOR_TEMPERATURE = "colorTemperature";
|
||||
public static final String CHANNEL_COLOR_TEMPERATURE_ABS = "colorTemperatureAbs";
|
||||
@@ -53,12 +52,10 @@ public class NanoleafBindingConstants {
|
||||
public static final String CHANNEL_RHYTHM_STATE = "rhythmState";
|
||||
public static final String CHANNEL_RHYTHM_ACTIVE = "rhythmActive";
|
||||
public static final String CHANNEL_RHYTHM_MODE = "rhythmMode";
|
||||
public static final String CHANNEL_PANEL_LAYOUT = "panelLayout";
|
||||
|
||||
// List of light panel channels
|
||||
public static final String CHANNEL_PANEL_COLOR = "panelColor";
|
||||
public static final String CHANNEL_PANEL_SINGLE_TAP = "singleTap";
|
||||
public static final String CHANNEL_PANEL_DOUBLE_TAP = "doubleTap";
|
||||
public static final String CHANNEL_PANEL_COLOR = "color";
|
||||
public static final String CHANNEL_PANEL_TAP = "tap";
|
||||
|
||||
// Nanoleaf OpenAPI URLs
|
||||
public static final String API_V1_BASE_URL = "/api/v1";
|
||||
|
||||
@@ -15,9 +15,6 @@ package org.openhab.binding.nanoleaf.internal;
|
||||
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@@ -25,19 +22,15 @@ import java.util.stream.Stream;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService;
|
||||
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
|
||||
import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
@@ -49,6 +42,7 @@ import org.slf4j.LoggerFactory;
|
||||
* and panel (thing) handlers.
|
||||
*
|
||||
* @author Martin Raepple - Initial contribution
|
||||
* @author Kai Kreuzer - made discovery a handler service
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.nanoleaf", service = ThingHandlerFactory.class)
|
||||
@@ -58,7 +52,6 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
|
||||
.unmodifiableSet(Stream.of(THING_TYPE_LIGHT_PANEL, THING_TYPE_CONTROLLER).collect(Collectors.toSet()));
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NanoleafHandlerFactory.class);
|
||||
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
|
||||
private final HttpClient httpClient;
|
||||
|
||||
@Activate
|
||||
@@ -77,7 +70,6 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
|
||||
NanoleafControllerHandler handler = new NanoleafControllerHandler((Bridge) thing, httpClient);
|
||||
registerDiscoveryService(handler);
|
||||
logger.debug("Nanoleaf controller handler created.");
|
||||
return handler;
|
||||
} else if (THING_TYPE_LIGHT_PANEL.equals(thingTypeUID)) {
|
||||
@@ -87,30 +79,4 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeHandler(ThingHandler thingHandler) {
|
||||
if (thingHandler instanceof NanoleafControllerHandler) {
|
||||
unregisterDiscoveryService(thingHandler.getThing());
|
||||
logger.debug("Nanoleaf controller handler removed.");
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void registerDiscoveryService(NanoleafControllerHandler bridgeHandler) {
|
||||
NanoleafPanelsDiscoveryService discoveryService = new NanoleafPanelsDiscoveryService(bridgeHandler);
|
||||
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(),
|
||||
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
|
||||
logger.debug("Discovery service for panels registered.");
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
private synchronized void unregisterDiscoveryService(Thing thing) {
|
||||
@Nullable
|
||||
ServiceRegistration<?> serviceReg = discoveryServiceRegs.remove(thing.getUID());
|
||||
// would require null check but "if (response!=null)" throws warning on comoile time :´-(
|
||||
if (serviceReg != null) {
|
||||
serviceReg.unregister();
|
||||
}
|
||||
logger.debug("Discovery service for panels removed.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -57,7 +58,7 @@ public class OpenAPIUtils {
|
||||
LOGGER.trace("RequestBuilder: Sending Request {}:{} {} ", requestURI.getHost(), requestURI.getPort(),
|
||||
requestURI.getPath());
|
||||
|
||||
return httpClient.newRequest(requestURI).method(method);
|
||||
return httpClient.newRequest(requestURI).method(method).timeout(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public static URI getUri(NanoleafControllerConfig controllerConfig, String apiOperation, @Nullable String query)
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.nanoleaf.internal.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
|
||||
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
|
||||
import org.openhab.core.io.console.Console;
|
||||
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
|
||||
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingRegistry;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* Console commands for interacting with Nanoleaf integration
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ConsoleCommandExtension.class)
|
||||
public class NanoleafCommandExtension extends AbstractConsoleCommandExtension {
|
||||
|
||||
private static final String CMD_LAYOUT = "layout";
|
||||
private final ThingRegistry thingRegistry;
|
||||
|
||||
@Activate
|
||||
public NanoleafCommandExtension(@Reference ThingRegistry thingRegistry) {
|
||||
super("nanoleaf", "Interact with the Nanoleaf integration.");
|
||||
this.thingRegistry = thingRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(String[] args, Console console) {
|
||||
if (args.length > 0) {
|
||||
String subCommand = args[0];
|
||||
switch (subCommand) {
|
||||
case CMD_LAYOUT:
|
||||
if (args.length == 1) {
|
||||
thingRegistry.getAll().forEach(thing -> {
|
||||
if (thing.getUID().getBindingId().equals(NanoleafBindingConstants.BINDING_ID)) {
|
||||
ThingHandler handler = thing.getHandler();
|
||||
if (handler instanceof NanoleafControllerHandler) {
|
||||
NanoleafControllerHandler nanoleafControllerHandler = (NanoleafControllerHandler) handler;
|
||||
String layout = nanoleafControllerHandler.getLayout();
|
||||
console.println("Layout of Nanoleaf controller '" + thing.getUID().getAsString()
|
||||
+ "' with label '" + thing.getLabel() + "':" + System.lineSeparator());
|
||||
console.println(layout);
|
||||
console.println(System.lineSeparator());
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (args.length == 2) {
|
||||
String uid = args[1];
|
||||
Thing thing = thingRegistry.get(new ThingUID(uid));
|
||||
if (thing != null) {
|
||||
ThingHandler handler = thing.getHandler();
|
||||
if (handler instanceof NanoleafControllerHandler) {
|
||||
NanoleafControllerHandler nanoleafControllerHandler = (NanoleafControllerHandler) handler;
|
||||
String layout = nanoleafControllerHandler.getLayout();
|
||||
console.println(layout);
|
||||
} else {
|
||||
console.println("Thing with UID '" + uid.toString()
|
||||
+ "' is not an initialized Nanoleaf controller.");
|
||||
}
|
||||
} else {
|
||||
console.println("Thing with UID '" + uid.toString() + "' does not exist.");
|
||||
}
|
||||
} else {
|
||||
printUsage(console);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.println("Unknown command '" + subCommand + "'");
|
||||
printUsage(console);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getUsages() {
|
||||
return Arrays.asList(buildCommandUsage(CMD_LAYOUT + " <thingUID>", "Prints the panel layout on the console."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.nanoleaf.internal.commanddescription;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
|
||||
import org.openhab.binding.nanoleaf.internal.handler.NanoleafControllerHandler;
|
||||
import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
|
||||
import org.openhab.core.types.CommandOption;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* This class provides the available effects as dynamic options as they are read from the Nanoleaf controller.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { DynamicCommandDescriptionProvider.class })
|
||||
public class NanoleafCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider
|
||||
implements NanoleafControllerListener, ThingHandlerService {
|
||||
|
||||
private @Nullable ChannelUID effectChannelUID;
|
||||
|
||||
private @Nullable NanoleafControllerHandler bridgeHandler;
|
||||
|
||||
@Override
|
||||
public void setThingHandler(ThingHandler handler) {
|
||||
this.bridgeHandler = (NanoleafControllerHandler) handler;
|
||||
bridgeHandler.registerControllerListener(this);
|
||||
effectChannelUID = new ChannelUID(handler.getThing().getUID(), NanoleafBindingConstants.CHANNEL_EFFECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return bridgeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
if (bridgeHandler != null) {
|
||||
bridgeHandler.unregisterControllerListener(this);
|
||||
}
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onControllerInfoFetched(@NonNull ThingUID bridge, @NonNull ControllerInfo controllerInfo) {
|
||||
List<@NonNull String> effects = controllerInfo.getEffects().getEffectsList();
|
||||
ChannelUID uid = effectChannelUID;
|
||||
if (effects != null && uid != null && uid.getThingUID().equals(bridge)) {
|
||||
List<@NonNull CommandOption> commandOptions = effects.stream() //
|
||||
.map(effect -> new CommandOption(effect, effect)) //
|
||||
.collect(Collectors.toList());
|
||||
setCommandOptions(uid, commandOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,10 +79,10 @@ public class NanoleafMDNSDiscoveryParticipant implements MDNSDiscoveryParticipan
|
||||
|
||||
logger.trace("Discovered nanoleaf host: {} port: {} firmWare: {} modelId: {} qualifiedName: {}", host, port,
|
||||
firmwareVersion, modelId, qualifiedName);
|
||||
logger.debug("Adding Nanoleaf controller {} with FW version {} found at {} {} to inbox", qualifiedName,
|
||||
logger.debug("Adding Nanoleaf controller {} with FW version {} found at {}:{} to inbox", qualifiedName,
|
||||
firmwareVersion, host, port);
|
||||
if (!OpenAPIUtils.checkRequiredFirmware(service.getPropertyString("md"), firmwareVersion)) {
|
||||
logger.warn("Nanoleaf controller firmware is too old. Must be {} or higher",
|
||||
logger.debug("Nanoleaf controller firmware is too old. Must be {} or higher",
|
||||
MODEL_ID_LIGHTPANELS.equals(modelId) ? API_MIN_FW_VER_LIGHTPANELS : API_MIN_FW_VER_CANVAS);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -41,36 +43,37 @@ import org.slf4j.LoggerFactory;
|
||||
* panels connected to the controller.
|
||||
*
|
||||
* @author Martin Raepple - Initial contribution
|
||||
* @author Kai Kreuzer - Made it a ThingHandlerService
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService implements NanoleafControllerListener {
|
||||
public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService
|
||||
implements NanoleafControllerListener, ThingHandlerService {
|
||||
|
||||
private static final int SEARCH_TIMEOUT_SECONDS = 60;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelsDiscoveryService.class);
|
||||
private final NanoleafControllerHandler bridgeHandler;
|
||||
private @Nullable NanoleafControllerHandler bridgeHandler;
|
||||
private @Nullable ControllerInfo controllerInfo;
|
||||
|
||||
/**
|
||||
* Constructs a new {@link NanoleafPanelsDiscoveryService} attached to the given bridge handler.
|
||||
*
|
||||
* @param nanoleafControllerHandler The bridge handler this discovery service is attached to
|
||||
* Constructs a new {@link NanoleafPanelsDiscoveryService}.
|
||||
*/
|
||||
public NanoleafPanelsDiscoveryService(NanoleafControllerHandler nanoleafControllerHandler) {
|
||||
public NanoleafPanelsDiscoveryService() {
|
||||
super(NanoleafHandlerFactory.SUPPORTED_THING_TYPES_UIDS, SEARCH_TIMEOUT_SECONDS, false);
|
||||
this.bridgeHandler = nanoleafControllerHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
if (bridgeHandler != null) {
|
||||
bridgeHandler.unregisterControllerListener(this);
|
||||
}
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting Nanoleaf panel discovery");
|
||||
bridgeHandler.registerControllerListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
logger.debug("Stopping Nanoleaf panel discovery");
|
||||
super.stopScan();
|
||||
bridgeHandler.unregisterControllerListener(this);
|
||||
createResultsFromControllerInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,38 +84,60 @@ public class NanoleafPanelsDiscoveryService extends AbstractDiscoveryService imp
|
||||
*/
|
||||
@Override
|
||||
public void onControllerInfoFetched(ThingUID bridge, ControllerInfo controllerInfo) {
|
||||
logger.debug("Discover panels connected to controller with id {}", bridge.getAsString());
|
||||
final PanelLayout panelLayout = controllerInfo.getPanelLayout();
|
||||
@Nullable
|
||||
Layout layout = panelLayout.getLayout();
|
||||
|
||||
if (layout != null && layout.getNumPanels() > 0) {
|
||||
@Nullable
|
||||
final List<PositionDatum> positionData = layout.getPositionData();
|
||||
if (positionData != null) {
|
||||
Iterator<PositionDatum> iterator = positionData.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
@Nullable
|
||||
PositionDatum panel = iterator.next();
|
||||
ThingUID newPanelThingUID = new ThingUID(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL, bridge,
|
||||
Integer.toString(panel.getPanelId()));
|
||||
|
||||
final Map<String, Object> properties = new HashMap<>(1);
|
||||
properties.put(CONFIG_PANEL_ID, panel.getPanelId());
|
||||
|
||||
DiscoveryResult newPanel = DiscoveryResultBuilder.create(newPanelThingUID).withBridge(bridge)
|
||||
.withProperties(properties).withLabel("Light Panel " + panel.getPanelId())
|
||||
.withRepresentationProperty(CONFIG_PANEL_ID).build();
|
||||
|
||||
logger.debug("Adding panel with id {} to inbox", panel.getPanelId());
|
||||
thingDiscovered(newPanel);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Couldn't add panels to inbox as layout position data was null");
|
||||
}
|
||||
this.controllerInfo = controllerInfo;
|
||||
}
|
||||
|
||||
private void createResultsFromControllerInfo() {
|
||||
ThingUID bridgeUID;
|
||||
if (bridgeHandler != null) {
|
||||
bridgeUID = bridgeHandler.getThing().getUID();
|
||||
} else {
|
||||
logger.info("No panels found or connected to controller");
|
||||
return;
|
||||
}
|
||||
if (controllerInfo != null) {
|
||||
final PanelLayout panelLayout = controllerInfo.getPanelLayout();
|
||||
@Nullable
|
||||
Layout layout = panelLayout.getLayout();
|
||||
|
||||
if (layout != null && layout.getNumPanels() > 0) {
|
||||
@Nullable
|
||||
final List<PositionDatum> positionData = layout.getPositionData();
|
||||
if (positionData != null) {
|
||||
Iterator<PositionDatum> iterator = positionData.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
@Nullable
|
||||
PositionDatum panel = iterator.next();
|
||||
ThingUID newPanelThingUID = new ThingUID(NanoleafBindingConstants.THING_TYPE_LIGHT_PANEL,
|
||||
bridgeUID, Integer.toString(panel.getPanelId()));
|
||||
|
||||
final Map<String, Object> properties = new HashMap<>(1);
|
||||
properties.put(CONFIG_PANEL_ID, panel.getPanelId());
|
||||
|
||||
DiscoveryResult newPanel = DiscoveryResultBuilder.create(newPanelThingUID).withBridge(bridgeUID)
|
||||
.withProperties(properties).withLabel("Light Panel " + panel.getPanelId())
|
||||
.withRepresentationProperty(CONFIG_PANEL_ID).build();
|
||||
|
||||
logger.debug("Adding panel with id {} to inbox", panel.getPanelId());
|
||||
thingDiscovered(newPanel);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Couldn't add panels to inbox as layout position data was null");
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.debug("No panels found or connected to controller");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(ThingHandler handler) {
|
||||
this.bridgeHandler = (NanoleafControllerHandler) handler;
|
||||
this.bridgeHandler.registerControllerListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return bridgeHandler;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
@@ -36,10 +40,11 @@ import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafException;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafInterruptedException;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
|
||||
import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
|
||||
import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider;
|
||||
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
|
||||
import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService;
|
||||
import org.openhab.binding.nanoleaf.internal.model.AuthToken;
|
||||
import org.openhab.binding.nanoleaf.internal.model.BooleanState;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Brightness;
|
||||
@@ -67,6 +72,7 @@ import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
@@ -81,15 +87,13 @@ import com.google.gson.JsonSyntaxException;
|
||||
*
|
||||
* @author Martin Raepple - Initial contribution
|
||||
* @author Stefan Höhn - Canvas Touch Support
|
||||
* @author Kai Kreuzer - refactoring, bug fixing and code clean up
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
|
||||
// Pairing interval in seconds
|
||||
private static final int PAIRING_INTERVAL = 25;
|
||||
|
||||
// Panel discovery interval in seconds
|
||||
private static final int PANEL_DISCOVERY_INTERVAL = 30;
|
||||
private static final int PAIRING_INTERVAL = 10;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
|
||||
private HttpClient httpClient;
|
||||
@@ -98,7 +102,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
// Pairing, update and panel discovery jobs and touch event job
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> panelDiscoveryJob;
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
|
||||
|
||||
// JSON parser for API responses
|
||||
@@ -120,7 +123,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing the controller (bridge)");
|
||||
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED);
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
|
||||
setAddress(config.address);
|
||||
setPort(config.port);
|
||||
@@ -157,13 +160,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
"@text/error.nanoleaf.controller.noToken");
|
||||
startPairingJob();
|
||||
stopUpdateJob();
|
||||
stopPanelDiscoveryJob();
|
||||
} else {
|
||||
logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs");
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
stopPairingJob();
|
||||
startUpdateJob();
|
||||
startPanelDiscoveryJob();
|
||||
startTouchJob();
|
||||
}
|
||||
} catch (IllegalArgumentException iae) {
|
||||
@@ -186,11 +185,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
updateFromControllerInfo();
|
||||
} else {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_POWER:
|
||||
case CHANNEL_COLOR:
|
||||
case CHANNEL_COLOR_TEMPERATURE:
|
||||
case CHANNEL_COLOR_TEMPERATURE_ABS:
|
||||
case CHANNEL_PANEL_LAYOUT:
|
||||
sendStateCommand(channelUID.getId(), command);
|
||||
break;
|
||||
case CHANNEL_EFFECT:
|
||||
@@ -218,25 +215,28 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
|
||||
@Override
|
||||
public void handleRemoval() {
|
||||
// delete token for openHAB
|
||||
ContentResponse deleteTokenResponse;
|
||||
try {
|
||||
Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_DELETE_USER,
|
||||
HttpMethod.DELETE);
|
||||
deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
|
||||
if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
|
||||
logger.warn("Failed to delete token for openHAB. Response code is {}", deleteTokenResponse.getStatus());
|
||||
return;
|
||||
scheduler.execute(() -> {
|
||||
// delete token for openHAB
|
||||
ContentResponse deleteTokenResponse;
|
||||
try {
|
||||
Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
|
||||
API_DELETE_USER, HttpMethod.DELETE);
|
||||
deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
|
||||
if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
|
||||
logger.warn("Failed to delete token for openHAB. Response code is {}",
|
||||
deleteTokenResponse.getStatus());
|
||||
return;
|
||||
}
|
||||
logger.debug("Successfully deleted token for openHAB from controller");
|
||||
} catch (NanoleafUnauthorizedException e) {
|
||||
logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
|
||||
} catch (NanoleafException ne) {
|
||||
logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
|
||||
}
|
||||
logger.debug("Successfully deleted token for openHAB from controller");
|
||||
} catch (NanoleafUnauthorizedException e) {
|
||||
logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
|
||||
} catch (NanoleafException ne) {
|
||||
logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
|
||||
}
|
||||
stopAllJobs();
|
||||
super.handleRemoval();
|
||||
logger.debug("Nanoleaf controller removed");
|
||||
stopAllJobs();
|
||||
super.handleRemoval();
|
||||
logger.debug("Nanoleaf controller removed");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -246,34 +246,37 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return List.of(NanoleafPanelsDiscoveryService.class, NanoleafCommandDescriptionProvider.class);
|
||||
}
|
||||
|
||||
public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
|
||||
logger.debug("Register new listener for controller {}", getThing().getUID());
|
||||
boolean result = controllerListeners.add(controllerListener);
|
||||
if (result) {
|
||||
startPanelDiscoveryJob();
|
||||
}
|
||||
return result;
|
||||
return controllerListeners.add(controllerListener);
|
||||
}
|
||||
|
||||
public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
|
||||
logger.debug("Unregister listener for controller {}", getThing().getUID());
|
||||
boolean result = controllerListeners.remove(controllerListener);
|
||||
if (result) {
|
||||
stopPanelDiscoveryJob();
|
||||
}
|
||||
return result;
|
||||
return controllerListeners.remove(controllerListener);
|
||||
}
|
||||
|
||||
public NanoleafControllerConfig getControllerConfig() {
|
||||
NanoleafControllerConfig config = new NanoleafControllerConfig();
|
||||
config.address = Objects.requireNonNullElse(getAddress(), "");
|
||||
config.port = getPort();
|
||||
config.refreshInterval = getRefreshIntervall();
|
||||
config.refreshInterval = getRefreshInterval();
|
||||
config.authToken = getAuthToken();
|
||||
config.deviceType = Objects.requireNonNullElse(getDeviceType(), "");
|
||||
return config;
|
||||
}
|
||||
|
||||
public String getLayout() {
|
||||
Layout layout = controllerInfo.getPanelLayout().getLayout();
|
||||
String layoutView = (layout != null) ? layout.getLayoutView() : "";
|
||||
return layoutView;
|
||||
}
|
||||
|
||||
public synchronized void startPairingJob() {
|
||||
if (pairingJob == null || pairingJob.isCancelled()) {
|
||||
logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
|
||||
@@ -293,8 +296,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
String localAuthToken = getAuthToken();
|
||||
if (localAuthToken != null && !localAuthToken.isEmpty()) {
|
||||
if (updateJob == null || updateJob.isCancelled()) {
|
||||
logger.debug("Start controller status job, repeat every {} sec", getRefreshIntervall());
|
||||
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshIntervall(),
|
||||
logger.debug("Start controller status job, repeat every {} sec", getRefreshInterval());
|
||||
updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshInterval(),
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
} else {
|
||||
@@ -311,24 +314,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void startPanelDiscoveryJob() {
|
||||
logger.debug("Starting panel discovery job. Has Controller-Listeners: {} panelDiscoveryJob: {}",
|
||||
!controllerListeners.isEmpty(), panelDiscoveryJob);
|
||||
if (!controllerListeners.isEmpty() && (panelDiscoveryJob == null || panelDiscoveryJob.isCancelled())) {
|
||||
logger.debug("Start panel discovery job, interval={} sec", PANEL_DISCOVERY_INTERVAL);
|
||||
panelDiscoveryJob = scheduler.scheduleWithFixedDelay(this::runPanelDiscovery, 0, PANEL_DISCOVERY_INTERVAL,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void stopPanelDiscoveryJob() {
|
||||
if (controllerListeners.isEmpty() && panelDiscoveryJob != null && !panelDiscoveryJob.isCancelled()) {
|
||||
logger.debug("Stop panel discovery job");
|
||||
panelDiscoveryJob.cancel(true);
|
||||
this.panelDiscoveryJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void startTouchJob() {
|
||||
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
|
||||
if (!config.deviceType.equals(DEVICE_TYPE_TOUCHSUPPORT)) {
|
||||
@@ -367,11 +352,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
try {
|
||||
updateFromControllerInfo();
|
||||
startTouchJob(); // if device type has changed, start touch detection.
|
||||
// controller might have been offline, e.g. for firmware update. In this case, return to online state
|
||||
if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
|
||||
logger.debug("Controller {} is back online", thing.getUID());
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (NanoleafUnauthorizedException nae) {
|
||||
logger.warn("Status update unauthorized: {}", nae.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
@@ -403,7 +384,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
return;
|
||||
}
|
||||
ContentResponse authTokenResponse = OpenAPIUtils
|
||||
.requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST).send();
|
||||
.requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST)
|
||||
.timeout(20, TimeUnit.SECONDS).send();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
|
||||
}
|
||||
@@ -429,7 +411,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
|
||||
stopPairingJob();
|
||||
startUpdateJob();
|
||||
startPanelDiscoveryJob();
|
||||
startTouchJob();
|
||||
} else {
|
||||
logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
|
||||
@@ -446,7 +427,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.noTokenReceived");
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
logger.warn("Cannot send authorization request to controller: ", e);
|
||||
logger.debug("Cannot send authorization request to controller: ", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.authRequest");
|
||||
} catch (RuntimeException e) {
|
||||
@@ -459,34 +440,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private void runPanelDiscovery() {
|
||||
logger.debug("Run panel discovery job");
|
||||
// Trigger a new discovery of connected panels
|
||||
for (NanoleafControllerListener controllerListener : controllerListeners) {
|
||||
try {
|
||||
controllerListener.onControllerInfoFetched(getThing().getUID(), receiveControllerInfo());
|
||||
} catch (NanoleafUnauthorizedException nue) {
|
||||
logger.warn("Panel discovery unauthorized: {}", nue.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.invalidToken");
|
||||
String localAuthToken = getAuthToken();
|
||||
if (localAuthToken == null || localAuthToken.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
|
||||
"@text/error.nanoleaf.controller.noToken");
|
||||
}
|
||||
} catch (NanoleafInterruptedException nie) {
|
||||
logger.info("Panel discovery has been stopped.");
|
||||
} catch (NanoleafException ne) {
|
||||
logger.warn("Failed to discover panels: ", ne);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.controller.communication");
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("Panel discovery job failed", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq
|
||||
*/
|
||||
@@ -579,14 +532,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
private void updateFromControllerInfo() throws NanoleafException {
|
||||
logger.debug("Update channels for controller {}", thing.getUID());
|
||||
this.controllerInfo = receiveControllerInfo();
|
||||
if (controllerInfo == null) {
|
||||
logger.debug("No Controller Info has been provided");
|
||||
return;
|
||||
}
|
||||
final State state = controllerInfo.getState();
|
||||
|
||||
OnOffType powerState = state.getOnOff();
|
||||
updateState(CHANNEL_POWER, powerState);
|
||||
|
||||
@Nullable
|
||||
Ct colorTemperature = state.getColorTemperature();
|
||||
@@ -662,6 +610,10 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
panelHandler.updatePanelColorChannel();
|
||||
}
|
||||
});
|
||||
|
||||
for (NanoleafControllerListener controllerListener : controllerListeners) {
|
||||
controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
|
||||
@@ -674,17 +626,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
private void sendStateCommand(String channel, Command command) throws NanoleafException {
|
||||
State stateObject = new State();
|
||||
switch (channel) {
|
||||
case CHANNEL_POWER:
|
||||
if (command instanceof OnOffType) {
|
||||
// On/Off command - turns controller on/off
|
||||
BooleanState state = new On();
|
||||
state.setValue(OnOffType.ON.equals(command));
|
||||
stateObject.setState(state);
|
||||
} else {
|
||||
logger.warn("Unhandled command type: {}", command.getClass().getName());
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_COLOR:
|
||||
if (command instanceof OnOffType) {
|
||||
// On/Off command - turns controller on/off
|
||||
@@ -780,13 +721,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_PANEL_LAYOUT:
|
||||
@Nullable
|
||||
Layout layout = controllerInfo.getPanelLayout().getLayout();
|
||||
String layoutView = (layout != null) ? layout.getLayoutView() : "";
|
||||
logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView);
|
||||
updateState(CHANNEL_PANEL_LAYOUT, OnOffType.OFF);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Unhandled command type: {}", command.getClass().getName());
|
||||
return;
|
||||
@@ -844,7 +778,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
private int getRefreshIntervall() {
|
||||
private int getRefreshInterval() {
|
||||
return refreshIntervall;
|
||||
}
|
||||
|
||||
@@ -871,7 +805,6 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
private void stopAllJobs() {
|
||||
stopPairingJob();
|
||||
stopUpdateJob();
|
||||
stopPanelDiscoveryJob();
|
||||
stopTouchJob();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ package org.openhab.binding.nanoleaf.internal.handler;
|
||||
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@@ -28,13 +28,21 @@ import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.binding.nanoleaf.internal.*;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafBadRequestException;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafException;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafNotFoundException;
|
||||
import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
|
||||
import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
|
||||
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Effects;
|
||||
import org.openhab.binding.nanoleaf.internal.model.Write;
|
||||
import org.openhab.core.library.types.*;
|
||||
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.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.CommonTriggerEvents;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
@@ -165,8 +173,9 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
private void sendRenderedEffectCommand(Command command) throws NanoleafException {
|
||||
logger.debug("Command Type: {}", command.getClass());
|
||||
HSBType currentPanelColor = getPanelColor();
|
||||
if (currentPanelColor != null)
|
||||
if (currentPanelColor != null) {
|
||||
logger.debug("currentPanelColor: {}", currentPanelColor.toString());
|
||||
}
|
||||
HSBType newPanelColor = new HSBType();
|
||||
|
||||
if (command instanceof HSBType) {
|
||||
@@ -205,11 +214,11 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
// transform to RGB
|
||||
PercentType[] rgbPercent = newPanelColor.toRGB();
|
||||
logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]);
|
||||
int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
|
||||
int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal(255)).intValue();
|
||||
int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
|
||||
int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal(255)).intValue();
|
||||
int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
|
||||
int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal(255)).intValue();
|
||||
logger.trace("Setting new rgb {} {} {}", red, green, blue);
|
||||
Bridge bridge = getBridge();
|
||||
@@ -253,8 +262,9 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
@Nullable
|
||||
HSBType panelColor = getPanelColor();
|
||||
logger.trace("updatePanelColorChannel: panelColor: {}", panelColor);
|
||||
if (panelColor != null)
|
||||
if (panelColor != null) {
|
||||
updateState(CHANNEL_PANEL_COLOR, panelColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,28 +275,14 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
public void updatePanelGesture(int gesture) {
|
||||
switch (gesture) {
|
||||
case 0:
|
||||
updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.ON);
|
||||
singleTapJob = scheduler.schedule(this::resetSingleTap, 1, TimeUnit.SECONDS);
|
||||
logger.debug("Asserting single tap of panel {} to ON", getPanelID());
|
||||
triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.SHORT_PRESSED);
|
||||
break;
|
||||
case 1:
|
||||
updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.ON);
|
||||
doubleTapJob = scheduler.schedule(this::resetDoubleTap, 1, TimeUnit.SECONDS);
|
||||
logger.debug("Asserting double tap of panel {} to ON", getPanelID());
|
||||
triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.DOUBLE_PRESSED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetSingleTap() {
|
||||
updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.OFF);
|
||||
logger.debug("Resetting single tap of panel {} to OFF", getPanelID());
|
||||
}
|
||||
|
||||
private void resetDoubleTap() {
|
||||
updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.OFF);
|
||||
logger.debug("Resetting double tap of panel {} to OFF", getPanelID());
|
||||
}
|
||||
|
||||
public String getPanelID() {
|
||||
String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString();
|
||||
return panelID;
|
||||
@@ -327,7 +323,7 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
} catch (NanoleafException nue) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.nanoleaf.panel.communication");
|
||||
logger.warn("Panel data could not be retrieved: {}", nue.getMessage());
|
||||
logger.debug("Panel data could not be retrieved: {}", nue.getMessage());
|
||||
}
|
||||
|
||||
return panelInfo.get(panelID);
|
||||
|
||||
@@ -60,7 +60,7 @@ public class Layout {
|
||||
* Returns an text representation for a canvas layout.
|
||||
*
|
||||
* Note only canvas supported currently due to its easy geometry
|
||||
*
|
||||
*
|
||||
* @return a String containing the layout
|
||||
*/
|
||||
public String getLayoutView() {
|
||||
@@ -97,6 +97,11 @@ public class Layout {
|
||||
|
||||
int shiftWidth = getSideLength() / 2;
|
||||
|
||||
if (shiftWidth == 0) {
|
||||
// seems we do not have squares here
|
||||
return "Cannot render layout. Please note that layout views are only supported for square panels.";
|
||||
}
|
||||
|
||||
int lineY = maxy;
|
||||
Map<Integer, PositionDatum> map;
|
||||
|
||||
@@ -108,8 +113,9 @@ public class Layout {
|
||||
@Nullable
|
||||
PositionDatum panel = positionData.get(index);
|
||||
|
||||
if (panel != null && panel.getPosY() == lineY)
|
||||
if (panel != null && panel.getPosY() == lineY) {
|
||||
map.put(panel.getPosX(), panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
lineY -= shiftWidth;
|
||||
@@ -118,14 +124,16 @@ public class Layout {
|
||||
@Nullable
|
||||
PositionDatum panel = map.get(x);
|
||||
view += String.format("%5s ", panel.getPanelId());
|
||||
} else
|
||||
} else {
|
||||
view += " ";
|
||||
}
|
||||
}
|
||||
view += "\n";
|
||||
view += System.lineSeparator();
|
||||
}
|
||||
|
||||
return view;
|
||||
} else
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<description>@text/thing-type.config.nanoleaf.controller.refreshInterval.description</description>
|
||||
<default>60</default>
|
||||
</parameter>
|
||||
<parameter name="deviceType" type="text" readOnly="true">
|
||||
<parameter name="deviceType" type="text">
|
||||
<label>@text/thing-type.config.nanoleaf.controller.deviceType.label</label>
|
||||
<description>@text/thing-type.config.nanoleaf.controller.deviceType.description</description>
|
||||
<default>lightPanels</default>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
binding.nanoleaf.name = Nanoleaf Binding
|
||||
binding.nanoleaf.description = Integrates the Nanoleaf Light Panels (v010221)
|
||||
binding.nanoleaf.description = Integrates the Nanoleaf light panels
|
||||
|
||||
# thing types
|
||||
thing-type.nanoleaf.controller.name = Nanoleaf Controller
|
||||
@@ -22,14 +22,6 @@ thing-type.config.nanoleaf.lightpanel.id.label = ID Of The Panel
|
||||
thing-type.config.nanoleaf.lightpanel.id.description = The ID of the panel assigned by the controller
|
||||
|
||||
# channel types
|
||||
channel-type.nanoleaf.power.label = Power
|
||||
channel-type.nanoleaf.power.description = Power state of the controller
|
||||
channel-type.nanoleaf.color.label = Color
|
||||
channel-type.nanoleaf.color.description = Color setting for an individual or all panels
|
||||
channel-type.nanoleaf.colorTemperature.label = Color Temperature
|
||||
channel-type.nanoleaf.colorTemperature.description = Color temperature in percent
|
||||
channel-type.nanoleaf.colorTemperatureAbs.label = Color Temperature (K)
|
||||
channel-type.nanoleaf.colorTemperatureAbs.description = Color temperature in Kelvin (K)
|
||||
channel-type.nanoleaf.colorMode.label = Color Mode
|
||||
channel-type.nanoleaf.colorMode.description = Current color mode: Effect, hue/saturation or color temperature
|
||||
channel-type.nanoleaf.effect.label = Effect
|
||||
@@ -40,21 +32,17 @@ channel-type.nanoleaf.rhythmActive.label = Rhythm Active
|
||||
channel-type.nanoleaf.rhythmActive.description = Activity state of the rhythm module
|
||||
channel-type.nanoleaf.rhythmMode.label = Rhythm Mode
|
||||
channel-type.nanoleaf.rhythmMode.description = Sound source for the rhythm module (microphone or aux cable)
|
||||
channel-type.nanoleaf.panelLayout.label = Panel Layout
|
||||
channel-type.nanoleaf.panelLayout.description = Creates a panel layout upon request
|
||||
channel-type.nanoleaf.panelColor.label = Panel Color
|
||||
channel-type.nanoleaf.panelColor.description = Color of the individual panel
|
||||
channel-type.nanoleaf.singleTap.label = SingleTap
|
||||
channel-type.nanoleaf.singleTap.description = Single tap on the panel
|
||||
channel-type.nanoleaf.doubleTap.label = DoubleTap
|
||||
channel-type.nanoleaf.doubleTap.description = Double tap on the panel
|
||||
channel-type.nanoleaf.tap.label = Button
|
||||
channel-type.nanoleaf.tap.description = Button events of the panel
|
||||
|
||||
# error messages
|
||||
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.
|
||||
error.nanoleaf.controller.incompatibleFirmware = Incompatible controller firmware. Remove the device, update the firmware, and add it again.
|
||||
error.nanoleaf.controller.noToken = No authorization token found. To start pairing, press the on-off button of the controller for 5-7 seconds until the LED starts flashing in a pattern.
|
||||
error.nanoleaf.controller.invalidToken = Invalid token. Replace with valid token or start pairing again by removing the invalid token from the configuration.
|
||||
error.nanoleaf.controller.communication = Communication failed. Please check configuration.
|
||||
error.nanoleaf.controller.communication = Communication failed. Please check your network and configuration.
|
||||
error.nanoleaf.controller.pairingFailed = Pairing failed. Press the on-off button for 5-7 seconds until the LED starts flashing in a pattern.
|
||||
error.nanoleaf.controller.invalidData = Pairing failed. Received invalid data
|
||||
error.nanoleaf.controller.noTokenReceived = Pairing failed. No authorization token received from controller.
|
||||
|
||||
@@ -22,14 +22,6 @@ thing-type.config.nanoleaf.lightpanel.id.label = Paneel ID
|
||||
thing-type.config.nanoleaf.lightpanel.id.description = Vom Controller vergebene ID eines Paneels
|
||||
|
||||
# channel types
|
||||
channel-type.nanoleaf.power.label = Power
|
||||
channel-type.nanoleaf.power.description = Ermöglicht das An- und Ausschalten des Nanoleaf Light Panels
|
||||
channel-type.nanoleaf.color.label = Farbe
|
||||
channel-type.nanoleaf.color.description = Farbe aller oder eines einzelnen Paneels
|
||||
channel-type.nanoleaf.colorTemperature.label = Farbtemperatur
|
||||
channel-type.nanoleaf.colorTemperature.description = Farbtemperatur in Prozent
|
||||
channel-type.nanoleaf.colorTemperatureAbs.label = Farbtemperatur (K)
|
||||
channel-type.nanoleaf.colorTemperatureAbs.description = Farbtemperatur in Kelvin (K)
|
||||
channel-type.nanoleaf.colorMode.label = Farbmodus
|
||||
channel-type.nanoleaf.colorMode.description = Effekt, Hue/Sat oder Farbtemperatur für alle Paneele.
|
||||
channel-type.nanoleaf.effect.label = Effekt
|
||||
@@ -40,21 +32,17 @@ channel-type.nanoleaf.rhythmActive.label = Rhythm Aktiv
|
||||
channel-type.nanoleaf.rhythmActive.description = Zeigt an ob das Mikrofon des Rhythm Modules ativ ist.
|
||||
channel-type.nanoleaf.rhythmMode.label = Rhythm Modus
|
||||
channel-type.nanoleaf.rhythmMode.description = Erlaubt den Wechsel zwischen eingebautem Mikrofon und AUX-Kabel.
|
||||
channel-type.nanoleaf.panelLayout.label = PanelLayout
|
||||
channel-type.nanoleaf.panelLayout.description = Erzeugt auf Anfrage ein Panel-Layout
|
||||
channel-type.nanoleaf.panelColor.label = Paneelfarbe
|
||||
channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels
|
||||
channel-type.nanoleaf.singleTap.label = Einzel-Tap
|
||||
channel-type.nanoleaf.singleTap.description = Panel wurde einmal angetippt
|
||||
channel-type.nanoleaf.doubleTap.label = Doppel-Tap
|
||||
channel-type.nanoleaf.doubleTap.description = Panel wurde zweimal hintereinander angetippt
|
||||
channel-type.nanoleaf.tap.label = Taster
|
||||
channel-type.nanoleaf.tap.description = Tastevents des Panels
|
||||
|
||||
# error messages
|
||||
error.nanoleaf.controller.noIp = IP/Host-Adresse und/oder Port sind für den Controller nicht konfiguriert.
|
||||
error.nanoleaf.controller.incompatibleFirmware = Inkompatible Controller-Firmware. Firmware aktualisieren und das Gerät neu hinzufügen.
|
||||
error.nanoleaf.controller.noToken = Kein Authentifizierungstoken gefunden. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken.
|
||||
error.nanoleaf.controller.invalidToken = Ungültiges Authentifizierungstoken. Token ändern oder das Pairing neu starten durch Löschen des ungültigen Tokens.
|
||||
error.nanoleaf.controller.communication = Kommunikationsfehler. Konfiguration des Controllers prüfen.
|
||||
error.nanoleaf.controller.communication = Kommunikationsfehler. Netzwerk und Konfiguration des Controllers prüfen.
|
||||
error.nanoleaf.controller.pairingFailed = Pairing fehlgeschlagen. Um das Pairing zu starten, den An/Aus-Knopf am Controller für 5-7 Sekunden lang gedrückt halten, bis die LED anfängt zu blinken.
|
||||
error.nanoleaf.controller.invalidData = Pairing fehlgeschlagen. Ungültige Daten vom Controller empfangen.
|
||||
error.nanoleaf.controller.noTokenReceived = Pairing fehlgeschlagen. Kein Authentifizierungstoken empfangen.
|
||||
|
||||
@@ -9,16 +9,14 @@
|
||||
<description>@text/thing-type.nanoleaf.controller.description</description>
|
||||
|
||||
<channels>
|
||||
<channel id="power" typeId="power"/>
|
||||
<channel id="color" typeId="color"/>
|
||||
<channel id="colorTemperature" typeId="colorTemperature"/>
|
||||
<channel id="colorTemperatureAbs" typeId="colorTemperatureAbs"/>
|
||||
<channel id="color" typeId="system.color"/>
|
||||
<channel id="colorTemperature" typeId="system.color-temperature"/>
|
||||
<channel id="colorTemperatureAbs" typeId="system.color-temperature-abs"/>
|
||||
<channel id="colorMode" typeId="colorMode"/>
|
||||
<channel id="effect" typeId="effect"/>
|
||||
<channel id="rhythmState" typeId="rhythmState"/>
|
||||
<channel id="rhythmActive" typeId="rhythmActive"/>
|
||||
<channel id="rhythmMode" typeId="rhythmMode"/>
|
||||
<channel id="panelLayout" typeId="panelLayout"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
@@ -40,9 +38,8 @@
|
||||
<label>@text/thing-type.nanoleaf.lightpanel.name</label>
|
||||
<description>@text/thing-type.nanoleaf.lightpanel.description</description>
|
||||
<channels>
|
||||
<channel id="panelColor" typeId="color"/>
|
||||
<channel id="singleTap" typeId="singleTap"/>
|
||||
<channel id="doubleTap" typeId="doubleTap"/>
|
||||
<channel id="color" typeId="system.color"/>
|
||||
<channel id="tap" typeId="system.button"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>id</representation-property>
|
||||
@@ -50,35 +47,13 @@
|
||||
<config-description-ref uri="thing-type:nanoleaf:lightpanel"/>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="power">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.nanoleaf.power.label</label>
|
||||
<description>@text/channel-type.nanoleaf.power.description</description>
|
||||
<category>Switch</category>
|
||||
<state readOnly="false">
|
||||
<options>
|
||||
<option value="ON">On</option>
|
||||
<option value="OFF">Off</option>
|
||||
</options>
|
||||
</state>
|
||||
<channel-type id="effect">
|
||||
<item-type>String</item-type>
|
||||
<label>@text/channel-type.nanoleaf.effect.label</label>
|
||||
<description>@text/channel-type.nanoleaf.effect.description</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="colorTemperature">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>@text/channel-type.nanoleaf.colorTemperature.label</label>
|
||||
<description>@text/channel-type.nanoleaf.colorTemperature.description</description>
|
||||
<state min="0" max="100" step="1"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="colorTemperatureAbs">
|
||||
<item-type>Number</item-type>
|
||||
<label>@text/channel-type.nanoleaf.colorTemperatureAbs.label</label>
|
||||
<description>@text/channel-type.nanoleaf.colorTemperatureAbs.description</description>
|
||||
<category>ColorLight</category>
|
||||
<state min="1200" max="6500" step="100"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="colorMode">
|
||||
<channel-type id="colorMode" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>@text/channel-type.nanoleaf.colorMode.label</label>
|
||||
<description>@text/channel-type.nanoleaf.colorMode.description</description>
|
||||
@@ -91,62 +66,30 @@
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="color">
|
||||
<item-type>Color</item-type>
|
||||
<label>@text/channel-type.nanoleaf.color.label</label>
|
||||
<description>@text/Color of the Light Panels</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="effect">
|
||||
<item-type>String</item-type>
|
||||
<label>@text/channel-type.nanoleaf.effect.label</label>
|
||||
<description>@text/channel-type.nanoleaf.effect.description</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="rhythmState">
|
||||
<channel-type id="rhythmState" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.nanoleaf.rhythmState.label</label>
|
||||
<description>@text/channel-type.nanoleaf.rhythmState.description</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="rhythmActive">
|
||||
<channel-type id="rhythmActive" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.nanoleaf.rhythmActive.label</label>
|
||||
<description>@text/channel-type.nanoleaf.rhythmActive.description</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="rhythmMode">
|
||||
<channel-type id="rhythmMode" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>@text/channel-type.nanoleaf.rhythmMode.label</label>
|
||||
<description>@text/channel-type.nanoleaf.rhythmMode.description</description>
|
||||
<state min="0" max="1">
|
||||
<options>
|
||||
<option value="0">Microphone</option>
|
||||
<option value="1">Aux cable</option>
|
||||
<option value="1">Aux Cable</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="singleTap">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.nanoleaf.singleTap.label</label>
|
||||
<description>@text/channel-type.nanoleaf.singleTap.description</description>
|
||||
<state readOnly="false"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="doubleTap">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.nanoleaf.doubleTap.label</label>
|
||||
<description>@text/channel-type.nanoleaf.doubleTap.description</description>
|
||||
<state readOnly="false"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="panelLayout">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.nanoleaf.panelLayout.label</label>
|
||||
<description>@text/channel-type.nanoleaf.panelLayout.description</description>
|
||||
<state readOnly="false"/>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
|
||||
Reference in New Issue
Block a user