From 70cca8fc778cecc3091dc99a1cf8a8c0188981bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Austvik?= Date: Sun, 11 Dec 2022 16:09:53 +0100 Subject: [PATCH] [nanoleaf] More color for less network calls (#13893) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [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 --- .../internal/NanoleafBindingConstants.java | 3 +- ...NanoleafControllerColorChangeListener.java | 30 +++ .../NanoleafPanelColorChangeListener.java | 33 +++ .../internal/colors/NanoleafPanelColors.java | 155 +++++++++++++ .../handler/NanoleafControllerHandler.java | 203 +++++++++++++++--- .../handler/NanoleafPanelHandler.java | 167 +++++--------- .../internal/layout/ConstantPanelState.java | 36 ++++ .../internal/layout/LivePanelState.java | 47 ++++ .../nanoleaf/internal/layout/PanelState.java | 45 +--- .../internal/layout/shape/PanelFactory.java | 3 +- .../internal/layout/NanoleafLayoutTest.java | 7 +- 11 files changed, 534 insertions(+), 195 deletions(-) create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafControllerColorChangeListener.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafPanelColorChangeListener.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafPanelColors.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ConstantPanelState.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LivePanelState.java diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java index 5109d8abc..96a1e6cee 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java @@ -91,7 +91,8 @@ public class NanoleafBindingConstants { public static final String SERVICE_TYPE = "_nanoleafapi._tcp.local."; // 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 public static final int BRIGHTNESS_STEP_SIZE = 5; diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafControllerColorChangeListener.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafControllerColorChangeListener.java new file mode 100644 index 000000000..346e11188 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafControllerColorChangeListener.java @@ -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(); +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafPanelColorChangeListener.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafPanelColorChangeListener.java new file mode 100644 index 000000000..e01d0c344 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafPanelColorChangeListener.java @@ -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); +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafPanelColors.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafPanelColors.java new file mode 100644 index 000000000..bb6d2ccc9 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/colors/NanoleafPanelColors.java @@ -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 panelColors = new ConcurrentHashMap<>(); + private final Map 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 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(); + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java index cd34d655d..21fbfb325 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java @@ -17,6 +17,8 @@ import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; 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.http.HttpMethod; 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.NanoleafControllerListener; 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.colors.NanoleafControllerColorChangeListener; +import org.openhab.binding.nanoleaf.internal.colors.NanoleafPanelColors; 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.layout.ConstantPanelState; 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.PanelState; 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.On; 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.Sat; import org.openhab.binding.nanoleaf.internal.model.State; 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.io.net.http.HttpClientFactory; 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 */ @NonNullByDefault -public class NanoleafControllerHandler extends BaseBridgeHandler { +public class NanoleafControllerHandler extends BaseBridgeHandler implements NanoleafControllerColorChangeListener { // Pairing interval in seconds private static final int PAIRING_INTERVAL = 10; @@ -110,6 +120,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { private @Nullable Request sseTouchjobRequest; private final List controllerListeners = new CopyOnWriteArrayList(); private PanelLayout previousPanelLayout = new PanelLayout(); + private final NanoleafPanelColors panelColors = new NanoleafPanelColors(); private @NonNullByDefault({}) ScheduledFuture pairingJob; private @NonNullByDefault({}) ScheduledFuture updateJob; @@ -154,6 +165,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { @Override public void initialize() { logger.debug("Initializing the controller (bridge)"); + this.panelColors.registerChangeListener(this); updateStatus(ThingStatus.UNKNOWN); NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class); setAddress(config.address); @@ -582,7 +594,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { if (panelHandler != null) { logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.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(), event.getGesture()); panelHandler.updatePanelGesture(event.getGesture()); @@ -648,34 +660,52 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { Brightness stateBrightness = state.getBrightness(); 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), - new PercentType(powerState == OnOffType.ON ? brightness : 0))); + updateState(CHANNEL_COLOR, stateColor); updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode())); updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF); updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode())); updateState(CHANNEL_RHYTHM_STATE, controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF); - // update the color channels of each panel - getThing().getThings().forEach(child -> { - NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler(); - if (panelHandler != null) { - logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID()); - panelHandler.updatePanelColorChannel(); - } - }); - + updatePanelColors(); + if (EFFECT_NAME_SOLID_COLOR.equals(controllerInfo.getEffects().getSelect())) { + setSolidColor(stateColor); + } updateProperties(); updateConfiguration(); updateLayout(controllerInfo.getPanelLayout()); - updateVisualState(controllerInfo.getPanelLayout()); + updateVisualState(controllerInfo.getPanelLayout(), powerState); for (NanoleafControllerListener controllerListener : controllerListeners) { 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 positionData = layout.getPositionData(); + if (positionData != null) { + List 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() { // only update the Thing config if value isn't set yet 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); - Bridge bridge = getThing(); - List things = bridge.getThings(); - if (things == null) { - logger.trace("No things to get state from!"); - return; - } - 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); - logger.trace("Getting panel state for {} things", things.size()); - PanelState panelState = new PanelState(things); byte[] bytes = NanoleafLayout.render(panelLayout, panelState, settings); if (bytes.length > 0) { updateState(stateChannel, new RawType(bytes, "image/png")); @@ -756,11 +786,9 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { return; } - Bridge bridge = getThing(); - List things = bridge.getThings(); try { 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) { updateState(layoutChannel, new RawType(bytes, "image/png")); 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()); s.setValue(((HSBType) command).getSaturation().intValue()); b.setValue(((HSBType) command).getBrightness().intValue()); + setSolidColor((HSBType) command); stateObject.setState(h); stateObject.setState(s); 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() { return address; } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java index 60b66196c..e80545dd9 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java @@ -16,23 +16,18 @@ 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 org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.StringContentProvider; 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.NanoleafNotFoundException; import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException; 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.model.Effects; 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.binding.BaseThingHandler; 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.RefreshType; import org.slf4j.Logger; @@ -65,7 +61,7 @@ import com.google.gson.Gson; * @author Stefan Höhn - Canvas Touch Support */ @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 MAX_PANEL_BRIGHTNESS = PercentType.HUNDRED; @@ -75,9 +71,7 @@ public class NanoleafPanelHandler extends BaseThingHandler { private final HttpClient httpClient; // JSON parser for API responses private final Gson gson = new Gson(); - - // holds current color data per panel - private final Map panelInfo = new HashMap<>(); + private HSBType currentPanelColor = HSBType.BLACK; private @NonNullByDefault({}) ScheduledFuture singleTapJob; private @NonNullByDefault({}) ScheduledFuture doubleTapJob; @@ -141,6 +135,14 @@ public class NanoleafPanelHandler extends BaseThingHandler { @Override public void handleRemoval() { 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(); } @@ -166,21 +168,27 @@ public class NanoleafPanelHandler extends BaseThingHandler { private void initializePanel(ThingStatusInfo panelStatus) { updateStatus(panelStatus.getStatus(), panelStatus.getStatusDetail()); + updateState(CHANNEL_PANEL_COLOR, currentPanelColor); logger.debug("Panel {} status changed to {}-{}", this.getThing().getUID(), panelStatus.getStatus(), 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 { logger.debug("Command Type: {}", command.getClass()); - HSBType currentPanelColor = getPanelColor(); - if (currentPanelColor != null) { - logger.debug("currentPanelColor: {}", currentPanelColor.toString()); - } - HSBType newPanelColor = new HSBType(); + logger.debug("currentPanelColor: {}", currentPanelColor); + HSBType newPanelColor = new HSBType(); if (command instanceof HSBType) { newPanelColor = (HSBType) command; - } else if (command instanceof OnOffType && (currentPanelColor != null)) { + } else if (command instanceof OnOffType) { if (OnOffType.ON.equals(command)) { newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), MAX_PANEL_BRIGHTNESS); @@ -188,11 +196,11 @@ public class NanoleafPanelHandler extends BaseThingHandler { newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), MIN_PANEL_BRIGHTNESS); } - } else if (command instanceof PercentType && (currentPanelColor != null)) { + } else if (command instanceof PercentType) { PercentType brightness = new PercentType( Math.max(MIN_PANEL_BRIGHTNESS.intValue(), ((PercentType) command).intValue())); 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(); if (command.equals(IncreaseDecreaseType.INCREASE)) { brightness = Math.min(MAX_PANEL_BRIGHTNESS.intValue(), brightness + BRIGHTNESS_STEP_SIZE); @@ -209,8 +217,8 @@ public class NanoleafPanelHandler extends BaseThingHandler { return; } // store panel's new HSB value - logger.trace("Setting new color {}", newPanelColor); - panelInfo.put(getThing().getConfiguration().get(CONFIG_PANEL_ID).toString(), newPanelColor); + logger.trace("Setting new color {} to panel {}", newPanelColor, getPanelID()); + setPanelColor(newPanelColor); // transform to RGB PercentType[] rgbPercent = newPanelColor.toRGB(); 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 * @@ -286,98 +285,32 @@ public class NanoleafPanelHandler extends BaseThingHandler { } } - public String getPanelID() { - String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString(); - return panelID; + public Integer getPanelID() { + return (Integer) getThing().getConfiguration().get(CONFIG_PANEL_ID); } - public @Nullable HSBType getColor() { - String panelID = getPanelID(); - return panelInfo.get(panelID); - } - - private @Nullable HSBType getPanelColor() { - String panelID = getPanelID(); - - // 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]))); - } - } - } + private void setPanelColor(HSBType color) { + Integer panelId = getPanelID(); + Bridge bridge = getBridge(); + if (bridge != null) { + ThingHandler handler = bridge.getHandler(); + if (handler instanceof NanoleafControllerHandler) { + ((NanoleafControllerHandler) handler).getColorInformation().setPanelColor(panelId, color); } 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; - 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]))); - } - } - } + logger.debug("Couldn't find handler for panel {}", panelId); } + } 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); + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ConstantPanelState.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ConstantPanelState.java new file mode 100644 index 000000000..7f6c0f4e7 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ConstantPanelState.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LivePanelState.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LivePanelState.java new file mode 100644 index 000000000..05b2f312b --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LivePanelState.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java index 02c6a7a94..1fb251ff6 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java @@ -10,21 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ - 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.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler; 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. @@ -32,37 +21,7 @@ import org.slf4j.LoggerFactory; * @author Jørgen Austvik - Initial contribution */ @NonNullByDefault -public class PanelState { +public interface PanelState { - private static final Logger logger = LoggerFactory.getLogger(PanelState.class); - private final Map panelStates = new HashMap<>(); - - public PanelState(List 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); - } + HSBType getHSBForPanel(Integer panelId); } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java index f0425f45b..2fa76a0d2 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java @@ -16,6 +16,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; +import java.util.Objects; import java.util.Queue; import org.eclipse.jdt.annotation.NonNull; @@ -36,7 +37,7 @@ public class PanelFactory { List result = new ArrayList<>(panels.size()); Deque panelStack = new ArrayDeque<>(panels); while (!panelStack.isEmpty()) { - PositionDatum panel = panelStack.peek(); + PositionDatum panel = Objects.requireNonNull(panelStack.peek()); final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); Panel shape = createPanel(shapeType, takeFirst(shapeType.getNumLightsPerShape(), panelStack)); result.add(shape); diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java index 2400808d3..e459d2c27 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java @@ -18,7 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -70,14 +69,10 @@ public class NanoleafLayoutTest { // 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), HSBType.fromRGB(120, 90, 30), HSBType.fromRGB(200, 150, 60) }; - public TestPanelState() { - super(Collections.emptyList()); - } - @Override public HSBType getHSBForPanel(Integer panelId) { return testColors[panelId % testColors.length];