]> git.basschouten.com Git - openhab-addons.git/commitdiff
[Nanoleaf] New Channel: State (#13746)
authorJørgen Austvik <jaustvik@acm.org>
Fri, 2 Dec 2022 20:14:53 +0000 (21:14 +0100)
committerGitHub <noreply@github.com>
Fri, 2 Dec 2022 20:14:53 +0000 (21:14 +0100)
* [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 <jaustvik@acm.org>
31 files changed:
bundles/org.openhab.binding.nanoleaf/README.md
bundles/org.openhab.binding.nanoleaf/doc/Layout.png
bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java [deleted file]
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java
bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java
bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties
bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml
bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json [new file with mode: 0644]
bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json [new file with mode: 0644]

index b8a635b936fac97afef1dc55bf80fb8e674a551b..39816999e6d693cefc2768eaac9ea6d49cdd1013 100644 (file)
@@ -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       |
 
 
 
index a8d684a0ce019fbf7009b9d57351c70d473aa48b..d716ffae1e27f8fa7d02069f7e4048720427cf38 100644 (file)
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 (file)
index 0000000..8665547
Binary files /dev/null and b/bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png differ
index e7e8a2fb5a7abe372300514bce4c0e81a48d7da8..db83c1fd78921cd8c3908f924f1cb6d95a570db5 100644 (file)
@@ -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";
index 41538ee05b5c737ada2f18acf8c8f374a4ee11f3..aede5dbb53659d527daa1254b4af40d5c6e294d0 100644 (file)
@@ -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)) {
index bd2efd45546cf26ee5126f14f83bf9695d48d1a4..d37a82f06c7145ab569e1801d75c2a2cf054ea3e 100644 (file)
@@ -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<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
+    private final List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
     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<Thing> 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<Thing> 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;
index 2c1658520657aefea4cfdb96d913ebc8d24e34d2..995eb8a3479e327a292febff790e0adf1d6506bf 100644 (file)
@@ -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<String, HSBType> panelInfo = new HashMap<>();
+    private final Map<String, HSBType> 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 (file)
index 0000000..be16a3b
--- /dev/null
@@ -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<ImagePoint2D> generateImagePoints(List<Point2D> 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<ImagePoint2D> toPictureLayout(List<Point2D> originals, int imageHeight, ImagePoint2D min,
+            double rotationRadians) {
+        List<ImagePoint2D> 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 (file)
index 0000000..4dcb006
--- /dev/null
@@ -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 (file)
index 0000000..2364f55
--- /dev/null
@@ -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;
+    }
+}
index 70724333eca8478e1251163f9c47f11ea090b9dd..61ceaa5497d7263e55c2a9d75de06e0b2bb3e737 100644 (file)
@@ -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<PositionDatum> panels = layout.getPositionData();
-        if (panels == null) {
+        List<PositionDatum> 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<Point2D> 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<Panel> 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<PositionDatum> panels, double rotationRadians) {
+    private static ImagePoint2D[] findSize(List<PositionDatum> 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<Panel> 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<Point2D> toPictureLayout(List<Point2D> originals, int imageHeight, Point2D min,
-            double rotationRadians) {
-        List<Point2D> result = new ArrayList<Point2D>(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 (file)
index 0000000..fba1804
--- /dev/null
@@ -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<Integer, HSBType> panelStates = new HashMap<>();
+
+    public PanelState(List<Thing> panels) {
+        for (Thing panel : panels) {
+            Integer panelId = Integer.valueOf(panel.getConfiguration().get(CONFIG_PANEL_ID).toString());
+            NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) panel.getHandler();
+            if (panelHandler != null) {
+                HSBType c = panelHandler.getColor();
+                HSBType color = (c == null) ? HSBType.BLACK : c;
+                panelStates.put(panelId, color);
+            }
+        }
+    }
+
+    public HSBType getHSBForPanel(Integer panelId) {
+        return panelStates.getOrDefault(panelId, HSBType.BLACK);
+    }
+}
index f90262e0e7ecef2972771305f2ac6c7570bc48ac..471dafb362268097996ba9c13cd80166b321ba69 100644 (file)
@@ -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 (file)
index 0000000..e04e43f
--- /dev/null
@@ -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);
+        }
+    }
+}
index a292335342d5ff94990cfff87f0cda45ac0357a4..762acb5c8528095fa231e313e1b7c697b77bb33d 100644 (file)
@@ -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<Point2D> outline) {
+    protected ImagePoint2D labelPosition(Graphics2D graphics, List<ImagePoint2D> 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 (file)
index 0000000..873333a
--- /dev/null
@@ -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<PositionDatum> corners;
+
+    public HexagonCorners(ShapeType shapeType, List<PositionDatum> corners) {
+        super(shapeType);
+
+        this.corners = Collections.unmodifiableList(new ArrayList<>(corners));
+    }
+
+    @Override
+    public List<Point2D> generateOutline() {
+        List<Point2D> 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<ImagePoint2D> 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<ImagePoint2D> 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 (file)
index 0000000..ef83142
--- /dev/null
@@ -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<ImagePoint2D> 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<Point2D> 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 (file)
index 0000000..ec15a0f
--- /dev/null
@@ -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<Panel> createPanels(List<PositionDatum> panels) {
+        List<Panel> result = new ArrayList<>(panels.size());
+        Deque<PositionDatum> 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<T> queue) {
+        List<T> 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> 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);
+        }
+    }
+}
index 0ae05dc2b55b2987529a4a18ab600369e17f40b0..e6600bcd90895cdbae2cea17f76865afe911e357 100644 (file)
  */
 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<Point2D> generateOutline() {
-        return Arrays.asList(getPosition());
+        return Arrays.asList(position);
     }
 
     @Override
-    public Point2D labelPosition(Graphics2D graphics, List<Point2D> 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());
+        }
     }
 }
index 99412ba2ea63e4594c02d3b7c40c1ac534639265..5e12c7bee26206796778e1060c4c1c7f39dd2656 100644 (file)
  */
 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<Point2D> 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<Point2D> 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<ImagePoint2D> 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<ImagePoint2D> 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<Point2D> 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<Point2D> 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 (file)
index 78e9ec0..0000000
+++ /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());
-        }
-    }
-}
index a9cf762843df2b973fa50f27f9fff862338cb19c..50c3cd8a9f78e33028ddc9822c0c8ac12e3c1d2c 100644 (file)
@@ -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<Point2D> outline) {
+    protected ImagePoint2D labelPosition(Graphics2D graphics, List<ImagePoint2D> 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));
     }
 }
index 586e89fc6e78aac618036ec2210df2b0694d916e..4bf64e0de8d35f1c7e330de05e44367441047421 100644 (file)
@@ -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<Point2D> 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<ImagePoint2D> 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() {
index e3ee3da9dbd8c7dccbabab21e74f93e28647a5c8..a730c5089d690867fe66103a9730f94217fb691c 100644 (file)
@@ -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.
index afc9142ad9e307e18470875cede2c946cfa3d3f9..d5ac6ea23e585daf48877b24cc8d54d5ddcfde0a 100644 (file)
@@ -19,6 +19,7 @@
                        <channel id="rhythmMode" typeId="rhythmMode"/>
                        <channel id="swipe" typeId="swipe"/>
                        <channel id="layout" typeId="layout"/>
+                       <channel id="currentState" typeId="state"/>
                </channels>
 
                <properties>
                <description>@text/channel-type.nanoleaf.layout.description</description>
        </channel-type>
 
+       <channel-type id="state">
+               <item-type>Image</item-type>
+               <label>@text/channel-type.nanoleaf.state.label</label>
+               <description>@text/channel-type.nanoleaf.state.description</description>
+       </channel-type>
+
 </thing:thing-descriptions>
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 (file)
index 0000000..c6344ab
--- /dev/null
@@ -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<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-r--r--");
+        FileAttribute<Set<PosixFilePermission>> 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 (file)
index 0000000..3d8da0d
--- /dev/null
@@ -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 (file)
index 0000000..2376436
--- /dev/null
@@ -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 (file)
index 0000000..4eec9a4
--- /dev/null
@@ -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 (file)
index 0000000..0da9bf3
--- /dev/null
@@ -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 (file)
index 0000000..24d854b
--- /dev/null
@@ -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
+        }
+    }
+}