From cad69c8de5995b93af084210217afffb1fd3d6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Austvik?= Date: Fri, 2 Dec 2022 21:14:53 +0100 Subject: [PATCH] [Nanoleaf] New Channel: State (#13746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [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 --- .../org.openhab.binding.nanoleaf/README.md | 14 +- .../doc/Layout.png | Bin 25495 -> 71541 bytes .../doc/NanoCanvas_rendered.png | Bin 0 -> 7422 bytes .../internal/NanoleafBindingConstants.java | 1 + .../internal/NanoleafHandlerFactory.java | 2 + .../handler/NanoleafControllerHandler.java | 61 +- .../handler/NanoleafPanelHandler.java | 21 +- .../internal/layout/DrawingSettings.java | 95 +++ .../internal/layout/ImagePoint2D.java | 45 + .../internal/layout/LayoutSettings.java | 52 ++ .../internal/layout/NanoleafLayout.java | 109 +-- .../nanoleaf/internal/layout/PanelState.java | 52 ++ .../nanoleaf/internal/layout/ShapeType.java | 46 +- .../shape/BarycentricTriangleGradient.java | 152 ++++ .../internal/layout/shape/Hexagon.java | 6 +- .../internal/layout/shape/HexagonCorners.java | 152 ++++ .../nanoleaf/internal/layout/shape/Panel.java | 79 ++ .../internal/layout/shape/PanelFactory.java | 93 +++ .../nanoleaf/internal/layout/shape/Point.java | 41 +- .../nanoleaf/internal/layout/shape/Shape.java | 81 +- .../internal/layout/shape/ShapeFactory.java | 44 - .../internal/layout/shape/Square.java | 11 +- .../internal/layout/shape/Triangle.java | 12 +- .../resources/OH-INF/i18n/nanoleaf.properties | 2 + .../resources/OH-INF/thing/lightpanels.xml | 7 + .../internal/layout/NanoleafLayoutTest.java | 92 ++ .../src/test/resources/lasvegas.json | 788 ++++++++++++++++++ .../src/test/resources/spaceinvader.json | 152 ++++ .../src/test/resources/squares.json | 172 ++++ .../src/test/resources/theduck.json | 129 +++ .../src/test/resources/wings.json | 143 ++++ 31 files changed, 2418 insertions(+), 236 deletions(-) create mode 100644 bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java delete mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json diff --git a/bundles/org.openhab.binding.nanoleaf/README.md b/bundles/org.openhab.binding.nanoleaf/README.md index b8a635b93..39816999e 100644 --- a/bundles/org.openhab.binding.nanoleaf/README.md +++ b/bundles/org.openhab.binding.nanoleaf/README.md @@ -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 | diff --git a/bundles/org.openhab.binding.nanoleaf/doc/Layout.png b/bundles/org.openhab.binding.nanoleaf/doc/Layout.png index a8d684a0ce019fbf7009b9d57351c70d473aa48b..d716ffae1e27f8fa7d02069f7e4048720427cf38 100644 GIT binary patch literal 71541 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

>B!`WRpc&EaS-i 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

    E;xD>0P{FdmiYqzKk8^OiPUmK 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 diff --git a/bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png b/bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png new file mode 100644 index 0000000000000000000000000000000000000000..866554743f43104c5a1ce36ffeaa745c0fd326f7 GIT binary patch literal 7422 zcmeHM>r)d~6px^dK!GaGI4TcYDYVVhe6SJ)l7QGDlr~Ub8Bq+akElp8gqT3cV-Txl zP_VShD2)jMV#fk?5CQ_p_&`d55Cu%hCRuqILP!Ec5+IOt7yQ8V4`^*??}t0P_ny0Z z=QsD9-}#-Jmq?EF_T1=+LZQ6lNW1r-P;N6Q)QZm4ZqNueCcX%T@^6jX9lbB}h^b$z zsGudcr7Iq$g^|9e=XvCLeEn|pxs5kS!L1*ZpI=A9d|$>6?_uBG7IR&Odid?+@h4Ak z`!1!7zu$T7kan|SMI>739mYj*FIG)OKCYF8^g3kDS)LV0Iu-KcU1O4+*2Pu1NJYWk zrer@_RW2!EqZmy*-Z#r${8y%9X|%xwC9<%jDBdv{D?%v$j=oja5>G zt1dV$iKy(CU*1ls;Z5en5^Xm8>_}`)a@P11-a0%aWgNyg>Ma_c^RAR^vWb*l9Jy^O z9(!2E5G+*4=Q`v&8imeE;f&qlGLAU9gPboTQ5Fy!Wjj|+OB8`CPvU=537*eVrnfQC zbq6H1LIB+sQc~zrWBrA`!Wpf2{`c&y;Fci1g%-#K+xcd{pv>_B-y^ba;#6sd6WHAl zpqlmT%~ku1y{KzrIG?qdhU+cvSfagxa+CijnI7>GCoy$ZxI_9Liw&|93CE5C(P@Pf zMln|^(p04;oGF};%je?ROdz^3HwmNcCn^o@qQKGDIpmv+66@b074uh+98S)@X&A?TQHB}|uIG6giVgan0N#I;Ln?P#igl*3U~tov$8=FWd^$uoI=#c-g@=vf0*U zYJ{A2y?e3PxmoHAY~W;ypSTv9+Seo)^Vv06mo#heCwPG<|K^QwGz^IrKvzF06? zj0MZ&4kwm3hD$Ja6fWo}vKdFQ$~qdI-+T% zI;biTBp&O+fstNtv49xf2UQLPIw8@Z$>*ln&02s!lUja))wH(p?9iZ*oemsKV^mo! zVfZ^HNj>fPV!FUi)KOYY8@SHN`3z6i{XM|(21DqDddMJlrd0*~ul9kffM4SOf}IFj zGrk1ux_0r=UiW~ib#E|&ecNa4=*fyVP7cfOj4M&Gm{m)8Hy#%8xhunyK87FO=ym^K z<+CrAggY5N1NzN51qVErV#K1rM|(hfSyK8v@T#38N!e95c==LAKoRYRY#|3h0j>@q zC?F^xap6A^9qSV=A5sN^fr~|6tt@?diI!Sy>Qp_L#!UnQc+=lufGrxhTCCi;3kv$- z5cAf&XvwzI!DfL`?FX}Ebm%J|r&wKJoc9=W;gvs}V1wPfEZhR?!!B=D`E{6BNV)C- zr%u2O^895~?2Qd@iTJY}km)-REp74kLCg?9kQ4In`a)WQc`+Icz)33Pp4(whh8nfe p)SwVc%GbPYcXWTdG6Y<1Rxe?DSkAU_=w1^Q_c?iY^{$k2e*x-^^qv3! literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java index e7e8a2fb5..db83c1fd7 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java index 41538ee05..aede5dbb5 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java @@ -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)) { diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java index bd2efd455..d37a82f06 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java @@ -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 controllerListeners = new CopyOnWriteArrayList(); + private final List controllerListeners = new CopyOnWriteArrayList(); 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 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 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; diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java index 2c1658520..995eb8a34 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java @@ -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 panelInfo = new HashMap<>(); + private final Map 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, diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java new file mode 100644 index 000000000..be16a3b22 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java @@ -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 generateImagePoints(List 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 toPictureLayout(List originals, int imageHeight, ImagePoint2D min, + double rotationRadians) { + List result = new ArrayList<>(originals.size()); + for (Point2D original : originals) { + result.add(toPictureLayout(original, imageHeight, min, rotationRadians)); + } + + return result; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java new file mode 100644 index 000000000..4dcb0060d --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java new file mode 100644 index 000000000..2364f5513 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java index 70724333e..61ceaa549 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java @@ -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 panels = layout.getPositionData(); - if (panels == null) { + List 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 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 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 panels, double rotationRadians) { + private static ImagePoint2D[] findSize(List 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 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 toPictureLayout(List originals, int imageHeight, Point2D min, - double rotationRadians) { - List result = new ArrayList(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) }; } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java new file mode 100644 index 000000000..fba1804fe --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java @@ -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 panelStates = new HashMap<>(); + + public PanelState(List 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); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java index f90262e0e..471dafb36 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java @@ -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; } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java new file mode 100644 index 000000000..e04e43fa4 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java @@ -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); + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java index a29233534..762acb5c8 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java @@ -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 outline) { + protected ImagePoint2D labelPosition(Graphics2D graphics, List 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)); } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java new file mode 100644 index 000000000..873333a76 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java @@ -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 corners; + + public HexagonCorners(ShapeType shapeType, List corners) { + super(shapeType); + + this.corners = Collections.unmodifiableList(new ArrayList<>(corners)); + } + + @Override + public List generateOutline() { + List 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 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 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())); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java new file mode 100644 index 000000000..ef831425a --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java @@ -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 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 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); +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java new file mode 100644 index 000000000..ec15a0f67 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java @@ -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 createPanels(List panels) { + List result = new ArrayList<>(panels.size()); + Deque 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 queue) { + List 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) { + 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); + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java index 0ae05dc2b..e6600bcd9 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java @@ -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 generateOutline() { - return Arrays.asList(getPosition()); + return Arrays.asList(position); } @Override - public Point2D labelPosition(Graphics2D graphics, List 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()); + } } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java index 99412ba2e..5e12c7bee 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java @@ -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 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 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 outline); + protected abstract ImagePoint2D labelPosition(Graphics2D graphics, List outline); + + @Override + public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) { + List 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()); + } + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java deleted file mode 100644 index 78e9ec088..000000000 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java +++ /dev/null @@ -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()); - } - } -} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java index a9cf76284..50c3cd8a9 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java @@ -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 outline) { + protected ImagePoint2D labelPosition(Graphics2D graphics, List 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)); } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java index 586e89fc6..4bf64e0de 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java @@ -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 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 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() { diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties index e3ee3da9d..a730c5089 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties @@ -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. diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml index afc9142ad..d5ac6ea23 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml @@ -19,6 +19,7 @@ + @@ -114,4 +115,10 @@ @text/channel-type.nanoleaf.layout.description + + Image + + @text/channel-type.nanoleaf.state.description + + diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java new file mode 100644 index 000000000..c6344abd5 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java @@ -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 permissions = PosixFilePermissions.fromString("rw-r--r--"); + FileAttribute> 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]; + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json new file mode 100644 index 000000000..3d8da0de1 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json @@ -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 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json new file mode 100644 index 000000000..2376436d1 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json @@ -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 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json new file mode 100644 index 000000000..4eec9a489 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json @@ -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 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json new file mode 100644 index 000000000..0da9bf3dd --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json @@ -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 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json new file mode 100644 index 000000000..24d854b67 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json @@ -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 + } + } +}