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.
+
+
+
## Thing Configuration
The controller thing has the following parameters:
| colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No |
| colorMode | String | Color mode of the light panels | Yes |
| effect | String | Selected effect of the light panels | No |
+| layout | Image | Shows the layout of your panels with IDs. | Yes |
| rhythmState | Switch | Connection state of the rhythm module | Yes |
| rhythmActive | Switch | Activity state of the rhythm module | Yes |
| rhythmMode | Number | Sound source for the rhythm module. 0=Microphone, 1=Aux cable | No |
-| swipe | Trigger | [Canvas / Shapes Only] Detects Swipes over the panel.LEFT, RIGHT, UP, DOWN events are supported. | YES |
+| state | Image | Shows the current state of your panels with colors. | Yes |
+| swipe | Trigger | [Canvas / Shapes Only] Detects Swipes over the panel.LEFT, RIGHT, UP, DOWN events are supported. | Yes |
public static final String CHANNEL_SWIPE_EVENT_LEFT = "LEFT";
public static final String CHANNEL_SWIPE_EVENT_RIGHT = "RIGHT";
public static final String CHANNEL_LAYOUT = "layout";
+ public static final String CHANNEL_STATE = "state";
// List of light panel channels
public static final String CHANNEL_PANEL_COLOR = "color";
this.httpClientFactory = httpClientFactory;
}
+ @Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Nullable
+ @Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (NanoleafBindingConstants.THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService;
+import org.openhab.binding.nanoleaf.internal.layout.LayoutSettings;
import org.openhab.binding.nanoleaf.internal.layout.NanoleafLayout;
+import org.openhab.binding.nanoleaf.internal.layout.PanelState;
import org.openhab.binding.nanoleaf.internal.model.AuthToken;
import org.openhab.binding.nanoleaf.internal.model.BooleanState;
import org.openhab.binding.nanoleaf.internal.model.Brightness;
private static final int CONNECT_TIMEOUT = 10;
private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
- private HttpClientFactory httpClientFactory;
- private HttpClient httpClient;
+ private final HttpClientFactory httpClientFactory;
+ private final HttpClient httpClient;
private @Nullable HttpClient httpClientSSETouchEvent;
private @Nullable Request sseTouchjobRequest;
- private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
+ private final List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
private PanelLayout previousPanelLayout = new PanelLayout();
private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
localhttpSSEClientTouchEvent.setIdleTimeout(CONNECT_TIMEOUT * 1000L);
sseTouchjobRequest = localhttpSSEClientTouchEvent.newRequest(eventUri);
final Request localSSETouchjobRequest = sseTouchjobRequest;
- int requestHashCode = -1;
if (localSSETouchjobRequest != null) {
- requestHashCode = localSSETouchjobRequest.hashCode();
+ int requestHashCode = localSSETouchjobRequest.hashCode();
logger.debug("tj: triggering new touch job request {} for {} with client {}", requestHashCode,
thing.getUID(), eventHashcode);
String s = StandardCharsets.UTF_8.decode(content).toString();
logger.debug("touch detected for controller {}", thing.getUID());
logger.trace("content {}", s);
- Scanner eventContent = new Scanner(s);
-
- while (eventContent.hasNextLine()) {
- String line = eventContent.nextLine().trim();
- if (line.startsWith("data:")) {
- String json = line.substring(5).trim();
-
- try {
- TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
- handleTouchEvents(Objects.requireNonNull(touchEvents));
- } catch (JsonSyntaxException e) {
- logger.error("Couldn't parse touch event json {}", json);
+ try (Scanner eventContent = new Scanner(s)) {
+ while (eventContent.hasNextLine()) {
+ String line = eventContent.nextLine().trim();
+ if (line.startsWith("data:")) {
+ String json = line.substring(5).trim();
+
+ try {
+ TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
+ handleTouchEvents(Objects.requireNonNull(touchEvents));
+ } catch (JsonSyntaxException e) {
+ logger.error("Couldn't parse touch event json {}", json);
+ }
}
}
}
-
- eventContent.close();
logger.debug("leaving touch onContent");
}).onResponseSuccess((response) -> {
logger.trace("tj: r={} touch event SUCCESS: {}", response.getRequest(), response);
updateProperties();
updateConfiguration();
updateLayout(controllerInfo.getPanelLayout());
+ updateState(controllerInfo.getPanelLayout());
for (NanoleafControllerListener controllerListener : controllerListeners) {
controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
}
}
+ private void updateState(PanelLayout panelLayout) {
+ ChannelUID stateChannel = new ChannelUID(getThing().getUID(), CHANNEL_STATE);
+
+ Bridge bridge = getThing();
+ List<Thing> things = bridge.getThings();
+ try {
+ LayoutSettings settings = new LayoutSettings(false, true, true, true);
+ byte[] bytes = NanoleafLayout.render(panelLayout, new PanelState(things), settings);
+ if (bytes.length > 0) {
+ updateState(stateChannel, new RawType(bytes, "image/png"));
+ }
+
+ previousPanelLayout = panelLayout;
+ } catch (IOException ioex) {
+ logger.warn("Failed to create state image", ioex);
+ }
+ }
+
private void updateLayout(PanelLayout panelLayout) {
ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT);
ThingHandlerCallback callback = getCallback();
return;
}
+ Bridge bridge = getThing();
+ List<Thing> things = bridge.getThings();
try {
- byte[] bytes = NanoleafLayout.render(panelLayout);
+ LayoutSettings settings = new LayoutSettings(true, false, true, false);
+ byte[] bytes = NanoleafLayout.render(panelLayout, new PanelState(things), settings);
if (bytes.length > 0) {
- updateState(CHANNEL_LAYOUT, new RawType(bytes, "image/png"));
+ updateState(layoutChannel, new RawType(bytes, "image/png"));
}
previousPanelLayout = panelLayout;
private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class);
- private HttpClient httpClient;
+ private final HttpClient httpClient;
// JSON parser for API responses
private final Gson gson = new Gson();
// holds current color data per panel
- private Map<String, HSBType> panelInfo = new HashMap<>();
+ private final Map<String, HSBType> panelInfo = new HashMap<>();
private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
Write write = new Write();
write.setCommand("display");
write.setAnimType("static");
- String panelID = this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString();
+ Integer panelID = Integer.valueOf(this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString());
@Nullable
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
write.setAnimData(String.format("1 %s 1 %d %d %d 0 10", panelID, red, green, blue));
} else {
// this is only used in special streaming situations with canvas which is not yet supported
- int quotient = Integer.divideUnsigned(Integer.valueOf(panelID), 256);
- int remainder = Integer.remainderUnsigned(Integer.valueOf(panelID), 256);
+ int quotient = Integer.divideUnsigned(panelID, 256);
+ int remainder = Integer.remainderUnsigned(panelID, 256);
write.setAnimData(
String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, red, green, blue));
}
return panelID;
}
+ public @Nullable HSBType getColor() {
+ String panelID = getPanelID();
+ return panelInfo.get(panelID);
+ }
+
private @Nullable HSBType getPanelColor() {
String panelID = getPanelID();
String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length);
for (int i = 0; i < panelDataPoints.length; i++) {
if (i % 8 == 0) {
- String idQuotient = panelDataPoints[i];
- String idRemainder = panelDataPoints[i + 1];
- Integer idNum = Integer.valueOf(idQuotient) * 256 + Integer.valueOf(idRemainder);
+ Integer idQuotient = Integer.valueOf(panelDataPoints[i]);
+ Integer idRemainder = Integer.valueOf(panelDataPoints[i + 1]);
+ Integer idNum = idQuotient * 256 + idRemainder;
if (String.valueOf(idNum).equals(panelID)) {
// found panel data - store it
panelInfo.put(panelID,
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
import java.awt.Color;
import java.awt.Graphics2D;
+import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
import java.util.List;
import javax.imageio.ImageIO;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
-import org.openhab.binding.nanoleaf.internal.layout.shape.Shape;
-import org.openhab.binding.nanoleaf.internal.layout.shape.ShapeFactory;
+import org.openhab.binding.nanoleaf.internal.layout.shape.Panel;
+import org.openhab.binding.nanoleaf.internal.layout.shape.PanelFactory;
import org.openhab.binding.nanoleaf.internal.model.GlobalOrientation;
import org.openhab.binding.nanoleaf.internal.model.Layout;
import org.openhab.binding.nanoleaf.internal.model.PanelLayout;
public class NanoleafLayout {
private static final Color COLOR_BACKGROUND = Color.WHITE;
- private static final Color COLOR_PANEL = Color.BLACK;
- private static final Color COLOR_SIDE = Color.GRAY;
- private static final Color COLOR_TEXT = Color.BLACK;
- public static byte[] render(PanelLayout panelLayout) throws IOException {
+ public static byte[] render(PanelLayout panelLayout, PanelState state, LayoutSettings settings) throws IOException {
double rotationRadians = 0;
GlobalOrientation globalOrientation = panelLayout.getGlobalOrientation();
if (globalOrientation != null) {
return new byte[] {};
}
- List<PositionDatum> panels = layout.getPositionData();
- if (panels == null) {
+ List<PositionDatum> positionDatums = layout.getPositionData();
+ if (positionDatums == null) {
return new byte[] {};
}
- Point2D size[] = findSize(panels, rotationRadians);
- final Point2D min = size[0];
- final Point2D max = size[1];
- Point2D prev = null;
- Point2D first = null;
+ ImagePoint2D size[] = findSize(positionDatums, rotationRadians);
+ final ImagePoint2D min = size[0];
+ final ImagePoint2D max = size[1];
- int sideCounter = 0;
BufferedImage image = new BufferedImage(
(max.getX() - min.getX()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH,
(max.getY() - min.getY()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH,
BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = image.createGraphics();
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
g2.setBackground(COLOR_BACKGROUND);
g2.clearRect(0, 0, image.getWidth(), image.getHeight());
- for (PositionDatum panel : panels) {
- final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType());
-
- Shape shape = ShapeFactory.CreateShape(shapeType, panel);
- List<Point2D> outline = toPictureLayout(shape.generateOutline(), image.getHeight(), min, rotationRadians);
- for (int i = 0; i < outline.size(); i++) {
- g2.setColor(COLOR_SIDE);
- Point2D pos = outline.get(i);
- Point2D nextPos = outline.get((i + 1) % outline.size());
- g2.drawLine(pos.getX(), pos.getY(), nextPos.getX(), nextPos.getY());
- }
-
- for (int i = 0; i < outline.size(); i++) {
- Point2D pos = outline.get(i);
- g2.setColor(COLOR_PANEL);
- g2.fillOval(pos.getX() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2,
- pos.getY() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2,
- NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS, NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS);
- }
-
- Point2D current = toPictureLayout(new Point2D(panel.getPosX(), panel.getPosY()), image.getHeight(), min,
- rotationRadians);
- if (sideCounter == 0) {
- first = current;
- }
-
- g2.setColor(COLOR_SIDE);
- final int expectedSides = shapeType.getNumSides();
- if (shapeType.getDrawingAlgorithm() == DrawingAlgorithm.CORNER) {
- // Special handling of Elements Hexagon Corners, where we get 6 corners instead of 1 shape. They seem to
- // come after each other in the JSON, so this algorithm connects them based on the number of sides the
- // shape is expected to have.
- if (sideCounter > 0 && sideCounter != expectedSides && prev != null) {
- g2.drawLine(prev.getX(), prev.getY(), current.getX(), current.getY());
- }
-
- sideCounter++;
-
- if (sideCounter == expectedSides && first != null) {
- g2.drawLine(current.getX(), current.getY(), first.getX(), first.getY());
- sideCounter = 0;
- }
- } else {
- sideCounter = 0;
- }
-
- prev = current;
-
- g2.setColor(COLOR_TEXT);
- Point2D textPos = shape.labelPosition(g2, outline);
- g2.drawString(Integer.toString(panel.getPanelId()), textPos.getX(), textPos.getY());
+ DrawingSettings dc = new DrawingSettings(settings, image.getHeight(), min, rotationRadians);
+ List<Panel> panels = PanelFactory.createPanels(positionDatums);
+ for (Panel panel : panels) {
+ panel.draw(g2, dc, state);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
return ((double) (maxValue - value)) * (Math.PI / 180);
}
- private static Point2D[] findSize(Collection<PositionDatum> panels, double rotationRadians) {
+ private static ImagePoint2D[] findSize(List<PositionDatum> positionDatums, double rotationRadians) {
int maxX = 0;
int maxY = 0;
int minX = 0;
int minY = 0;
- for (PositionDatum panel : panels) {
- ShapeType shapeType = ShapeType.valueOf(panel.getShapeType());
- Shape shape = ShapeFactory.CreateShape(shapeType, panel);
+ List<Panel> panels = PanelFactory.createPanels(positionDatums);
+ for (Panel shape : panels) {
for (Point2D point : shape.generateOutline()) {
var rotated = point.rotate(rotationRadians);
maxX = Math.max(rotated.getX(), maxX);
}
}
- return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) };
- }
-
- private static Point2D toPictureLayout(Point2D original, int imageHeight, Point2D min, double rotationRadians) {
- Point2D rotated = original.rotate(rotationRadians);
- Point2D translated = new Point2D(NanoleafBindingConstants.LAYOUT_BORDER_WIDTH + rotated.getX() - min.getX(),
- imageHeight - NanoleafBindingConstants.LAYOUT_BORDER_WIDTH - rotated.getY() + min.getY());
- return translated;
- }
-
- private static List<Point2D> toPictureLayout(List<Point2D> originals, int imageHeight, Point2D min,
- double rotationRadians) {
- List<Point2D> result = new ArrayList<Point2D>(originals.size());
- for (Point2D original : originals) {
- result.add(toPictureLayout(original, imageHeight, min, rotationRadians));
- }
-
- return result;
+ return new ImagePoint2D[] { new ImagePoint2D(minX, minY), new ImagePoint2D(maxX, maxY) };
}
}
--- /dev/null
+/**
+ * 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);
+ }
+}
@NonNullByDefault
public enum ShapeType {
// side lengths are taken from https://forum.nanoleaf.me/docs chapter 3.3
- UNKNOWN("Unknown", -1, 0, 0, DrawingAlgorithm.NONE),
- TRIANGLE("Triangle", 0, 150, 3, DrawingAlgorithm.TRIANGLE),
- RHYTHM("Rhythm", 1, 0, 1, DrawingAlgorithm.NONE),
- SQUARE("Square", 2, 100, 0, DrawingAlgorithm.SQUARE),
- CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0, DrawingAlgorithm.SQUARE),
- CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0, DrawingAlgorithm.SQUARE),
- SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6, DrawingAlgorithm.HEXAGON),
- SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3, DrawingAlgorithm.TRIANGLE),
- SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3, DrawingAlgorithm.TRIANGLE),
- SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0, DrawingAlgorithm.NONE),
- ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6, DrawingAlgorithm.HEXAGON),
- ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 33.5 / 58, 6, DrawingAlgorithm.CORNER),
- LINES_CONNECTOR("Lines Connector", 16, 11, 1, DrawingAlgorithm.LINE),
- LIGHT_LINES("Light Lines", 17, 154, 1, DrawingAlgorithm.LINE),
- LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1, DrawingAlgorithm.LINE),
- CONTROLLER_CAP("Controller Cap", 19, 11, 0, DrawingAlgorithm.NONE),
- POWER_CONNECTOR("Power Connector", 20, 11, 0, DrawingAlgorithm.NONE);
+ UNKNOWN("Unknown", -1, 0, 0, 1, DrawingAlgorithm.NONE),
+ TRIANGLE("Triangle", 0, 150, 3, 1, DrawingAlgorithm.TRIANGLE),
+ RHYTHM("Rhythm", 1, 0, 1, 1, DrawingAlgorithm.NONE),
+ SQUARE("Square", 2, 100, 0, 1, DrawingAlgorithm.SQUARE),
+ CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0, 1, DrawingAlgorithm.SQUARE),
+ CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0, 1, DrawingAlgorithm.SQUARE),
+ SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6, 1, DrawingAlgorithm.HEXAGON),
+ SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3, 1, DrawingAlgorithm.TRIANGLE),
+ SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3, 1, DrawingAlgorithm.TRIANGLE),
+ SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0, 1, DrawingAlgorithm.NONE),
+ ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6, 1, DrawingAlgorithm.HEXAGON),
+ ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 58, 6, 6, DrawingAlgorithm.CORNER),
+ LINES_CONNECTOR("Lines Connector", 16, 11, 1, 1, DrawingAlgorithm.LINE),
+ LIGHT_LINES("Light Lines", 17, 154, 1, 1, DrawingAlgorithm.LINE),
+ LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1, 1, DrawingAlgorithm.LINE),
+ CONTROLLER_CAP("Controller Cap", 19, 11, 0, 1, DrawingAlgorithm.NONE),
+ POWER_CONNECTOR("Power Connector", 20, 11, 0, 1, DrawingAlgorithm.NONE);
private final String name;
private final int id;
- private final double sideLength;
+ private final int sideLength;
private final int numSides;
+ private final int numLights;
private final DrawingAlgorithm drawingAlgorithm;
- ShapeType(String name, int id, double sideLenght, int numSides, DrawingAlgorithm drawingAlgorithm) {
+ ShapeType(String name, int id, int sideLenght, int numSides, int numLights, DrawingAlgorithm drawingAlgorithm) {
this.name = name;
this.id = id;
this.sideLength = sideLenght;
this.numSides = numSides;
+ this.numLights = numLights;
this.drawingAlgorithm = drawingAlgorithm;
}
return id;
}
- public double getSideLength() {
+ public int getSideLength() {
return sideLength;
}
return numSides;
}
+ public int getNumLightsPerShape() {
+ return numLights;
+ }
+
public DrawingAlgorithm getDrawingAlgorithm() {
return drawingAlgorithm;
}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D;
import org.openhab.binding.nanoleaf.internal.layout.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
*/
@NonNullByDefault
public class Hexagon extends Shape {
+
public Hexagon(ShapeType shapeType, int panelId, Point2D position, int orientation) {
super(shapeType, panelId, position, orientation);
}
}
@Override
- public Point2D labelPosition(Graphics2D graphics, List<Point2D> outline) {
+ protected ImagePoint2D labelPosition(Graphics2D graphics, List<ImagePoint2D> outline) {
Point2D[] bounds = findBounds(outline);
int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2;
int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2;
Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics);
- return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2));
+ return new ImagePoint2D(midX - (int) (rect.getWidth() / 2), midY + (int) (rect.getHeight() / 2));
}
}
--- /dev/null
+/**
+ * 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()));
+ }
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
*/
package org.openhab.binding.nanoleaf.internal.layout.shape;
+import java.awt.Color;
import java.awt.Graphics2D;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings;
+import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D;
+import org.openhab.binding.nanoleaf.internal.layout.PanelState;
import org.openhab.binding.nanoleaf.internal.layout.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
+import org.openhab.core.library.types.HSBType;
/**
* A shape without any area.
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
-public class Point extends Shape {
- public Point(ShapeType shapeType, int panelId, Point2D position, int orientation) {
- super(shapeType, panelId, position, orientation);
+public class Point extends Panel {
+
+ private static final int POINT_DIAMETER = 4;
+
+ private final Point2D position;
+ private final int panelId;
+
+ public Point(ShapeType shapeType, int panelId, Point2D position) {
+ super(shapeType);
+ this.position = position;
+ this.panelId = panelId;
}
@Override
public List<Point2D> generateOutline() {
- return Arrays.asList(getPosition());
+ return Arrays.asList(position);
}
@Override
- public Point2D labelPosition(Graphics2D graphics, List<Point2D> outline) {
- return outline.get(0);
+ public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) {
+ ImagePoint2D pos = settings.generateImagePoint(position);
+
+ if (settings.shouldFillWithColor()) {
+ HSBType color = state.getHSBForPanel(panelId);
+ graphics.setColor(new Color(color.getRGB()));
+ graphics.fillOval(pos.getX(), pos.getY(), POINT_DIAMETER, POINT_DIAMETER);
+ }
+
+ if (settings.shouldDrawOutline()) {
+ graphics.setColor(settings.getOutlineColor());
+ graphics.drawOval(pos.getX(), pos.getY(), POINT_DIAMETER, POINT_DIAMETER);
+ }
+
+ if (settings.shouldDrawLabels()) {
+ graphics.setColor(settings.getLabelColor());
+ graphics.drawString(Integer.toString(panelId), pos.getX(), pos.getY());
+ }
}
}
*/
package org.openhab.binding.nanoleaf.internal.layout.shape;
+import java.awt.Color;
import java.awt.Graphics2D;
+import java.awt.Polygon;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings;
+import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D;
+import org.openhab.binding.nanoleaf.internal.layout.PanelState;
import org.openhab.binding.nanoleaf.internal.layout.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
+import org.openhab.core.library.types.HSBType;
/**
- * Shape that can be drawn.
+ * Draws shapes, which are panels with a single LED.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
-public abstract class Shape {
- private final ShapeType shapeType;
- private final int panelId;
+public abstract class Shape extends Panel {
+
private final Point2D position;
private final int orientation;
+ private final int panelId;
public Shape(ShapeType shapeType, int panelId, Point2D position, int orientation) {
- this.shapeType = shapeType;
- this.panelId = panelId;
+ super(shapeType);
this.position = position;
this.orientation = orientation;
+ this.panelId = panelId;
}
- public int getPanelId() {
- return panelId;
- };
-
public Point2D getPosition() {
return position;
}
return orientation;
};
- public ShapeType getShapeType() {
- return shapeType;
+ protected int getPanelId() {
+ return panelId;
}
+ @Override
+ public abstract List<Point2D> generateOutline();
+
/**
- * @return The opposite points of the minimum bounding rectangle around this shape.
+ * @param graphics The picture to draw on
+ * @param outline Outline of the shape to draw inside
+ * @return The position where the label of the shape should be placed
*/
- public Point2D[] findBounds(List<Point2D> outline) {
- int minX = Integer.MAX_VALUE;
- int minY = Integer.MAX_VALUE;
- int maxX = Integer.MIN_VALUE;
- int maxY = Integer.MIN_VALUE;
+ protected abstract ImagePoint2D labelPosition(Graphics2D graphics, List<ImagePoint2D> outline);
- for (Point2D point : outline) {
- maxX = Math.max(point.getX(), maxX);
- maxY = Math.max(point.getY(), maxY);
- minX = Math.min(point.getX(), minX);
- minY = Math.min(point.getY(), minY);
+ @Override
+ public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) {
+ List<ImagePoint2D> outline = settings.generateImagePoints(generateOutline());
+
+ Polygon p = new Polygon();
+ for (int i = 0; i < outline.size(); i++) {
+ ImagePoint2D pos = outline.get(i);
+ p.addPoint(pos.getX(), pos.getY());
}
- return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) };
- }
+ HSBType color = state.getHSBForPanel(getPanelId());
+ graphics.setColor(new Color(color.getRGB()));
+ if (settings.shouldFillWithColor()) {
+ graphics.fillPolygon(p);
+ }
- /**
- * @return The points that make up this shape.
- */
- public abstract List<Point2D> generateOutline();
+ if (settings.shouldDrawOutline()) {
+ graphics.setColor(settings.getOutlineColor());
+ graphics.drawPolygon(p);
+ }
- /**
- * @return The position where the label of the shape should be placed
- */
- public abstract Point2D labelPosition(Graphics2D graphics, List<Point2D> outline);
+ if (settings.shouldDrawLabels()) {
+ graphics.setColor(settings.getLabelColor());
+ ImagePoint2D textPos = labelPosition(graphics, outline);
+ graphics.drawString(Integer.toString(getPanelId()), textPos.getX(), textPos.getY());
+ }
+ }
}
+++ /dev/null
-/**
- * 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());
- }
- }
-}
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D;
import org.openhab.binding.nanoleaf.internal.layout.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
}
@Override
- public Point2D labelPosition(Graphics2D graphics, List<Point2D> outline) {
+ protected ImagePoint2D labelPosition(Graphics2D graphics, List<ImagePoint2D> outline) {
// Center of square is average of oposite corners
- Point2D p0 = outline.get(0);
- Point2D p2 = outline.get(2);
+ ImagePoint2D p0 = outline.get(0);
+ ImagePoint2D p2 = outline.get(2);
Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics);
- return new Point2D((p0.getX() + p2.getX()) / 2 - (int) (rect.getWidth() / 2),
- (p0.getY() + p2.getY()) / 2 - (int) (rect.getHeight() / 2));
+ return new ImagePoint2D((p0.getX() + p2.getX()) / 2 - (int) (rect.getWidth() / 2),
+ (p0.getY() + p2.getY()) / 2 + (int) (rect.getHeight() / 2));
}
}
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D;
import org.openhab.binding.nanoleaf.internal.layout.Point2D;
import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
*/
@NonNullByDefault
public class Triangle extends Shape {
+
public Triangle(ShapeType shapeType, int panelId, Point2D position, int orientation) {
super(shapeType, panelId, position, orientation);
}
}
@Override
- public Point2D labelPosition(Graphics2D graphics, List<Point2D> outline) {
- Point2D[] bounds = findBounds(outline);
- int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2;
- int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2;
+ protected ImagePoint2D labelPosition(Graphics2D graphics, List<ImagePoint2D> outline) {
+ Point2D centroid = new Point2D((outline.get(0).getX() + outline.get(1).getX() + outline.get(2).getX()) / 3,
+ (outline.get(0).getY() + outline.get(1).getY() + outline.get(2).getY()) / 3);
Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics);
- return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2));
+ return new ImagePoint2D(centroid.getX() - (int) (rect.getWidth() / 2),
+ centroid.getY() + (int) (rect.getHeight() / 2));
}
private boolean pointsUp() {
channel-type.nanoleaf.swipe.description = Swipe over the panels
channel-type.nanoleaf.layout.label = Layout
channel-type.nanoleaf.layout.description = Layout of the panels
+channel-type.nanoleaf.state.label = State
+channel-type.nanoleaf.state.description = Current state of the panels
# error messages
error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller.
<channel id="rhythmMode" typeId="rhythmMode"/>
<channel id="swipe" typeId="swipe"/>
<channel id="layout" typeId="layout"/>
+ <channel id="currentState" typeId="state"/>
</channels>
<properties>
<description>@text/channel-type.nanoleaf.layout.description</description>
</channel-type>
+ <channel-type id="state">
+ <item-type>Image</item-type>
+ <label>@text/channel-type.nanoleaf.state.label</label>
+ <description>@text/channel-type.nanoleaf.state.description</description>
+ </channel-type>
+
</thing:thing-descriptions>
--- /dev/null
+/**
+ * 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];
+ }
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ }
+}