[nanoleaf] More color for less network calls (#13893)

* [nanoleaf] More color for less network calls

This is a refactoring that moves the "get panel color" out of the
panel handler and into a separate class, with callbacks.

This makes us do only one REST call to get colors instead of one per
panel that is a thing. It also lets us retrieve colors for all panels -
 also those that doesn't have a thing in OpenHAB,

While testing this out, I found a bug where solid colors set in the app
wasn't reflected in neither the controller nor panel channels, and that
should also be fixed now.

Signed-off-by: Jørgen Austvik <jaustvik@acm.org>
This commit is contained in:
Jørgen Austvik 2022-12-11 16:09:53 +01:00 committed by GitHub
parent 1dbcaf8f0c
commit 70cca8fc77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 534 additions and 195 deletions

View File

@ -91,7 +91,8 @@ public class NanoleafBindingConstants {
public static final String SERVICE_TYPE = "_nanoleafapi._tcp.local."; public static final String SERVICE_TYPE = "_nanoleafapi._tcp.local.";
// Effect/scene name for static color // Effect/scene name for static color
public static final String EFFECT_NAME_STATIC_COLOR = "*Dynamic*"; public static final String EFFECT_NAME_STATIC_COLOR = "*Static*";
public static final String EFFECT_NAME_SOLID_COLOR = "*Solid*";
// Color channels increase/decrease brightness step size // Color channels increase/decrease brightness step size
public static final int BRIGHTNESS_STEP_SIZE = 5; public static final int BRIGHTNESS_STEP_SIZE = 5;

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 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.colors;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A listener used to notify panels when they change color.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public interface NanoleafControllerColorChangeListener {
/**
* This method is called after any panel changes its color.
*/
void onPanelChangedColor();
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2022 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.colors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.HSBType;
/**
* A listener used to notify panels when they change color.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public interface NanoleafPanelColorChangeListener {
/**
* This method is called after a panel changes its color
*
* @param newColor the new color of the panel
*/
void onPanelChangedColor(HSBType newColor);
}

View File

@ -0,0 +1,155 @@
/**
* Copyright (c) 2010-2022 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.colors;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler;
import org.openhab.core.library.types.HSBType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Stores information about panels and their colors, while sending notifications to panels and controllers
* about updated states.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class NanoleafPanelColors {
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelColors.class);
// holds current color data per panel
private final Map<Integer, HSBType> panelColors = new ConcurrentHashMap<>();
private final Map<Integer, NanoleafPanelColorChangeListener> panelChangeListeners = new ConcurrentHashMap<>();
private @Nullable NanoleafControllerColorChangeListener controllerListener;
private boolean updatePanelColorNoController(Integer panelId, HSBType color) {
boolean updatePanel = false;
if (panelColors.containsKey(panelId)) {
HSBType existingColor = panelColors.get(panelId);
if (existingColor != null && !existingColor.equals(color)) {
// Color change - update the panel thing
updatePanel = true;
}
} else {
// First time we see this panels color - update the panel thing
updatePanel = true;
}
panelColors.put(panelId, color);
if (updatePanel) {
@Nullable
NanoleafPanelColorChangeListener panelHandler = panelChangeListeners.get(panelId);
if (panelHandler != null) {
panelHandler.onPanelChangedColor(color);
}
}
return updatePanel;
}
private void updatePanelColor(Integer panelId, HSBType color) {
boolean updatePanel = updatePanelColorNoController(panelId, color);
if (updatePanel) {
notifyControllerListener();
}
}
private void notifyControllerListener() {
NanoleafControllerColorChangeListener privateControllerListener = controllerListener;
if (privateControllerListener != null) {
privateControllerListener.onPanelChangedColor();
}
}
/**
* Retrieves the color of the panel. Used by the panels to read their state.
*
* @param panelId The id of the panel
* @return The color of the panel
*/
public @Nullable HSBType getPanelColor(Integer panelId) {
return panelColors.get(panelId);
}
/**
* Called from panels to update the state.
*
* @param panelId The panel that received the update
* @param color The new color of the panel
*/
public void setPanelColor(Integer panelId, HSBType color) {
updatePanelColor(panelId, color);
}
public void registerChangeListener(Integer panelId, NanoleafPanelHandler panelListener) {
logger.trace("Adding color change listener for panel {}", panelId);
panelChangeListeners.put(panelId, panelListener);
}
public void unregisterChangeListener(Integer panelId) {
logger.trace("Removing color change listener for panel {}", panelId);
panelChangeListeners.remove(panelId);
}
public void registerChangeListener(NanoleafControllerColorChangeListener controllerListener) {
logger.trace("Setting color change listener for controller");
this.controllerListener = controllerListener;
}
/**
* Returns the color of a panel.
*
* @param panelId The panel
* @param defaultColor Default color if panel is missing color information
* @return Color of the panel
*/
public HSBType getColor(Integer panelId, HSBType defaultColor) {
return panelColors.getOrDefault(panelId, defaultColor);
}
/**
* Returns true if we have color information for the given panel.
*
* @param panelId The panel to check if has color
* @return true if we have color information about the panel
*/
public boolean hasColor(Integer panelId) {
return panelColors.containsKey(panelId);
}
/**
* Sets all panels to the same color. This will make controller repaint only once.
*
* @param panelIds Panels to update
* @param color The color for all panels
*/
public void setMultiple(List<Integer> panelIds, HSBType color) {
logger.debug("Setting all panels to color {}", color);
boolean updatePanel = false;
for (Integer panelId : panelIds) {
updatePanel |= updatePanelColorNoController(panelId, color);
}
if (updatePanel) {
notifyControllerListener();
}
}
}

View File

@ -17,6 +17,8 @@ import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -36,15 +38,21 @@ import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.nanoleaf.internal.NanoleafBadRequestException;
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants; import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener; import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
import org.openhab.binding.nanoleaf.internal.NanoleafException; 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.NanoleafUnauthorizedException;
import org.openhab.binding.nanoleaf.internal.OpenAPIUtils; import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
import org.openhab.binding.nanoleaf.internal.colors.NanoleafControllerColorChangeListener;
import org.openhab.binding.nanoleaf.internal.colors.NanoleafPanelColors;
import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider; import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig; import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService; import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService;
import org.openhab.binding.nanoleaf.internal.layout.ConstantPanelState;
import org.openhab.binding.nanoleaf.internal.layout.LayoutSettings; import org.openhab.binding.nanoleaf.internal.layout.LayoutSettings;
import org.openhab.binding.nanoleaf.internal.layout.LivePanelState;
import org.openhab.binding.nanoleaf.internal.layout.NanoleafLayout; import org.openhab.binding.nanoleaf.internal.layout.NanoleafLayout;
import org.openhab.binding.nanoleaf.internal.layout.PanelState; import org.openhab.binding.nanoleaf.internal.layout.PanelState;
import org.openhab.binding.nanoleaf.internal.model.AuthToken; import org.openhab.binding.nanoleaf.internal.model.AuthToken;
@ -58,10 +66,12 @@ import org.openhab.binding.nanoleaf.internal.model.IntegerState;
import org.openhab.binding.nanoleaf.internal.model.Layout; import org.openhab.binding.nanoleaf.internal.model.Layout;
import org.openhab.binding.nanoleaf.internal.model.On; import org.openhab.binding.nanoleaf.internal.model.On;
import org.openhab.binding.nanoleaf.internal.model.PanelLayout; import org.openhab.binding.nanoleaf.internal.model.PanelLayout;
import org.openhab.binding.nanoleaf.internal.model.PositionDatum;
import org.openhab.binding.nanoleaf.internal.model.Rhythm; import org.openhab.binding.nanoleaf.internal.model.Rhythm;
import org.openhab.binding.nanoleaf.internal.model.Sat; import org.openhab.binding.nanoleaf.internal.model.Sat;
import org.openhab.binding.nanoleaf.internal.model.State; import org.openhab.binding.nanoleaf.internal.model.State;
import org.openhab.binding.nanoleaf.internal.model.TouchEvents; import org.openhab.binding.nanoleaf.internal.model.TouchEvents;
import org.openhab.binding.nanoleaf.internal.model.Write;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
@ -96,7 +106,7 @@ import com.google.gson.JsonSyntaxException;
* @author Kai Kreuzer - refactoring, bug fixing and code clean up * @author Kai Kreuzer - refactoring, bug fixing and code clean up
*/ */
@NonNullByDefault @NonNullByDefault
public class NanoleafControllerHandler extends BaseBridgeHandler { public class NanoleafControllerHandler extends BaseBridgeHandler implements NanoleafControllerColorChangeListener {
// Pairing interval in seconds // Pairing interval in seconds
private static final int PAIRING_INTERVAL = 10; private static final int PAIRING_INTERVAL = 10;
@ -110,6 +120,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
private @Nullable Request sseTouchjobRequest; private @Nullable Request sseTouchjobRequest;
private final List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>(); private final List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
private PanelLayout previousPanelLayout = new PanelLayout(); private PanelLayout previousPanelLayout = new PanelLayout();
private final NanoleafPanelColors panelColors = new NanoleafPanelColors();
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob; private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
private @NonNullByDefault({}) ScheduledFuture<?> updateJob; private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
@ -154,6 +165,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
@Override @Override
public void initialize() { public void initialize() {
logger.debug("Initializing the controller (bridge)"); logger.debug("Initializing the controller (bridge)");
this.panelColors.registerChangeListener(this);
updateStatus(ThingStatus.UNKNOWN); updateStatus(ThingStatus.UNKNOWN);
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class); NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
setAddress(config.address); setAddress(config.address);
@ -582,7 +594,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
if (panelHandler != null) { if (panelHandler != null) {
logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(), logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
event.getPanelId()); event.getPanelId());
if (panelHandler.getPanelID().equals(event.getPanelId())) { if (panelHandler.getPanelID().equals(Integer.valueOf(event.getPanelId()))) {
logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(), logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
event.getGesture()); event.getGesture());
panelHandler.updatePanelGesture(event.getGesture()); panelHandler.updatePanelGesture(event.getGesture());
@ -648,34 +660,52 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
Brightness stateBrightness = state.getBrightness(); Brightness stateBrightness = state.getBrightness();
int brightness = stateBrightness != null ? stateBrightness.getValue() : 0; int brightness = stateBrightness != null ? stateBrightness.getValue() : 0;
HSBType stateColor = new HSBType(new DecimalType(hue), new PercentType(saturation),
new PercentType(powerState == OnOffType.ON ? brightness : 0));
updateState(CHANNEL_COLOR, new HSBType(new DecimalType(hue), new PercentType(saturation), updateState(CHANNEL_COLOR, stateColor);
new PercentType(powerState == OnOffType.ON ? brightness : 0)));
updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode())); updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode()));
updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF); updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF);
updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode())); updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode()));
updateState(CHANNEL_RHYTHM_STATE, updateState(CHANNEL_RHYTHM_STATE,
controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF); controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF);
// update the color channels of each panel updatePanelColors();
getThing().getThings().forEach(child -> { if (EFFECT_NAME_SOLID_COLOR.equals(controllerInfo.getEffects().getSelect())) {
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler(); setSolidColor(stateColor);
if (panelHandler != null) { }
logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID());
panelHandler.updatePanelColorChannel();
}
});
updateProperties(); updateProperties();
updateConfiguration(); updateConfiguration();
updateLayout(controllerInfo.getPanelLayout()); updateLayout(controllerInfo.getPanelLayout());
updateVisualState(controllerInfo.getPanelLayout()); updateVisualState(controllerInfo.getPanelLayout(), powerState);
for (NanoleafControllerListener controllerListener : controllerListeners) { for (NanoleafControllerListener controllerListener : controllerListeners) {
controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo); controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
} }
} }
private void setSolidColor(HSBType color) {
// If the panels are set to solid color, they are read from the state
PanelLayout panelLayout = controllerInfo.getPanelLayout();
Layout layout = panelLayout.getLayout();
if (layout != null) {
List<PositionDatum> positionData = layout.getPositionData();
if (positionData != null) {
List<Integer> allPanelIds = new ArrayList<>(positionData.size());
for (PositionDatum pd : positionData) {
allPanelIds.add(pd.getPanelId());
}
panelColors.setMultiple(allPanelIds, color);
} else {
logger.debug("Missing position datum when setting solid color for {}", getThing().getUID());
}
} else {
logger.debug("Missing layout when setting solid color for {}", getThing().getUID());
}
}
private void updateConfiguration() { private void updateConfiguration() {
// only update the Thing config if value isn't set yet // only update the Thing config if value isn't set yet
if (getConfig().get(NanoleafControllerConfig.DEVICE_TYPE) == null) { if (getConfig().get(NanoleafControllerConfig.DEVICE_TYPE) == null) {
@ -711,20 +741,20 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
} }
} }
private void updateVisualState(PanelLayout panelLayout) { private void updateVisualState(PanelLayout panelLayout, OnOffType powerState) {
ChannelUID stateChannel = new ChannelUID(getThing().getUID(), CHANNEL_VISUAL_STATE); ChannelUID stateChannel = new ChannelUID(getThing().getUID(), CHANNEL_VISUAL_STATE);
Bridge bridge = getThing();
List<Thing> things = bridge.getThings();
if (things == null) {
logger.trace("No things to get state from!");
return;
}
try { try {
PanelState panelState;
if (OnOffType.OFF.equals(powerState)) {
// If powered off: show all panels as black
panelState = new ConstantPanelState(HSBType.BLACK);
} else {
// Static color for panels, use it
panelState = new LivePanelState(panelColors);
}
LayoutSettings settings = new LayoutSettings(false, true, true, true); LayoutSettings settings = new LayoutSettings(false, true, true, true);
logger.trace("Getting panel state for {} things", things.size());
PanelState panelState = new PanelState(things);
byte[] bytes = NanoleafLayout.render(panelLayout, panelState, settings); byte[] bytes = NanoleafLayout.render(panelLayout, panelState, settings);
if (bytes.length > 0) { if (bytes.length > 0) {
updateState(stateChannel, new RawType(bytes, "image/png")); updateState(stateChannel, new RawType(bytes, "image/png"));
@ -756,11 +786,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
return; return;
} }
Bridge bridge = getThing();
List<Thing> things = bridge.getThings();
try { try {
LayoutSettings settings = new LayoutSettings(true, false, true, false); LayoutSettings settings = new LayoutSettings(true, false, true, false);
byte[] bytes = NanoleafLayout.render(panelLayout, new PanelState(things), settings); byte[] bytes = NanoleafLayout.render(panelLayout, new LivePanelState(panelColors), settings);
if (bytes.length > 0) { if (bytes.length > 0) {
updateState(layoutChannel, new RawType(bytes, "image/png")); updateState(layoutChannel, new RawType(bytes, "image/png"));
logger.trace("Rendered layout of panel {} in updateState has {} bytes", getThing().getUID(), logger.trace("Rendered layout of panel {} in updateState has {} bytes", getThing().getUID(),
@ -799,6 +827,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
h.setValue(((HSBType) command).getHue().intValue()); h.setValue(((HSBType) command).getHue().intValue());
s.setValue(((HSBType) command).getSaturation().intValue()); s.setValue(((HSBType) command).getSaturation().intValue());
b.setValue(((HSBType) command).getBrightness().intValue()); b.setValue(((HSBType) command).getBrightness().intValue());
setSolidColor((HSBType) command);
stateObject.setState(h); stateObject.setState(h);
stateObject.setState(s); stateObject.setState(s);
stateObject.setState(b); stateObject.setState(b);
@ -919,6 +948,126 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
} }
} }
private boolean hasStaticEffect() {
return EFFECT_NAME_STATIC_COLOR.equals(controllerInfo.getEffects().getSelect())
|| EFFECT_NAME_SOLID_COLOR.equals(controllerInfo.getEffects().getSelect());
}
/**
* Checks if we are in a mode where color changes should be rendered.
*
* @return True if a color change on a panel should be rendered
*/
private boolean showsUpdatedColors() {
if (!hasStaticEffect()) {
return false;
}
State state = controllerInfo.getState();
OnOffType powerState = state.getOnOff();
return OnOffType.ON.equals(powerState);
}
@Override
public void onPanelChangedColor() {
if (showsUpdatedColors()) {
// Update the visual state if a panel has changed color
updateVisualState(controllerInfo.getPanelLayout(), controllerInfo.getState().getOnOff());
}
}
/**
* For individual panels to get access to the panel colors.
*
* @return Information about colors of panels.
*/
public NanoleafPanelColors getColorInformation() {
return panelColors;
}
private void updatePanelColors() {
// get panel color data from controller
try {
Effects effects = new Effects();
Write write = new Write();
write.setCommand("request");
write.setAnimName(EFFECT_NAME_STATIC_COLOR);
effects.setWrite(write);
Bridge bridge = getBridge();
if (bridge != null) {
NanoleafControllerHandler handler = (NanoleafControllerHandler) bridge.getHandler();
if (handler != null) {
NanoleafControllerConfig config = handler.getControllerConfig();
logger.debug("Sending Request from Panel for getColor()");
Request setPanelUpdateRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
HttpMethod.PUT);
setPanelUpdateRequest.content(new StringContentProvider(gson.toJson(effects)), "application/json");
ContentResponse panelData = OpenAPIUtils.sendOpenAPIRequest(setPanelUpdateRequest);
// parse panel data
parsePanelData(config, panelData);
}
}
} catch (NanoleafNotFoundException nfe) {
logger.debug("Panel data could not be retrieved as no data was returned (static type missing?) : {}",
nfe.getMessage());
} catch (NanoleafBadRequestException nfe) {
logger.debug(
"Panel data could not be retrieved as request not expected(static type missing / dynamic type on) : {}",
nfe.getMessage());
} catch (NanoleafException nue) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.panel.communication");
logger.debug("Panel data could not be retrieved: {}", nue.getMessage());
}
}
void parsePanelData(NanoleafControllerConfig config, ContentResponse panelData) {
// panelData is in format (numPanels, (PanelId, 1, R, G, B, W, TransitionTime) * numPanel)
@Nullable
Write response = null;
String panelDataContent = panelData.getContentAsString();
try {
response = gson.fromJson(panelDataContent, Write.class);
} catch (JsonSyntaxException jse) {
logger.warn("Unable to parse panel data information from Nanoleaf", jse);
logger.trace("Panel Data which couldn't be parsed: {}", panelDataContent);
}
if (response != null) {
String[] tokenizedData = response.getAnimData().split(" ");
if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
|| config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
// panelData is in format (numPanels (PanelId 1 R G B W TransitionTime) * numPanel)
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 1, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 7 == 0) {
// found panel data - store it
panelColors.setPanelColor(Integer.valueOf(panelDataPoints[i]),
HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 2]),
Integer.parseInt(panelDataPoints[i + 3]),
Integer.parseInt(panelDataPoints[i + 4])));
}
}
} else {
// panelData is in format (0 numPanels (quotient(panelID) remainder(panelID) R G B W 0
// quotient(TransitionTime) remainder(TransitionTime)) * numPanel)
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 8 == 0) {
Integer idQuotient = Integer.valueOf(panelDataPoints[i]);
Integer idRemainder = Integer.valueOf(panelDataPoints[i + 1]);
Integer idNum = idQuotient * 256 + idRemainder;
// found panel data - store it
panelColors.setPanelColor(idNum, HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 3]),
Integer.parseInt(panelDataPoints[i + 4]), Integer.parseInt(panelDataPoints[i + 5])));
}
}
}
}
}
private @Nullable String getAddress() { private @Nullable String getAddress() {
return address; return address;
} }

View File

@ -16,23 +16,18 @@ import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; 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.ScheduledFuture;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.nanoleaf.internal.NanoleafBadRequestException;
import org.openhab.binding.nanoleaf.internal.NanoleafException; 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.NanoleafUnauthorizedException;
import org.openhab.binding.nanoleaf.internal.OpenAPIUtils; import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
import org.openhab.binding.nanoleaf.internal.colors.NanoleafPanelColorChangeListener;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig; import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.model.Effects; import org.openhab.binding.nanoleaf.internal.model.Effects;
import org.openhab.binding.nanoleaf.internal.model.Write; import org.openhab.binding.nanoleaf.internal.model.Write;
@ -50,6 +45,7 @@ import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler; import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -65,7 +61,7 @@ import com.google.gson.Gson;
* @author Stefan Höhn - Canvas Touch Support * @author Stefan Höhn - Canvas Touch Support
*/ */
@NonNullByDefault @NonNullByDefault
public class NanoleafPanelHandler extends BaseThingHandler { public class NanoleafPanelHandler extends BaseThingHandler implements NanoleafPanelColorChangeListener {
private static final PercentType MIN_PANEL_BRIGHTNESS = PercentType.ZERO; private static final PercentType MIN_PANEL_BRIGHTNESS = PercentType.ZERO;
private static final PercentType MAX_PANEL_BRIGHTNESS = PercentType.HUNDRED; private static final PercentType MAX_PANEL_BRIGHTNESS = PercentType.HUNDRED;
@ -75,9 +71,7 @@ public class NanoleafPanelHandler extends BaseThingHandler {
private final HttpClient httpClient; private final HttpClient httpClient;
// JSON parser for API responses // JSON parser for API responses
private final Gson gson = new Gson(); private final Gson gson = new Gson();
private HSBType currentPanelColor = HSBType.BLACK;
// holds current color data per panel
private final Map<String, HSBType> panelInfo = new HashMap<>();
private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob; private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob; private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
@ -141,6 +135,14 @@ public class NanoleafPanelHandler extends BaseThingHandler {
@Override @Override
public void handleRemoval() { public void handleRemoval() {
logger.debug("Nanoleaf panel {} removed", getThing().getUID()); logger.debug("Nanoleaf panel {} removed", getThing().getUID());
Bridge bridge = getBridge();
if (bridge != null) {
ThingHandler handler = bridge.getHandler();
if (handler instanceof NanoleafControllerHandler) {
((NanoleafControllerHandler) handler).getColorInformation().unregisterChangeListener(getPanelID());
}
}
super.handleRemoval(); super.handleRemoval();
} }
@ -166,21 +168,27 @@ public class NanoleafPanelHandler extends BaseThingHandler {
private void initializePanel(ThingStatusInfo panelStatus) { private void initializePanel(ThingStatusInfo panelStatus) {
updateStatus(panelStatus.getStatus(), panelStatus.getStatusDetail()); updateStatus(panelStatus.getStatus(), panelStatus.getStatusDetail());
updateState(CHANNEL_PANEL_COLOR, currentPanelColor);
logger.debug("Panel {} status changed to {}-{}", this.getThing().getUID(), panelStatus.getStatus(), logger.debug("Panel {} status changed to {}-{}", this.getThing().getUID(), panelStatus.getStatus(),
panelStatus.getStatusDetail()); panelStatus.getStatusDetail());
Bridge bridge = getBridge();
if (bridge != null) {
ThingHandler handler = bridge.getHandler();
if (handler instanceof NanoleafControllerHandler) {
((NanoleafControllerHandler) handler).getColorInformation().registerChangeListener(getPanelID(), this);
}
}
} }
private void sendRenderedEffectCommand(Command command) throws NanoleafException { private void sendRenderedEffectCommand(Command command) throws NanoleafException {
logger.debug("Command Type: {}", command.getClass()); logger.debug("Command Type: {}", command.getClass());
HSBType currentPanelColor = getPanelColor(); logger.debug("currentPanelColor: {}", currentPanelColor);
if (currentPanelColor != null) {
logger.debug("currentPanelColor: {}", currentPanelColor.toString());
}
HSBType newPanelColor = new HSBType();
HSBType newPanelColor = new HSBType();
if (command instanceof HSBType) { if (command instanceof HSBType) {
newPanelColor = (HSBType) command; newPanelColor = (HSBType) command;
} else if (command instanceof OnOffType && (currentPanelColor != null)) { } else if (command instanceof OnOffType) {
if (OnOffType.ON.equals(command)) { if (OnOffType.ON.equals(command)) {
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
MAX_PANEL_BRIGHTNESS); MAX_PANEL_BRIGHTNESS);
@ -188,11 +196,11 @@ public class NanoleafPanelHandler extends BaseThingHandler {
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
MIN_PANEL_BRIGHTNESS); MIN_PANEL_BRIGHTNESS);
} }
} else if (command instanceof PercentType && (currentPanelColor != null)) { } else if (command instanceof PercentType) {
PercentType brightness = new PercentType( PercentType brightness = new PercentType(
Math.max(MIN_PANEL_BRIGHTNESS.intValue(), ((PercentType) command).intValue())); Math.max(MIN_PANEL_BRIGHTNESS.intValue(), ((PercentType) command).intValue()));
newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), brightness); newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), brightness);
} else if (command instanceof IncreaseDecreaseType && (currentPanelColor != null)) { } else if (command instanceof IncreaseDecreaseType) {
int brightness = currentPanelColor.getBrightness().intValue(); int brightness = currentPanelColor.getBrightness().intValue();
if (command.equals(IncreaseDecreaseType.INCREASE)) { if (command.equals(IncreaseDecreaseType.INCREASE)) {
brightness = Math.min(MAX_PANEL_BRIGHTNESS.intValue(), brightness + BRIGHTNESS_STEP_SIZE); brightness = Math.min(MAX_PANEL_BRIGHTNESS.intValue(), brightness + BRIGHTNESS_STEP_SIZE);
@ -209,8 +217,8 @@ public class NanoleafPanelHandler extends BaseThingHandler {
return; return;
} }
// store panel's new HSB value // store panel's new HSB value
logger.trace("Setting new color {}", newPanelColor); logger.trace("Setting new color {} to panel {}", newPanelColor, getPanelID());
panelInfo.put(getThing().getConfiguration().get(CONFIG_PANEL_ID).toString(), newPanelColor); setPanelColor(newPanelColor);
// transform to RGB // transform to RGB
PercentType[] rgbPercent = newPanelColor.toRGB(); PercentType[] rgbPercent = newPanelColor.toRGB();
logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]); logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]);
@ -258,15 +266,6 @@ public class NanoleafPanelHandler extends BaseThingHandler {
} }
} }
public void updatePanelColorChannel() {
@Nullable
HSBType panelColor = getPanelColor();
logger.trace("updatePanelColorChannel: panelColor: {}", panelColor);
if (panelColor != null) {
updateState(CHANNEL_PANEL_COLOR, panelColor);
}
}
/** /**
* Apply the gesture to the panel * Apply the gesture to the panel
* *
@ -286,98 +285,32 @@ public class NanoleafPanelHandler extends BaseThingHandler {
} }
} }
public String getPanelID() { public Integer getPanelID() {
String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString(); return (Integer) getThing().getConfiguration().get(CONFIG_PANEL_ID);
return panelID;
} }
public @Nullable HSBType getColor() { private void setPanelColor(HSBType color) {
String panelID = getPanelID(); Integer panelId = getPanelID();
return panelInfo.get(panelID); Bridge bridge = getBridge();
} if (bridge != null) {
ThingHandler handler = bridge.getHandler();
private @Nullable HSBType getPanelColor() { if (handler instanceof NanoleafControllerHandler) {
String panelID = getPanelID(); ((NanoleafControllerHandler) handler).getColorInformation().setPanelColor(panelId, color);
// get panel color data from controller
try {
Effects effects = new Effects();
Write write = new Write();
write.setCommand("request");
write.setAnimName("*Static*");
effects.setWrite(write);
Bridge bridge = getBridge();
if (bridge != null) {
NanoleafControllerHandler handler = (NanoleafControllerHandler) bridge.getHandler();
if (handler != null) {
NanoleafControllerConfig config = handler.getControllerConfig();
logger.debug("Sending Request from Panel for getColor()");
Request setPanelUpdateRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
HttpMethod.PUT);
setPanelUpdateRequest.content(new StringContentProvider(gson.toJson(effects)), "application/json");
ContentResponse panelData = OpenAPIUtils.sendOpenAPIRequest(setPanelUpdateRequest);
// parse panel data
parsePanelData(panelID, config, panelData);
}
}
} catch (NanoleafNotFoundException nfe) {
logger.debug("Panel data could not be retrieved as no data was returned (static type missing?) : {}",
nfe.getMessage());
} catch (NanoleafBadRequestException nfe) {
logger.debug(
"Panel data could not be retrieved as request not expected(static type missing / dynamic type on) : {}",
nfe.getMessage());
} catch (NanoleafException nue) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/error.nanoleaf.panel.communication");
logger.debug("Panel data could not be retrieved: {}", nue.getMessage());
}
return panelInfo.get(panelID);
}
void parsePanelData(String panelID, NanoleafControllerConfig config, ContentResponse panelData) {
// panelData is in format (numPanels, (PanelId, 1, R, G, B, W, TransitionTime) * numPanel)
@Nullable
Write response = gson.fromJson(panelData.getContentAsString(), Write.class);
if (response != null) {
String[] tokenizedData = response.getAnimData().split(" ");
if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
|| config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
// panelData is in format (numPanels (PanelId 1 R G B W TransitionTime) * numPanel)
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 1, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 7 == 0) {
String id = panelDataPoints[i];
if (id.equals(panelID)) {
// found panel data - store it
panelInfo.put(panelID,
HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 2]),
Integer.parseInt(panelDataPoints[i + 3]),
Integer.parseInt(panelDataPoints[i + 4])));
}
}
}
} else { } else {
// panelData is in format (0 numPanels (quotient(panelID) remainder(panelID) R G B W 0 logger.debug("Couldn't find handler for panel {}", panelId);
// quotient(TransitionTime) remainder(TransitionTime)) * numPanel)
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 8 == 0) {
Integer idQuotient = Integer.valueOf(panelDataPoints[i]);
Integer idRemainder = Integer.valueOf(panelDataPoints[i + 1]);
Integer idNum = idQuotient * 256 + idRemainder;
if (String.valueOf(idNum).equals(panelID)) {
// found panel data - store it
panelInfo.put(panelID,
HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 3]),
Integer.parseInt(panelDataPoints[i + 4]),
Integer.parseInt(panelDataPoints[i + 5])));
}
}
}
} }
} else {
logger.debug("Couldn't find bridge for panel {}", panelId);
} }
} }
@Override
public void onPanelChangedColor(HSBType newColor) {
if (logger.isTraceEnabled()) {
logger.trace("updatePanelColorChannel: panelColor: {} for panel {}", newColor, getPanelID());
}
currentPanelColor = newColor;
updateState(CHANNEL_PANEL_COLOR, newColor);
}
} }

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2022 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.layout;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.HSBType;
/**
* Always returns the same color for all panels.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class ConstantPanelState implements PanelState {
private final HSBType color;
public ConstantPanelState(HSBType color) {
this.color = color;
}
@Override
public HSBType getHSBForPanel(Integer panelId) {
return color;
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2022 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.layout;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.colors.NanoleafPanelColors;
import org.openhab.core.library.types.HSBType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Stores the state of the panels.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class LivePanelState implements PanelState {
private static final Logger logger = LoggerFactory.getLogger(LivePanelState.class);
private final NanoleafPanelColors panelColors;
public LivePanelState(NanoleafPanelColors panelColors) {
this.panelColors = panelColors;
}
@Override
public HSBType getHSBForPanel(Integer panelId) {
if (logger.isTraceEnabled()) {
if (!panelColors.hasColor(panelId)) {
logger.trace("Failed to get color for panel {}, falling back to black", panelId);
}
}
return panelColors.getColor(panelId, HSBType.BLACK);
}
}

View File

@ -10,21 +10,10 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.binding.nanoleaf.internal.layout; package org.openhab.binding.nanoleaf.internal.layout;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.CONFIG_PANEL_ID;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler;
import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.HSBType;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Stores the state of the panels. * Stores the state of the panels.
@ -32,37 +21,7 @@ import org.slf4j.LoggerFactory;
* @author Jørgen Austvik - Initial contribution * @author Jørgen Austvik - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class PanelState { public interface PanelState {
private static final Logger logger = LoggerFactory.getLogger(PanelState.class); HSBType getHSBForPanel(Integer panelId);
private final Map<Integer, HSBType> panelStates = new HashMap<>();
public PanelState(List<Thing> panels) {
for (Thing panel : panels) {
Integer panelId = Integer.valueOf(panel.getConfiguration().get(CONFIG_PANEL_ID).toString());
NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) panel.getHandler();
if (panelHandler != null) {
HSBType c = panelHandler.getColor();
if (c == null) {
logger.trace("Panel {}: Failed to get color", panelId);
}
HSBType color = (c == null) ? HSBType.BLACK : c;
panelStates.put(panelId, color);
} else {
logger.trace("Panel {}: Couldn't find handler", panelId);
}
}
}
public HSBType getHSBForPanel(Integer panelId) {
if (logger.isTraceEnabled()) {
if (!panelStates.containsKey(panelId)) {
logger.trace("Failed to get color for panel {}, falling back to black", panelId);
}
}
return panelStates.getOrDefault(panelId, HSBType.BLACK);
}
} }

View File

@ -16,6 +16,7 @@ import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Queue; import java.util.Queue;
import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNull;
@ -36,7 +37,7 @@ public class PanelFactory {
List<Panel> result = new ArrayList<>(panels.size()); List<Panel> result = new ArrayList<>(panels.size());
Deque<PositionDatum> panelStack = new ArrayDeque<>(panels); Deque<PositionDatum> panelStack = new ArrayDeque<>(panels);
while (!panelStack.isEmpty()) { while (!panelStack.isEmpty()) {
PositionDatum panel = panelStack.peek(); PositionDatum panel = Objects.requireNonNull(panelStack.peek());
final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType());
Panel shape = createPanel(shapeType, takeFirst(shapeType.getNumLightsPerShape(), panelStack)); Panel shape = createPanel(shapeType, takeFirst(shapeType.getNumLightsPerShape(), panelStack));
result.add(shape); result.add(shape);

View File

@ -18,7 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -70,14 +69,10 @@ public class NanoleafLayoutTest {
// Files.write(permanentOutFile, result); // Files.write(permanentOutFile, result);
} }
private class TestPanelState extends PanelState { private class TestPanelState implements PanelState {
private final HSBType testColors[] = { HSBType.fromRGB(160, 120, 40), HSBType.fromRGB(80, 60, 20), private final HSBType testColors[] = { HSBType.fromRGB(160, 120, 40), HSBType.fromRGB(80, 60, 20),
HSBType.fromRGB(120, 90, 30), HSBType.fromRGB(200, 150, 60) }; HSBType.fromRGB(120, 90, 30), HSBType.fromRGB(200, 150, 60) };
public TestPanelState() {
super(Collections.emptyList());
}
@Override @Override
public HSBType getHSBForPanel(Integer panelId) { public HSBType getHSBForPanel(Integer panelId) {
return testColors[panelId % testColors.length]; return testColors[panelId % testColors.length];