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
5 changes: 3 additions & 2 deletions bundles/org.openhab.binding.nanoleaf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ You can set the **color** for each panel and in the case of a Nanoleaf Canvas or
| Light Panels | NL22 | Triangles 1st Generation | X | - |
| Shapes Triangle | NL42 | Triangles 2nd Generation (rounded edges) | X | X |
| Shapes Hexagon | NL42 | Hexagons | X | X |
| Elements Hexagon | NL52 | Elements Hexagons | X | X |
stefan-hoehn marked this conversation as resolved.
Show resolved Hide resolved
| Shapes Mini Triangles | NL42 | Mini Triangles | x | X |
| Canvas | NL29 | Squares | X | X |

Expand Down Expand Up @@ -70,8 +71,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 +93,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 All @@ -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", "NL52");
austvik marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -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,13 +67,15 @@
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;
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -664,6 +669,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 +711,33 @@ private void updateProperties() {
}
}

private void updateLayout(PanelLayout panelLayout) {
austvik marked this conversation as resolved.
Show resolved Hide resolved
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)) {
austvik marked this conversation as resolved.
Show resolved Hide resolved
logger.trace("Not rendering panel layout as it is the same as previous rendered panel layout");
return;
}

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

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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
austvik marked this conversation as resolved.
Show resolved Hide resolved
LINE;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* 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 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 {

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

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) {
stefan-hoehn marked this conversation as resolved.
Show resolved Hide resolved
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 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) {
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;
}
}
Loading