[Nanoleaf] Visualize layout (#13552)

* Visualize Nanoleaf layout
* Only calculate image if channel is linked
* White background image
* Render more shapes

Signed-off-by: Jørgen Austvik <jaustvik@acm.org>
This commit is contained in:
Jørgen Austvik
2022-11-12 23:00:08 +01:00
committed by GitHub
parent 160e0c2548
commit fbd06ec709
25 changed files with 1278 additions and 29 deletions

View File

@@ -58,6 +58,7 @@ public class NanoleafBindingConstants {
public static final String CHANNEL_SWIPE_EVENT_DOWN = "DOWN";
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";
// List of light panel channels
public static final String CHANNEL_PANEL_COLOR = "color";
@@ -78,7 +79,7 @@ public class NanoleafBindingConstants {
public static final String API_MIN_FW_VER_CANVAS = "1.1.0";
public static final String MODEL_ID_LIGHTPANELS = "NL22";
public static final List<String> MODELS_WITH_TOUCHSUPPORT = Arrays.asList("NL29", "NL42");
public static final List<String> MODELS_WITH_TOUCHSUPPORT = Arrays.asList("NL29", "NL42", "NL47", "NL48", "NL52");
public static final String DEVICE_TYPE_LIGHTPANELS = "lightPanels";
public static final String DEVICE_TYPE_TOUCHSUPPORT = "canvas"; // we need to keep this enum for backward
// compatibility even though not only canvas type
@@ -93,4 +94,8 @@ public class NanoleafBindingConstants {
// Color channels increase/decrease brightness step size
public static final int BRIGHTNESS_STEP_SIZE = 5;
// Layout rendering
public static final int LAYOUT_LIGHT_RADIUS = 8;
public static final int LAYOUT_BORDER_WIDTH = 30;
}

View File

@@ -154,11 +154,7 @@ public class OpenAPIUtils {
for (int i = 0; i < currentVer.length; ++i) {
if (currentVer[i] != requiredVer[i]) {
if (currentVer[i] > requiredVer[i]) {
return true;
}
return false;
return (currentVer[i] > requiredVer[i]);
}
}

View File

@@ -14,6 +14,7 @@ package org.openhab.binding.nanoleaf.internal.handler;
import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
@@ -43,6 +44,7 @@ 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.NanoleafLayout;
import org.openhab.binding.nanoleaf.internal.model.AuthToken;
import org.openhab.binding.nanoleaf.internal.model.BooleanState;
import org.openhab.binding.nanoleaf.internal.model.Brightness;
@@ -65,6 +67,7 @@ import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@@ -72,6 +75,7 @@ import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
@@ -103,6 +107,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
private @Nullable HttpClient httpClientSSETouchEvent;
private @Nullable Request sseTouchjobRequest;
private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
private PanelLayout previousPanelLayout = new PanelLayout();
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
@@ -664,6 +669,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
updateProperties();
updateConfiguration();
updateLayout(controllerInfo.getPanelLayout());
for (NanoleafControllerListener controllerListener : controllerListeners) {
controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
@@ -705,6 +711,33 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
}
}
private void updateLayout(PanelLayout panelLayout) {
ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT);
ThingHandlerCallback callback = getCallback();
if (callback != null) {
if (!callback.isChannelLinked(layoutChannel)) {
// Don't generate image unless it is used
return;
}
}
if (previousPanelLayout.equals(panelLayout)) {
logger.trace("Not rendering panel layout as it is the same as previous rendered panel layout");
return;
}
try {
byte[] bytes = NanoleafLayout.render(panelLayout);
if (bytes.length > 0) {
updateState(CHANNEL_LAYOUT, new RawType(bytes, "image/png"));
}
previousPanelLayout = panelLayout;
} catch (IOException ioex) {
logger.warn("Failed to create layout image", ioex);
}
}
private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
ContentResponse controllerlInfoJSON = OpenAPIUtils.sendOpenAPIRequest(OpenAPIUtils.requestBuilder(httpClient,
getControllerConfig(), API_GET_CONTROLLER_INFO, HttpMethod.GET));

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nanoleaf.internal.layout;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Differentiates how shapes must be drawn
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public enum DrawingAlgorithm {
NONE,
SQUARE,
TRIANGLE,
HEXAGON,
CORNER,
LINE;
}

View File

@@ -0,0 +1,184 @@
/**
* 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.awt.Graphics2D;
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.model.GlobalOrientation;
import org.openhab.binding.nanoleaf.internal.model.Layout;
import org.openhab.binding.nanoleaf.internal.model.PanelLayout;
import org.openhab.binding.nanoleaf.internal.model.PositionDatum;
/**
* Renders the Nanoleaf layout to an image.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
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 {
double rotationRadians = 0;
GlobalOrientation globalOrientation = panelLayout.getGlobalOrientation();
if (globalOrientation != null) {
rotationRadians = calculateRotationRadians(globalOrientation);
}
Layout layout = panelLayout.getLayout();
if (layout == null) {
return new byte[] {};
}
List<PositionDatum> panels = layout.getPositionData();
if (panels == 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;
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.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());
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(image, "png", out);
return out.toByteArray();
}
private static double calculateRotationRadians(GlobalOrientation globalOrientation) {
Integer maxObj = globalOrientation.getMax();
int maxValue = maxObj == null ? 360 : (int) maxObj;
int value = globalOrientation.getValue(); // 0 - 360 measured counter clockwise.
return ((double) (maxValue - value)) * (Math.PI / 180);
}
private static Point2D[] findSize(Collection<PositionDatum> panels, 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);
for (Point2D point : shape.generateOutline()) {
var rotated = point.rotate(rotationRadians);
maxX = Math.max(rotated.getX(), maxX);
maxY = Math.max(rotated.getY(), maxY);
minX = Math.min(rotated.getX(), minX);
minY = Math.min(rotated.getY(), minY);
}
}
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;
}
}

View File

@@ -0,0 +1,81 @@
/**
* 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 2D space.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class Point2D {
private final int x;
private final int y;
public Point2D(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
/**
* Rotates the point a given amount of radians.
*
* @param radians The amount to rotate the point
* @return A new point which is rotated
*/
public Point2D rotate(double radians) {
double sinAngle = Math.sin(radians);
double cosAngle = Math.cos(radians);
int newX = (int) (cosAngle * x - sinAngle * y);
int newY = (int) (sinAngle * x + cosAngle * y);
return new Point2D(newX, newY);
}
/**
* Move the point in x and y direction.
*
* @param moveX Amount to move in x direction
* @param moveY Amount to move in y direction
* @return
*/
public Point2D move(int moveX, int moveY) {
return new Point2D(getX() + moveX, getY() + moveY);
}
/**
* Move the point in x and y direction,.
*
* @param offset Offset to move
* @return
*/
public Point2D move(Point2D offset) {
return move(offset.getX(), offset.getY());
}
@Override
public String toString() {
return String.format("x:%d, y:%d", x, y);
}
}

View File

@@ -0,0 +1,87 @@
/**
* 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;
/**
* Information about the different Nanoleaf shapes.
*
* @author Jørgen Austvik - Initial contribution
*/
@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);
private final String name;
private final int id;
private final double sideLength;
private final int numSides;
private final DrawingAlgorithm drawingAlgorithm;
ShapeType(String name, int id, double sideLenght, int numSides, DrawingAlgorithm drawingAlgorithm) {
this.name = name;
this.id = id;
this.sideLength = sideLenght;
this.numSides = numSides;
this.drawingAlgorithm = drawingAlgorithm;
}
public String getName() {
return name;
}
public int getId() {
return id;
}
public double getSideLength() {
return sideLength;
}
public int getNumSides() {
return numSides;
}
public DrawingAlgorithm getDrawingAlgorithm() {
return drawingAlgorithm;
}
public static ShapeType valueOf(int id) {
for (ShapeType shapeType : values()) {
if (shapeType.getId() == id) {
return shapeType;
}
}
return ShapeType.UNKNOWN;
}
}

View File

@@ -0,0 +1,56 @@
/**
* 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.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.layout.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
/**
* A hexagon shape.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class Hexagon extends Shape {
public Hexagon(ShapeType shapeType, int panelId, Point2D position, int orientation) {
super(shapeType, panelId, position, orientation);
}
@Override
public List<Point2D> generateOutline() {
Point2D v1 = new Point2D((int) getShapeType().getSideLength(), 0);
Point2D v2 = v1.rotate((1.0 / 3.0) * Math.PI);
Point2D v3 = v1.rotate((2.0 / 3.0) * Math.PI);
Point2D v4 = v1.rotate((3.0 / 3.0) * Math.PI);
Point2D v5 = v1.rotate((4.0 / 3.0) * Math.PI);
Point2D v6 = v1.rotate((5.0 / 3.0) * Math.PI);
return Arrays.asList(v1.move(getPosition()), v2.move(getPosition()), v3.move(getPosition()),
v4.move(getPosition()), v5.move(getPosition()), v6.move(getPosition()));
}
@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;
Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics);
return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2));
}
}

View File

@@ -0,0 +1,43 @@
/**
* 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.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.layout.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
/**
* A shape without any area.
*
* @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);
}
@Override
public List<Point2D> generateOutline() {
return Arrays.asList(getPosition());
}
@Override
public Point2D labelPosition(Graphics2D graphics, List<Point2D> outline) {
return outline.get(0);
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
/**
* Shape that can be drawn.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public abstract class Shape {
private final ShapeType shapeType;
private final int panelId;
private final Point2D position;
private final int orientation;
public Shape(ShapeType shapeType, int panelId, Point2D position, int orientation) {
this.shapeType = shapeType;
this.panelId = panelId;
this.position = position;
this.orientation = orientation;
}
public int getPanelId() {
return panelId;
};
public Point2D getPosition() {
return position;
}
public int getOrientation() {
return orientation;
};
public ShapeType getShapeType() {
return shapeType;
}
/**
* @return The opposite points of the minimum bounding rectangle around this shape.
*/
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;
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);
}
return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) };
}
/**
* @return The points that make up this shape.
*/
public abstract List<Point2D> generateOutline();
/**
* @return The position where the label of the shape should be placed
*/
public abstract Point2D labelPosition(Graphics2D graphics, List<Point2D> outline);
}

View File

@@ -0,0 +1,44 @@
/**
* 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());
}
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.layout.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
/**
* A square shape.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class Square extends Shape {
public Square(ShapeType shapeType, int panelId, Point2D position, int orientation) {
super(shapeType, panelId, position, orientation);
}
@Override
public List<Point2D> generateOutline() {
int sideLength = (int) getShapeType().getSideLength();
Point2D current = getPosition();
Point2D corner2 = new Point2D(current.getX() + sideLength, current.getY());
Point2D corner3 = new Point2D(current.getX() + sideLength, current.getY() + sideLength);
Point2D corner4 = new Point2D(current.getX(), current.getY() + sideLength);
return Arrays.asList(getPosition(), corner2, corner3, corner4);
}
@Override
public Point2D labelPosition(Graphics2D graphics, List<Point2D> outline) {
// Center of square is average of oposite corners
Point2D p0 = outline.get(0);
Point2D 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));
}
}

View File

@@ -0,0 +1,64 @@
/**
* 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.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.layout.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
/**
* A triangular shape.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class Triangle extends Shape {
public Triangle(ShapeType shapeType, int panelId, Point2D position, int orientation) {
super(shapeType, panelId, position, orientation);
}
@Override
public List<Point2D> generateOutline() {
int height = (int) (getShapeType().getSideLength() * Math.sqrt(3) / 2);
Point2D v1;
if (pointsUp()) {
v1 = new Point2D(0, height * 2 / 3);
} else {
v1 = new Point2D(0, -height * 2 / 3);
}
Point2D v2 = v1.rotate((2.0 / 3.0) * Math.PI);
Point2D v3 = v1.rotate((-2.0 / 3.0) * Math.PI);
return Arrays.asList(v1.move(getPosition()), v2.move(getPosition()), v3.move(getPosition()));
}
@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;
Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics);
return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2));
}
private boolean pointsUp() {
// Upward: even multiple of 60 degrees rotation
return ((getOrientation() / 60) % 2) == 0;
}
}

View File

@@ -12,6 +12,8 @@
*/
package org.openhab.binding.nanoleaf.internal.model;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -27,6 +29,15 @@ public class GlobalOrientation {
private @Nullable Integer max;
private @Nullable Integer min;
public GlobalOrientation() {
}
public GlobalOrientation(Integer min, Integer max, int value) {
this.min = min;
this.max = max;
this.value = value;
}
public int getValue() {
return value;
}
@@ -50,4 +61,30 @@ public class GlobalOrientation {
public void setMin(Integer min) {
this.min = min;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GlobalOrientation go = (GlobalOrientation) o;
return (value == go.getValue()) && (Objects.equals(min, go.getMin())) && (Objects.equals(max, go.getMax()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
Integer x = max;
Integer i = min;
result = prime * result + value;
result = prime * result + ((x == null) ? 0 : x.hashCode());
result = prime * result + ((i == null) ? 0 : i.hashCode());
return result;
}
}

View File

@@ -12,6 +12,7 @@
*/
package org.openhab.binding.nanoleaf.internal.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@@ -36,6 +37,14 @@ public class Layout {
private @Nullable List<PositionDatum> positionData = null;
public Layout() {
}
public Layout(List<PositionDatum> positionData) {
this.positionData = new ArrayList<>(positionData);
this.numPanels = positionData.size();
}
public int getNumPanels() {
return numPanels;
}
@@ -143,4 +152,48 @@ public class Layout {
return "";
}
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Layout l = (Layout) o;
if (numPanels != l.getNumPanels()) {
return false;
}
List<PositionDatum> pd = getPositionData();
List<PositionDatum> otherPd = l.getPositionData();
if (pd == null && otherPd == null) {
return true;
}
if (pd == null || otherPd == null) {
return false;
}
return pd.equals(otherPd);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getNumPanels();
List<PositionDatum> pd = getPositionData();
if (pd != null) {
for (PositionDatum p : pd) {
result = prime * result + p.hashCode();
}
}
return result;
}
}

View File

@@ -26,6 +26,14 @@ public class PanelLayout {
private @Nullable Layout layout;
private @Nullable GlobalOrientation globalOrientation;
public PanelLayout() {
}
public PanelLayout(GlobalOrientation globalOrientation, Layout layout) {
this.globalOrientation = globalOrientation;
this.layout = layout;
}
public @Nullable Layout getLayout() {
return layout;
}
@@ -41,4 +49,69 @@ public class PanelLayout {
public void setGlobalOrientation(GlobalOrientation globalOrientation) {
this.globalOrientation = globalOrientation;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PanelLayout pl = (PanelLayout) o;
// For a panel layout to be equal to another panel layouit, all inner data structures must
// be equal, or they must be null both in this object or the object it is compared with.
GlobalOrientation go = globalOrientation;
GlobalOrientation otherGo = pl.getGlobalOrientation();
boolean goEquals = false;
if (go == null || otherGo == null) {
if (go == null && otherGo == null) {
// If one of the global oriantations are null, the other must also be null
// for them to be equal
goEquals = true;
}
} else {
goEquals = go.equals(otherGo);
}
if (goEquals == false) {
// No reason to compare layout if global oriantation is different
return false;
}
Layout l = layout;
Layout otherL = pl.getLayout();
if (l == null && otherL == null) {
return true;
}
if (l == null || otherL == null) {
return false;
}
return l.equals(otherL);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
GlobalOrientation go = globalOrientation;
if (go != null) {
result = prime * result + go.hashCode();
}
Layout l = layout;
if (l != null) {
result = prime * result + l.hashCode();
}
return result;
}
}

View File

@@ -12,10 +12,9 @@
*/
package org.openhab.binding.nanoleaf.internal.model;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
import com.google.gson.annotations.SerializedName;
@@ -37,21 +36,15 @@ public class PositionDatum {
@SerializedName("shapeType")
private int shapeType;
private static Map<Integer, Integer> panelSizes = new HashMap<Integer, Integer>();
public PositionDatum() {
// initialize constant sidelengths for panels. See https://forum.nanoleaf.me/docs chapter 3.3
if (panelSizes.isEmpty()) {
panelSizes.put(0, 150); // Triangle
panelSizes.put(1, 0); // Rhythm N/A
panelSizes.put(2, 100); // Square
panelSizes.put(3, 100); // Control Square Master
panelSizes.put(4, 100); // Control Square Passive
panelSizes.put(7, 67); // Hexagon
panelSizes.put(8, 134); // Triangle Shapes
panelSizes.put(9, 67); // Mini Triangle Shapes
panelSizes.put(12, 0); // Shapes Controller (N/A)
}
}
public PositionDatum(int panelId, int posX, int posY, int orientation, int shapeType) {
this.panelId = panelId;
this.posX = posX;
this.posY = posY;
this.orientation = orientation;
this.shapeType = shapeType;
}
public int getPanelId() {
@@ -105,6 +98,33 @@ public class PositionDatum {
}
public Integer getPanelSize() {
return panelSizes.getOrDefault(shapeType, 0);
return (int) ShapeType.valueOf(shapeType).getSideLength();
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PositionDatum pd = (PositionDatum) o;
return (posX == pd.getPosX()) && (posY == pd.getPosY()) && (orientation == pd.getOrientation())
&& (shapeType == pd.getShapeType()) && (panelId == pd.getPanelId());
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + posX;
result = prime * result + posY;
result = prime * result + orientation;
result = prime * result + shapeType;
result = prime * result + panelId;
return result;
}
}

View File

@@ -38,6 +38,8 @@ channel-type.nanoleaf.tap.label = Button
channel-type.nanoleaf.tap.description = Button events of the panel
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
# error messages
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.

View File

@@ -18,6 +18,7 @@
<channel id="rhythmActive" typeId="rhythmActive"/>
<channel id="rhythmMode" typeId="rhythmMode"/>
<channel id="swipe" typeId="swipe"/>
<channel id="layout" typeId="layout"/>
</channels>
<properties>
@@ -107,4 +108,10 @@
</event>
</channel-type>
<channel-type id="layout">
<item-type>Image</item-type>
<label>@text/channel-type.nanoleaf.layout.label</label>
<description>@text/channel-type.nanoleaf.layout.description</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,66 @@
/**
* 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.model;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test for global orientation
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class GlobalOrientationTest {
@Nullable
GlobalOrientation go1;
@Nullable
GlobalOrientation go2; // Different from go1
@Nullable
GlobalOrientation go3; // Same as go1
@BeforeEach
public void setUp() {
go1 = new GlobalOrientation(0, 360, 180);
go2 = new GlobalOrientation(0, 360, 267);
go3 = new GlobalOrientation(0, 360, 180);
}
@Test
public void testHashCode() {
GlobalOrientation g1 = go1;
GlobalOrientation g2 = go2;
GlobalOrientation g3 = go3;
if (g1 != null && g2 != null && g3 != null) {
assertThat(g1.hashCode(), is(equalTo(g3.hashCode())));
assertThat(g2.hashCode(), is(not(equalTo(g3.hashCode()))));
} else {
assertThat("Should be initialized", false);
}
}
@Test
public void testEquals() {
assertThat(go1, is(equalTo(go3)));
assertThat(go2, is(not(equalTo(go3))));
assertThat(go3, is(not(equalTo(null))));
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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.model;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test for global orientation
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class LayoutTest {
@Nullable
private Layout lo1;
@Nullable
private Layout lo2; // Different from l1
@Nullable
private Layout lo3; // Same as l1
@BeforeEach
public void setUp() {
PositionDatum pd1 = new PositionDatum(100, 200, 270, 123, 12);
PositionDatum pd2 = new PositionDatum(100, 220, 240, 123, 2);
PositionDatum pd3 = new PositionDatum(100, 200, 270, 123, 12);
lo1 = new Layout(Arrays.asList(pd1, pd3));
lo2 = new Layout(Arrays.asList(pd1, pd2));
lo3 = new Layout(Arrays.asList(pd1, pd3));
}
@Test
public void testHashCode() {
Layout l1 = lo1;
Layout l2 = lo2;
Layout l3 = lo3;
if (l1 != null && l2 != null && l3 != null) {
assertThat(l1.hashCode(), is(equalTo(l3.hashCode())));
assertThat(l2.hashCode(), is(not(equalTo(l3.hashCode()))));
} else {
assertThat("Should be initialized", false);
}
}
@Test
public void testEquals() {
assertThat(lo1, is(equalTo(lo3)));
assertThat(lo2, is(not(equalTo(lo3))));
assertThat(lo3, is(not(equalTo(null))));
}
}

View File

@@ -0,0 +1,78 @@
/**
* 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.model;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test for global orientation
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class PanelLayoutTest {
@Nullable
private PanelLayout pl1;
@Nullable
private PanelLayout pl2; // Different from pl1
@Nullable
private PanelLayout pl3; // Equal to pl1
@BeforeEach
public void setUp() {
PositionDatum pd1 = new PositionDatum(100, 200, 270, 123, 12);
PositionDatum pd2 = new PositionDatum(100, 220, 240, 123, 2);
PositionDatum pd3 = new PositionDatum(100, 200, 270, 123, 12);
Layout l1 = new Layout(Arrays.asList(pd1, pd3));
Layout l2 = new Layout(Arrays.asList(pd1, pd2));
Layout l3 = new Layout(Arrays.asList(pd1, pd3));
GlobalOrientation go1 = new GlobalOrientation(0, 360, 180);
pl1 = new PanelLayout(go1, l1);
pl2 = new PanelLayout(go1, l2);
pl3 = new PanelLayout(go1, l3);
}
@Test
public void testHashCode() {
PanelLayout p1 = pl1;
PanelLayout p2 = pl2;
PanelLayout p3 = pl3;
if (p1 != null && p2 != null && p3 != null) {
assertThat(p1.hashCode(), is(equalTo(p3.hashCode())));
assertThat(p2.hashCode(), is(not(equalTo(p3.hashCode()))));
} else {
assertThat("Should be initialized", false);
}
}
@Test
public void testEquals() {
assertThat(pl1, is(equalTo(pl3)));
assertThat(pl2, is(not(equalTo(pl3))));
assertThat(pl3, is(not(equalTo(null))));
}
}

View File

@@ -0,0 +1,66 @@
/**
* 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.model;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test for global orientation
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class PositionDatumTest {
@Nullable
private PositionDatum pd1;
@Nullable
private PositionDatum pd2; // different from pd1
@Nullable
private PositionDatum pd3; // same as pd1
@BeforeEach
public void setUp() {
pd1 = new PositionDatum(100, 200, 270, 123, 12);
pd2 = new PositionDatum(100, 220, 240, 123, 2);
pd3 = new PositionDatum(100, 200, 270, 123, 12);
}
@Test
public void testHashCode() {
PositionDatum p1 = pd1;
PositionDatum p2 = pd2;
PositionDatum p3 = pd3;
if (p1 != null && p2 != null && p3 != null) {
assertThat(p1.hashCode(), is(equalTo(p3.hashCode())));
assertThat(p2.hashCode(), is(not(equalTo(p3.hashCode()))));
} else {
assertThat("Should be initialized", false);
}
}
@Test
public void testEquals() {
assertThat(pd1, is(equalTo(pd3)));
assertThat(pd2, is(not(equalTo(pd3))));
assertThat(pd3, is(not(equalTo(null))));
}
}