mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[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:
parent
45cbf52cf3
commit
cad69c8de5
@ -104,7 +104,15 @@ Compare the following output with the right picture at the beginning of the arti
|
||||
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
|
||||
|
||||
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 |
|
||||
| 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 |
|
||||
|
||||
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 70 KiB |
BIN
bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png
Normal file
BIN
bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
@ -59,6 +59,7 @@ public class NanoleafBindingConstants {
|
||||
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";
|
||||
|
@ -57,11 +57,13 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory {
|
||||
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)) {
|
||||
|
@ -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.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;
|
||||
@ -101,12 +103,12 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
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;
|
||||
@ -515,9 +517,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
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);
|
||||
@ -525,23 +526,21 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
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);
|
||||
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()) {
|
||||
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 {
|
||||
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);
|
||||
@ -670,6 +669,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
updateProperties();
|
||||
updateConfiguration();
|
||||
updateLayout(controllerInfo.getPanelLayout());
|
||||
updateState(controllerInfo.getPanelLayout());
|
||||
|
||||
for (NanoleafControllerListener controllerListener : controllerListeners) {
|
||||
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) {
|
||||
ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT);
|
||||
ThingHandlerCallback callback = getCallback();
|
||||
@ -726,10 +744,13 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
|
||||
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;
|
||||
|
@ -72,12 +72,12 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
|
||||
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;
|
||||
@ -227,7 +227,7 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
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) {
|
||||
@ -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));
|
||||
} 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));
|
||||
}
|
||||
@ -288,6 +288,11 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
return panelID;
|
||||
}
|
||||
|
||||
public @Nullable HSBType getColor() {
|
||||
String panelID = getPanelID();
|
||||
return panelInfo.get(panelID);
|
||||
}
|
||||
|
||||
private @Nullable HSBType getPanelColor() {
|
||||
String panelID = getPanelID();
|
||||
|
||||
@ -357,9 +362,9 @@ public class NanoleafPanelHandler extends BaseThingHandler {
|
||||
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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -15,19 +15,18 @@ package org.openhab.binding.nanoleaf.internal.layout;
|
||||
|
||||
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;
|
||||
@ -42,11 +41,8 @@ import org.openhab.binding.nanoleaf.internal.model.PositionDatum;
|
||||
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) {
|
||||
@ -58,78 +54,31 @@ public class NanoleafLayout {
|
||||
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();
|
||||
@ -144,15 +93,14 @@ public class NanoleafLayout {
|
||||
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);
|
||||
@ -162,23 +110,6 @@ public class NanoleafLayout {
|
||||
}
|
||||
}
|
||||
|
||||
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) };
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -23,35 +23,37 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
@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;
|
||||
}
|
||||
|
||||
@ -63,7 +65,7 @@ public enum ShapeType {
|
||||
return id;
|
||||
}
|
||||
|
||||
public double getSideLength() {
|
||||
public int getSideLength() {
|
||||
return sideLength;
|
||||
}
|
||||
|
||||
@ -71,6 +73,10 @@ public enum ShapeType {
|
||||
return numSides;
|
||||
}
|
||||
|
||||
public int getNumLightsPerShape() {
|
||||
return numLights;
|
||||
}
|
||||
|
||||
public DrawingAlgorithm getDrawingAlgorithm() {
|
||||
return drawingAlgorithm;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import java.util.Arrays;
|
||||
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;
|
||||
|
||||
@ -28,6 +29,7 @@ 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);
|
||||
}
|
||||
@ -45,12 +47,12 @@ public class Hexagon extends Shape {
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -12,13 +12,18 @@
|
||||
*/
|
||||
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.
|
||||
@ -26,18 +31,42 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType;
|
||||
* @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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,36 +12,38 @@
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@ -50,36 +52,45 @@ public abstract class Shape {
|
||||
return orientation;
|
||||
};
|
||||
|
||||
public ShapeType getShapeType() {
|
||||
return shapeType;
|
||||
protected int getPanelId() {
|
||||
return panelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
@Override
|
||||
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
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import java.util.Arrays;
|
||||
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;
|
||||
|
||||
@ -44,14 +45,14 @@ public class Square extends Shape {
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import java.util.Arrays;
|
||||
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;
|
||||
|
||||
@ -28,6 +29,7 @@ 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);
|
||||
}
|
||||
@ -48,13 +50,13 @@ public class Triangle extends Shape {
|
||||
}
|
||||
|
||||
@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() {
|
||||
|
@ -40,6 +40,8 @@ channel-type.nanoleaf.swipe.label = Swipe
|
||||
channel-type.nanoleaf.swipe.description = Swipe over the panels
|
||||
channel-type.nanoleaf.layout.label = Layout
|
||||
channel-type.nanoleaf.layout.description = Layout of the panels
|
||||
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.
|
||||
|
@ -19,6 +19,7 @@
|
||||
<channel id="rhythmMode" typeId="rhythmMode"/>
|
||||
<channel id="swipe" typeId="swipe"/>
|
||||
<channel id="layout" typeId="layout"/>
|
||||
<channel id="currentState" typeId="state"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
@ -114,4 +115,10 @@
|
||||
<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>
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user