Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Nanoleaf] Add channel for visualizing layout #13552

Merged
merged 13 commits into from
Nov 12, 2022
4 changes: 2 additions & 2 deletions bundles/org.openhab.binding.nanoleaf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ In this case:

### Panel Layout

Unfortunately it is not easy to find out which panel gets which id, and this becomes pretty important if you have lots of them and want to assign rules.
austvik marked this conversation as resolved.
Show resolved Hide resolved

For canvas that use square panels, you can request the layout through a [console command](https://www.openhab.org/docs/administration/console.html):

then issue the following command:
Expand All @@ -94,6 +92,8 @@ Compare the following output with the right picture at the beginning of the arti
41451

```

For other canvases, use the Layout channel on the controller to get a picture of the layout with the thing IDs.
austvik marked this conversation as resolved.
Show resolved Hide resolved

## Thing Configuration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,7 @@ public static boolean checkRequiredFirmware(@Nullable String modelId, @Nullable

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]);
austvik marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

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;
Expand Down Expand Up @@ -43,6 +44,7 @@
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;
Expand All @@ -65,6 +67,7 @@
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;
Expand Down Expand Up @@ -664,6 +667,7 @@ private void updateFromControllerInfo() throws NanoleafException {

updateProperties();
updateConfiguration();
updateLayout(controllerInfo.getPanelLayout());

for (NanoleafControllerListener controllerListener : controllerListeners) {
controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
Expand Down Expand Up @@ -705,6 +709,17 @@ private void updateProperties() {
}
}

private void updateLayout(PanelLayout panelLayout) {
austvik marked this conversation as resolved.
Show resolved Hide resolved
try {
jlaur marked this conversation as resolved.
Show resolved Hide resolved
byte[] bytes = NanoleafLayout.render(panelLayout);
if (bytes.length > 0) {
updateState(CHANNEL_LAYOUT, new RawType(bytes, "image/png"));
}
} 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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* 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.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
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.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 Nonoleaf layout to an image.
stefan-hoehn marked this conversation as resolved.
Show resolved Hide resolved
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class NanoleafLayout {

public static byte[] render(PanelLayout panelLayout) throws IOException {
double rotationRadians = 0;
GlobalOrientation globalOrientation = panelLayout.getGlobalOrientation();
if (globalOrientation != null) {
rotationRadians = calculateRotation(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);
Point2D min = size[0];
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();
for (PositionDatum panel : panels) {
final int expectedSides = ShapeType.valueOf(panel.getShapeType()).getNumSides();
var rotated = new Point2D(panel.getPosX(), panel.getPosY()).rotate(rotationRadians);

Point2D current = new Point2D(NanoleafBindingConstants.LAYOUT_BORDER_WIDTH + rotated.getX() - min.getX(),
NanoleafBindingConstants.LAYOUT_BORDER_WIDTH - rotated.getY() - min.getY());

g2.fillOval(current.getX() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2,
current.getY() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2,
NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS, NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS);
g2.drawString(Integer.toString(panel.getPanelId()), current.getX(), current.getY());

if (sideCounter == 0) {
first = current;
}

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;
}

prev = current;
}

ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(image, "png", out);
return out.toByteArray();
}

private static double calculateRotation(GlobalOrientation globalOrientation) {
stefan-hoehn marked this conversation as resolved.
Show resolved Hide resolved
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) {
var rotated = new Point2D(panel.getPosX(), panel.getPosY()).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) };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
austvik marked this conversation as resolved.
Show resolved Hide resolved
* 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;

/**
* Simple pair class.
austvik marked this conversation as resolved.
Show resolved Hide resolved
*
* @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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* Information about the different Nanoleaf shapes.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public enum ShapeType {
TRIANGLE("Triangle", 0, 150, 3),
RHYTHM("Rhythm", 1, 0, 1),
SQUUARE("Square", 2, 100, 4),
CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0),
CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0),
SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6),
SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3),
SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3),
SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0),
ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6),
ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 33.5 / 58, 6),
LINES_CONNECTOR("Lines Connector", 16, 11, 1),
LIGHT_LINES("Light Lines", 17, 154, 1),
LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1),
CONTROLLER_CAP("Controller Cap", 19, 11, 0),
POWER_CONNECTOR("Power Connector", 20, 11, 0);

private final String name;
private final int id;
private final double sideLength;
private final int numSides;

ShapeType(String name, int id, double sideLenght, int numSides) {
this.name = name;
this.id = id;
this.sideLength = sideLenght;
this.numSides = numSides;
}

public String getName() {
return name;
}

public int getId() {
return id;
}

public double getSideLength() {
return sideLength;
}

public int getNumSides() {
return numSides;
}

public static ShapeType valueOf(int id) {
for (ShapeType shapeType : values()) {
if (shapeType.getId() == id) {
return shapeType;
}
}

throw new IllegalArgumentException("Unknown shape type with id " + id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
austvik marked this conversation as resolved.
Show resolved Hide resolved
channel-type.nanoleaf.layout.description = Layout of the shapes
austvik marked this conversation as resolved.
Show resolved Hide resolved

# error messages
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>