[Nanoleaf] New Channel: State (#13746)

* [Nanoleaf] New Channel: State

Shows an image of the state of the panels with color.

Also makes the layout slightly prettier. This is less functional than the layout, and more eyecandy.

Signed-off-by: Jørgen Austvik <jaustvik@acm.org>
This commit is contained in:
Jørgen Austvik 2022-12-02 21:14:53 +01:00 committed by GitHub
parent 45cbf52cf3
commit cad69c8de5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 2418 additions and 236 deletions

View File

@ -104,7 +104,15 @@ Compare the following output with the right picture at the beginning of the arti
41451 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 ## Thing Configuration
The controller thing has the following parameters: 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 | | colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No |
| colorMode | String | Color mode of the light panels | Yes | | colorMode | String | Color mode of the light panels | Yes |
| effect | String | Selected effect of the light panels | No | | 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 | | rhythmState | Switch | Connection state of the rhythm module | Yes |
| rhythmActive | Switch | Activity 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 | | 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 |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -59,6 +59,7 @@ public class NanoleafBindingConstants {
public static final String CHANNEL_SWIPE_EVENT_LEFT = "LEFT"; public static final String CHANNEL_SWIPE_EVENT_LEFT = "LEFT";
public static final String CHANNEL_SWIPE_EVENT_RIGHT = "RIGHT"; public static final String CHANNEL_SWIPE_EVENT_RIGHT = "RIGHT";
public static final String CHANNEL_LAYOUT = "layout"; public static final String CHANNEL_LAYOUT = "layout";
public static final String CHANNEL_STATE = "state";
// List of light panel channels // List of light panel channels
public static final String CHANNEL_PANEL_COLOR = "color"; public static final String CHANNEL_PANEL_COLOR = "color";

View File

@ -57,11 +57,13 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
this.httpClientFactory = httpClientFactory; this.httpClientFactory = httpClientFactory;
} }
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) { public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
} }
@Nullable @Nullable
@Override
protected ThingHandler createHandler(Thing thing) { protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (NanoleafBindingConstants.THING_TYPE_CONTROLLER.equals(thingTypeUID)) { if (NanoleafBindingConstants.THING_TYPE_CONTROLLER.equals(thingTypeUID)) {

View 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.commanddescription.NanoleafCommandDescriptionProvider;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig; import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService; 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.NanoleafLayout;
import org.openhab.binding.nanoleaf.internal.layout.PanelState;
import org.openhab.binding.nanoleaf.internal.model.AuthToken; import org.openhab.binding.nanoleaf.internal.model.AuthToken;
import org.openhab.binding.nanoleaf.internal.model.BooleanState; import org.openhab.binding.nanoleaf.internal.model.BooleanState;
import org.openhab.binding.nanoleaf.internal.model.Brightness; 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 static final int CONNECT_TIMEOUT = 10;
private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class); private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
private HttpClientFactory httpClientFactory; private final HttpClientFactory httpClientFactory;
private HttpClient httpClient; private final HttpClient httpClient;
private @Nullable HttpClient httpClientSSETouchEvent; private @Nullable HttpClient httpClientSSETouchEvent;
private @Nullable Request sseTouchjobRequest; 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 PanelLayout previousPanelLayout = new PanelLayout();
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob; private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
@ -515,9 +517,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
localhttpSSEClientTouchEvent.setIdleTimeout(CONNECT_TIMEOUT * 1000L); localhttpSSEClientTouchEvent.setIdleTimeout(CONNECT_TIMEOUT * 1000L);
sseTouchjobRequest = localhttpSSEClientTouchEvent.newRequest(eventUri); sseTouchjobRequest = localhttpSSEClientTouchEvent.newRequest(eventUri);
final Request localSSETouchjobRequest = sseTouchjobRequest; final Request localSSETouchjobRequest = sseTouchjobRequest;
int requestHashCode = -1;
if (localSSETouchjobRequest != null) { if (localSSETouchjobRequest != null) {
requestHashCode = localSSETouchjobRequest.hashCode(); int requestHashCode = localSSETouchjobRequest.hashCode();
logger.debug("tj: triggering new touch job request {} for {} with client {}", requestHashCode, logger.debug("tj: triggering new touch job request {} for {} with client {}", requestHashCode,
thing.getUID(), eventHashcode); thing.getUID(), eventHashcode);
@ -525,23 +526,21 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
String s = StandardCharsets.UTF_8.decode(content).toString(); String s = StandardCharsets.UTF_8.decode(content).toString();
logger.debug("touch detected for controller {}", thing.getUID()); logger.debug("touch detected for controller {}", thing.getUID());
logger.trace("content {}", s); logger.trace("content {}", s);
Scanner eventContent = new Scanner(s); try (Scanner eventContent = new Scanner(s)) {
while (eventContent.hasNextLine()) {
String line = eventContent.nextLine().trim();
if (line.startsWith("data:")) {
String json = line.substring(5).trim();
while (eventContent.hasNextLine()) { try {
String line = eventContent.nextLine().trim(); TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
if (line.startsWith("data:")) { handleTouchEvents(Objects.requireNonNull(touchEvents));
String json = line.substring(5).trim(); } catch (JsonSyntaxException e) {
logger.error("Couldn't parse touch event json {}", json);
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"); logger.debug("leaving touch onContent");
}).onResponseSuccess((response) -> { }).onResponseSuccess((response) -> {
logger.trace("tj: r={} touch event SUCCESS: {}", response.getRequest(), response); logger.trace("tj: r={} touch event SUCCESS: {}", response.getRequest(), response);
@ -670,6 +669,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
updateProperties(); updateProperties();
updateConfiguration(); updateConfiguration();
updateLayout(controllerInfo.getPanelLayout()); updateLayout(controllerInfo.getPanelLayout());
updateState(controllerInfo.getPanelLayout());
for (NanoleafControllerListener controllerListener : controllerListeners) { for (NanoleafControllerListener controllerListener : controllerListeners) {
controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo); 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) { private void updateLayout(PanelLayout panelLayout) {
ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT); ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT);
ThingHandlerCallback callback = getCallback(); ThingHandlerCallback callback = getCallback();
@ -726,10 +744,13 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
return; return;
} }
Bridge bridge = getThing();
List<Thing> things = bridge.getThings();
try { 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) { if (bytes.length > 0) {
updateState(CHANNEL_LAYOUT, new RawType(bytes, "image/png")); updateState(layoutChannel, new RawType(bytes, "image/png"));
} }
previousPanelLayout = panelLayout; previousPanelLayout = panelLayout;

View File

@ -72,12 +72,12 @@ public class NanoleafPanelHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class); private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class);
private HttpClient httpClient; private final HttpClient httpClient;
// JSON parser for API responses // JSON parser for API responses
private final Gson gson = new Gson(); private final Gson gson = new Gson();
// holds current color data per panel // 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<?> singleTapJob;
private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob; private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
@ -227,7 +227,7 @@ public class NanoleafPanelHandler extends BaseThingHandler {
Write write = new Write(); Write write = new Write();
write.setCommand("display"); write.setCommand("display");
write.setAnimType("static"); 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 @Nullable
BridgeHandler handler = bridge.getHandler(); BridgeHandler handler = bridge.getHandler();
if (handler != null) { 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)); write.setAnimData(String.format("1 %s 1 %d %d %d 0 10", panelID, red, green, blue));
} else { } else {
// this is only used in special streaming situations with canvas which is not yet supported // this is only used in special streaming situations with canvas which is not yet supported
int quotient = Integer.divideUnsigned(Integer.valueOf(panelID), 256); int quotient = Integer.divideUnsigned(panelID, 256);
int remainder = Integer.remainderUnsigned(Integer.valueOf(panelID), 256); int remainder = Integer.remainderUnsigned(panelID, 256);
write.setAnimData( write.setAnimData(
String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, red, green, blue)); 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; return panelID;
} }
public @Nullable HSBType getColor() {
String panelID = getPanelID();
return panelInfo.get(panelID);
}
private @Nullable HSBType getPanelColor() { private @Nullable HSBType getPanelColor() {
String panelID = getPanelID(); String panelID = getPanelID();
@ -357,9 +362,9 @@ public class NanoleafPanelHandler extends BaseThingHandler {
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length); String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) { for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 8 == 0) { if (i % 8 == 0) {
String idQuotient = panelDataPoints[i]; Integer idQuotient = Integer.valueOf(panelDataPoints[i]);
String idRemainder = panelDataPoints[i + 1]; Integer idRemainder = Integer.valueOf(panelDataPoints[i + 1]);
Integer idNum = Integer.valueOf(idQuotient) * 256 + Integer.valueOf(idRemainder); Integer idNum = idQuotient * 256 + idRemainder;
if (String.valueOf(idNum).equals(panelID)) { if (String.valueOf(idNum).equals(panelID)) {
// found panel data - store it // found panel data - store it
panelInfo.put(panelID, panelInfo.put(panelID,

View File

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

View File

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

View File

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

View File

@ -15,19 +15,18 @@ package org.openhab.binding.nanoleaf.internal.layout;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants; import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
import org.openhab.binding.nanoleaf.internal.layout.shape.Shape; import org.openhab.binding.nanoleaf.internal.layout.shape.Panel;
import org.openhab.binding.nanoleaf.internal.layout.shape.ShapeFactory; import org.openhab.binding.nanoleaf.internal.layout.shape.PanelFactory;
import org.openhab.binding.nanoleaf.internal.model.GlobalOrientation; import org.openhab.binding.nanoleaf.internal.model.GlobalOrientation;
import org.openhab.binding.nanoleaf.internal.model.Layout; import org.openhab.binding.nanoleaf.internal.model.Layout;
import org.openhab.binding.nanoleaf.internal.model.PanelLayout; import org.openhab.binding.nanoleaf.internal.model.PanelLayout;
@ -42,11 +41,8 @@ import org.openhab.binding.nanoleaf.internal.model.PositionDatum;
public class NanoleafLayout { public class NanoleafLayout {
private static final Color COLOR_BACKGROUND = Color.WHITE; 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; double rotationRadians = 0;
GlobalOrientation globalOrientation = panelLayout.getGlobalOrientation(); GlobalOrientation globalOrientation = panelLayout.getGlobalOrientation();
if (globalOrientation != null) { if (globalOrientation != null) {
@ -58,78 +54,31 @@ public class NanoleafLayout {
return new byte[] {}; return new byte[] {};
} }
List<PositionDatum> panels = layout.getPositionData(); List<PositionDatum> positionDatums = layout.getPositionData();
if (panels == null) { if (positionDatums == null) {
return new byte[] {}; return new byte[] {};
} }
Point2D size[] = findSize(panels, rotationRadians); ImagePoint2D size[] = findSize(positionDatums, rotationRadians);
final Point2D min = size[0]; final ImagePoint2D min = size[0];
final Point2D max = size[1]; final ImagePoint2D max = size[1];
Point2D prev = null;
Point2D first = null;
int sideCounter = 0;
BufferedImage image = new BufferedImage( BufferedImage image = new BufferedImage(
(max.getX() - min.getX()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH, (max.getX() - min.getX()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH,
(max.getY() - min.getY()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH, (max.getY() - min.getY()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH,
BufferedImage.TYPE_INT_RGB); BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = image.createGraphics(); 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.setBackground(COLOR_BACKGROUND);
g2.clearRect(0, 0, image.getWidth(), image.getHeight()); g2.clearRect(0, 0, image.getWidth(), image.getHeight());
for (PositionDatum panel : panels) { DrawingSettings dc = new DrawingSettings(settings, image.getHeight(), min, rotationRadians);
final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); List<Panel> panels = PanelFactory.createPanels(positionDatums);
for (Panel panel : panels) {
Shape shape = ShapeFactory.CreateShape(shapeType, panel); panel.draw(g2, dc, state);
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(); ByteArrayOutputStream out = new ByteArrayOutputStream();
@ -144,15 +93,14 @@ public class NanoleafLayout {
return ((double) (maxValue - value)) * (Math.PI / 180); 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 maxX = 0;
int maxY = 0; int maxY = 0;
int minX = 0; int minX = 0;
int minY = 0; int minY = 0;
for (PositionDatum panel : panels) { List<Panel> panels = PanelFactory.createPanels(positionDatums);
ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); for (Panel shape : panels) {
Shape shape = ShapeFactory.CreateShape(shapeType, panel);
for (Point2D point : shape.generateOutline()) { for (Point2D point : shape.generateOutline()) {
var rotated = point.rotate(rotationRadians); var rotated = point.rotate(rotationRadians);
maxX = Math.max(rotated.getX(), maxX); maxX = Math.max(rotated.getX(), maxX);
@ -162,23 +110,6 @@ public class NanoleafLayout {
} }
} }
return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) }; return new ImagePoint2D[] { new ImagePoint2D(minX, minY), new ImagePoint2D(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,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);
}
}

View File

@ -23,35 +23,37 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
@NonNullByDefault @NonNullByDefault
public enum ShapeType { public enum ShapeType {
// side lengths are taken from https://forum.nanoleaf.me/docs chapter 3.3 // side lengths are taken from https://forum.nanoleaf.me/docs chapter 3.3
UNKNOWN("Unknown", -1, 0, 0, DrawingAlgorithm.NONE), UNKNOWN("Unknown", -1, 0, 0, 1, DrawingAlgorithm.NONE),
TRIANGLE("Triangle", 0, 150, 3, DrawingAlgorithm.TRIANGLE), TRIANGLE("Triangle", 0, 150, 3, 1, DrawingAlgorithm.TRIANGLE),
RHYTHM("Rhythm", 1, 0, 1, DrawingAlgorithm.NONE), RHYTHM("Rhythm", 1, 0, 1, 1, DrawingAlgorithm.NONE),
SQUARE("Square", 2, 100, 0, DrawingAlgorithm.SQUARE), SQUARE("Square", 2, 100, 0, 1, DrawingAlgorithm.SQUARE),
CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0, DrawingAlgorithm.SQUARE), CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0, 1, DrawingAlgorithm.SQUARE),
CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0, DrawingAlgorithm.SQUARE), CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0, 1, DrawingAlgorithm.SQUARE),
SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6, DrawingAlgorithm.HEXAGON), SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6, 1, DrawingAlgorithm.HEXAGON),
SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3, DrawingAlgorithm.TRIANGLE), SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3, 1, DrawingAlgorithm.TRIANGLE),
SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3, DrawingAlgorithm.TRIANGLE), SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3, 1, DrawingAlgorithm.TRIANGLE),
SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0, DrawingAlgorithm.NONE), SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0, 1, DrawingAlgorithm.NONE),
ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6, DrawingAlgorithm.HEXAGON), ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6, 1, DrawingAlgorithm.HEXAGON),
ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 33.5 / 58, 6, DrawingAlgorithm.CORNER), ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 58, 6, 6, DrawingAlgorithm.CORNER),
LINES_CONNECTOR("Lines Connector", 16, 11, 1, DrawingAlgorithm.LINE), LINES_CONNECTOR("Lines Connector", 16, 11, 1, 1, DrawingAlgorithm.LINE),
LIGHT_LINES("Light Lines", 17, 154, 1, DrawingAlgorithm.LINE), LIGHT_LINES("Light Lines", 17, 154, 1, 1, DrawingAlgorithm.LINE),
LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1, DrawingAlgorithm.LINE), LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1, 1, DrawingAlgorithm.LINE),
CONTROLLER_CAP("Controller Cap", 19, 11, 0, DrawingAlgorithm.NONE), CONTROLLER_CAP("Controller Cap", 19, 11, 0, 1, DrawingAlgorithm.NONE),
POWER_CONNECTOR("Power Connector", 20, 11, 0, DrawingAlgorithm.NONE); POWER_CONNECTOR("Power Connector", 20, 11, 0, 1, DrawingAlgorithm.NONE);
private final String name; private final String name;
private final int id; private final int id;
private final double sideLength; private final int sideLength;
private final int numSides; private final int numSides;
private final int numLights;
private final DrawingAlgorithm drawingAlgorithm; 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.name = name;
this.id = id; this.id = id;
this.sideLength = sideLenght; this.sideLength = sideLenght;
this.numSides = numSides; this.numSides = numSides;
this.numLights = numLights;
this.drawingAlgorithm = drawingAlgorithm; this.drawingAlgorithm = drawingAlgorithm;
} }
@ -63,7 +65,7 @@ public enum ShapeType {
return id; return id;
} }
public double getSideLength() { public int getSideLength() {
return sideLength; return sideLength;
} }
@ -71,6 +73,10 @@ public enum ShapeType {
return numSides; return numSides;
} }
public int getNumLightsPerShape() {
return numLights;
}
public DrawingAlgorithm getDrawingAlgorithm() { public DrawingAlgorithm getDrawingAlgorithm() {
return drawingAlgorithm; return drawingAlgorithm;
} }

View File

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

View File

@ -18,6 +18,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType; import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
@ -28,6 +29,7 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
*/ */
@NonNullByDefault @NonNullByDefault
public class Hexagon extends Shape { public class Hexagon extends Shape {
public Hexagon(ShapeType shapeType, int panelId, Point2D position, int orientation) { public Hexagon(ShapeType shapeType, int panelId, Point2D position, int orientation) {
super(shapeType, panelId, position, orientation); super(shapeType, panelId, position, orientation);
} }
@ -45,12 +47,12 @@ public class Hexagon extends Shape {
} }
@Override @Override
public Point2D labelPosition(Graphics2D graphics, List<Point2D> outline) { protected ImagePoint2D labelPosition(Graphics2D graphics, List<ImagePoint2D> outline) {
Point2D[] bounds = findBounds(outline); Point2D[] bounds = findBounds(outline);
int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2;
int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2; int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2;
Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); 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));
} }
} }

View File

@ -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()));
}
}

View File

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

View File

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

View File

@ -12,13 +12,18 @@
*/ */
package org.openhab.binding.nanoleaf.internal.layout.shape; package org.openhab.binding.nanoleaf.internal.layout.shape;
import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType; import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
import org.openhab.core.library.types.HSBType;
/** /**
* A shape without any area. * A shape without any area.
@ -26,18 +31,42 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
* @author Jørgen Austvik - Initial contribution * @author Jørgen Austvik - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class Point extends Shape { public class Point extends Panel {
public Point(ShapeType shapeType, int panelId, Point2D position, int orientation) {
super(shapeType, panelId, position, orientation); 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 @Override
public List<Point2D> generateOutline() { public List<Point2D> generateOutline() {
return Arrays.asList(getPosition()); return Arrays.asList(position);
} }
@Override @Override
public Point2D labelPosition(Graphics2D graphics, List<Point2D> outline) { public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) {
return outline.get(0); 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());
}
} }
} }

View File

@ -12,36 +12,38 @@
*/ */
package org.openhab.binding.nanoleaf.internal.layout.shape; package org.openhab.binding.nanoleaf.internal.layout.shape;
import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Polygon;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType; 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 * @author Jørgen Austvik - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public abstract class Shape { public abstract class Shape extends Panel {
private final ShapeType shapeType;
private final int panelId;
private final Point2D position; private final Point2D position;
private final int orientation; private final int orientation;
private final int panelId;
public Shape(ShapeType shapeType, int panelId, Point2D position, int orientation) { public Shape(ShapeType shapeType, int panelId, Point2D position, int orientation) {
this.shapeType = shapeType; super(shapeType);
this.panelId = panelId;
this.position = position; this.position = position;
this.orientation = orientation; this.orientation = orientation;
this.panelId = panelId;
} }
public int getPanelId() {
return panelId;
};
public Point2D getPosition() { public Point2D getPosition() {
return position; return position;
} }
@ -50,36 +52,45 @@ public abstract class Shape {
return orientation; return orientation;
}; };
public ShapeType getShapeType() { protected int getPanelId() {
return shapeType; return panelId;
} }
/** @Override
* @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(); public abstract List<Point2D> generateOutline();
/** /**
* @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 * @return The position where the label of the shape should be placed
*/ */
public abstract Point2D labelPosition(Graphics2D graphics, List<Point2D> outline); protected abstract ImagePoint2D labelPosition(Graphics2D graphics, List<ImagePoint2D> outline);
@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());
}
HSBType color = state.getHSBForPanel(getPanelId());
graphics.setColor(new Color(color.getRGB()));
if (settings.shouldFillWithColor()) {
graphics.fillPolygon(p);
}
if (settings.shouldDrawOutline()) {
graphics.setColor(settings.getOutlineColor());
graphics.drawPolygon(p);
}
if (settings.shouldDrawLabels()) {
graphics.setColor(settings.getLabelColor());
ImagePoint2D textPos = labelPosition(graphics, outline);
graphics.drawString(Integer.toString(getPanelId()), textPos.getX(), textPos.getY());
}
}
} }

View File

@ -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());
}
}
}

View File

@ -18,6 +18,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType; import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
@ -44,14 +45,14 @@ public class Square extends Shape {
} }
@Override @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 // Center of square is average of oposite corners
Point2D p0 = outline.get(0); ImagePoint2D p0 = outline.get(0);
Point2D p2 = outline.get(2); ImagePoint2D p2 = outline.get(2);
Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics);
return new Point2D((p0.getX() + p2.getX()) / 2 - (int) (rect.getWidth() / 2), return new ImagePoint2D((p0.getX() + p2.getX()) / 2 - (int) (rect.getWidth() / 2),
(p0.getY() + p2.getY()) / 2 - (int) (rect.getHeight() / 2)); (p0.getY() + p2.getY()) / 2 + (int) (rect.getHeight() / 2));
} }
} }

View File

@ -18,6 +18,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType; import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
@ -28,6 +29,7 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
*/ */
@NonNullByDefault @NonNullByDefault
public class Triangle extends Shape { public class Triangle extends Shape {
public Triangle(ShapeType shapeType, int panelId, Point2D position, int orientation) { public Triangle(ShapeType shapeType, int panelId, Point2D position, int orientation) {
super(shapeType, panelId, position, orientation); super(shapeType, panelId, position, orientation);
} }
@ -48,13 +50,13 @@ public class Triangle extends Shape {
} }
@Override @Override
public Point2D labelPosition(Graphics2D graphics, List<Point2D> outline) { protected ImagePoint2D labelPosition(Graphics2D graphics, List<ImagePoint2D> outline) {
Point2D[] bounds = findBounds(outline); Point2D centroid = new Point2D((outline.get(0).getX() + outline.get(1).getX() + outline.get(2).getX()) / 3,
int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; (outline.get(0).getY() + outline.get(1).getY() + outline.get(2).getY()) / 3);
int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2;
Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); 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() { private boolean pointsUp() {

View File

@ -40,6 +40,8 @@ channel-type.nanoleaf.swipe.label = Swipe
channel-type.nanoleaf.swipe.description = Swipe over the panels channel-type.nanoleaf.swipe.description = Swipe over the panels
channel-type.nanoleaf.layout.label = Layout channel-type.nanoleaf.layout.label = Layout
channel-type.nanoleaf.layout.description = Layout of the panels 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 messages
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller. error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.

View File

@ -19,6 +19,7 @@
<channel id="rhythmMode" typeId="rhythmMode"/> <channel id="rhythmMode" typeId="rhythmMode"/>
<channel id="swipe" typeId="swipe"/> <channel id="swipe" typeId="swipe"/>
<channel id="layout" typeId="layout"/> <channel id="layout" typeId="layout"/>
<channel id="currentState" typeId="state"/>
</channels> </channels>
<properties> <properties>
@ -114,4 +115,10 @@
<description>@text/channel-type.nanoleaf.layout.description</description> <description>@text/channel-type.nanoleaf.layout.description</description>
</channel-type> </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> </thing:thing-descriptions>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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