From: Jørgen Austvik Date: Fri, 2 Dec 2022 20:14:53 +0000 (+0100) Subject: [Nanoleaf] New Channel: State (#13746) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=cad69c8de5995b93af084210217afffb1fd3d6eb;p=openhab-addons.git [Nanoleaf] New Channel: State (#13746) * [Nanoleaf] New Channel: State Shows an image of the state of the panels with color. Also makes the layout slightly prettier. This is less functional than the layout, and more eyecandy. Signed-off-by: Jørgen Austvik --- diff --git a/bundles/org.openhab.binding.nanoleaf/README.md b/bundles/org.openhab.binding.nanoleaf/README.md index b8a635b936..39816999e6 100644 --- a/bundles/org.openhab.binding.nanoleaf/README.md +++ b/bundles/org.openhab.binding.nanoleaf/README.md @@ -104,7 +104,15 @@ Compare the following output with the right picture at the beginning of the arti 41451 ``` - + +## State + +The state channel shows an image of the panels on the wall. +You have to configure things for each panel to get the correct color. +Since the colors of the panels can make it difficult to see the panel ids, please use the layout channel where the background color is always white to identify them. + +![Image](doc/NanoCanvas_rendered.jpg) + ## Thing Configuration The controller thing has the following parameters: @@ -137,10 +145,12 @@ The controller bridge has the following channels: | colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No | | colorMode | String | Color mode of the light panels | Yes | | effect | String | Selected effect of the light panels | No | +| layout | Image | Shows the layout of your panels with IDs. | Yes | | rhythmState | Switch | Connection state of the rhythm module | Yes | | rhythmActive | Switch | Activity state of the rhythm module | Yes | | rhythmMode | Number | Sound source for the rhythm module. 0=Microphone, 1=Aux cable | No | -| swipe | Trigger | [Canvas / Shapes Only] Detects Swipes over the panel.LEFT, RIGHT, UP, DOWN events are supported. | YES | +| state | Image | Shows the current state of your panels with colors. | Yes | +| swipe | Trigger | [Canvas / Shapes Only] Detects Swipes over the panel.LEFT, RIGHT, UP, DOWN events are supported. | Yes | diff --git a/bundles/org.openhab.binding.nanoleaf/doc/Layout.png b/bundles/org.openhab.binding.nanoleaf/doc/Layout.png index a8d684a0ce..d716ffae1e 100644 Binary files a/bundles/org.openhab.binding.nanoleaf/doc/Layout.png and b/bundles/org.openhab.binding.nanoleaf/doc/Layout.png differ diff --git a/bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png b/bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png new file mode 100644 index 0000000000..866554743f Binary files /dev/null and b/bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png differ 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 e7e8a2fb5a..db83c1fd78 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 @@ -59,6 +59,7 @@ public class NanoleafBindingConstants { public static final String CHANNEL_SWIPE_EVENT_LEFT = "LEFT"; public static final String CHANNEL_SWIPE_EVENT_RIGHT = "RIGHT"; public static final String CHANNEL_LAYOUT = "layout"; + public static final String CHANNEL_STATE = "state"; // List of light panel channels public static final String CHANNEL_PANEL_COLOR = "color"; diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java index 41538ee05b..aede5dbb53 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java @@ -57,11 +57,13 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory { this.httpClientFactory = httpClientFactory; } + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); } @Nullable + @Override protected ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (NanoleafBindingConstants.THING_TYPE_CONTROLLER.equals(thingTypeUID)) { 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 bd2efd4554..d37a82f06c 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 @@ -44,7 +44,9 @@ 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.layout.LayoutSettings; import org.openhab.binding.nanoleaf.internal.layout.NanoleafLayout; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; import org.openhab.binding.nanoleaf.internal.model.AuthToken; import org.openhab.binding.nanoleaf.internal.model.BooleanState; import org.openhab.binding.nanoleaf.internal.model.Brightness; @@ -101,12 +103,12 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { private static final int CONNECT_TIMEOUT = 10; private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class); - private HttpClientFactory httpClientFactory; - private HttpClient httpClient; + private final HttpClientFactory httpClientFactory; + private final HttpClient httpClient; private @Nullable HttpClient httpClientSSETouchEvent; private @Nullable Request sseTouchjobRequest; - private List controllerListeners = new CopyOnWriteArrayList(); + private final List controllerListeners = new CopyOnWriteArrayList(); private PanelLayout previousPanelLayout = new PanelLayout(); private @NonNullByDefault({}) ScheduledFuture pairingJob; @@ -515,9 +517,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { localhttpSSEClientTouchEvent.setIdleTimeout(CONNECT_TIMEOUT * 1000L); sseTouchjobRequest = localhttpSSEClientTouchEvent.newRequest(eventUri); final Request localSSETouchjobRequest = sseTouchjobRequest; - int requestHashCode = -1; if (localSSETouchjobRequest != null) { - requestHashCode = localSSETouchjobRequest.hashCode(); + int requestHashCode = localSSETouchjobRequest.hashCode(); logger.debug("tj: triggering new touch job request {} for {} with client {}", requestHashCode, thing.getUID(), eventHashcode); @@ -525,23 +526,21 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { String s = StandardCharsets.UTF_8.decode(content).toString(); logger.debug("touch detected for controller {}", thing.getUID()); logger.trace("content {}", s); - Scanner eventContent = new Scanner(s); - - while (eventContent.hasNextLine()) { - String line = eventContent.nextLine().trim(); - if (line.startsWith("data:")) { - String json = line.substring(5).trim(); - - try { - TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class); - handleTouchEvents(Objects.requireNonNull(touchEvents)); - } catch (JsonSyntaxException e) { - logger.error("Couldn't parse touch event json {}", json); + try (Scanner eventContent = new Scanner(s)) { + while (eventContent.hasNextLine()) { + String line = eventContent.nextLine().trim(); + if (line.startsWith("data:")) { + String json = line.substring(5).trim(); + + try { + TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class); + handleTouchEvents(Objects.requireNonNull(touchEvents)); + } catch (JsonSyntaxException e) { + logger.error("Couldn't parse touch event json {}", json); + } } } } - - eventContent.close(); logger.debug("leaving touch onContent"); }).onResponseSuccess((response) -> { logger.trace("tj: r={} touch event SUCCESS: {}", response.getRequest(), response); @@ -670,6 +669,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { updateProperties(); updateConfiguration(); updateLayout(controllerInfo.getPanelLayout()); + updateState(controllerInfo.getPanelLayout()); for (NanoleafControllerListener controllerListener : controllerListeners) { controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo); @@ -711,6 +711,24 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { } } + private void updateState(PanelLayout panelLayout) { + ChannelUID stateChannel = new ChannelUID(getThing().getUID(), CHANNEL_STATE); + + Bridge bridge = getThing(); + List things = bridge.getThings(); + try { + LayoutSettings settings = new LayoutSettings(false, true, true, true); + byte[] bytes = NanoleafLayout.render(panelLayout, new PanelState(things), settings); + if (bytes.length > 0) { + updateState(stateChannel, new RawType(bytes, "image/png")); + } + + previousPanelLayout = panelLayout; + } catch (IOException ioex) { + logger.warn("Failed to create state image", ioex); + } + } + private void updateLayout(PanelLayout panelLayout) { ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT); ThingHandlerCallback callback = getCallback(); @@ -726,10 +744,13 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { return; } + Bridge bridge = getThing(); + List things = bridge.getThings(); try { - byte[] bytes = NanoleafLayout.render(panelLayout); + LayoutSettings settings = new LayoutSettings(true, false, true, false); + byte[] bytes = NanoleafLayout.render(panelLayout, new PanelState(things), settings); if (bytes.length > 0) { - updateState(CHANNEL_LAYOUT, new RawType(bytes, "image/png")); + updateState(layoutChannel, new RawType(bytes, "image/png")); } previousPanelLayout = panelLayout; 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 2c16585206..995eb8a347 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 @@ -72,12 +72,12 @@ public class NanoleafPanelHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class); - private HttpClient httpClient; + private final HttpClient httpClient; // JSON parser for API responses private final Gson gson = new Gson(); // holds current color data per panel - private Map panelInfo = new HashMap<>(); + private final Map panelInfo = new HashMap<>(); private @NonNullByDefault({}) ScheduledFuture singleTapJob; private @NonNullByDefault({}) ScheduledFuture doubleTapJob; @@ -227,7 +227,7 @@ public class NanoleafPanelHandler extends BaseThingHandler { Write write = new Write(); write.setCommand("display"); write.setAnimType("static"); - String panelID = this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString(); + Integer panelID = Integer.valueOf(this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString()); @Nullable BridgeHandler handler = bridge.getHandler(); if (handler != null) { @@ -239,8 +239,8 @@ public class NanoleafPanelHandler extends BaseThingHandler { write.setAnimData(String.format("1 %s 1 %d %d %d 0 10", panelID, red, green, blue)); } else { // this is only used in special streaming situations with canvas which is not yet supported - int quotient = Integer.divideUnsigned(Integer.valueOf(panelID), 256); - int remainder = Integer.remainderUnsigned(Integer.valueOf(panelID), 256); + int quotient = Integer.divideUnsigned(panelID, 256); + int remainder = Integer.remainderUnsigned(panelID, 256); write.setAnimData( String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, red, green, blue)); } @@ -288,6 +288,11 @@ public class NanoleafPanelHandler extends BaseThingHandler { return panelID; } + public @Nullable HSBType getColor() { + String panelID = getPanelID(); + return panelInfo.get(panelID); + } + private @Nullable HSBType getPanelColor() { String panelID = getPanelID(); @@ -357,9 +362,9 @@ public class NanoleafPanelHandler extends BaseThingHandler { String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length); for (int i = 0; i < panelDataPoints.length; i++) { if (i % 8 == 0) { - String idQuotient = panelDataPoints[i]; - String idRemainder = panelDataPoints[i + 1]; - Integer idNum = Integer.valueOf(idQuotient) * 256 + Integer.valueOf(idRemainder); + 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, diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java new file mode 100644 index 0000000000..be16a3b22a --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java @@ -0,0 +1,95 @@ +/** + * 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 java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants; + +/** + * Information to the drawing algorithm about which style to use and how to draw. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class DrawingSettings { + + private static final Color COLOR_SIDE = Color.GRAY; + private static final Color COLOR_TEXT = Color.BLACK; + + private final LayoutSettings layoutSettings; + private final int imageHeight; + private final ImagePoint2D min; + private final double rotationRadians; + + public DrawingSettings(LayoutSettings layoutSettings, int imageHeight, ImagePoint2D min, double rotationRadians) { + this.imageHeight = imageHeight; + this.min = min; + this.rotationRadians = rotationRadians; + this.layoutSettings = layoutSettings; + } + + public boolean shouldDrawLabels() { + return layoutSettings.shouldDrawLabels(); + } + + public boolean shouldDrawCorners() { + return layoutSettings.shouldDrawCorners(); + } + + public boolean shouldDrawOutline() { + return layoutSettings.shouldDrawOutline(); + } + + public boolean shouldFillWithColor() { + return layoutSettings.shouldFillWithColor(); + } + + public Color getOutlineColor() { + return COLOR_SIDE; + } + + public Color getLabelColor() { + return COLOR_TEXT; + } + + public ImagePoint2D generateImagePoint(Point2D point) { + return toPictureLayout(point, imageHeight, min, rotationRadians); + } + + public List generateImagePoints(List points) { + return toPictureLayout(points, imageHeight, min, rotationRadians); + } + + private static ImagePoint2D toPictureLayout(Point2D original, int imageHeight, ImagePoint2D min, + double rotationRadians) { + Point2D rotated = original.rotate(rotationRadians); + ImagePoint2D translated = new ImagePoint2D( + NanoleafBindingConstants.LAYOUT_BORDER_WIDTH + rotated.getX() - min.getX(), + imageHeight - NanoleafBindingConstants.LAYOUT_BORDER_WIDTH - rotated.getY() + min.getY()); + return translated; + } + + private static List toPictureLayout(List originals, int imageHeight, ImagePoint2D min, + double rotationRadians) { + List result = new ArrayList<>(originals.size()); + for (Point2D original : originals) { + result.add(toPictureLayout(original, imageHeight, min, rotationRadians)); + } + + return result; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java new file mode 100644 index 0000000000..4dcb0060d0 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java @@ -0,0 +1,45 @@ +/** + * 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; + +/** + * Coordinate in the 2D space of the image. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ImagePoint2D { + private final int x; + private final int y; + + public ImagePoint2D(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public String toString() { + return String.format("image coordinate x:%d, y:%d", x, y); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java new file mode 100644 index 0000000000..2364f5513f --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java @@ -0,0 +1,52 @@ +/** + * 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; + +/** + * Settigns used for layout. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class LayoutSettings { + + private final boolean drawLabels; + private final boolean drawCorners; + private final boolean drawOutline; + private final boolean fillColor; + + public LayoutSettings(boolean drawLabels, boolean drawCorners, boolean drawOutline, boolean fillColor) { + this.drawLabels = drawLabels; + this.drawCorners = drawCorners; + this.drawOutline = drawOutline; + this.fillColor = fillColor; + } + + public boolean shouldDrawLabels() { + return drawLabels; + } + + public boolean shouldDrawCorners() { + return drawCorners; + } + + public boolean shouldDrawOutline() { + return drawOutline; + } + + public boolean shouldFillWithColor() { + return fillColor; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java index 70724333ec..61ceaa5497 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java @@ -15,19 +15,18 @@ package org.openhab.binding.nanoleaf.internal.layout; import java.awt.Color; import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import javax.imageio.ImageIO; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants; -import org.openhab.binding.nanoleaf.internal.layout.shape.Shape; -import org.openhab.binding.nanoleaf.internal.layout.shape.ShapeFactory; +import org.openhab.binding.nanoleaf.internal.layout.shape.Panel; +import org.openhab.binding.nanoleaf.internal.layout.shape.PanelFactory; import org.openhab.binding.nanoleaf.internal.model.GlobalOrientation; import org.openhab.binding.nanoleaf.internal.model.Layout; import org.openhab.binding.nanoleaf.internal.model.PanelLayout; @@ -42,11 +41,8 @@ import org.openhab.binding.nanoleaf.internal.model.PositionDatum; public class NanoleafLayout { private static final Color COLOR_BACKGROUND = Color.WHITE; - private static final Color COLOR_PANEL = Color.BLACK; - private static final Color COLOR_SIDE = Color.GRAY; - private static final Color COLOR_TEXT = Color.BLACK; - public static byte[] render(PanelLayout panelLayout) throws IOException { + public static byte[] render(PanelLayout panelLayout, PanelState state, LayoutSettings settings) throws IOException { double rotationRadians = 0; GlobalOrientation globalOrientation = panelLayout.getGlobalOrientation(); if (globalOrientation != null) { @@ -58,78 +54,31 @@ public class NanoleafLayout { return new byte[] {}; } - List panels = layout.getPositionData(); - if (panels == null) { + List positionDatums = layout.getPositionData(); + if (positionDatums == null) { return new byte[] {}; } - Point2D size[] = findSize(panels, rotationRadians); - final Point2D min = size[0]; - final Point2D max = size[1]; - Point2D prev = null; - Point2D first = null; + ImagePoint2D size[] = findSize(positionDatums, rotationRadians); + final ImagePoint2D min = size[0]; + final ImagePoint2D max = size[1]; - int sideCounter = 0; BufferedImage image = new BufferedImage( (max.getX() - min.getX()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH, (max.getY() - min.getY()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = image.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g2.setBackground(COLOR_BACKGROUND); g2.clearRect(0, 0, image.getWidth(), image.getHeight()); - for (PositionDatum panel : panels) { - final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); - - Shape shape = ShapeFactory.CreateShape(shapeType, panel); - List outline = toPictureLayout(shape.generateOutline(), image.getHeight(), min, rotationRadians); - for (int i = 0; i < outline.size(); i++) { - g2.setColor(COLOR_SIDE); - Point2D pos = outline.get(i); - Point2D nextPos = outline.get((i + 1) % outline.size()); - g2.drawLine(pos.getX(), pos.getY(), nextPos.getX(), nextPos.getY()); - } - - for (int i = 0; i < outline.size(); i++) { - Point2D pos = outline.get(i); - g2.setColor(COLOR_PANEL); - g2.fillOval(pos.getX() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2, - pos.getY() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2, - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS, NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS); - } - - Point2D current = toPictureLayout(new Point2D(panel.getPosX(), panel.getPosY()), image.getHeight(), min, - rotationRadians); - if (sideCounter == 0) { - first = current; - } - - g2.setColor(COLOR_SIDE); - final int expectedSides = shapeType.getNumSides(); - if (shapeType.getDrawingAlgorithm() == DrawingAlgorithm.CORNER) { - // Special handling of Elements Hexagon Corners, where we get 6 corners instead of 1 shape. They seem to - // come after each other in the JSON, so this algorithm connects them based on the number of sides the - // shape is expected to have. - if (sideCounter > 0 && sideCounter != expectedSides && prev != null) { - g2.drawLine(prev.getX(), prev.getY(), current.getX(), current.getY()); - } - - sideCounter++; - - if (sideCounter == expectedSides && first != null) { - g2.drawLine(current.getX(), current.getY(), first.getX(), first.getY()); - sideCounter = 0; - } - } else { - sideCounter = 0; - } - - prev = current; - - g2.setColor(COLOR_TEXT); - Point2D textPos = shape.labelPosition(g2, outline); - g2.drawString(Integer.toString(panel.getPanelId()), textPos.getX(), textPos.getY()); + DrawingSettings dc = new DrawingSettings(settings, image.getHeight(), min, rotationRadians); + List panels = PanelFactory.createPanels(positionDatums); + for (Panel panel : panels) { + panel.draw(g2, dc, state); } ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -144,15 +93,14 @@ public class NanoleafLayout { return ((double) (maxValue - value)) * (Math.PI / 180); } - private static Point2D[] findSize(Collection panels, double rotationRadians) { + private static ImagePoint2D[] findSize(List positionDatums, double rotationRadians) { int maxX = 0; int maxY = 0; int minX = 0; int minY = 0; - for (PositionDatum panel : panels) { - ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); - Shape shape = ShapeFactory.CreateShape(shapeType, panel); + List panels = PanelFactory.createPanels(positionDatums); + for (Panel shape : panels) { for (Point2D point : shape.generateOutline()) { var rotated = point.rotate(rotationRadians); maxX = Math.max(rotated.getX(), maxX); @@ -162,23 +110,6 @@ public class NanoleafLayout { } } - return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) }; - } - - private static Point2D toPictureLayout(Point2D original, int imageHeight, Point2D min, double rotationRadians) { - Point2D rotated = original.rotate(rotationRadians); - Point2D translated = new Point2D(NanoleafBindingConstants.LAYOUT_BORDER_WIDTH + rotated.getX() - min.getX(), - imageHeight - NanoleafBindingConstants.LAYOUT_BORDER_WIDTH - rotated.getY() + min.getY()); - return translated; - } - - private static List toPictureLayout(List originals, int imageHeight, Point2D min, - double rotationRadians) { - List result = new ArrayList(originals.size()); - for (Point2D original : originals) { - result.add(toPictureLayout(original, imageHeight, min, rotationRadians)); - } - - return result; + return new ImagePoint2D[] { new ImagePoint2D(minX, minY), new ImagePoint2D(maxX, maxY) }; } } 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 new file mode 100644 index 0000000000..fba1804fe8 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java @@ -0,0 +1,52 @@ +/** + * 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 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; + +/** + * Stores the state of the panels. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class PanelState { + + 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(); + HSBType color = (c == null) ? HSBType.BLACK : c; + panelStates.put(panelId, color); + } + } + } + + public HSBType getHSBForPanel(Integer panelId) { + return panelStates.getOrDefault(panelId, HSBType.BLACK); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java index f90262e0e7..471dafb362 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java @@ -23,35 +23,37 @@ import org.eclipse.jdt.annotation.NonNullByDefault; @NonNullByDefault public enum ShapeType { // side lengths are taken from https://forum.nanoleaf.me/docs chapter 3.3 - UNKNOWN("Unknown", -1, 0, 0, DrawingAlgorithm.NONE), - TRIANGLE("Triangle", 0, 150, 3, DrawingAlgorithm.TRIANGLE), - RHYTHM("Rhythm", 1, 0, 1, DrawingAlgorithm.NONE), - SQUARE("Square", 2, 100, 0, DrawingAlgorithm.SQUARE), - CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0, DrawingAlgorithm.SQUARE), - CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0, DrawingAlgorithm.SQUARE), - SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6, DrawingAlgorithm.HEXAGON), - SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3, DrawingAlgorithm.TRIANGLE), - SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3, DrawingAlgorithm.TRIANGLE), - SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0, DrawingAlgorithm.NONE), - ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6, DrawingAlgorithm.HEXAGON), - ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 33.5 / 58, 6, DrawingAlgorithm.CORNER), - LINES_CONNECTOR("Lines Connector", 16, 11, 1, DrawingAlgorithm.LINE), - LIGHT_LINES("Light Lines", 17, 154, 1, DrawingAlgorithm.LINE), - LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1, DrawingAlgorithm.LINE), - CONTROLLER_CAP("Controller Cap", 19, 11, 0, DrawingAlgorithm.NONE), - POWER_CONNECTOR("Power Connector", 20, 11, 0, DrawingAlgorithm.NONE); + UNKNOWN("Unknown", -1, 0, 0, 1, DrawingAlgorithm.NONE), + TRIANGLE("Triangle", 0, 150, 3, 1, DrawingAlgorithm.TRIANGLE), + RHYTHM("Rhythm", 1, 0, 1, 1, DrawingAlgorithm.NONE), + SQUARE("Square", 2, 100, 0, 1, DrawingAlgorithm.SQUARE), + CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0, 1, DrawingAlgorithm.SQUARE), + CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0, 1, DrawingAlgorithm.SQUARE), + SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6, 1, DrawingAlgorithm.HEXAGON), + SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3, 1, DrawingAlgorithm.TRIANGLE), + SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3, 1, DrawingAlgorithm.TRIANGLE), + SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0, 1, DrawingAlgorithm.NONE), + ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6, 1, DrawingAlgorithm.HEXAGON), + ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 58, 6, 6, DrawingAlgorithm.CORNER), + LINES_CONNECTOR("Lines Connector", 16, 11, 1, 1, DrawingAlgorithm.LINE), + LIGHT_LINES("Light Lines", 17, 154, 1, 1, DrawingAlgorithm.LINE), + LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1, 1, DrawingAlgorithm.LINE), + CONTROLLER_CAP("Controller Cap", 19, 11, 0, 1, DrawingAlgorithm.NONE), + POWER_CONNECTOR("Power Connector", 20, 11, 0, 1, DrawingAlgorithm.NONE); private final String name; private final int id; - private final double sideLength; + private final int sideLength; private final int numSides; + private final int numLights; private final DrawingAlgorithm drawingAlgorithm; - ShapeType(String name, int id, double sideLenght, int numSides, DrawingAlgorithm drawingAlgorithm) { + ShapeType(String name, int id, int sideLenght, int numSides, int numLights, DrawingAlgorithm drawingAlgorithm) { this.name = name; this.id = id; this.sideLength = sideLenght; this.numSides = numSides; + this.numLights = numLights; this.drawingAlgorithm = drawingAlgorithm; } @@ -63,7 +65,7 @@ public enum ShapeType { return id; } - public double getSideLength() { + public int getSideLength() { return sideLength; } @@ -71,6 +73,10 @@ public enum ShapeType { return numSides; } + public int getNumLightsPerShape() { + return numLights; + } + public DrawingAlgorithm getDrawingAlgorithm() { return drawingAlgorithm; } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java new file mode 100644 index 0000000000..e04e43fa49 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java @@ -0,0 +1,152 @@ +/** + * 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.shape; + +import java.awt.Color; +import java.awt.Paint; +import java.awt.PaintContext; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; +import java.awt.image.DataBufferInt; +import java.awt.image.PackedColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; + +/** + * Paint for triangles with one color in each corner. Used to make gradients between the colors when + * dividing a hexagon into 6 triangles. + * + * https://codeplea.com/triangular-interpolation is instructive for the math. + * + * Inspired by + * https://github.com/hageldave/JPlotter/blob/9c92731f3b29a2cdb14f3dfdeeed6fffde37eee4/jplotter/src/main/java/hageldave/jplotter/util/BarycentricGradientPaint.java, + * for how to integrate it into Java AWT but kept so simple that I could understand it. It was however far too big to + * use as a dependency. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class BarycentricTriangleGradient implements Paint { + + private final Color color1; + private final Color color2; + private final Color color3; + + private final ImagePoint2D corner1; + private final ImagePoint2D corner2; + private final ImagePoint2D corner3; + + public BarycentricTriangleGradient(ImagePoint2D corner1, Color color1, ImagePoint2D corner2, Color color2, + ImagePoint2D corner3, Color color3) { + this.corner1 = corner1; + this.corner2 = corner2; + this.corner3 = corner3; + this.color1 = color1; + this.color2 = color2; + this.color3 = color3; + } + + @Override + public @Nullable PaintContext createContext(@Nullable ColorModel cm, @Nullable Rectangle deviceBounds, + @Nullable Rectangle2D userBounds, @Nullable AffineTransform xform, @Nullable RenderingHints hints) { + return new BarycentricTriangleGradientContext(corner1, color1, corner2, color2, corner3, color3); + } + + @Override + public int getTransparency() { + return OPAQUE; + } + + private class BarycentricTriangleGradientContext implements PaintContext { + + private final Color color1; + private final Color color2; + private final Color color3; + + private final ImagePoint2D corner1; + private final ImagePoint2D corner2; + private final ImagePoint2D corner3; + + private final PackedColorModel colorModel = (PackedColorModel) ColorModel.getRGBdefault(); + + public BarycentricTriangleGradientContext(ImagePoint2D corner1, Color color1, ImagePoint2D corner2, + Color color2, ImagePoint2D corner3, Color color3) { + this.corner1 = corner1; + this.corner2 = corner2; + this.corner3 = corner3; + this.color1 = color1; + this.color2 = color2; + this.color3 = color3; + } + + @Override + public void dispose() { + } + + @Override + public @Nullable ColorModel getColorModel() { + return colorModel; + } + + @Override + public Raster getRaster(int x, int y, int w, int h) { + int[] data = new int[h * w]; + DataBufferInt buffer = new DataBufferInt(data, w * h); + WritableRaster raster = Raster.createPackedRaster(buffer, w, h, w, colorModel.getMasks(), null); + + float denominator = 1f / (((corner2.getY() - corner3.getY()) * (corner1.getX() - corner3.getX())) + + ((corner3.getX() - corner2.getX()) * (corner1.getY() - corner3.getY()))); + + for (int yPos = 0; yPos < h; yPos++) { + int imageY = y + yPos; + for (int xPos = 0; xPos < w; xPos++) { + int imageX = xPos + x; + + float weight1 = (((corner2.getY() - corner3.getY()) * (imageX - corner3.getX())) + + ((corner3.getX() - corner2.getX()) * (imageY - corner3.getY()))) * denominator; + float weight2 = (((corner3.getY() - corner1.getY()) * (imageX - corner3.getX())) + + ((corner1.getX() - corner3.getX()) * (imageY - corner3.getY()))) * denominator; + float weight3 = 1 - weight1 - weight2; + + if (weight1 < 0 || weight2 < 0 || weight3 < 0) { + // Outside of triangle + data[yPos * w + xPos] = 0; + } else { + Color c = mergeColors(weight1, color1, weight2, color2, weight3, color3); + data[yPos * w + xPos] = c.getRGB(); + } + } + } + + return raster; + } + + private Color mergeColors(float weight1, Color color1, float weight2, Color color2, float weight3, + Color color3) { + float normalize = 1f / (weight1 + weight2 + weight3); + float r = (color1.getRed() * weight1 + color2.getRed() * weight2 + color3.getRed() * weight3) * normalize; + float g = (color1.getGreen() * weight1 + color2.getGreen() * weight2 + color3.getGreen() * weight3) + * normalize; + float b = (color1.getBlue() * weight1 + color2.getBlue() * weight2 + color3.getBlue() * weight3) + * normalize; + return new Color((int) r, (int) g, (int) b); + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java index a292335342..762acb5c85 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; @@ -28,6 +29,7 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType; */ @NonNullByDefault public class Hexagon extends Shape { + public Hexagon(ShapeType shapeType, int panelId, Point2D position, int orientation) { super(shapeType, panelId, position, orientation); } @@ -45,12 +47,12 @@ public class Hexagon extends Shape { } @Override - public Point2D labelPosition(Graphics2D graphics, List outline) { + protected ImagePoint2D labelPosition(Graphics2D graphics, List outline) { Point2D[] bounds = findBounds(outline); int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2; Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); - return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2)); + return new ImagePoint2D(midX - (int) (rect.getWidth() / 2), midY + (int) (rect.getHeight() / 2)); } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java new file mode 100644 index 0000000000..873333a76b --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java @@ -0,0 +1,152 @@ +/** + * 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.shape; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; +import org.openhab.binding.nanoleaf.internal.model.PositionDatum; +import org.openhab.core.library.types.HSBType; + +/** + * A hexagon shape. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class HexagonCorners extends Panel { + + private static final int CORNER_DIAMETER = 4; + + private final List corners; + + public HexagonCorners(ShapeType shapeType, List corners) { + super(shapeType); + + this.corners = Collections.unmodifiableList(new ArrayList<>(corners)); + } + + @Override + public List generateOutline() { + List result = new ArrayList<>(corners.size()); + for (PositionDatum corner : corners) { + result.add(new Point2D(corner.getPosX(), corner.getPosY())); + } + + return result; + } + + @Override + public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) { + List outline = settings.generateImagePoints(generateOutline()); + Polygon p = new Polygon(); + for (int i = 0; i < outline.size(); i++) { + ImagePoint2D pos = outline.get(i); + p.addPoint(pos.getX(), pos.getY()); + } + + if (settings.shouldFillWithColor()) { + Color averageColor = getAverageColor(state); + graphics.setColor(averageColor); + graphics.fillPolygon(p); + + // Draw color cradient + ImagePoint2D center = findCenter(outline); + for (int i = 0; i < outline.size(); i++) { + ImagePoint2D corner1Pos = outline.get(i); + ImagePoint2D corner2Pos = outline.get((i + 1) % outline.size()); + + PositionDatum corner1 = corners.get(i); + PositionDatum corner2 = corners.get((i + 1) % outline.size()); + + Color corner1Color = getColor(corner1.getPanelId(), state); + Color corner2Color = getColor(corner2.getPanelId(), state); + graphics.setPaint(new BarycentricTriangleGradient( + new ImagePoint2D(corner1Pos.getX(), corner1Pos.getY()), corner1Color, + new ImagePoint2D(corner2Pos.getX(), corner2Pos.getY()), corner2Color, center, averageColor)); + + Polygon wedge = new Polygon(); + wedge.addPoint(corner1Pos.getX(), corner1Pos.getY()); + wedge.addPoint(corner2Pos.getX(), corner2Pos.getY()); + wedge.addPoint(center.getX(), center.getY()); + graphics.fillPolygon(p); + } + } + + if (settings.shouldDrawOutline()) { + graphics.setColor(settings.getOutlineColor()); + graphics.drawPolygon(p); + } + + if (settings.shouldDrawCorners()) { + for (PositionDatum corner : corners) { + ImagePoint2D position = settings.generateImagePoint(new Point2D(corner.getPosX(), corner.getPosY())); + graphics.setColor(getColor(corner.getPanelId(), state)); + graphics.fillOval(position.getX() - CORNER_DIAMETER / 2, position.getY() - CORNER_DIAMETER / 2, + CORNER_DIAMETER, CORNER_DIAMETER); + + if (settings.shouldDrawOutline()) { + graphics.setColor(settings.getOutlineColor()); + graphics.drawOval(position.getX() - CORNER_DIAMETER / 2, position.getY() - CORNER_DIAMETER / 2, + CORNER_DIAMETER, CORNER_DIAMETER); + } + } + } + + if (settings.shouldDrawLabels()) { + graphics.setColor(settings.getLabelColor()); + + for (PositionDatum corner : corners) { + ImagePoint2D position = settings.generateImagePoint(new Point2D(corner.getPosX(), corner.getPosY())); + graphics.drawString(Integer.toString(corner.getPanelId()), position.getX(), position.getY()); + } + } + } + + private ImagePoint2D findCenter(List outline) { + Point2D[] bounds = findBounds(outline); + int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; + int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2; + return new ImagePoint2D(midX, midY); + } + + private static Color getColor(int panelId, PanelState state) { + HSBType color = state.getHSBForPanel(panelId); + return new Color(color.getRGB()); + } + + private Color getAverageColor(PanelState state) { + float r = 0; + float g = 0; + float b = 0; + for (PositionDatum corner : corners) { + Color c = getColor(corner.getPanelId(), state); + r += c.getRed() * c.getRed(); + g += c.getGreen() * c.getGreen(); + b += c.getBlue() * c.getBlue(); + } + + return new Color((int) Math.sqrt((double) r / corners.size()), (int) Math.sqrt((double) g / corners.size()), + (int) Math.sqrt((double) b / corners.size())); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java new file mode 100644 index 0000000000..ef831425ad --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java @@ -0,0 +1,79 @@ +/** + * 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.shape; + +import java.awt.Graphics2D; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; + +/** + * Panel is a physical piece of plastic you place on the wall and connect to other panels. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public abstract class Panel { + private final ShapeType shapeType; + + public Panel(ShapeType shapeType) { + this.shapeType = shapeType; + } + + public ShapeType getShapeType() { + return shapeType; + } + + /** + * Calculates the minimal bounding rectangle around an outline. + * + * @param outline The outline to find the minimal bounding rectangle around + * @return The opposite points of the minimum bounding rectangle around this shape. + */ + public Point2D[] findBounds(List outline) { + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + + for (ImagePoint2D point : outline) { + maxX = Math.max(point.getX(), maxX); + maxY = Math.max(point.getY(), maxY); + minX = Math.min(point.getX(), minX); + minY = Math.min(point.getY(), minY); + } + + return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) }; + } + + /** + * Generate the outline of the shape. + * + * @return The points that make up this shape. + */ + public abstract List generateOutline(); + + /** + * Draws the shape on the the supplied graphics. + * + * @param graphics The picture to draw on + * @param settings Information on how to draw + * @param state The state of the panels to draw + */ + public abstract void draw(Graphics2D graphics, DrawingSettings settings, PanelState state); +} 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 new file mode 100644 index 0000000000..ec15a0f674 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java @@ -0,0 +1,93 @@ +/** + * 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.shape; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.Queue; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; +import org.openhab.binding.nanoleaf.internal.model.PositionDatum; + +/** + * Create the correct chape for a given shape type. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class PanelFactory { + + public static List createPanels(List panels) { + List result = new ArrayList<>(panels.size()); + Deque panelStack = new ArrayDeque<>(panels); + while (!panelStack.isEmpty()) { + PositionDatum panel = panelStack.peek(); + final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); + Panel shape = createPanel(shapeType, takeFirst(shapeType.getNumLightsPerShape(), panelStack)); + result.add(shape); + } + + return result; + } + + /** + * Return the first n elements from the stack. + * + * @param n The number of elements to return + * @param stack The stack top get elements from + * @return The first n elements of the stack. + */ + private static <@NonNull T> List<@NonNull T> takeFirst(int n, Queue queue) { + List result = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + var res = queue.poll(); + if (res != null) { + result.add(res); + } + } + + return result; + } + + private static Panel createPanel(ShapeType shapeType, List positionDatum) { + switch (shapeType.getDrawingAlgorithm()) { + case SQUARE: + PositionDatum squareShape = positionDatum.get(0); + Point2D pos1 = new Point2D(squareShape.getPosX(), squareShape.getPosY()); + return new Square(shapeType, squareShape.getPanelId(), pos1, squareShape.getOrientation()); + + case TRIANGLE: + PositionDatum triangleShape = positionDatum.get(0); + Point2D pos2 = new Point2D(triangleShape.getPosX(), triangleShape.getPosY()); + return new Triangle(shapeType, triangleShape.getPanelId(), pos2, triangleShape.getOrientation()); + + case HEXAGON: + PositionDatum hexShape = positionDatum.get(0); + Point2D pos3 = new Point2D(hexShape.getPosX(), hexShape.getPosY()); + return new Hexagon(shapeType, hexShape.getPanelId(), pos3, hexShape.getOrientation()); + + case CORNER: + return new HexagonCorners(shapeType, positionDatum); + + default: + PositionDatum shape = positionDatum.get(0); + Point2D pos4 = new Point2D(shape.getPosX(), shape.getPosY()); + return new Point(shapeType, shape.getPanelId(), pos4); + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java index 0ae05dc2b5..e6600bcd90 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java @@ -12,13 +12,18 @@ */ package org.openhab.binding.nanoleaf.internal.layout.shape; +import java.awt.Color; import java.awt.Graphics2D; import java.util.Arrays; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; +import org.openhab.core.library.types.HSBType; /** * A shape without any area. @@ -26,18 +31,42 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType; * @author Jørgen Austvik - Initial contribution */ @NonNullByDefault -public class Point extends Shape { - public Point(ShapeType shapeType, int panelId, Point2D position, int orientation) { - super(shapeType, panelId, position, orientation); +public class Point extends Panel { + + private static final int POINT_DIAMETER = 4; + + private final Point2D position; + private final int panelId; + + public Point(ShapeType shapeType, int panelId, Point2D position) { + super(shapeType); + this.position = position; + this.panelId = panelId; } @Override public List generateOutline() { - return Arrays.asList(getPosition()); + return Arrays.asList(position); } @Override - public Point2D labelPosition(Graphics2D graphics, List outline) { - return outline.get(0); + public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) { + ImagePoint2D pos = settings.generateImagePoint(position); + + if (settings.shouldFillWithColor()) { + HSBType color = state.getHSBForPanel(panelId); + graphics.setColor(new Color(color.getRGB())); + graphics.fillOval(pos.getX(), pos.getY(), POINT_DIAMETER, POINT_DIAMETER); + } + + if (settings.shouldDrawOutline()) { + graphics.setColor(settings.getOutlineColor()); + graphics.drawOval(pos.getX(), pos.getY(), POINT_DIAMETER, POINT_DIAMETER); + } + + if (settings.shouldDrawLabels()) { + graphics.setColor(settings.getLabelColor()); + graphics.drawString(Integer.toString(panelId), pos.getX(), pos.getY()); + } } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java index 99412ba2ea..5e12c7bee2 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java @@ -12,36 +12,38 @@ */ package org.openhab.binding.nanoleaf.internal.layout.shape; +import java.awt.Color; import java.awt.Graphics2D; +import java.awt.Polygon; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; +import org.openhab.core.library.types.HSBType; /** - * Shape that can be drawn. + * Draws shapes, which are panels with a single LED. * * @author Jørgen Austvik - Initial contribution */ @NonNullByDefault -public abstract class Shape { - private final ShapeType shapeType; - private final int panelId; +public abstract class Shape extends Panel { + private final Point2D position; private final int orientation; + private final int panelId; public Shape(ShapeType shapeType, int panelId, Point2D position, int orientation) { - this.shapeType = shapeType; - this.panelId = panelId; + super(shapeType); this.position = position; this.orientation = orientation; + this.panelId = panelId; } - public int getPanelId() { - return panelId; - }; - public Point2D getPosition() { return position; } @@ -50,36 +52,45 @@ public abstract class Shape { return orientation; }; - public ShapeType getShapeType() { - return shapeType; + protected int getPanelId() { + return panelId; } + @Override + public abstract List generateOutline(); + /** - * @return The opposite points of the minimum bounding rectangle around this shape. + * @param graphics The picture to draw on + * @param outline Outline of the shape to draw inside + * @return The position where the label of the shape should be placed */ - public Point2D[] findBounds(List outline) { - int minX = Integer.MAX_VALUE; - int minY = Integer.MAX_VALUE; - int maxX = Integer.MIN_VALUE; - int maxY = Integer.MIN_VALUE; + protected abstract ImagePoint2D labelPosition(Graphics2D graphics, List outline); - for (Point2D point : outline) { - maxX = Math.max(point.getX(), maxX); - maxY = Math.max(point.getY(), maxY); - minX = Math.min(point.getX(), minX); - minY = Math.min(point.getY(), minY); + @Override + public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) { + List outline = settings.generateImagePoints(generateOutline()); + + Polygon p = new Polygon(); + for (int i = 0; i < outline.size(); i++) { + ImagePoint2D pos = outline.get(i); + p.addPoint(pos.getX(), pos.getY()); } - return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) }; - } + HSBType color = state.getHSBForPanel(getPanelId()); + graphics.setColor(new Color(color.getRGB())); + if (settings.shouldFillWithColor()) { + graphics.fillPolygon(p); + } - /** - * @return The points that make up this shape. - */ - public abstract List generateOutline(); + if (settings.shouldDrawOutline()) { + graphics.setColor(settings.getOutlineColor()); + graphics.drawPolygon(p); + } - /** - * @return The position where the label of the shape should be placed - */ - public abstract Point2D labelPosition(Graphics2D graphics, List outline); + if (settings.shouldDrawLabels()) { + graphics.setColor(settings.getLabelColor()); + ImagePoint2D textPos = labelPosition(graphics, outline); + graphics.drawString(Integer.toString(getPanelId()), textPos.getX(), textPos.getY()); + } + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java deleted file mode 100644 index 78e9ec0882..0000000000 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * 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.shape; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.nanoleaf.internal.layout.Point2D; -import org.openhab.binding.nanoleaf.internal.layout.ShapeType; -import org.openhab.binding.nanoleaf.internal.model.PositionDatum; - -/** - * Create the correct chape for a given shape type. - * - * @author Jørgen Austvik - Initial contribution - */ -@NonNullByDefault -public class ShapeFactory { - - public static Shape CreateShape(ShapeType shapeType, PositionDatum positionDatum) { - Point2D pos = new Point2D(positionDatum.getPosX(), positionDatum.getPosY()); - switch (shapeType.getDrawingAlgorithm()) { - case SQUARE: - return new Square(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); - - case TRIANGLE: - return new Triangle(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); - - case HEXAGON: - return new Hexagon(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); - - default: - return new Point(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); - } - } -} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java index a9cf762843..50c3cd8a9f 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; @@ -44,14 +45,14 @@ public class Square extends Shape { } @Override - public Point2D labelPosition(Graphics2D graphics, List outline) { + protected ImagePoint2D labelPosition(Graphics2D graphics, List outline) { // Center of square is average of oposite corners - Point2D p0 = outline.get(0); - Point2D p2 = outline.get(2); + ImagePoint2D p0 = outline.get(0); + ImagePoint2D p2 = outline.get(2); Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); - return new Point2D((p0.getX() + p2.getX()) / 2 - (int) (rect.getWidth() / 2), - (p0.getY() + p2.getY()) / 2 - (int) (rect.getHeight() / 2)); + return new ImagePoint2D((p0.getX() + p2.getX()) / 2 - (int) (rect.getWidth() / 2), + (p0.getY() + p2.getY()) / 2 + (int) (rect.getHeight() / 2)); } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java index 586e89fc6e..4bf64e0de8 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; @@ -28,6 +29,7 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType; */ @NonNullByDefault public class Triangle extends Shape { + public Triangle(ShapeType shapeType, int panelId, Point2D position, int orientation) { super(shapeType, panelId, position, orientation); } @@ -48,13 +50,13 @@ public class Triangle extends Shape { } @Override - public Point2D labelPosition(Graphics2D graphics, List outline) { - Point2D[] bounds = findBounds(outline); - int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; - int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2; + protected ImagePoint2D labelPosition(Graphics2D graphics, List outline) { + Point2D centroid = new Point2D((outline.get(0).getX() + outline.get(1).getX() + outline.get(2).getX()) / 3, + (outline.get(0).getY() + outline.get(1).getY() + outline.get(2).getY()) / 3); Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); - return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2)); + return new ImagePoint2D(centroid.getX() - (int) (rect.getWidth() / 2), + centroid.getY() + (int) (rect.getHeight() / 2)); } private boolean pointsUp() { diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties index e3ee3da9db..a730c5089d 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties @@ -40,6 +40,8 @@ channel-type.nanoleaf.swipe.label = Swipe channel-type.nanoleaf.swipe.description = Swipe over the panels channel-type.nanoleaf.layout.label = Layout channel-type.nanoleaf.layout.description = Layout of the panels +channel-type.nanoleaf.state.label = State +channel-type.nanoleaf.state.description = Current state of the panels # error messages error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller. diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml index afc9142ad9..d5ac6ea23e 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml @@ -19,6 +19,7 @@ + @@ -114,4 +115,10 @@ @text/channel-type.nanoleaf.layout.description + + Image + + @text/channel-type.nanoleaf.state.description + + 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 new file mode 100644 index 0000000000..c6344abd54 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java @@ -0,0 +1,92 @@ +/** + * 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 static org.junit.jupiter.api.Assertions.assertNotNull; +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.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openhab.binding.nanoleaf.internal.model.ControllerInfo; +import org.openhab.binding.nanoleaf.internal.model.PanelLayout; +import org.openhab.core.library.types.HSBType; + +import com.google.gson.Gson; + +/** + * Test for layout + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class NanoleafLayoutTest { + + @TempDir + static @Nullable Path temporaryDirectory; + + @ParameterizedTest + @ValueSource(strings = { "lasvegas.json", "theduck.json", "squares.json", "wings.json", "spaceinvader.json" }) + public void testFile(String fileName) throws Exception { + Path file = Path.of("src/test/resources/", fileName); + assertTrue(Files.exists(file), "File should exist: " + file); + + Gson gson = new Gson(); + ControllerInfo controllerInfo = gson.fromJson(Files.readString(file, Charset.defaultCharset()), + ControllerInfo.class); + assertNotNull(controllerInfo, "File should contain controller info: " + file); + + PanelLayout panelLayout = controllerInfo.getPanelLayout(); + assertNotNull(panelLayout, "The controller info should contain panel layout"); + + LayoutSettings settings = new LayoutSettings(true, true, true, true); + byte[] result = NanoleafLayout.render(panelLayout, new TestPanelState(), settings); + assertNotNull(result, "Should be able to render the layout: " + fileName); + assertTrue(result.length > 0, "Should get content back, but got " + result.length + "bytes"); + + Set permissions = PosixFilePermissions.fromString("rw-r--r--"); + FileAttribute> attributes = PosixFilePermissions.asFileAttribute(permissions); + Path outFile = Files.createTempFile(temporaryDirectory, fileName.replace(".json", ""), ".png", attributes); + Files.write(outFile, result); + + // For inspecting images on own computer + // Path permanentOutFile = Files.createFile(Path.of("/tmp", fileName.replace(".json", "") + ".png"), + // attributes); + // Files.write(permanentOutFile, result); + } + + private class TestPanelState extends 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]; + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json new file mode 100644 index 0000000000..3d8da0de15 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json @@ -0,0 +1,788 @@ +{ + "name": "Elements AB01", + "serialNo": "12345", + "manufacturer": "Nanoleaf", + "firmwareVersion": "6.5.1", + "hardwareVersion": "1.1-0", + "model": "NL52", + "discovery": {}, + "effects": { + "effectsList": [ + "Bloom", + "Calming Waterfall", + "Clouds", + "Ember", + "Fireflies", + "Glimmer", + "Sahara Night", + "Slow Glimmer", + "Splash", + "Sunbeam", + "Warm Waves" + ], + "select": "Slow Glimmer" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 235, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 103, + "sideLength": 67, + "positionData": [ + { + "panelId": 26651, + "x": 159, + "y": 224, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 63706, + "x": 134, + "y": 181, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 51864, + "x": 84, + "y": 181, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 23129, + "x": 59, + "y": 224, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 43801, + "x": 84, + "y": 268, + "o": 120, + "shapeType": 15 + }, + { + "panelId": 15320, + "x": 134, + "y": 268, + "o": 60, + "shapeType": 15 + }, + { + "panelId": 62298, + "x": 185, + "y": 210, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 25499, + "x": 235, + "y": 210, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 37595, + "x": 260, + "y": 166, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 538, + "x": 235, + "y": 123, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 12376, + "x": 185, + "y": 123, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 41113, + "x": 159, + "y": 166, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 41368, + "x": 285, + "y": 181, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 37850, + "x": 260, + "y": 224, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 795, + "x": 285, + "y": 268, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 62043, + "x": 335, + "y": 268, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 25242, + "x": 360, + "y": 224, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 38623, + "x": 335, + "y": 181, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 45271, + "x": 360, + "y": 282, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 8214, + "x": 386, + "y": 326, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 53590, + "x": 436, + "y": 326, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 16791, + "x": 461, + "y": 282, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 31199, + "x": 436, + "y": 239, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 59678, + "x": 386, + "y": 239, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 778, + "x": 386, + "y": 355, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 37835, + "x": 360, + "y": 398, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 25227, + "x": 386, + "y": 442, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 62026, + "x": 436, + "y": 442, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 1551, + "x": 461, + "y": 398, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 38606, + "x": 436, + "y": 355, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 11722, + "x": 335, + "y": 413, + "o": 660, + "shapeType": 15 + }, + { + "panelId": 48395, + "x": 285, + "y": 413, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 19531, + "x": 260, + "y": 456, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 56458, + "x": 285, + "y": 500, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 61128, + "x": 335, + "y": 500, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 32265, + "x": 360, + "y": 456, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 63667, + "x": 185, + "y": 558, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 26738, + "x": 235, + "y": 558, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 39218, + "x": 260, + "y": 514, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 2547, + "x": 235, + "y": 471, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 15281, + "x": 185, + "y": 471, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 43888, + "x": 159, + "y": 514, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 2795, + "x": 185, + "y": 587, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 64427, + "x": 159, + "y": 630, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 27498, + "x": 185, + "y": 674, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 22824, + "x": 235, + "y": 674, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 51689, + "x": 260, + "y": 630, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 14505, + "x": 235, + "y": 587, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 32057, + "x": 360, + "y": 514, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 35961, + "x": 386, + "y": 558, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 7352, + "x": 436, + "y": 558, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 12026, + "x": 461, + "y": 514, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 48699, + "x": 436, + "y": 471, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 20347, + "x": 386, + "y": 471, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 17188, + "x": 436, + "y": 674, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 31596, + "x": 461, + "y": 630, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 60333, + "x": 436, + "y": 587, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 6893, + "x": 386, + "y": 587, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 35372, + "x": 360, + "y": 630, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 47214, + "x": 386, + "y": 674, + "o": 120, + "shapeType": 15 + }, + { + "panelId": 4006, + "x": 285, + "y": 732, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 40807, + "x": 335, + "y": 732, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 44325, + "x": 360, + "y": 688, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 15844, + "x": 335, + "y": 645, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 52388, + "x": 285, + "y": 645, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 23653, + "x": 260, + "y": 688, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 31275, + "x": 461, + "y": 688, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 18537, + "x": 486, + "y": 732, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 55464, + "x": 536, + "y": 732, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 10728, + "x": 561, + "y": 688, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 47401, + "x": 536, + "y": 645, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 22904, + "x": 486, + "y": 645, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 48871, + "x": 461, + "y": 805, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 19106, + "x": 486, + "y": 848, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 55907, + "x": 536, + "y": 848, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 11043, + "x": 561, + "y": 805, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 48098, + "x": 536, + "y": 761, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 35232, + "x": 486, + "y": 761, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 57198, + "x": 561, + "y": 921, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 20399, + "x": 536, + "y": 877, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 32237, + "x": 486, + "y": 877, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 60716, + "x": 461, + "y": 921, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 7276, + "x": 486, + "y": 964, + "o": 120, + "shapeType": 15 + }, + { + "panelId": 36013, + "x": 536, + "y": 964, + "o": 60, + "shapeType": 15 + }, + { + "panelId": 7941, + "x": 486, + "y": 1080, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 36804, + "x": 536, + "y": 1080, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 32388, + "x": 561, + "y": 1037, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 60997, + "x": 536, + "y": 993, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 56327, + "x": 486, + "y": 993, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 19654, + "x": 461, + "y": 1037, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 23647, + "x": 587, + "y": 1051, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 28189, + "x": 561, + "y": 1095, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 65244, + "x": 587, + "y": 1138, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 3996, + "x": 637, + "y": 1138, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 40797, + "x": 662, + "y": 1095, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 27416, + "x": 637, + "y": 1051, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 9035, + "x": 260, + "y": 50, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 53771, + "x": 235, + "y": 7, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 17098, + "x": 185, + "y": 7, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 28808, + "x": 159, + "y": 50, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 57417, + "x": 185, + "y": 94, + "o": 120, + "shapeType": 15 + }, + { + "panelId": 4361, + "x": 235, + "y": 94, + "o": 60, + "shapeType": 15 + }, + { + "panelId": 0, + "x": 50, + "y": 190, + "o": 120, + "shapeType": 12 + } + ] + } + }, + "qkihnokomhartlnp": {}, + "schedules": {}, + "state": { + "brightness": { + "value": 36, + "max": 100, + "min": 0 + }, + "colorMode": "effect", + "ct": { + "value": 3803, + "max": 4000, + "min": 1500 + }, + "hue": { + "value": 0, + "max": 360, + "min": 0 + }, + "on": { + "value": true + }, + "sat": { + "value": 0, + "max": 100, + "min": 0 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json new file mode 100644 index 0000000000..2376436d19 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json @@ -0,0 +1,152 @@ +{ + "name": "Nanoleaf Light Panels", + "serialNo": "S007", + "manufacturer": "Nanoleaf", + "firmwareVersion": "5.1.0", + "hardwareVersion": "1.6-2", + "model": "NL22", + "cloudHash": {}, + "discovery": {}, + "effects": { + "effectsList": [ + "20 Minute Sunset", + "Color Burst", + "Fireworks", + "Flames", + "Forest", + "Inner Peace", + "Jungle", + "Meteor Shower", + "Nemo", + "Northern Lights", + "Paint Splatter", + "Pulse Pop Beats", + "Rhythmic Northern Lights", + "Ripple", + "Romantic", + "Snowfall", + "Sound Bar", + "Streaking Notes", + "Falling Whites" + ], + "select": "Forest" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 0, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 9, + "sideLength": 150, + "positionData": [ + { + "panelId": 145, + "x": 374, + "y": 43, + "o": 60, + "shapeType": 0 + }, + { + "panelId": 106, + "x": 374, + "y": 129, + "o": 120, + "shapeType": 0 + }, + { + "panelId": 175, + "x": 299, + "y": 173, + "o": 180, + "shapeType": 0 + }, + { + "panelId": 215, + "x": 224, + "y": 129, + "o": 0, + "shapeType": 0 + }, + { + "panelId": 231, + "x": 149, + "y": 173, + "o": 60, + "shapeType": 0 + }, + { + "panelId": 59, + "x": 74, + "y": 129, + "o": 0, + "shapeType": 0 + }, + { + "panelId": 186, + "x": 74, + "y": 43, + "o": 180, + "shapeType": 0 + }, + { + "panelId": 61, + "x": 149, + "y": 259, + "o": 240, + "shapeType": 0 + }, + { + "panelId": 94, + "x": 299, + "y": 259, + "o": 240, + "shapeType": 0 + } + ] + } + }, + "rhythm": { + "auxAvailable": false, + "firmwareVersion": "2.4.3", + "hardwareVersion": "2.0", + "rhythmActive": false, + "rhythmConnected": true, + "rhythmId": 123, + "rhythmMode": 0, + "rhythmPos": { + "x": 0.0, + "y": 0.0, + "o": 240.0 + } + }, + "schedules": {}, + "state": { + "brightness": { + "value": 100, + "max": 100, + "min": 0 + }, + "colorMode": "effect", + "ct": { + "value": 6500, + "max": 6500, + "min": 1200 + }, + "hue": { + "value": 0, + "max": 360, + "min": 0 + }, + "on": { + "value": false + }, + "sat": { + "value": 0, + "max": 100, + "min": 0 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json new file mode 100644 index 0000000000..4eec9a489d --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json @@ -0,0 +1,172 @@ +{ + "name": "Canvas Squares", + "serialNo": "S987654321", + "manufacturer": "Nanoleaf", + "firmwareVersion": "6.5.1", + "hardwareVersion": "2.2-4", + "model": "NL29", + "discovery": {}, + "effects": { + "effectsList": [ + "Bedtime", + "Color Burst", + "Falling Whites", + "Fireworks", + "Fireworks and Firecrackers", + "Flames", + "Forest", + "Inner Peace", + "Meteor Shower", + "Nemo", + "Northern Lights", + "Paint Splatter", + "Pulse Pop Beats", + "Radial Sound Bar", + "Rhythmic Northern Lights", + "Romantic", + "Sound Bar", + "Streaking Notes" + ], + "select": "*Solid*" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 0, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 14, + "sideLength": 100, + "positionData": [ + { + "panelId": 12250, + "x": 300, + "y": 0, + "o": 0, + "shapeType": 3 + }, + { + "panelId": 8134, + "x": 300, + "y": 100, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 58086, + "x": 200, + "y": 100, + "o": 270, + "shapeType": 2 + }, + { + "panelId": 38724, + "x": 300, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 48111, + "x": 200, + "y": 200, + "o": 270, + "shapeType": 2 + }, + { + "panelId": 56093, + "x": 100, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 55836, + "x": 0, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 31413, + "x": 100, + "y": 300, + "o": 90, + "shapeType": 2 + }, + { + "panelId": 9162, + "x": 300, + "y": 300, + "o": 90, + "shapeType": 2 + }, + { + "panelId": 13276, + "x": 400, + "y": 300, + "o": 90, + "shapeType": 2 + }, + { + "panelId": 17870, + "x": 400, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 5164, + "x": 500, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 64279, + "x": 600, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 39755, + "x": 500, + "y": 100, + "o": 90, + "shapeType": 2 + } + ] + } + }, + "qkihnokomhartlnp": {}, + "schedules": {}, + "state": { + "brightness": { + "value": 77, + "max": 100, + "min": 0 + }, + "colorMode": "ct", + "ct": { + "value": 2700, + "max": 6500, + "min": 1200 + }, + "hue": { + "value": 28, + "max": 360, + "min": 0 + }, + "on": { + "value": false + }, + "sat": { + "value": 66, + "max": 100, + "min": 0 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json new file mode 100644 index 0000000000..0da9bf3ddc --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json @@ -0,0 +1,129 @@ +{ + "name": "The Duck", + "serialNo": "S123", + "manufacturer": "Nanoleaf", + "firmwareVersion": "6.5.1", + "hardwareVersion": "1.2-0", + "model": "NL42", + "discovery": {}, + "effects": { + "effectsList": [ + "20 Minute Sunset", + "Beatdrop", + "Blaze", + "Cocoa Beach", + "Cotton Candy", + "Date Night", + "Hip Hop", + "Hot Sauce", + "Jungle", + "Lightscape", + "Morning Sky", + "Northern Lights", + "Pop Rocks", + "Prism", + "Starlight", + "Sundown", + "Waterfall" + ], + "select": "*Solid*" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 59, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 8, + "sideLength": 0, + "positionData": [ + { + "panelId": 49632, + "x": 59, + "y": 56, + "o": 0, + "shapeType": 8 + }, + { + "panelId": 34671, + "x": 126, + "y": 56, + "o": 60, + "shapeType": 9 + }, + { + "panelId": 36406, + "x": 126, + "y": 95, + "o": 120, + "shapeType": 9 + }, + { + "panelId": 39807, + "x": 159, + "y": 114, + "o": 180, + "shapeType": 9 + }, + { + "panelId": 42632, + "x": 159, + "y": 153, + "o": 120, + "shapeType": 9 + }, + { + "panelId": 15767, + "x": 126, + "y": 172, + "o": 180, + "shapeType": 9 + }, + { + "panelId": 32797, + "x": 126, + "y": 250, + "o": 120, + "shapeType": 7 + }, + { + "panelId": 0, + "x": 0, + "y": 52, + "o": 60, + "shapeType": 12 + } + ] + } + }, + "qkihnokomhartlnp": {}, + "schedules": {}, + "state": { + "brightness": { + "value": 100, + "max": 100, + "min": 0 + }, + "colorMode": "hs", + "ct": { + "value": 5000, + "max": 6500, + "min": 1200 + }, + "hue": { + "value": 40, + "max": 360, + "min": 0 + }, + "on": { + "value": false + }, + "sat": { + "value": 60, + "max": 100, + "min": 0 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json new file mode 100644 index 0000000000..24d854b678 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json @@ -0,0 +1,143 @@ +{ + "name": "Winds", + "serialNo": "S123456789", + "manufacturer": "Nanoleaf", + "firmwareVersion": "6.5.1", + "hardwareVersion": "1.6-0", + "model": "NL42", + "discovery": {}, + "effects": { + "effectsList": [ + "Beatdrop", + "Blaze", + "Cocoa Beach", + "Cotton Candy", + "Date Night", + "Hip Hop", + "Hot Sauce", + "Jungle", + "Lightscape", + "Morning Sky", + "Northern Lights", + "Pop Rocks", + "Prism", + "Starlight", + "Sundown", + "Waterfall", + "Falling Whites" + ], + "select": "*Solid*" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 299, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 10, + "sideLength": 134, + "positionData": [ + { + "panelId": 1837, + "x": 268, + "y": 437, + "o": 0, + "shapeType": 8 + }, + { + "panelId": 37923, + "x": 234, + "y": 534, + "o": 180, + "shapeType": 8 + }, + { + "panelId": 59975, + "x": 167, + "y": 611, + "o": 240, + "shapeType": 8 + }, + { + "panelId": 20510, + "x": 100, + "y": 650, + "o": 300, + "shapeType": 8 + }, + { + "panelId": 31270, + "x": 0, + "y": 669, + "o": 120, + "shapeType": 8 + }, + { + "panelId": 25862, + "x": 335, + "y": 359, + "o": 60, + "shapeType": 8 + }, + { + "panelId": 24968, + "x": 368, + "y": 263, + "o": 0, + "shapeType": 8 + }, + { + "panelId": 923, + "x": 368, + "y": 185, + "o": 60, + "shapeType": 8 + }, + { + "panelId": 34168, + "x": 335, + "y": 89, + "o": 120, + "shapeType": 8 + }, + { + "panelId": 0, + "x": 234, + "y": 388, + "o": 180, + "shapeType": 12 + } + ] + } + }, + "qkihnokomhartlnp": {}, + "schedules": {}, + "state": { + "brightness": { + "value": 100, + "max": 100, + "min": 0 + }, + "colorMode": "hs", + "ct": { + "value": 2700, + "max": 6500, + "min": 1200 + }, + "hue": { + "value": 45, + "max": 360, + "min": 0 + }, + "on": { + "value": false + }, + "sat": { + "value": 80, + "max": 100, + "min": 0 + } + } +}